├── runtime ├── logs │ └── .gitignore ├── views │ └── .gitignore └── .gitignore ├── windows.bat ├── preview ├── chat.png └── login.png ├── public ├── favicon.ico ├── 404.html └── static │ ├── js │ ├── common.js │ ├── index.js │ └── chatroom.js │ └── lib │ ├── js.cookie.min.js │ ├── bootstrap │ └── css │ │ ├── bootstrap-reboot.min.css │ │ ├── bootstrap-reboot.rtl.min.css │ │ ├── bootstrap-reboot.rtl.css │ │ ├── bootstrap-reboot.css │ │ ├── bootstrap-reboot.min.css.map │ │ └── bootstrap-reboot.rtl.min.css.map │ └── message.js ├── config ├── plugin │ └── webman │ │ └── gateway-worker │ │ ├── app.php │ │ └── process.php ├── message-class.php ├── dependence.php ├── middleware.php ├── container.php ├── bootstrap.php ├── exception.php ├── autoload.php ├── static.php ├── redis.php ├── translation.php ├── view.php ├── app.php ├── route.php ├── log.php ├── server.php ├── database.php ├── process.php ├── .env.example.php └── session.php ├── .github └── FUNDING.yml ├── .gitignore ├── app ├── exception │ ├── WebSocketNoSendException.php │ ├── WebSocketAuthenticationException.php │ ├── WebSocketAuthExpception.php │ └── Handler.php ├── support │ ├── interface │ │ └── ChatInterface.php │ └── websocket │ │ ├── MessageProcessing.php │ │ └── type │ │ └── TextMessage.php ├── enum │ └── WebSocketMsgType.php ├── model │ ├── Test.php │ └── User.php ├── validate │ └── UserValidate.php ├── view │ ├── layouts │ │ └── app.html │ └── index │ │ ├── index.html │ │ └── chatroom.html ├── middleware │ └── StaticFile.php ├── functions.php └── controller │ └── IndexController.php ├── support ├── Request.php ├── Response.php ├── Plugin.php ├── bootstrap.php └── helpers.php ├── phinx.php ├── process ├── Task.php └── Monitor.php ├── database └── migrations │ └── 20220811030439_users.php ├── LICENSE ├── composer.json ├── README.md ├── windows.php ├── start.php └── plugin └── webman └── gateway └── Events.php /runtime/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /runtime/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /windows.bat: -------------------------------------------------------------------------------- 1 | CHCP 65001 2 | php windows.php 3 | pause -------------------------------------------------------------------------------- /runtime/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !logs 3 | !views 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /preview/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getda/Liao/HEAD/preview/chat.png -------------------------------------------------------------------------------- /preview/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getda/Liao/HEAD/preview/login.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getda/Liao/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /config/plugin/webman/gateway-worker/app.php: -------------------------------------------------------------------------------- 1 | true, 4 | ]; -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | open_collective: walkor 4 | patreon: walkor 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.vscode 3 | /vendor 4 | *.log 5 | config/.env.php 6 | /tests/tmp 7 | /tests/.phpunit.result.cache 8 | /phinx 9 | /composer.phar 10 | -------------------------------------------------------------------------------- /app/exception/WebSocketNoSendException.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 404 Not Found - webman 4 | 5 | 6 |
7 |

404 Not Found

8 |
9 |
10 |
webman
11 | 12 | 13 | -------------------------------------------------------------------------------- /app/support/interface/ChatInterface.php: -------------------------------------------------------------------------------- 1 | \app\support\websocket\type\PingMessage::class, 10 | 'text' => \app\support\websocket\type\TextMessage::class, 11 | ]; -------------------------------------------------------------------------------- /app/exception/WebSocketAuthenticationException.php: -------------------------------------------------------------------------------- 1 | { 10 | return res.json() 11 | }).catch(res => { 12 | console.log("ERROR", res) 13 | }) 14 | } -------------------------------------------------------------------------------- /app/exception/WebSocketAuthExpception.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return []; -------------------------------------------------------------------------------- /config/middleware.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return []; -------------------------------------------------------------------------------- /config/container.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return new Webman\Container; -------------------------------------------------------------------------------- /app/model/Test.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return [ 16 | support\bootstrap\Session::class, 17 | support\bootstrap\LaravelDb::class, 18 | ]; 19 | -------------------------------------------------------------------------------- /config/exception.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return [ 16 | // '' => support\exception\Handler::class, 17 | '' => App\exception\Handler::class, 18 | ]; -------------------------------------------------------------------------------- /public/static/js/index.js: -------------------------------------------------------------------------------- 1 | const data = function () { 2 | return { 3 | form: { 4 | nickname: undefined, 5 | password: undefined 6 | }, 7 | join() { 8 | if (!this.form.nickname || !this.form.password) { 9 | return false; 10 | } 11 | request('/join', this.form).then(res => { 12 | if (res.code === 0) { 13 | Message.warn(res.msg) 14 | return false; 15 | } 16 | window.location.href = res.url; 17 | }) 18 | } 19 | }; 20 | } -------------------------------------------------------------------------------- /support/Request.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | namespace support; 16 | 17 | /** 18 | * Class Request 19 | * @package support 20 | */ 21 | class Request extends \Webman\Http\Request 22 | { 23 | 24 | } -------------------------------------------------------------------------------- /support/Response.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | namespace support; 16 | 17 | /** 18 | * Class Response 19 | * @package support 20 | */ 21 | class Response extends \Webman\Http\Response 22 | { 23 | 24 | } -------------------------------------------------------------------------------- /config/autoload.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return [ 16 | 'files' => [ 17 | base_path() . '/app/functions.php', 18 | base_path() . '/support/Request.php', 19 | base_path() . '/support/Response.php', 20 | ] 21 | ]; 22 | -------------------------------------------------------------------------------- /config/static.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | /** 16 | * Static file settings 17 | */ 18 | return [ 19 | 'enable' => true, 20 | 'middleware' => [ // Static file Middleware 21 | //app\middleware\StaticFile::class, 22 | ], 23 | ]; -------------------------------------------------------------------------------- /app/validate/UserValidate.php: -------------------------------------------------------------------------------- 1 | 'require|chsAlphaNum|max:18', 15 | 'password' => 'require|alphaNum|min:6|max:10', 16 | ]; 17 | 18 | protected $message = [ 19 | 'name.require' => '请输入名称', 20 | 'name.chsAlphaNum' => '名称只能是汉字、数字、字母组合', 21 | 'name.max' => '名称最大18位', 22 | 'password.require' => '请输入口令', 23 | 'password.alphaNum' => '口令只能是数字、字母组合', 24 | 'password.min' => '口令最低6位', 25 | 'password.max' => '口令最大10位', 26 | ]; 27 | } -------------------------------------------------------------------------------- /config/redis.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return [ 16 | 'default' => [ 17 | 'host' => envs('REDIS_HOST', '127.0.0.1'), 18 | 'password' => envs('REDIS_PASSWORD', null), 19 | 'port' => envs('REDIS_PORT', 6379), 20 | 'database' => envs('REDIS_DATABASE', 0), 21 | ], 22 | ]; 23 | -------------------------------------------------------------------------------- /config/translation.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | /** 16 | * Multilingual configuration 17 | */ 18 | return [ 19 | // Default language 20 | 'locale' => 'zh_CN', 21 | // Fallback language 22 | 'fallback_locale' => ['zh_CN', 'en'], 23 | // Folder where language files are stored 24 | 'path' => base_path() . '/resource/translations', 25 | ]; -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | use support\view\Raw; 16 | use support\view\Twig; 17 | use support\view\Blade; 18 | use support\view\ThinkPHP; 19 | 20 | return [ 21 | 'handler' => ThinkPHP::class, 22 | 'options' => [ 23 | 'view_suffix' => 'html', 24 | 'tpl_begin' => '{', 25 | 'tpl_end' => '}', 26 | ], 27 | ]; 28 | -------------------------------------------------------------------------------- /phinx.php: -------------------------------------------------------------------------------- 1 | [ 6 | "migrations" => "database/migrations", 7 | "seeds" => "database/seeds", 8 | ], 9 | 'environments' => [ 10 | 'default_migration_table' => 'phinxlog', 11 | 'default_environment' => 'default', 12 | 'default' => [ 13 | 'adapter' => 'mysql', 14 | 'host' => $config['DB_HOST'] ?? '127.0.0.1', 15 | 'name' => $config['DB_DATABASE'] ?? 'test', 16 | 'user' => $config['DB_USERNAME'] ?? 'root', 17 | 'pass' => $config['DB_PASSWORD'] ?? 'root', 18 | 'port' => $config['DB_PORT'] ?? '3306', 19 | 'charset' => 'utf8mb4', 20 | ] 21 | ], 22 | 'version_order' => 'creation', 23 | ]; 24 | -------------------------------------------------------------------------------- /process/Task.php: -------------------------------------------------------------------------------- 1 | message->data = $data; 18 | $this->message->user = Gateway::getSession($clientId); 19 | // 处理方法 20 | if (method_exists($this->message, 'handler')) { 21 | $this->message->handler(); 22 | } 23 | } 24 | 25 | public function send(): string 26 | { 27 | return json_encode(array_merge([ 28 | 'type' => $this->data['type'], 29 | ], $this->message->send()), JSON_UNESCAPED_UNICODE); 30 | } 31 | } -------------------------------------------------------------------------------- /app/view/layouts/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | {block name="title"}Liao{/block} - Liao 9 | 10 | {block name="style"}{/block} 11 | 12 | 13 |
14 | {block name="content"}{/block} 15 |
16 | 17 | 18 | 19 | 20 | 21 | {block name="script-link"}{/block} 22 | 23 | -------------------------------------------------------------------------------- /config/app.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | use support\Request; 16 | 17 | return [ 18 | 'debug' => envs('DEBUG', 'false'), 19 | 'default_timezone' => 'Asia/Shanghai', 20 | 'request_class' => Request::class, 21 | 'public_path' => base_path() . DIRECTORY_SEPARATOR . 'public', 22 | 'runtime_path' => base_path(false) . DIRECTORY_SEPARATOR . 'runtime', 23 | 'controller_suffix' => '', 24 | 'frontend_websocket_url' => envs('FRONTEND_WEBSOCKET_URL', 'ws://127.0.0.1:8282') 25 | ]; 26 | -------------------------------------------------------------------------------- /config/route.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | use Webman\Route; 16 | use App\controller; 17 | 18 | Route::disableDefaultRoute(); 19 | 20 | Route::add(['GET', 'POST'],'/', [controller\IndexController::class, 'index'])->name('index'); 21 | Route::get('/chatroom', [controller\IndexController::class, 'chatroom'])->name('chatroom'); 22 | Route::post('/join', [controller\IndexController::class, 'join'])->name('join'); 23 | Route::post('/logout', [controller\IndexController::class, 'logout'])->name('logout'); 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /database/migrations/20220811030439_users.php: -------------------------------------------------------------------------------- 1 | table('users'); 11 | $table->addColumn('name', 'string', [ 12 | 'comment' => "用户名称", 13 | ]) 14 | ->addColumn('password', 'string', [ 15 | 'comment' => "登录密码", 16 | ]) 17 | ->addColumn('last_login_ip', 'string', [ 18 | 'comment' => "上次登录ip", 19 | 'null' => true 20 | ]) 21 | ->addColumn('last_login_at', 'string', [ 22 | 'comment' => "上次登录时间", 23 | 'null' => true 24 | ]) 25 | ->addIndex('name', ['unique' => true]) 26 | ->addTimestamps() 27 | ->save(); 28 | } 29 | 30 | public function down() 31 | { 32 | $table = $this->table('users'); 33 | $table->drop()->save(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/view/index/index.html: -------------------------------------------------------------------------------- 1 | {extend name="layouts/app" /} 2 | 3 | {block name="title"}即将进入聊天室...{/block} 4 | 5 | {block name="content"} 6 |
7 |
8 |
9 | 11 | 13 |
14 |
15 | 16 |
17 |
18 |
19 | {/block} 20 | {block name="script-link"} 21 | 22 | {/block} -------------------------------------------------------------------------------- /config/log.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return [ 16 | 'default' => [ 17 | 'handlers' => [ 18 | [ 19 | 'class' => Monolog\Handler\RotatingFileHandler::class, 20 | 'constructor' => [ 21 | runtime_path() . '/logs/webman.log', 22 | 7, //$maxFiles 23 | Monolog\Logger::DEBUG, 24 | ], 25 | 'formatter' => [ 26 | 'class' => Monolog\Formatter\LineFormatter::class, 27 | 'constructor' => [null, 'Y-m-d H:i:s', true], 28 | ], 29 | ] 30 | ], 31 | ], 32 | ]; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 walkor and contributors (see https://github.com/walkor/webman/contributors) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/server.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return [ 16 | 'listen' => envs('SERVER_LISTEN', 'http://0.0.0.0:8787'), 17 | 'transport' => 'tcp', 18 | 'context' => [], 19 | 'name' => 'webman', 20 | 'count' => cpu_count() * 2, 21 | 'user' => '', 22 | 'group' => '', 23 | 'reusePort' => false, 24 | 'event_loop' => '', 25 | 'stop_timeout' => 2, 26 | 'pid_file' => runtime_path().'/webman.pid', 27 | 'status_file' => runtime_path().'/webman.status', 28 | 'stdout_file' => runtime_path().'/logs/stdout.log', 29 | 'log_file' => runtime_path().'/logs/workerman.log', 30 | 'max_package_size' => 10 * 1024 * 1024, 31 | ]; 32 | -------------------------------------------------------------------------------- /app/exception/Handler.php: -------------------------------------------------------------------------------- 1 | clientId, json_encode([ 35 | 'msg' => $e->getMessage(), 36 | 'auth' => WebSocketMsgType::AUTH 37 | ], JSON_UNESCAPED_UNICODE)); 38 | sleep(1); 39 | Gateway::closeClient($e->clientId); 40 | } 41 | 42 | return parent::render($request, $e); 43 | } 44 | } -------------------------------------------------------------------------------- /config/database.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | return [ 15 | // 默认数据库 16 | 'default' => envs('DB_CONNECTION', 'mysql'), 17 | 18 | // 各种数据库配置 19 | 'connections' => [ 20 | 'mysql' => [ 21 | 'driver' => 'mysql', 22 | 'host' => envs('DB_HOST'), 23 | 'port' => envs('DB_PORT'), 24 | 'database' => envs('DB_DATABASE'), 25 | 'username' => envs('DB_USERNAME'), 26 | 'password' => envs('DB_PASSWORD'), 27 | 'unix_socket' => '', 28 | 'charset' => 'utf8mb4', 29 | 'collation' => 'utf8mb4_unicode_ci', 30 | 'prefix' => '', 31 | 'strict' => true, 32 | 'engine' => null, 33 | ], 34 | ], 35 | ]; 36 | -------------------------------------------------------------------------------- /config/process.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | 16 | return [ 17 | // File update detection and automatic reload 18 | 'monitor' => [ 19 | 'handler' => process\Monitor::class, 20 | 'reloadable' => false, 21 | 'constructor' => [ 22 | // Monitor these directories 23 | 'monitor_dir' => [ 24 | app_path(), 25 | config_path(), 26 | base_path().'/process', 27 | base_path().'/support', 28 | base_path().'/resource', 29 | base_path().'/.env', 30 | ], 31 | // Files with these suffixes will be monitored 32 | 'monitor_extensions' => [ 33 | 'php', 'html', 'htm', 'env', 34 | ], 35 | ], 36 | ], 37 | 'crontab' => [ 38 | 'handler' => process\Task::class, 39 | ], 40 | ]; 41 | -------------------------------------------------------------------------------- /config/.env.example.php: -------------------------------------------------------------------------------- 1 | false, 9 | // WEB 服务 10 | 'SERVER_LISTEN' => "http://0.0.0.0:8787", 11 | // WEBSOCKET 服务 12 | 'WEBSOCKET_LISTEN' => "websocket://0.0.0.0:8282", 13 | // WEBSOCKET 连接地址,用于前台 14 | 'FRONTEND_WEBSOCKET_URL' => "ws://127.0.0.1:8282", 15 | ]; 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Database 20 | |-------------------------------------------------------------------------- 21 | */ 22 | $database = [ 23 | 'DB_CONNECTION' => 'mysql', 24 | 'DB_HOST' => '127.0.0.1', 25 | 'DB_PORT' => '3306', 26 | 'DB_DATABASE' => 'test', 27 | 'DB_USERNAME' => 'root', 28 | 'DB_PASSWORD' => '', 29 | ]; 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Redis 34 | |-------------------------------------------------------------------------- 35 | */ 36 | $redis = [ 37 | 'REDIS_HOST' => '127.0.0.1', 38 | 'REDIS_PORT' => 6379, 39 | 'REDIS_DATABASE' => 0, 40 | 'REDIS_PASSWORD' => null, 41 | ]; 42 | 43 | return array_merge($app, $database, $redis); -------------------------------------------------------------------------------- /app/middleware/StaticFile.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | namespace App\middleware; 16 | 17 | use Webman\MiddlewareInterface; 18 | use Webman\Http\Response; 19 | use Webman\Http\Request; 20 | 21 | /** 22 | * Class StaticFile 23 | * @package app\middleware 24 | */ 25 | class StaticFile implements MiddlewareInterface 26 | { 27 | public function process(Request $request, callable $next): Response 28 | { 29 | // Access to files beginning with. Is prohibited 30 | if (strpos($request->path(), '/.') !== false) { 31 | return response('

403 forbidden

', 403); 32 | } 33 | /** @var Response $response */ 34 | $response = $next($request); 35 | // Add cross domain HTTP header 36 | /*$response->withHeaders([ 37 | 'Access-Control-Allow-Origin' => '*', 38 | 'Access-Control-Allow-Credentials' => 'true', 39 | ]);*/ 40 | return $response; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/plugin/webman/gateway-worker/process.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'handler' => Gateway::class, 10 | 'listen' => envs('WEBSOCKET_LISTEN', 'websocket://0.0.0.0:8282'), 11 | 'count' => cpu_count(), 12 | 'constructor' => [ 13 | 'config' => [ 14 | 'lanIp' => '127.0.0.1', 15 | 'startPort' => 2300, 16 | 'pingInterval' => 25, 17 | 'pingData' => '{"type":"ping"}', 18 | 'registerAddress' => '127.0.0.1:1236', 19 | 'onConnect' => function () { 20 | }, 21 | ], 22 | ], 23 | ], 24 | 'worker' => [ 25 | 'handler' => BusinessWorker::class, 26 | 'count' => cpu_count() * 2, 27 | 'constructor' => [ 28 | 'config' => [ 29 | 'eventHandler' => plugin\webman\gateway\Events::class, 30 | 'name' => 'ChatBusinessWorker', 31 | 'registerAddress' => '127.0.0.1:1236', 32 | ], 33 | ], 34 | ], 35 | 'register' => [ 36 | 'handler' => Register::class, 37 | 'listen' => 'text://0.0.0.0:1236', 38 | 'count' => 1, // Must be 1 39 | 'constructor' => [], 40 | ], 41 | ]; 42 | -------------------------------------------------------------------------------- /app/support/websocket/type/TextMessage.php: -------------------------------------------------------------------------------- 1 | message; 25 | } 26 | 27 | public function handler() 28 | { 29 | $this->storage(); 30 | } 31 | 32 | /** 33 | * 写入 redis 34 | * Author 王小大 [m@wangxiaoda.com] 35 | * DateTime 2022/8/14 15:54 36 | * @return void 37 | * @throws WebSocketNoSendException 38 | */ 39 | public function storage() 40 | { 41 | $content = $this->data['content']; 42 | if (!$content) { 43 | websocket_send($this->user['id'], '消息内容不能为空!', WebSocketMsgType::ERROR); 44 | websocket_no_send(); 45 | } 46 | $this->message = [ 47 | 'uid' => $this->user['id'], 48 | 'nickname' => $this->user['name'], 49 | 'content' => $content, 50 | 'send_time' => date('Y-m-d H:i:s'), 51 | ]; 52 | // 写入消息 53 | Redis::zAdd("liao:group_{$this->user['group_id']}", millisecond(), 54 | json_encode($this->message, JSON_UNESCAPED_UNICODE)); 55 | } 56 | } -------------------------------------------------------------------------------- /config/session.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return [ 16 | 17 | 'type' => 'file', // or redis or redis_cluster 18 | 19 | 'handler' => Webman\FileSessionHandler::class, 20 | 21 | 'config' => [ 22 | 'file' => [ 23 | 'save_path' => runtime_path() . '/sessions', 24 | ], 25 | 'redis' => [ 26 | 'host' => '127.0.0.1', 27 | 'port' => 6379, 28 | 'auth' => '', 29 | 'timeout' => 2, 30 | 'database' => '', 31 | 'prefix' => 'redis_session_', 32 | ], 33 | 'redis_cluster' => [ 34 | 'host' => ['127.0.0.1:7000', '127.0.0.1:7001', '127.0.0.1:7001'], 35 | 'timeout' => 2, 36 | 'auth' => '', 37 | 'prefix' => 'redis_session_', 38 | ] 39 | ], 40 | 41 | 'session_name' => 'PHPSID', 42 | 43 | 'auto_update_timestamp' => false, 44 | 45 | 'lifetime' => 7*24*60*60, 46 | 47 | 'cookie_lifetime' => 365*24*60*60, 48 | 49 | 'cookie_path' => '/', 50 | 51 | 'domain' => '', 52 | 53 | 'http_only' => true, 54 | 55 | 'secure' => false, 56 | 57 | 'same_site' => '', 58 | 59 | 'gc_probability' => [1, 1000], 60 | 61 | ]; 62 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workerman/webman", 3 | "type": "project", 4 | "keywords": [ 5 | "high performance", 6 | "http service" 7 | ], 8 | "homepage": "http://www.workerman.net", 9 | "license": "MIT", 10 | "description": "High performance HTTP Service Framework.", 11 | "authors": [ 12 | { 13 | "name": "walkor", 14 | "email": "walkor@workerman.net", 15 | "homepage": "http://www.workerman.net", 16 | "role": "Developer" 17 | } 18 | ], 19 | "support": { 20 | "email": "walkor@workerman.net", 21 | "issues": "https://github.com/walkor/webman/issues", 22 | "forum": "http://wenda.workerman.net/", 23 | "wiki": "http://workerman.net/doc/webman", 24 | "source": "https://github.com/walkor/webman" 25 | }, 26 | "require": { 27 | "php": ">=8.0", 28 | "workerman/webman-framework": "^1.3.14", 29 | "monolog/monolog": "^2.0", 30 | "illuminate/database": "^8.0", 31 | "illuminate/pagination": "^8.0", 32 | "illuminate/events": "^8.0", 33 | "symfony/var-dumper": "^5.4", 34 | "psr/container": "^1.1.1", 35 | "robmorgan/phinx": "^0.12.12", 36 | "topthink/think-template": "^2.0", 37 | "illuminate/redis": "^8.0", 38 | "topthink/think-validate": "^2.0", 39 | "symfony/cache": "^5.0", 40 | "webman/gateway-worker": "^1.0", 41 | "ext-json": "*", 42 | "workerman/crontab": "^1.0" 43 | }, 44 | "suggest": { 45 | "ext-event": "For better performance. " 46 | }, 47 | "autoload": { 48 | "psr-4": { 49 | "": "./", 50 | "App\\": "./app" 51 | }, 52 | "files": [ 53 | "./support/helpers.php" 54 | ] 55 | }, 56 | "scripts": { 57 | "post-package-install": [ 58 | "support\\Plugin::install" 59 | ], 60 | "post-package-update": [ 61 | "support\\Plugin::install" 62 | ], 63 | "pre-package-uninstall": [ 64 | "support\\Plugin::uninstall" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /public/static/lib/js.cookie.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Minified by jsDelivr using Terser v3.14.1. 3 | * Original file: /npm/js-cookie@2.2.1/src/js.cookie.js 4 | * 5 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files 6 | */ 7 | !function(e){var n;if("function"==typeof define&&define.amd&&(define(e),n=!0),"object"==typeof exports&&(module.exports=e(),n=!0),!n){var t=window.Cookies,o=window.Cookies=e();o.noConflict=function(){return window.Cookies=t,o}}}(function(){function e(){for(var e=0,n={};egetOperation(); 11 | $autoload = method_exists($operation, 'getPackage') ? $operation->getPackage()->getAutoload() : $operation->getTargetPackage()->getAutoload(); 12 | if (!isset($autoload['psr-4'])) { 13 | return; 14 | } 15 | foreach ($autoload['psr-4'] as $namespace => $path) { 16 | $install_function = "\\{$namespace}Install::install"; 17 | $plugin_const = "\\{$namespace}Install::WEBMAN_PLUGIN"; 18 | if (defined($plugin_const) && is_callable($install_function)) { 19 | $install_function(); 20 | } 21 | } 22 | } 23 | 24 | public static function update($event) 25 | { 26 | static::install($event); 27 | } 28 | 29 | public static function uninstall($event) 30 | { 31 | static::findHepler(); 32 | $autoload = $event->getOperation()->getPackage()->getAutoload(); 33 | if (!isset($autoload['psr-4'])) { 34 | return; 35 | } 36 | foreach ($autoload['psr-4'] as $namespace => $path) { 37 | $uninstall_function = "\\{$namespace}Install::uninstall"; 38 | $plugin_const = "\\{$namespace}Install::WEBMAN_PLUGIN"; 39 | if (defined($plugin_const) && is_callable($uninstall_function)) { 40 | $uninstall_function(); 41 | } 42 | } 43 | } 44 | 45 | protected static function findHepler() 46 | { 47 | // Plugin.php in vendor 48 | $file = __DIR__ . '/../../../../../support/helpers.php'; 49 | if (is_file($file)) { 50 | require_once $file; 51 | return; 52 | } 53 | // Plugin.php in webman 54 | require_once __DIR__ . '/helpers.php'; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /app/model/User.php: -------------------------------------------------------------------------------- 1 | strlen($value) === 60 ? $value : password_hash($value, PASSWORD_DEFAULT) 30 | ); 31 | } 32 | 33 | /** 34 | * Author 王小大 [m@wangxiaoda.com] 35 | * Date 2022/8/12 13:07 36 | * @return string 37 | */ 38 | public function authKey(): string 39 | { 40 | $salt = cache_remember("salt_{$this->id}", static::EXPIRE_AT, function () { 41 | return md5(Str::random(32).time()); 42 | }); 43 | 44 | return md5($this->id.$this->password.$salt); 45 | } 46 | 47 | /** 48 | * 密码烟杂 49 | * Author 王小大 [m@wangxiaoda.com] 50 | * Date 2022/8/12 14:57 51 | * @param string $password 52 | * @return bool 53 | */ 54 | public function checkPassword(string $password): bool 55 | { 56 | return password_verify($password, $this->password); 57 | } 58 | 59 | /** 60 | * Author 王小大 [m@wangxiaoda.com] 61 | * DateTime 2022/8/11 22:17 62 | * @param int $uid 63 | * @param string $identity 64 | * @return false|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|Model 65 | */ 66 | public static function checkIdentityByUid($uid, $identity) 67 | { 68 | if (empty($uid)) { 69 | return false; 70 | } 71 | $user = User::query()->find((int) $uid); 72 | if (!$user) { 73 | return false; 74 | } 75 | 76 | return $user->authKey() === $identity ? $user : false; 77 | } 78 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Liao 2 | #### 环境 3 | * PHP 8.0+ 4 | * Mysql 5.7+ 5 | * Redis 6 | 7 | ### 预览效果 8 | ![Login](https://raw.githubusercontent.com/getda/Liao/main/preview/login.png) 9 | ![Chat](https://raw.githubusercontent.com/getda/Liao/main/preview/chat.png) 10 | 11 | ### 部署说明 12 | 1. 克隆代码并安装扩展 13 | ```shell 14 | git clone git@github.com:getda/Liao.git liao 15 | cd liao 16 | composer install 17 | ``` 18 | 19 | 2. 将 `config` 目录下的 `.env.example.php` 重命名为 `.env.php` 并配置里面的相应信息 20 | ```php 21 | false, 29 | // WEB 服务 30 | 'SERVER_LISTEN' => "http://0.0.0.0:8787", 31 | // WEBSOCKET 服务 32 | 'WEBSOCKET_LISTEN' => "websocket://0.0.0.0:8282", 33 | // WEBSCOKET 连接地址,用于前台 34 | 'FRONTEND_WEBSOCKET_URL' => "ws://127.0.0.1:8282", 35 | ]; 36 | 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | Database 40 | |-------------------------------------------------------------------------- 41 | */ 42 | $database = [ 43 | 'DB_CONNECTION' => 'mysql', 44 | 'DB_HOST' => '127.0.0.1', 45 | 'DB_PORT' => '3306', 46 | 'DB_DATABASE' => 'test', 47 | 'DB_USERNAME' => 'root', 48 | 'DB_PASSWORD' => '', 49 | ]; 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Redis 54 | |-------------------------------------------------------------------------- 55 | */ 56 | $redis = [ 57 | 'REDIS_HOST' => '127.0.0.1', 58 | 'REDIS_PORT' => 6379, 59 | 'REDIS_DATABASE' => 0, 60 | 'REDIS_PASSWORD' => null, 61 | ]; 62 | 63 | return array_merge($app, $database, $redis); 64 | ``` 65 | 3. 执行数据表迁移 66 | ```shell 67 | php vendor/bin/phinx migrate 68 | ``` 69 | 4. 启动服务 70 | ```shell 71 | # 调试 72 | php start.php start 73 | # 生成环境 74 | php start.php start -d 75 | ``` 76 | 77 | 5. 配置域名 78 | > 见:[https://www.workerman.net/doc/webman/others/nginx-proxy.html](https://www.workerman.net/doc/webman/others/nginx-proxy.html) 79 | 80 | ### 更多说明 81 | 本项目基于 [webman](https://www.workerman.net/webman) 开发,仅用于测试学习参考。 -------------------------------------------------------------------------------- /app/functions.php: -------------------------------------------------------------------------------- 1 | 1, 13 | ], $data)); 14 | 15 | return response($json, $code, ['Content-Type' => 'application/json']); 16 | } 17 | 18 | function send_message($message, $code = 422, $data = []) 19 | { 20 | $json = json_encode(array_merge([ 21 | 'code' => 0, 22 | 'msg' => $message, 23 | ], $data)); 24 | 25 | return response($json, $code, ['Content-Type' => 'application/json']); 26 | } 27 | 28 | function cache_remember($key, $ttl, $callback) 29 | { 30 | $value = \support\Cache::get($key); 31 | 32 | if (!is_null($value)) { 33 | return $value; 34 | } 35 | 36 | $value = value($callback); 37 | \support\Cache::set($key, $value, $ttl ?: 0); 38 | return $value; 39 | } 40 | 41 | /** 42 | * WebSocket 抛出异常,代表不回复任务信息 43 | * Author 王小大 [m@wangxiaoda.com] 44 | * DateTime 2022/8/13 18:18 45 | * @return void 46 | * @throws WebSocketNoSendException 47 | */ 48 | function websocket_no_send(): void 49 | { 50 | throw new WebSocketNoSendException(); 51 | } 52 | 53 | function websocket_send($uid, string|array|null $message, $type = "", $code = 0) 54 | { 55 | if (!is_array($message)) { 56 | $message['msg'] = $message; 57 | } 58 | 59 | $data = array_merge([ 60 | 'code' => $code, 61 | 'type' => $type, 62 | ], $message); 63 | 64 | Gateway::sendToUid($uid, json_encode($data, JSON_UNESCAPED_UNICODE)); 65 | } 66 | 67 | 68 | function websocket_send_group($groupId, array $message, $type = "", $code = 0) 69 | { 70 | if (!is_array($message)) { 71 | $message['msg'] = $message; 72 | } 73 | 74 | $data = array_merge([ 75 | 'code' => $code, 76 | 'type' => $type, 77 | ], $message); 78 | 79 | Gateway::sendToGroup($groupId, json_encode($data, JSON_UNESCAPED_UNICODE)); 80 | } 81 | 82 | /** 83 | * 十三位时间戳 84 | * Author 王小大 [m@wangxiaoda.com] 85 | * DateTime 2022/8/14 15:51 86 | * @return float 87 | */ 88 | function millisecond() { 89 | [$t1, $t2] = explode(' ', microtime()); 90 | return (float)sprintf('%.0f',(floatval($t1)+floatval($t2))*1000); 91 | } -------------------------------------------------------------------------------- /support/bootstrap.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | use Dotenv\Dotenv; 16 | use support\Container; 17 | use Webman\Config; 18 | use Webman\Route; 19 | use Webman\Middleware; 20 | 21 | $worker = $worker ?? null; 22 | 23 | if ($timezone = config('app.default_timezone')) { 24 | date_default_timezone_set($timezone); 25 | } 26 | 27 | set_error_handler(function ($level, $message, $file = '', $line = 0, $context = []) { 28 | if (error_reporting() & $level) { 29 | throw new ErrorException($message, 0, $level, $file, $line); 30 | } 31 | }); 32 | 33 | if ($worker) { 34 | register_shutdown_function(function ($start_time) { 35 | if (time() - $start_time <= 1) { 36 | sleep(1); 37 | } 38 | }, time()); 39 | } 40 | 41 | if (class_exists('Dotenv\Dotenv') && file_exists(base_path() . '/.env')) { 42 | if (method_exists('Dotenv\Dotenv', 'createUnsafeImmutable')) { 43 | Dotenv::createUnsafeImmutable(base_path())->load(); 44 | } else { 45 | Dotenv::createMutable(base_path())->load(); 46 | } 47 | } 48 | 49 | Config::reload(config_path(), ['route', 'container']); 50 | 51 | foreach (config('plugin', []) as $firm => $projects) { 52 | foreach ($projects as $name => $project) { 53 | foreach ($project['autoload']['files'] ?? [] as $file) { 54 | include_once $file; 55 | } 56 | } 57 | } 58 | 59 | foreach (config('autoload.files', []) as $file) { 60 | include_once $file; 61 | } 62 | 63 | $container = Container::instance(); 64 | Route::container($container); 65 | Middleware::container($container); 66 | 67 | Middleware::load(config('middleware', [])); 68 | foreach (config('plugin', []) as $firm => $projects) { 69 | foreach ($projects as $name => $project) { 70 | Middleware::load($project['middleware'] ?? []); 71 | } 72 | } 73 | Middleware::load(['__static__' => config('static.middleware', [])]); 74 | 75 | foreach (config('bootstrap', []) as $class_name) { 76 | /** @var \Webman\Bootstrap $class_name */ 77 | $class_name::start($worker); 78 | } 79 | 80 | foreach (config('plugin', []) as $firm => $projects) { 81 | foreach ($projects as $name => $project) { 82 | foreach ($project['bootstrap'] ?? [] as $class_name) { 83 | /** @var \Webman\Bootstrap $class_name */ 84 | $class_name::start($worker); 85 | } 86 | } 87 | } 88 | 89 | Route::load(config_path()); 90 | 91 | -------------------------------------------------------------------------------- /app/controller/IndexController.php: -------------------------------------------------------------------------------- 1 | cookie('uid'); 26 | $identityHash = (string) $request->cookie('identity'); 27 | if ($uid && $identityHash) { 28 | return redirect(route('chatroom')); 29 | } 30 | 31 | return view('index/index'); 32 | } 33 | 34 | /** 35 | * 加入房间 36 | * Author 王小大 [m@wangxiaoda.com] 37 | * DateTime 2022/8/17 7:03 38 | * @param Request $request 39 | * @return \support\Response 40 | */ 41 | public function join(Request $request) 42 | { 43 | $data = $request->post(); 44 | $validate = new UserValidate(); 45 | if (!$validate->check($data)) { 46 | return send_message($validate->getError()); 47 | } 48 | $user = User::query()->firstOrCreate(['name' => $data['nickname']], [ 49 | 'password' => $data['password'], 50 | ]); 51 | if ($user && !$user->checkPassword($data['password'])) { 52 | return send_message("口令错误啦~"); 53 | } 54 | // 判断是否在线 55 | if (Gateway::isUidOnline($user->id)) { 56 | return send_message("当前账号已经在线啦,请换个账号试试!"); 57 | } 58 | $userId = $user->id; 59 | $authKey = $user->authKey(); 60 | 61 | return send(['url' => route('chatroom', ['uid' => $userId, 'identity' => $authKey])]); 62 | } 63 | 64 | /** 65 | * 聊天界面 66 | * Author 王小大 [m@wangxiaoda.com] 67 | * DateTime 2022/8/11 0:09 68 | * @param Request $request 69 | * @return \support\Response 70 | */ 71 | public function chatroom(Request $request) 72 | { 73 | $uid = (int) $request->cookie('uid') ?: $request->get('uid', ''); 74 | $identity = (string) $request->cookie('identity') ?: $request->get('identity', ''); 75 | // 验证并获取用户信息 76 | $user = User::checkIdentityByUid($uid, $identity); 77 | if (!$user) { 78 | return $this->logout(); 79 | } 80 | 81 | return view( 82 | 'index/chatroom', 83 | ['chatroomName' => "欢乐聊天室", 'user' => $user->only(['id', 'name', 'created_at'])] 84 | )->cookie('uid', $uid, User::EXPIRE_AT) 85 | ->cookie('identity', $identity, User::EXPIRE_AT); 86 | } 87 | 88 | /** 89 | * 退出 90 | * Author 王小大 [m@wangxiaoda.com] 91 | * DateTime 2022/8/11 0:09 92 | * @return \support\Response 93 | */ 94 | public function logout() 95 | { 96 | return redirect(route('index')) 97 | ->cookie('uid', '', 0) 98 | ->cookie('identity', '', 0); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /windows.php: -------------------------------------------------------------------------------- 1 | $config) { 24 | $file_content = << $projects) { 56 | foreach ($projects as $name => $project) { 57 | foreach ($project['process'] ?? [] as $process_name => $config) { 58 | $file_content = <<checkAllFilesChange()) { 108 | $status = proc_get_status($resource); 109 | $pid = $status['pid']; 110 | shell_exec("taskkill /F /T /PID $pid"); 111 | proc_close($resource); 112 | $resource = popen_processes($process_files); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /start.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | load(); 23 | } else { 24 | Dotenv::createMutable(base_path())->load(); 25 | } 26 | } 27 | 28 | Config::load(config_path(), ['route', 'container']); 29 | 30 | $error_reporting = config('app.error_reporting'); 31 | if (isset($error_reporting)) { 32 | error_reporting($error_reporting); 33 | } 34 | 35 | if ($timezone = config('app.default_timezone')) { 36 | date_default_timezone_set($timezone); 37 | } 38 | 39 | $runtime_logs_path = runtime_path() . DIRECTORY_SEPARATOR . 'logs'; 40 | if ( !file_exists($runtime_logs_path) || !is_dir($runtime_logs_path) ) { 41 | if (!mkdir($runtime_logs_path,0777,true)) { 42 | throw new \RuntimeException("Failed to create runtime logs directory. Please check the permission."); 43 | } 44 | } 45 | 46 | $runtime_views_path = runtime_path() . DIRECTORY_SEPARATOR . 'views'; 47 | if ( !file_exists($runtime_views_path) || !is_dir($runtime_views_path) ) { 48 | if (!mkdir($runtime_views_path,0777,true)) { 49 | throw new \RuntimeException("Failed to create runtime views directory. Please check the permission."); 50 | } 51 | } 52 | 53 | Worker::$onMasterReload = function () { 54 | if (function_exists('opcache_get_status')) { 55 | if ($status = opcache_get_status()) { 56 | if (isset($status['scripts']) && $scripts = $status['scripts']) { 57 | foreach (array_keys($scripts) as $file) { 58 | opcache_invalidate($file, true); 59 | } 60 | } 61 | } 62 | } 63 | }; 64 | 65 | $config = config('server'); 66 | Worker::$pidFile = $config['pid_file']; 67 | Worker::$stdoutFile = $config['stdout_file']; 68 | Worker::$logFile = $config['log_file']; 69 | Worker::$eventLoopClass = $config['event_loop'] ?? ''; 70 | TcpConnection::$defaultMaxPackageSize = $config['max_package_size'] ?? 10 * 1024 * 1024; 71 | if (property_exists(Worker::class, 'statusFile')) { 72 | Worker::$statusFile = $config['status_file'] ?? ''; 73 | } 74 | if (property_exists(Worker::class, 'stopTimeout')) { 75 | Worker::$stopTimeout = $config['stop_timeout'] ?? 2; 76 | } 77 | 78 | if ($config['listen']) { 79 | $worker = new Worker($config['listen'], $config['context']); 80 | $property_map = [ 81 | 'name', 82 | 'count', 83 | 'user', 84 | 'group', 85 | 'reusePort', 86 | 'transport', 87 | 'protocol' 88 | ]; 89 | foreach ($property_map as $property) { 90 | if (isset($config[$property])) { 91 | $worker->$property = $config[$property]; 92 | } 93 | } 94 | 95 | $worker->onWorkerStart = function ($worker) { 96 | require_once base_path() . '/support/bootstrap.php'; 97 | $app = new App($worker, Container::instance(), Log::channel('default'), app_path(), public_path()); 98 | Http::requestClass(config('app.request_class', config('server.request_class', Request::class))); 99 | $worker->onMessage = [$app, 'onMessage']; 100 | }; 101 | } 102 | 103 | // Windows does not support custom processes. 104 | if (\DIRECTORY_SEPARATOR === '/') { 105 | foreach (config('process', []) as $process_name => $config) { 106 | worker_start($process_name, $config); 107 | } 108 | foreach (config('plugin', []) as $firm => $projects) { 109 | foreach ($projects as $name => $project) { 110 | foreach ($project['process'] ?? [] as $process_name => $config) { 111 | worker_start("plugin.$firm.$name.$process_name", $config); 112 | } 113 | } 114 | } 115 | } 116 | 117 | Worker::runAll(); 118 | -------------------------------------------------------------------------------- /plugin/webman/gateway/Events.php: -------------------------------------------------------------------------------- 1 | 0) { 27 | Gateway::sendToClient($clientId, json_encode([ 28 | 'msg' => "当前账号已经在线啦,请换个账号试试!", 29 | 'type' => WebSocketMsgType::ERROR, 30 | ], JSON_UNESCAPED_UNICODE)); 31 | // 延迟一秒断开链接 32 | sleep(1); 33 | return Gateway::closeClient($clientId); 34 | } 35 | // 验证 Auth 36 | try { 37 | $user = static::checkAuth($clientId, $getData); 38 | } catch (WebSocketAuthenticationException $exception) { 39 | Gateway::sendToClient($exception->clientId, json_encode([ 40 | 'msg' => $exception->getMessage(), 41 | 'type' => WebSocketMsgType::AUTH, 42 | ], JSON_UNESCAPED_UNICODE)); 43 | // 延迟一秒断开链接 44 | sleep(1); 45 | return Gateway::closeClient($clientId); 46 | } 47 | // 下放聊天记录 48 | $start = strtotime("-1 hours")."000"; 49 | $end = time()."000"; 50 | $chatRecord = Redis::zrangebyscore("liao:group_{$user['group_id']}", $start, $end); 51 | websocket_send($user['id'], ['list' => $chatRecord], WebSocketMsgType::INIT); 52 | // 更新房间信息 53 | // static::updateRoomInfo(); 54 | } 55 | 56 | public static function onMessage($clientId, $message) 57 | { 58 | $data = json_decode($message, true); 59 | $type = $data['type'] ?? 'no'; 60 | if ($class = config("message-class.{$type}")) { 61 | try { 62 | $user = Gateway::getSession($clientId); 63 | $message = new MessageProcessing(new $class(), $clientId, $data); 64 | Gateway::sendToGroup($user['group_id'], $message->send()); 65 | } catch (WebSocketNoSendException $exception) { 66 | // 不做任何操作 67 | } 68 | } else { 69 | Gateway::sendToClient($clientId, "【原样返回】{$message}"); 70 | } 71 | } 72 | 73 | public static function onClose($clientId) 74 | { 75 | } 76 | 77 | /** 78 | * 身份验证 79 | * Author 王世杰 [m@wangxiaoda.com] 80 | * Date 2022/8/15 10:07 81 | * @param $clientId 82 | * @param $data 83 | * @return false|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model 84 | * @throws \Throwable 85 | */ 86 | public static function checkAuth($clientId, $data) 87 | { 88 | $uid = $data['uid'] ?? null; 89 | $identity = $data['identity'] ?? null; 90 | $room = $data['room'] ?? 1; 91 | 92 | throw_if(blank($uid) || blank($identity), new WebSocketAuthenticationException($clientId, "缺少身份验证参数!")); 93 | 94 | $user = User::checkIdentityByUid($uid, $identity); 95 | 96 | throw_if(!$user, new WebSocketAuthenticationException($clientId)); 97 | 98 | // 关联uid 99 | Gateway::bindUid($clientId, $user->id); 100 | // 加入到房间 101 | Gateway::joinGroup($clientId, $room); 102 | // 记录用户信息 103 | $user->offsetSet('group_id', $room); // 群信息 104 | $user->offsetSet('client_id', $clientId); // clientId 105 | Gateway::setSession($clientId, $user->toArray()); 106 | 107 | return $user; 108 | } 109 | 110 | public static function updateRoomInfo() 111 | { 112 | websocket_send_group(1, [ 113 | 'online' => Gateway::getAllUidCount(), 114 | 'roomOnline' => Gateway::getAllGroupUidCount()[1] ?? 0, 115 | ], WebSocketMsgType::ROOM_INFO); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /public/static/js/chatroom.js: -------------------------------------------------------------------------------- 1 | const data = function () { 2 | return { 3 | roomInfoShow: false, 4 | roomInfo: { 5 | online: 0, 6 | roomOnline: 0, 7 | }, 8 | websocket: undefined, 9 | message: "", 10 | pingTimer: undefined, 11 | messageBox: [], 12 | uid: undefined, 13 | identity: undefined, 14 | room: 1, 15 | // 发送 ping 16 | ping() { 17 | return setInterval(() => { 18 | this.websocket.send(JSON.stringify({ 19 | "type": "ping" 20 | })); 21 | console.log("发送ping"); 22 | }, 20000); 23 | }, 24 | init() { 25 | this.uid = Cookies.get('uid'); 26 | this.identity = Cookies.get('identity'); 27 | this.websocket = new WebSocket(`${WEBSOCKET_URL}?uid=${this.uid}&identity=${this.identity}&room=${this.room}`); 28 | let pingTimer = this.ping(); 29 | 30 | this.websocket.onopen = (event) => { 31 | } 32 | this.websocket.onmessage = (data) => { 33 | let dataJson = this.parseJson(data.data); 34 | // 处理消息内容 35 | this.handle(dataJson); 36 | } 37 | this.websocket.onclose = function (event) { 38 | clearInterval(pingTimer); 39 | alert("服务器连接断开了!") 40 | } 41 | 42 | // 聚焦 43 | document.getElementById('message').focus(); 44 | 45 | }, 46 | /** 47 | * 发送文本消息 48 | * @returns {boolean} 49 | */ 50 | sendMessage() { 51 | if (this.websocket.readyState !== WebSocket.OPEN) { 52 | Message.error("已与服务器断开连接,请刷新重试!"); 53 | return false; 54 | } 55 | if (!this.message) { 56 | Message.warn("消息内容不能为空"); 57 | return false; 58 | } 59 | let data = { 60 | type: "text", 61 | content: this.message 62 | }; 63 | this.websocket.send(JSON.stringify(data)); 64 | this.message = ""; 65 | }, 66 | handle(data) { 67 | let type = data?.type, privateObject = {}; 68 | 69 | /** 70 | * 授权验证失败 71 | * @returns {boolean} 72 | */ 73 | privateObject.auth = () => { 74 | Message.error("授权验证失败!"); 75 | return false; 76 | }; 77 | 78 | /** 79 | * ping 80 | */ 81 | privateObject.ping = () => { 82 | console.log("pong"); 83 | }; 84 | 85 | /** 86 | * 房间信息更新 87 | */ 88 | privateObject.roomInfo = () => { 89 | this.roomInfo.online = data.online; 90 | this.roomInfo.roomOnline = data.roomOnline; 91 | }; 92 | 93 | /** 94 | * 初始化消息内容 95 | */ 96 | privateObject.init = () => { 97 | if (data && Array.isArray(data.list) && data.list.length > 0) { 98 | data.list.map(item => { 99 | let itemArray = this.parseJson(item); 100 | itemArray && this.messageBox.push({ 101 | uid: itemArray.uid, 102 | nickname: itemArray.nickname, 103 | content: itemArray.content, 104 | send_time: itemArray.send_time 105 | }); 106 | }); 107 | this.scrollBottom(); 108 | } 109 | }; 110 | 111 | /** 112 | * 聊天内容 113 | */ 114 | privateObject.text = () => { 115 | this.messageBox.push({ 116 | uid: data.uid, 117 | nickname: data.nickname, 118 | content: data.content, 119 | send_time: data.send_time 120 | }); 121 | this.scrollBottom(); 122 | }; 123 | 124 | /** 125 | * 提示消息 126 | */ 127 | privateObject.error = () => { 128 | Message.warn(data.msg); 129 | }; 130 | 131 | privateObject.default = () => { 132 | console.warn("不存在的类型!"); 133 | }; 134 | 135 | let func = privateObject[type] || privateObject['default']; 136 | return func(); 137 | }, 138 | /** 139 | * 解析 JSON 140 | * @param str 141 | * @returns {null|any} 142 | */ 143 | parseJson(str) { 144 | try { 145 | return JSON.parse(str); 146 | } catch (e) { 147 | return null; 148 | } 149 | }, 150 | scrollBottom() { 151 | let element = document.getElementById("message-box"); 152 | setTimeout(() => { 153 | element.scrollTop = element.scrollHeight + 150; 154 | }, 20); 155 | } 156 | }; 157 | } -------------------------------------------------------------------------------- /app/view/index/chatroom.html: -------------------------------------------------------------------------------- 1 | {extend name="layouts/app" /} 2 | 3 | {block name="title"}欢迎{$user.name}来到{$chatroomName}{/block} 4 | 5 | {block name="style"} 6 | 16 | {/block} 17 | 18 | {block name="content"} 19 |
20 |
21 |
22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
{$chatroomName}
35 |
36 |
37 | 38 |
39 |
40 |
41 |
42 |
43 | 71 |
72 |
73 |
74 |
75 |
76 | 77 | 78 |
79 |

80 | * 81 | 请勿发送违法违规内容。文明交谈,尊重他人,谢谢! 82 |

83 |

84 | 注: 85 | 每个房间仅保存1小时内的聊天记录! 86 |

87 |
88 |
89 |
90 | {/block} 91 | 92 | {block name="script-link"} 93 | 96 | 97 | {/block} -------------------------------------------------------------------------------- /public/static/lib/bootstrap/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.2.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2022 The Bootstrap Authors 4 | * Copyright 2011-2022 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-2xl:2rem;--bs-border-radius-pill:50rem;--bs-link-color:#0d6efd;--bs-link-hover-color:#0a58ca;--bs-code-color:#d63384;--bs-highlight-bg:#fff3cd}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid;opacity:.25}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.1875em;background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:var(--bs-link-color);text-decoration:underline}a:hover{color:var(--bs-link-hover-color)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important} 7 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /public/static/lib/bootstrap/css/bootstrap-reboot.rtl.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.2.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2022 The Bootstrap Authors 4 | * Copyright 2011-2022 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-2xl:2rem;--bs-border-radius-pill:50rem;--bs-link-color:#0d6efd;--bs-link-hover-color:#0a58ca;--bs-code-color:#d63384;--bs-highlight-bg:#fff3cd}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid;opacity:.25}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.1875em;background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:var(--bs-link-color);text-decoration:underline}a:hover{color:var(--bs-link-hover-color)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important} 7 | /*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */ -------------------------------------------------------------------------------- /public/static/lib/message.js: -------------------------------------------------------------------------------- 1 | (function(win) { 2 | const createDom = (className = '', tag = 'div') => { 3 | const ele = document.createElement(tag) 4 | ele.className = className 5 | return ele 6 | } 7 | /** 8 | * 根据驼峰字符串生成css属性的键 9 | * @param camelCaseName 10 | * @returns {string} 11 | */ 12 | const getCssAttrName = (camelCaseName) => { 13 | if (typeof camelCaseName !== 'string' || camelCaseName === '') { 14 | return '' 15 | } 16 | // 获取所有的大写字符 17 | let upperCaseChars = camelCaseName.match(/[A-Z]/g) 18 | // 去除重复的项 19 | upperCaseChars = [...(new Set(upperCaseChars))] 20 | // 循环将大写字母换为 “-?”的格式 21 | upperCaseChars.forEach(char => { 22 | camelCaseName = camelCaseName.replace(new RegExp(char, 'g'), `-${char.toLocaleLowerCase()}`) 23 | }) 24 | return camelCaseName 25 | } 26 | /** 27 | * 根据css对象生成相应的css字符串 28 | * @param obj 29 | * @returns {string} 30 | */ 31 | const cssFromObj = (obj) => { 32 | if (typeof obj === 'object' && obj !== null) { 33 | const cssArr = [] 34 | Object.keys(obj).forEach(key => { 35 | cssArr.push(`${getCssAttrName(key)}: ${obj[key]};`) 36 | }) 37 | return cssArr.join('') 38 | } 39 | return '' 40 | } 41 | const commonCss = { 42 | width: '70%', 43 | // height: '30px', 44 | backgroundColor: '#ebeef5', 45 | border: '1px solid #ebeef5', 46 | borderRadius: '4px', 47 | padding: '15px 15px 15px 20px', 48 | overflow: 'hidden', 49 | textOverFlow: 'ellipsis', 50 | wordBreak: 'nowrap', 51 | color: '#fff', 52 | fontSize: '14px', 53 | position: 'fixed', 54 | margin: '0 calc(30% / 2)', 55 | boxSizing: 'border-box', 56 | display: 'flex', 57 | flexWrap: 'nowrap', 58 | zIndex: 1024 59 | } 60 | const errorCss = { 61 | ...commonCss, 62 | backgroundColor: '#fef0f0', 63 | borderColor: '#fde2e2', 64 | color: '#f56c6c' 65 | } 66 | const successCss = { 67 | ...commonCss, 68 | backgroundColor: '#f0f9eb', 69 | borderColor: '#e1f3d8', 70 | color: '#67c23a' 71 | } 72 | const warnCss = { 73 | ...commonCss, 74 | backgroundColor: '#fdf6ec', 75 | borderColor: '#faecd8', 76 | color: '#e6a23c' 77 | } 78 | const infoCss = { 79 | ...commonCss, 80 | borderColor: '#edf2fc', 81 | color: '#909399' 82 | } 83 | class Message { 84 | constructor (text, config, cssConfig = {}) { 85 | this.config = { 86 | // 类型 info,error,success,warn 87 | type: 'info', 88 | // 提示内容 89 | text: text || '提示', 90 | // 位置: top, bottom 91 | position: 'top', 92 | // 距离窗口的距离,取决于position(top即位距离顶部的距离,bottom即为距离底部的距离) 93 | distance: '30px', 94 | // 弹出的时间 95 | duration: 3000, 96 | // 弹出延迟 97 | delay: 0, 98 | closable: true, 99 | icon: '' 100 | } 101 | this.init(config, cssConfig) 102 | this.addEventListener() 103 | } 104 | 105 | init (config, cssConfig) { 106 | // 取一个id值,方便之后精确查找该元素 107 | this.id = `message-${Date.now()}` 108 | this.cssConfig = cssConfig 109 | Object.assign(this.config, config) 110 | // 组css 111 | const cssObj = { 112 | // eslint-disable-next-line no-eval 113 | ...eval(`${this.config.type}Css`), 114 | ...this.cssConfig, 115 | [this.config.position]: this.config.distance 116 | } 117 | // 消息容器 118 | const box = createDom('hxl-message', 'div') 119 | box.style = cssFromObj(cssObj) 120 | box.id = this.id 121 | // 生成小图标 122 | const icon = createDom(`icon ${this.config.icon}`, 'i') 123 | box.appendChild(icon) 124 | // 生成消息体u 125 | const text = createDom('hxl-message-text', 'span') 126 | text.innerHTML = this.config.text 127 | text.style = cssFromObj({ 128 | flex: '1' 129 | }) 130 | box.appendChild(text) 131 | if (this.config.closable) { 132 | const close = createDom('hxl-close', 'div') 133 | this.closeDom = close 134 | close.style = cssFromObj({ 135 | cursor: 'pointer' 136 | // color: '#FFF' 137 | }) 138 | // 实际上这里应该是一个图标,但是懒得整了 139 | close.innerText = 'x' 140 | box.appendChild(close) 141 | } 142 | this.box = box 143 | } 144 | 145 | addEventListener () { 146 | this.closeDom && (this.closeDom.onclick = function () { 147 | this.hide() 148 | }.bind(this)) 149 | } 150 | 151 | show () { 152 | const that = this 153 | // 延迟几秒在显示 154 | this.t1 = setTimeout(() => { 155 | document.body.appendChild(that.box) 156 | that.timeout = setTimeout(() => { 157 | that.hide() 158 | clearTimeout(that.timeout) 159 | }, that.config.duration) 160 | clearTimeout(this.t1) 161 | }, this.config.delay) 162 | } 163 | 164 | hide () { 165 | const box = document.querySelector(`#${this.id}`) 166 | box && typeof box.remove === 'function' && box.remove() 167 | clearTimeout(this.timeout) 168 | } 169 | 170 | static error (text) { 171 | Message.handleShow(text, 'error') 172 | } 173 | 174 | static handleShow (text, type) { 175 | const message = new Message(text, { type }) 176 | message.show() 177 | } 178 | 179 | static success (text) { 180 | Message.handleShow(text, 'success') 181 | } 182 | 183 | static info (text) { 184 | Message.handleShow(text, 'info') 185 | } 186 | 187 | static warn (text) { 188 | Message.handleShow(text, 'warn') 189 | } 190 | } 191 | 192 | win.Message = Message 193 | })(window) -------------------------------------------------------------------------------- /process/Monitor.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | namespace process; 16 | 17 | use Workerman\Timer; 18 | use Workerman\Worker; 19 | 20 | /** 21 | * Class FileMonitor 22 | * @package process 23 | */ 24 | class Monitor 25 | { 26 | /** 27 | * @var array 28 | */ 29 | protected $_paths = []; 30 | 31 | /** 32 | * @var array 33 | */ 34 | protected $_extensions = []; 35 | 36 | /** 37 | * FileMonitor constructor. 38 | * @param $monitor_dir 39 | * @param $monitor_extensions 40 | * @param $memory_limit 41 | */ 42 | public function __construct($monitor_dir, $monitor_extensions, $memory_limit = null) 43 | { 44 | $this->_paths = (array)$monitor_dir; 45 | $this->_extensions = $monitor_extensions; 46 | if (!Worker::getAllWorkers()) { 47 | return; 48 | } 49 | $disable_functions = explode(',', ini_get('disable_functions')); 50 | if (in_array('exec', $disable_functions, true)) { 51 | echo "\nMonitor file change turned off because exec() has been disabled by disable_functions setting in " . PHP_CONFIG_FILE_PATH . "/php.ini\n"; 52 | } else { 53 | if (!Worker::$daemonize) { 54 | Timer::add(1, function () { 55 | $this->checkAllFilesChange(); 56 | }); 57 | } 58 | } 59 | 60 | $memory_limit = $this->getMemoryLimit($memory_limit); 61 | if ($memory_limit && DIRECTORY_SEPARATOR === '/') { 62 | Timer::add(60, [$this, 'checkMemory'], [$memory_limit]); 63 | } 64 | } 65 | 66 | /** 67 | * @param $monitor_dir 68 | */ 69 | public function checkFilesChange($monitor_dir) 70 | { 71 | static $last_mtime, $too_many_files_check; 72 | if (!$last_mtime) { 73 | $last_mtime = time(); 74 | } 75 | clearstatcache(); 76 | if (!is_dir($monitor_dir)) { 77 | if (!is_file($monitor_dir)) { 78 | return; 79 | } 80 | $iterator = [new \SplFileInfo($monitor_dir)]; 81 | } else { 82 | // recursive traversal directory 83 | $dir_iterator = new \RecursiveDirectoryIterator($monitor_dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS); 84 | $iterator = new \RecursiveIteratorIterator($dir_iterator); 85 | } 86 | $count = 0; 87 | foreach ($iterator as $file) { 88 | $count ++; 89 | /** var SplFileInfo $file */ 90 | if (is_dir($file)) { 91 | continue; 92 | } 93 | // check mtime 94 | if ($last_mtime < $file->getMTime() && in_array($file->getExtension(), $this->_extensions, true)) { 95 | $var = 0; 96 | exec(PHP_BINARY . " -l " . $file, $out, $var); 97 | if ($var) { 98 | $last_mtime = $file->getMTime(); 99 | continue; 100 | } 101 | $last_mtime = $file->getMTime(); 102 | echo $file . " update and reload\n"; 103 | // send SIGUSR1 signal to master process for reload 104 | if (DIRECTORY_SEPARATOR === '/') { 105 | posix_kill(posix_getppid(), SIGUSR1); 106 | } else { 107 | return true; 108 | } 109 | break; 110 | } 111 | } 112 | if (!$too_many_files_check && $count > 1000) { 113 | echo "Monitor: There are too many files ($count files) in $monitor_dir which makes file monitoring very slow\n"; 114 | $too_many_files_check = 1; 115 | } 116 | } 117 | 118 | /** 119 | * @return bool 120 | */ 121 | public function checkAllFilesChange() 122 | { 123 | foreach ($this->_paths as $path) { 124 | if ($this->checkFilesChange($path)) { 125 | return true; 126 | } 127 | } 128 | return false; 129 | } 130 | 131 | /** 132 | * @param $memory_limit 133 | * @return void 134 | */ 135 | public function checkMemory($memory_limit) 136 | { 137 | $ppid = posix_getppid(); 138 | $children_file = "/proc/$ppid/task/$ppid/children"; 139 | if (!is_file($children_file) || !($children = file_get_contents($children_file))) { 140 | return; 141 | } 142 | foreach (explode(' ', $children) as $pid) { 143 | $pid = (int)$pid; 144 | $status_file = "/proc/$pid/status"; 145 | if (!is_file($status_file) || !($status = file_get_contents($status_file))) { 146 | continue; 147 | } 148 | $mem = 0; 149 | if (preg_match('/VmRSS\s*?:\s*?(\d+?)\s*?kB/', $status, $match)) { 150 | $mem = $match[1]; 151 | } 152 | $mem = (int)($mem / 1024); 153 | if ($mem >= $memory_limit) { 154 | posix_kill($pid, SIGINT); 155 | } 156 | } 157 | } 158 | 159 | /** 160 | * Get memory limit 161 | * @return float 162 | */ 163 | protected function getMemoryLimit($memory_limit) 164 | { 165 | if ($memory_limit === 0) { 166 | return 0; 167 | } 168 | $use_php_ini = false; 169 | if (!$memory_limit) { 170 | $memory_limit = ini_get('memory_limit'); 171 | $use_php_ini = true; 172 | } 173 | 174 | if ($memory_limit == -1) { 175 | return 0; 176 | } 177 | $unit = $memory_limit[strlen($memory_limit) - 1]; 178 | if ($unit == 'G') { 179 | $memory_limit = 1024 * (int)$memory_limit; 180 | } else if ($unit == 'M') { 181 | $memory_limit = (int)$memory_limit; 182 | } else if ($unit == 'K') { 183 | $memory_limit = (int)($memory_limit / 1024); 184 | } else { 185 | $memory_limit = (int)($memory_limit / (1024 * 1024)); 186 | } 187 | if ($memory_limit < 30) { 188 | $memory_limit = 30; 189 | } 190 | if ($use_php_ini) { 191 | $memory_limit = (int)(0.8 * $memory_limit); 192 | } 193 | return $memory_limit; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /public/static/lib/bootstrap/css/bootstrap-reboot.rtl.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.2.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2022 The Bootstrap Authors 4 | * Copyright 2011-2022 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | */ 7 | :root { 8 | --bs-blue: #0d6efd; 9 | --bs-indigo: #6610f2; 10 | --bs-purple: #6f42c1; 11 | --bs-pink: #d63384; 12 | --bs-red: #dc3545; 13 | --bs-orange: #fd7e14; 14 | --bs-yellow: #ffc107; 15 | --bs-green: #198754; 16 | --bs-teal: #20c997; 17 | --bs-cyan: #0dcaf0; 18 | --bs-black: #000; 19 | --bs-white: #fff; 20 | --bs-gray: #6c757d; 21 | --bs-gray-dark: #343a40; 22 | --bs-gray-100: #f8f9fa; 23 | --bs-gray-200: #e9ecef; 24 | --bs-gray-300: #dee2e6; 25 | --bs-gray-400: #ced4da; 26 | --bs-gray-500: #adb5bd; 27 | --bs-gray-600: #6c757d; 28 | --bs-gray-700: #495057; 29 | --bs-gray-800: #343a40; 30 | --bs-gray-900: #212529; 31 | --bs-primary: #0d6efd; 32 | --bs-secondary: #6c757d; 33 | --bs-success: #198754; 34 | --bs-info: #0dcaf0; 35 | --bs-warning: #ffc107; 36 | --bs-danger: #dc3545; 37 | --bs-light: #f8f9fa; 38 | --bs-dark: #212529; 39 | --bs-primary-rgb: 13, 110, 253; 40 | --bs-secondary-rgb: 108, 117, 125; 41 | --bs-success-rgb: 25, 135, 84; 42 | --bs-info-rgb: 13, 202, 240; 43 | --bs-warning-rgb: 255, 193, 7; 44 | --bs-danger-rgb: 220, 53, 69; 45 | --bs-light-rgb: 248, 249, 250; 46 | --bs-dark-rgb: 33, 37, 41; 47 | --bs-white-rgb: 255, 255, 255; 48 | --bs-black-rgb: 0, 0, 0; 49 | --bs-body-color-rgb: 33, 37, 41; 50 | --bs-body-bg-rgb: 255, 255, 255; 51 | --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 52 | --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 53 | --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); 54 | --bs-body-font-family: var(--bs-font-sans-serif); 55 | --bs-body-font-size: 1rem; 56 | --bs-body-font-weight: 400; 57 | --bs-body-line-height: 1.5; 58 | --bs-body-color: #212529; 59 | --bs-body-bg: #fff; 60 | --bs-border-width: 1px; 61 | --bs-border-style: solid; 62 | --bs-border-color: #dee2e6; 63 | --bs-border-color-translucent: rgba(0, 0, 0, 0.175); 64 | --bs-border-radius: 0.375rem; 65 | --bs-border-radius-sm: 0.25rem; 66 | --bs-border-radius-lg: 0.5rem; 67 | --bs-border-radius-xl: 1rem; 68 | --bs-border-radius-2xl: 2rem; 69 | --bs-border-radius-pill: 50rem; 70 | --bs-link-color: #0d6efd; 71 | --bs-link-hover-color: #0a58ca; 72 | --bs-code-color: #d63384; 73 | --bs-highlight-bg: #fff3cd; 74 | } 75 | 76 | *, 77 | *::before, 78 | *::after { 79 | box-sizing: border-box; 80 | } 81 | 82 | @media (prefers-reduced-motion: no-preference) { 83 | :root { 84 | scroll-behavior: smooth; 85 | } 86 | } 87 | 88 | body { 89 | margin: 0; 90 | font-family: var(--bs-body-font-family); 91 | font-size: var(--bs-body-font-size); 92 | font-weight: var(--bs-body-font-weight); 93 | line-height: var(--bs-body-line-height); 94 | color: var(--bs-body-color); 95 | text-align: var(--bs-body-text-align); 96 | background-color: var(--bs-body-bg); 97 | -webkit-text-size-adjust: 100%; 98 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 99 | } 100 | 101 | hr { 102 | margin: 1rem 0; 103 | color: inherit; 104 | border: 0; 105 | border-top: 1px solid; 106 | opacity: 0.25; 107 | } 108 | 109 | h6, h5, h4, h3, h2, h1 { 110 | margin-top: 0; 111 | margin-bottom: 0.5rem; 112 | font-weight: 500; 113 | line-height: 1.2; 114 | } 115 | 116 | h1 { 117 | font-size: calc(1.375rem + 1.5vw); 118 | } 119 | @media (min-width: 1200px) { 120 | h1 { 121 | font-size: 2.5rem; 122 | } 123 | } 124 | 125 | h2 { 126 | font-size: calc(1.325rem + 0.9vw); 127 | } 128 | @media (min-width: 1200px) { 129 | h2 { 130 | font-size: 2rem; 131 | } 132 | } 133 | 134 | h3 { 135 | font-size: calc(1.3rem + 0.6vw); 136 | } 137 | @media (min-width: 1200px) { 138 | h3 { 139 | font-size: 1.75rem; 140 | } 141 | } 142 | 143 | h4 { 144 | font-size: calc(1.275rem + 0.3vw); 145 | } 146 | @media (min-width: 1200px) { 147 | h4 { 148 | font-size: 1.5rem; 149 | } 150 | } 151 | 152 | h5 { 153 | font-size: 1.25rem; 154 | } 155 | 156 | h6 { 157 | font-size: 1rem; 158 | } 159 | 160 | p { 161 | margin-top: 0; 162 | margin-bottom: 1rem; 163 | } 164 | 165 | abbr[title] { 166 | -webkit-text-decoration: underline dotted; 167 | text-decoration: underline dotted; 168 | cursor: help; 169 | -webkit-text-decoration-skip-ink: none; 170 | text-decoration-skip-ink: none; 171 | } 172 | 173 | address { 174 | margin-bottom: 1rem; 175 | font-style: normal; 176 | line-height: inherit; 177 | } 178 | 179 | ol, 180 | ul { 181 | padding-right: 2rem; 182 | } 183 | 184 | ol, 185 | ul, 186 | dl { 187 | margin-top: 0; 188 | margin-bottom: 1rem; 189 | } 190 | 191 | ol ol, 192 | ul ul, 193 | ol ul, 194 | ul ol { 195 | margin-bottom: 0; 196 | } 197 | 198 | dt { 199 | font-weight: 700; 200 | } 201 | 202 | dd { 203 | margin-bottom: 0.5rem; 204 | margin-right: 0; 205 | } 206 | 207 | blockquote { 208 | margin: 0 0 1rem; 209 | } 210 | 211 | b, 212 | strong { 213 | font-weight: bolder; 214 | } 215 | 216 | small { 217 | font-size: 0.875em; 218 | } 219 | 220 | mark { 221 | padding: 0.1875em; 222 | background-color: var(--bs-highlight-bg); 223 | } 224 | 225 | sub, 226 | sup { 227 | position: relative; 228 | font-size: 0.75em; 229 | line-height: 0; 230 | vertical-align: baseline; 231 | } 232 | 233 | sub { 234 | bottom: -0.25em; 235 | } 236 | 237 | sup { 238 | top: -0.5em; 239 | } 240 | 241 | a { 242 | color: var(--bs-link-color); 243 | text-decoration: underline; 244 | } 245 | a:hover { 246 | color: var(--bs-link-hover-color); 247 | } 248 | 249 | a:not([href]):not([class]), a:not([href]):not([class]):hover { 250 | color: inherit; 251 | text-decoration: none; 252 | } 253 | 254 | pre, 255 | code, 256 | kbd, 257 | samp { 258 | font-family: var(--bs-font-monospace); 259 | font-size: 1em; 260 | } 261 | 262 | pre { 263 | display: block; 264 | margin-top: 0; 265 | margin-bottom: 1rem; 266 | overflow: auto; 267 | font-size: 0.875em; 268 | } 269 | pre code { 270 | font-size: inherit; 271 | color: inherit; 272 | word-break: normal; 273 | } 274 | 275 | code { 276 | font-size: 0.875em; 277 | color: var(--bs-code-color); 278 | word-wrap: break-word; 279 | } 280 | a > code { 281 | color: inherit; 282 | } 283 | 284 | kbd { 285 | padding: 0.1875rem 0.375rem; 286 | font-size: 0.875em; 287 | color: var(--bs-body-bg); 288 | background-color: var(--bs-body-color); 289 | border-radius: 0.25rem; 290 | } 291 | kbd kbd { 292 | padding: 0; 293 | font-size: 1em; 294 | } 295 | 296 | figure { 297 | margin: 0 0 1rem; 298 | } 299 | 300 | img, 301 | svg { 302 | vertical-align: middle; 303 | } 304 | 305 | table { 306 | caption-side: bottom; 307 | border-collapse: collapse; 308 | } 309 | 310 | caption { 311 | padding-top: 0.5rem; 312 | padding-bottom: 0.5rem; 313 | color: #6c757d; 314 | text-align: right; 315 | } 316 | 317 | th { 318 | text-align: inherit; 319 | text-align: -webkit-match-parent; 320 | } 321 | 322 | thead, 323 | tbody, 324 | tfoot, 325 | tr, 326 | td, 327 | th { 328 | border-color: inherit; 329 | border-style: solid; 330 | border-width: 0; 331 | } 332 | 333 | label { 334 | display: inline-block; 335 | } 336 | 337 | button { 338 | border-radius: 0; 339 | } 340 | 341 | button:focus:not(:focus-visible) { 342 | outline: 0; 343 | } 344 | 345 | input, 346 | button, 347 | select, 348 | optgroup, 349 | textarea { 350 | margin: 0; 351 | font-family: inherit; 352 | font-size: inherit; 353 | line-height: inherit; 354 | } 355 | 356 | button, 357 | select { 358 | text-transform: none; 359 | } 360 | 361 | [role=button] { 362 | cursor: pointer; 363 | } 364 | 365 | select { 366 | word-wrap: normal; 367 | } 368 | select:disabled { 369 | opacity: 1; 370 | } 371 | 372 | [list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { 373 | display: none !important; 374 | } 375 | 376 | button, 377 | [type=button], 378 | [type=reset], 379 | [type=submit] { 380 | -webkit-appearance: button; 381 | } 382 | button:not(:disabled), 383 | [type=button]:not(:disabled), 384 | [type=reset]:not(:disabled), 385 | [type=submit]:not(:disabled) { 386 | cursor: pointer; 387 | } 388 | 389 | ::-moz-focus-inner { 390 | padding: 0; 391 | border-style: none; 392 | } 393 | 394 | textarea { 395 | resize: vertical; 396 | } 397 | 398 | fieldset { 399 | min-width: 0; 400 | padding: 0; 401 | margin: 0; 402 | border: 0; 403 | } 404 | 405 | legend { 406 | float: right; 407 | width: 100%; 408 | padding: 0; 409 | margin-bottom: 0.5rem; 410 | font-size: calc(1.275rem + 0.3vw); 411 | line-height: inherit; 412 | } 413 | @media (min-width: 1200px) { 414 | legend { 415 | font-size: 1.5rem; 416 | } 417 | } 418 | legend + * { 419 | clear: right; 420 | } 421 | 422 | ::-webkit-datetime-edit-fields-wrapper, 423 | ::-webkit-datetime-edit-text, 424 | ::-webkit-datetime-edit-minute, 425 | ::-webkit-datetime-edit-hour-field, 426 | ::-webkit-datetime-edit-day-field, 427 | ::-webkit-datetime-edit-month-field, 428 | ::-webkit-datetime-edit-year-field { 429 | padding: 0; 430 | } 431 | 432 | ::-webkit-inner-spin-button { 433 | height: auto; 434 | } 435 | 436 | [type=search] { 437 | outline-offset: -2px; 438 | -webkit-appearance: textfield; 439 | } 440 | 441 | [type="tel"], 442 | [type="url"], 443 | [type="email"], 444 | [type="number"] { 445 | direction: ltr; 446 | } 447 | ::-webkit-search-decoration { 448 | -webkit-appearance: none; 449 | } 450 | 451 | ::-webkit-color-swatch-wrapper { 452 | padding: 0; 453 | } 454 | 455 | ::-webkit-file-upload-button { 456 | font: inherit; 457 | -webkit-appearance: button; 458 | } 459 | 460 | ::file-selector-button { 461 | font: inherit; 462 | -webkit-appearance: button; 463 | } 464 | 465 | output { 466 | display: inline-block; 467 | } 468 | 469 | iframe { 470 | border: 0; 471 | } 472 | 473 | summary { 474 | display: list-item; 475 | cursor: pointer; 476 | } 477 | 478 | progress { 479 | vertical-align: baseline; 480 | } 481 | 482 | [hidden] { 483 | display: none !important; 484 | } 485 | /*# sourceMappingURL=bootstrap-reboot.rtl.css.map */ -------------------------------------------------------------------------------- /public/static/lib/bootstrap/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.2.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2022 The Bootstrap Authors 4 | * Copyright 2011-2022 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | */ 7 | :root { 8 | --bs-blue: #0d6efd; 9 | --bs-indigo: #6610f2; 10 | --bs-purple: #6f42c1; 11 | --bs-pink: #d63384; 12 | --bs-red: #dc3545; 13 | --bs-orange: #fd7e14; 14 | --bs-yellow: #ffc107; 15 | --bs-green: #198754; 16 | --bs-teal: #20c997; 17 | --bs-cyan: #0dcaf0; 18 | --bs-black: #000; 19 | --bs-white: #fff; 20 | --bs-gray: #6c757d; 21 | --bs-gray-dark: #343a40; 22 | --bs-gray-100: #f8f9fa; 23 | --bs-gray-200: #e9ecef; 24 | --bs-gray-300: #dee2e6; 25 | --bs-gray-400: #ced4da; 26 | --bs-gray-500: #adb5bd; 27 | --bs-gray-600: #6c757d; 28 | --bs-gray-700: #495057; 29 | --bs-gray-800: #343a40; 30 | --bs-gray-900: #212529; 31 | --bs-primary: #0d6efd; 32 | --bs-secondary: #6c757d; 33 | --bs-success: #198754; 34 | --bs-info: #0dcaf0; 35 | --bs-warning: #ffc107; 36 | --bs-danger: #dc3545; 37 | --bs-light: #f8f9fa; 38 | --bs-dark: #212529; 39 | --bs-primary-rgb: 13, 110, 253; 40 | --bs-secondary-rgb: 108, 117, 125; 41 | --bs-success-rgb: 25, 135, 84; 42 | --bs-info-rgb: 13, 202, 240; 43 | --bs-warning-rgb: 255, 193, 7; 44 | --bs-danger-rgb: 220, 53, 69; 45 | --bs-light-rgb: 248, 249, 250; 46 | --bs-dark-rgb: 33, 37, 41; 47 | --bs-white-rgb: 255, 255, 255; 48 | --bs-black-rgb: 0, 0, 0; 49 | --bs-body-color-rgb: 33, 37, 41; 50 | --bs-body-bg-rgb: 255, 255, 255; 51 | --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 52 | --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 53 | --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); 54 | --bs-body-font-family: var(--bs-font-sans-serif); 55 | --bs-body-font-size: 1rem; 56 | --bs-body-font-weight: 400; 57 | --bs-body-line-height: 1.5; 58 | --bs-body-color: #212529; 59 | --bs-body-bg: #fff; 60 | --bs-border-width: 1px; 61 | --bs-border-style: solid; 62 | --bs-border-color: #dee2e6; 63 | --bs-border-color-translucent: rgba(0, 0, 0, 0.175); 64 | --bs-border-radius: 0.375rem; 65 | --bs-border-radius-sm: 0.25rem; 66 | --bs-border-radius-lg: 0.5rem; 67 | --bs-border-radius-xl: 1rem; 68 | --bs-border-radius-2xl: 2rem; 69 | --bs-border-radius-pill: 50rem; 70 | --bs-link-color: #0d6efd; 71 | --bs-link-hover-color: #0a58ca; 72 | --bs-code-color: #d63384; 73 | --bs-highlight-bg: #fff3cd; 74 | } 75 | 76 | *, 77 | *::before, 78 | *::after { 79 | box-sizing: border-box; 80 | } 81 | 82 | @media (prefers-reduced-motion: no-preference) { 83 | :root { 84 | scroll-behavior: smooth; 85 | } 86 | } 87 | 88 | body { 89 | margin: 0; 90 | font-family: var(--bs-body-font-family); 91 | font-size: var(--bs-body-font-size); 92 | font-weight: var(--bs-body-font-weight); 93 | line-height: var(--bs-body-line-height); 94 | color: var(--bs-body-color); 95 | text-align: var(--bs-body-text-align); 96 | background-color: var(--bs-body-bg); 97 | -webkit-text-size-adjust: 100%; 98 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 99 | } 100 | 101 | hr { 102 | margin: 1rem 0; 103 | color: inherit; 104 | border: 0; 105 | border-top: 1px solid; 106 | opacity: 0.25; 107 | } 108 | 109 | h6, h5, h4, h3, h2, h1 { 110 | margin-top: 0; 111 | margin-bottom: 0.5rem; 112 | font-weight: 500; 113 | line-height: 1.2; 114 | } 115 | 116 | h1 { 117 | font-size: calc(1.375rem + 1.5vw); 118 | } 119 | @media (min-width: 1200px) { 120 | h1 { 121 | font-size: 2.5rem; 122 | } 123 | } 124 | 125 | h2 { 126 | font-size: calc(1.325rem + 0.9vw); 127 | } 128 | @media (min-width: 1200px) { 129 | h2 { 130 | font-size: 2rem; 131 | } 132 | } 133 | 134 | h3 { 135 | font-size: calc(1.3rem + 0.6vw); 136 | } 137 | @media (min-width: 1200px) { 138 | h3 { 139 | font-size: 1.75rem; 140 | } 141 | } 142 | 143 | h4 { 144 | font-size: calc(1.275rem + 0.3vw); 145 | } 146 | @media (min-width: 1200px) { 147 | h4 { 148 | font-size: 1.5rem; 149 | } 150 | } 151 | 152 | h5 { 153 | font-size: 1.25rem; 154 | } 155 | 156 | h6 { 157 | font-size: 1rem; 158 | } 159 | 160 | p { 161 | margin-top: 0; 162 | margin-bottom: 1rem; 163 | } 164 | 165 | abbr[title] { 166 | -webkit-text-decoration: underline dotted; 167 | text-decoration: underline dotted; 168 | cursor: help; 169 | -webkit-text-decoration-skip-ink: none; 170 | text-decoration-skip-ink: none; 171 | } 172 | 173 | address { 174 | margin-bottom: 1rem; 175 | font-style: normal; 176 | line-height: inherit; 177 | } 178 | 179 | ol, 180 | ul { 181 | padding-left: 2rem; 182 | } 183 | 184 | ol, 185 | ul, 186 | dl { 187 | margin-top: 0; 188 | margin-bottom: 1rem; 189 | } 190 | 191 | ol ol, 192 | ul ul, 193 | ol ul, 194 | ul ol { 195 | margin-bottom: 0; 196 | } 197 | 198 | dt { 199 | font-weight: 700; 200 | } 201 | 202 | dd { 203 | margin-bottom: 0.5rem; 204 | margin-left: 0; 205 | } 206 | 207 | blockquote { 208 | margin: 0 0 1rem; 209 | } 210 | 211 | b, 212 | strong { 213 | font-weight: bolder; 214 | } 215 | 216 | small { 217 | font-size: 0.875em; 218 | } 219 | 220 | mark { 221 | padding: 0.1875em; 222 | background-color: var(--bs-highlight-bg); 223 | } 224 | 225 | sub, 226 | sup { 227 | position: relative; 228 | font-size: 0.75em; 229 | line-height: 0; 230 | vertical-align: baseline; 231 | } 232 | 233 | sub { 234 | bottom: -0.25em; 235 | } 236 | 237 | sup { 238 | top: -0.5em; 239 | } 240 | 241 | a { 242 | color: var(--bs-link-color); 243 | text-decoration: underline; 244 | } 245 | a:hover { 246 | color: var(--bs-link-hover-color); 247 | } 248 | 249 | a:not([href]):not([class]), a:not([href]):not([class]):hover { 250 | color: inherit; 251 | text-decoration: none; 252 | } 253 | 254 | pre, 255 | code, 256 | kbd, 257 | samp { 258 | font-family: var(--bs-font-monospace); 259 | font-size: 1em; 260 | } 261 | 262 | pre { 263 | display: block; 264 | margin-top: 0; 265 | margin-bottom: 1rem; 266 | overflow: auto; 267 | font-size: 0.875em; 268 | } 269 | pre code { 270 | font-size: inherit; 271 | color: inherit; 272 | word-break: normal; 273 | } 274 | 275 | code { 276 | font-size: 0.875em; 277 | color: var(--bs-code-color); 278 | word-wrap: break-word; 279 | } 280 | a > code { 281 | color: inherit; 282 | } 283 | 284 | kbd { 285 | padding: 0.1875rem 0.375rem; 286 | font-size: 0.875em; 287 | color: var(--bs-body-bg); 288 | background-color: var(--bs-body-color); 289 | border-radius: 0.25rem; 290 | } 291 | kbd kbd { 292 | padding: 0; 293 | font-size: 1em; 294 | } 295 | 296 | figure { 297 | margin: 0 0 1rem; 298 | } 299 | 300 | img, 301 | svg { 302 | vertical-align: middle; 303 | } 304 | 305 | table { 306 | caption-side: bottom; 307 | border-collapse: collapse; 308 | } 309 | 310 | caption { 311 | padding-top: 0.5rem; 312 | padding-bottom: 0.5rem; 313 | color: #6c757d; 314 | text-align: left; 315 | } 316 | 317 | th { 318 | text-align: inherit; 319 | text-align: -webkit-match-parent; 320 | } 321 | 322 | thead, 323 | tbody, 324 | tfoot, 325 | tr, 326 | td, 327 | th { 328 | border-color: inherit; 329 | border-style: solid; 330 | border-width: 0; 331 | } 332 | 333 | label { 334 | display: inline-block; 335 | } 336 | 337 | button { 338 | border-radius: 0; 339 | } 340 | 341 | button:focus:not(:focus-visible) { 342 | outline: 0; 343 | } 344 | 345 | input, 346 | button, 347 | select, 348 | optgroup, 349 | textarea { 350 | margin: 0; 351 | font-family: inherit; 352 | font-size: inherit; 353 | line-height: inherit; 354 | } 355 | 356 | button, 357 | select { 358 | text-transform: none; 359 | } 360 | 361 | [role=button] { 362 | cursor: pointer; 363 | } 364 | 365 | select { 366 | word-wrap: normal; 367 | } 368 | select:disabled { 369 | opacity: 1; 370 | } 371 | 372 | [list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { 373 | display: none !important; 374 | } 375 | 376 | button, 377 | [type=button], 378 | [type=reset], 379 | [type=submit] { 380 | -webkit-appearance: button; 381 | } 382 | button:not(:disabled), 383 | [type=button]:not(:disabled), 384 | [type=reset]:not(:disabled), 385 | [type=submit]:not(:disabled) { 386 | cursor: pointer; 387 | } 388 | 389 | ::-moz-focus-inner { 390 | padding: 0; 391 | border-style: none; 392 | } 393 | 394 | textarea { 395 | resize: vertical; 396 | } 397 | 398 | fieldset { 399 | min-width: 0; 400 | padding: 0; 401 | margin: 0; 402 | border: 0; 403 | } 404 | 405 | legend { 406 | float: left; 407 | width: 100%; 408 | padding: 0; 409 | margin-bottom: 0.5rem; 410 | font-size: calc(1.275rem + 0.3vw); 411 | line-height: inherit; 412 | } 413 | @media (min-width: 1200px) { 414 | legend { 415 | font-size: 1.5rem; 416 | } 417 | } 418 | legend + * { 419 | clear: left; 420 | } 421 | 422 | ::-webkit-datetime-edit-fields-wrapper, 423 | ::-webkit-datetime-edit-text, 424 | ::-webkit-datetime-edit-minute, 425 | ::-webkit-datetime-edit-hour-field, 426 | ::-webkit-datetime-edit-day-field, 427 | ::-webkit-datetime-edit-month-field, 428 | ::-webkit-datetime-edit-year-field { 429 | padding: 0; 430 | } 431 | 432 | ::-webkit-inner-spin-button { 433 | height: auto; 434 | } 435 | 436 | [type=search] { 437 | outline-offset: -2px; 438 | -webkit-appearance: textfield; 439 | } 440 | 441 | /* rtl:raw: 442 | [type="tel"], 443 | [type="url"], 444 | [type="email"], 445 | [type="number"] { 446 | direction: ltr; 447 | } 448 | */ 449 | ::-webkit-search-decoration { 450 | -webkit-appearance: none; 451 | } 452 | 453 | ::-webkit-color-swatch-wrapper { 454 | padding: 0; 455 | } 456 | 457 | ::-webkit-file-upload-button { 458 | font: inherit; 459 | -webkit-appearance: button; 460 | } 461 | 462 | ::file-selector-button { 463 | font: inherit; 464 | -webkit-appearance: button; 465 | } 466 | 467 | output { 468 | display: inline-block; 469 | } 470 | 471 | iframe { 472 | border: 0; 473 | } 474 | 475 | summary { 476 | display: list-item; 477 | cursor: pointer; 478 | } 479 | 480 | progress { 481 | vertical-align: baseline; 482 | } 483 | 484 | [hidden] { 485 | display: none !important; 486 | } 487 | 488 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /support/helpers.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | use support\Request; 16 | use support\Response; 17 | use support\Translation; 18 | use support\Container; 19 | use support\view\Raw; 20 | use support\view\Blade; 21 | use support\view\ThinkPHP; 22 | use support\view\Twig; 23 | use Workerman\Worker; 24 | use Webman\App; 25 | use Webman\Config; 26 | use Webman\Route; 27 | 28 | // Phar support. 29 | if (is_phar()) { 30 | define('BASE_PATH', dirname(__DIR__)); 31 | } else { 32 | define('BASE_PATH', realpath(__DIR__ . '/../')); 33 | } 34 | define('WEBMAN_VERSION', '1.3.0'); 35 | 36 | /** 37 | * @param $return_phar 38 | * @return false|string 39 | */ 40 | function base_path($return_phar = true) 41 | { 42 | static $real_path = ''; 43 | if (!$real_path) { 44 | $real_path = is_phar() ? dirname(Phar::running(false)) : BASE_PATH; 45 | } 46 | return $return_phar ? BASE_PATH : $real_path; 47 | } 48 | 49 | /** 50 | * @return string 51 | */ 52 | function app_path() 53 | { 54 | return BASE_PATH . DIRECTORY_SEPARATOR . 'app'; 55 | } 56 | 57 | /** 58 | * @return string 59 | */ 60 | function public_path() 61 | { 62 | static $path = ''; 63 | if (!$path) { 64 | $path = config('app.public_path', BASE_PATH . DIRECTORY_SEPARATOR . 'public'); 65 | } 66 | return $path; 67 | } 68 | 69 | /** 70 | * @return string 71 | */ 72 | function config_path() 73 | { 74 | return BASE_PATH . DIRECTORY_SEPARATOR . 'config'; 75 | } 76 | 77 | /** 78 | * Phar support. 79 | * Compatible with the 'realpath' function in the phar file. 80 | * 81 | * @return string 82 | */ 83 | function runtime_path() 84 | { 85 | static $path = ''; 86 | if (!$path) { 87 | $path = config('app.runtime_path', BASE_PATH . DIRECTORY_SEPARATOR . 'runtime'); 88 | } 89 | return $path; 90 | } 91 | 92 | /** 93 | * @param int $status 94 | * @param array $headers 95 | * @param string $body 96 | * @return Response 97 | */ 98 | function response($body = '', $status = 200, $headers = array()) 99 | { 100 | return new Response($status, $headers, $body); 101 | } 102 | 103 | /** 104 | * @param $data 105 | * @param int $options 106 | * @return Response 107 | */ 108 | function json($data, $options = JSON_UNESCAPED_UNICODE) 109 | { 110 | return new Response(200, ['Content-Type' => 'application/json'], json_encode($data, $options)); 111 | } 112 | 113 | /** 114 | * @param $xml 115 | * @return Response 116 | */ 117 | function xml($xml) 118 | { 119 | if ($xml instanceof SimpleXMLElement) { 120 | $xml = $xml->asXML(); 121 | } 122 | return new Response(200, ['Content-Type' => 'text/xml'], $xml); 123 | } 124 | 125 | /** 126 | * @param $data 127 | * @param string $callback_name 128 | * @return Response 129 | */ 130 | function jsonp($data, $callback_name = 'callback') 131 | { 132 | if (!is_scalar($data) && null !== $data) { 133 | $data = json_encode($data); 134 | } 135 | return new Response(200, [], "$callback_name($data)"); 136 | } 137 | 138 | /** 139 | * @param $location 140 | * @param int $status 141 | * @param array $headers 142 | * @return Response 143 | */ 144 | function redirect($location, $status = 302, $headers = []) 145 | { 146 | $response = new Response($status, ['Location' => $location]); 147 | if (!empty($headers)) { 148 | $response->withHeaders($headers); 149 | } 150 | return $response; 151 | } 152 | 153 | /** 154 | * @param $template 155 | * @param array $vars 156 | * @param null $app 157 | * @return Response 158 | */ 159 | function view($template, $vars = [], $app = null) 160 | { 161 | static $handler; 162 | if (null === $handler) { 163 | $handler = config('view.handler'); 164 | } 165 | return new Response(200, [], $handler::render($template, $vars, $app)); 166 | } 167 | 168 | /** 169 | * @param $template 170 | * @param array $vars 171 | * @param null $app 172 | * @return Response 173 | */ 174 | function raw_view($template, $vars = [], $app = null) 175 | { 176 | return new Response(200, [], Raw::render($template, $vars, $app)); 177 | } 178 | 179 | /** 180 | * @param $template 181 | * @param array $vars 182 | * @param null $app 183 | * @return Response 184 | */ 185 | function blade_view($template, $vars = [], $app = null) 186 | { 187 | return new Response(200, [], Blade::render($template, $vars, $app)); 188 | } 189 | 190 | /** 191 | * @param $template 192 | * @param array $vars 193 | * @param null $app 194 | * @return Response 195 | */ 196 | function think_view($template, $vars = [], $app = null) 197 | { 198 | return new Response(200, [], ThinkPHP::render($template, $vars, $app)); 199 | } 200 | 201 | /** 202 | * @param $template 203 | * @param array $vars 204 | * @param null $app 205 | * @return Response 206 | */ 207 | function twig_view($template, $vars = [], $app = null) 208 | { 209 | return new Response(200, [], Twig::render($template, $vars, $app)); 210 | } 211 | 212 | /** 213 | * @return Request 214 | */ 215 | function request() 216 | { 217 | return App::request(); 218 | } 219 | 220 | /** 221 | * @param $key 222 | * @param null $default 223 | * @return mixed 224 | */ 225 | function config($key = null, $default = null) 226 | { 227 | return Config::get($key, $default); 228 | } 229 | 230 | /** 231 | * @param $name 232 | * @param ...$parameters 233 | * @return string 234 | */ 235 | function route($name, ...$parameters) 236 | { 237 | $route = Route::getByName($name); 238 | if (!$route) { 239 | return ''; 240 | } 241 | 242 | if (!$parameters) { 243 | return $route->url(); 244 | } 245 | 246 | if (is_array(current($parameters))) { 247 | $parameters = current($parameters); 248 | } 249 | 250 | return $route->url($parameters); 251 | } 252 | 253 | /** 254 | * @param mixed $key 255 | * @param mixed $default 256 | * @return mixed 257 | */ 258 | function session($key = null, $default = null) 259 | { 260 | $session = request()->session(); 261 | if (null === $key) { 262 | return $session; 263 | } 264 | if (\is_array($key)) { 265 | $session->put($key); 266 | return null; 267 | } 268 | if (\strpos($key, '.')) { 269 | $key_array = \explode('.', $key); 270 | $value = $session->all(); 271 | foreach ($key_array as $index) { 272 | if (!isset($value[$index])) { 273 | return $default; 274 | } 275 | $value = $value[$index]; 276 | } 277 | return $value; 278 | } 279 | return $session->get($key, $default); 280 | } 281 | 282 | /** 283 | * @param null|string $id 284 | * @param array $parameters 285 | * @param string|null $domain 286 | * @param string|null $locale 287 | * @return string 288 | */ 289 | function trans(string $id, array $parameters = [], string $domain = null, string $locale = null) 290 | { 291 | $res = Translation::trans($id, $parameters, $domain, $locale); 292 | return $res === '' ? $id : $res; 293 | } 294 | 295 | /** 296 | * @param null|string $locale 297 | * @return string 298 | */ 299 | function locale(string $locale = null) 300 | { 301 | if (!$locale) { 302 | return Translation::getLocale(); 303 | } 304 | Translation::setLocale($locale); 305 | } 306 | 307 | /** 308 | * 404 not found 309 | * 310 | * @return Response 311 | */ 312 | function not_found() 313 | { 314 | return new Response(404, [], file_get_contents(public_path() . '/404.html')); 315 | } 316 | 317 | /** 318 | * Copy dir. 319 | * @param $source 320 | * @param $dest 321 | * @param bool $overwrite 322 | * @return void 323 | */ 324 | function copy_dir($source, $dest, $overwrite = false) 325 | { 326 | if (is_dir($source)) { 327 | if (!is_dir($dest)) { 328 | mkdir($dest); 329 | } 330 | $files = scandir($source); 331 | foreach ($files as $file) { 332 | if ($file !== "." && $file !== "..") { 333 | copy_dir("$source/$file", "$dest/$file"); 334 | } 335 | } 336 | } else if (file_exists($source) && ($overwrite || !file_exists($dest))) { 337 | copy($source, $dest); 338 | } 339 | } 340 | 341 | /** 342 | * Remove dir. 343 | * @param $dir 344 | * @return bool 345 | */ 346 | function remove_dir($dir) 347 | { 348 | if (is_link($dir) || is_file($dir)) { 349 | return unlink($dir); 350 | } 351 | $files = array_diff(scandir($dir), array('.', '..')); 352 | foreach ($files as $file) { 353 | (is_dir("$dir/$file") && !is_link($dir)) ? remove_dir("$dir/$file") : unlink("$dir/$file"); 354 | } 355 | return rmdir($dir); 356 | } 357 | 358 | /** 359 | * @param $worker 360 | * @param $class 361 | */ 362 | function worker_bind($worker, $class) 363 | { 364 | $callback_map = [ 365 | 'onConnect', 366 | 'onMessage', 367 | 'onClose', 368 | 'onError', 369 | 'onBufferFull', 370 | 'onBufferDrain', 371 | 'onWorkerStop', 372 | 'onWebSocketConnect' 373 | ]; 374 | foreach ($callback_map as $name) { 375 | if (method_exists($class, $name)) { 376 | $worker->$name = [$class, $name]; 377 | } 378 | } 379 | if (method_exists($class, 'onWorkerStart')) { 380 | call_user_func([$class, 'onWorkerStart'], $worker); 381 | } 382 | } 383 | 384 | /** 385 | * @param $process_name 386 | * @param $config 387 | * @return void 388 | */ 389 | function worker_start($process_name, $config) 390 | { 391 | $worker = new Worker($config['listen'] ?? null, $config['context'] ?? []); 392 | $property_map = [ 393 | 'count', 394 | 'user', 395 | 'group', 396 | 'reloadable', 397 | 'reusePort', 398 | 'transport', 399 | 'protocol', 400 | ]; 401 | $worker->name = $process_name; 402 | foreach ($property_map as $property) { 403 | if (isset($config[$property])) { 404 | $worker->$property = $config[$property]; 405 | } 406 | } 407 | 408 | $worker->onWorkerStart = function ($worker) use ($config) { 409 | require_once base_path() . '/support/bootstrap.php'; 410 | 411 | foreach ($config['services'] ?? [] as $server) { 412 | if (!class_exists($server['handler'])) { 413 | echo "process error: class {$server['handler']} not exists\r\n"; 414 | continue; 415 | } 416 | $listen = new Worker($server['listen'] ?? null, $server['context'] ?? []); 417 | if (isset($server['listen'])) { 418 | echo "listen: {$server['listen']}\n"; 419 | } 420 | $instance = Container::make($server['handler'], $server['constructor'] ?? []); 421 | worker_bind($listen, $instance); 422 | $listen->listen(); 423 | } 424 | 425 | if (isset($config['handler'])) { 426 | if (!class_exists($config['handler'])) { 427 | echo "process error: class {$config['handler']} not exists\r\n"; 428 | return; 429 | } 430 | 431 | $instance = Container::make($config['handler'], $config['constructor'] ?? []); 432 | worker_bind($worker, $instance); 433 | } 434 | 435 | }; 436 | } 437 | 438 | /** 439 | * Phar support. 440 | * Compatible with the 'realpath' function in the phar file. 441 | * 442 | * @param string $file_path 443 | * @return string 444 | */ 445 | function get_realpath(string $file_path): string 446 | { 447 | if (strpos($file_path, 'phar://') === 0) { 448 | return $file_path; 449 | } else { 450 | return realpath($file_path); 451 | } 452 | } 453 | 454 | /** 455 | * @return bool 456 | */ 457 | function is_phar() 458 | { 459 | return class_exists(\Phar::class, false) && Phar::running(); 460 | } 461 | 462 | /** 463 | * @return int 464 | */ 465 | function cpu_count() 466 | { 467 | // Windows does not support the number of processes setting. 468 | if (\DIRECTORY_SEPARATOR === '\\') { 469 | return 1; 470 | } 471 | $count = 4; 472 | if (is_callable('shell_exec')) { 473 | if (strtolower(PHP_OS) === 'darwin') { 474 | $count = (int)shell_exec('sysctl -n machdep.cpu.core_count'); 475 | } else { 476 | $count = (int)shell_exec('nproc'); 477 | } 478 | } 479 | return $count > 0 ? $count : 4; 480 | } 481 | 482 | function envs($key, $default = null) { 483 | static $env_config = []; 484 | if (!$env_config) { 485 | $env_config = include config_path().'/.env.php'; 486 | } 487 | 488 | return $env_config[$key]??$default; 489 | } 490 | -------------------------------------------------------------------------------- /public/static/lib/bootstrap/css/bootstrap-reboot.min.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../../scss/mixins/_banner.scss","../../scss/_root.scss","../../scss/vendor/_rfs.scss","../../scss/_reboot.scss","dist/css/bootstrap-reboot.css","../../scss/mixins/_border-radius.scss"],"names":[],"mappings":"AACE;;;;;ACDF,MAQI,UAAA,QAAA,YAAA,QAAA,YAAA,QAAA,UAAA,QAAA,SAAA,QAAA,YAAA,QAAA,YAAA,QAAA,WAAA,QAAA,UAAA,QAAA,UAAA,QAAA,WAAA,KAAA,WAAA,KAAA,UAAA,QAAA,eAAA,QAIA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAIA,aAAA,QAAA,eAAA,QAAA,aAAA,QAAA,UAAA,QAAA,aAAA,QAAA,YAAA,QAAA,WAAA,QAAA,UAAA,QAIA,iBAAA,EAAA,CAAA,GAAA,CAAA,IAAA,mBAAA,GAAA,CAAA,GAAA,CAAA,IAAA,iBAAA,EAAA,CAAA,GAAA,CAAA,GAAA,cAAA,EAAA,CAAA,GAAA,CAAA,IAAA,iBAAA,GAAA,CAAA,GAAA,CAAA,EAAA,gBAAA,GAAA,CAAA,EAAA,CAAA,GAAA,eAAA,GAAA,CAAA,GAAA,CAAA,IAAA,cAAA,EAAA,CAAA,EAAA,CAAA,GAGF,eAAA,GAAA,CAAA,GAAA,CAAA,IACA,eAAA,CAAA,CAAA,CAAA,CAAA,EACA,oBAAA,EAAA,CAAA,EAAA,CAAA,GACA,iBAAA,GAAA,CAAA,GAAA,CAAA,IAMA,qBAAA,SAAA,CAAA,aAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,WAAA,CAAA,iBAAA,CAAA,KAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBACA,oBAAA,cAAA,CAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,aAAA,CAAA,UACA,cAAA,2EAOA,sBAAA,0BC4PI,oBAAA,KD1PJ,sBAAA,IACA,sBAAA,IACA,gBAAA,QAIA,aAAA,KAIA,kBAAA,IACA,kBAAA,MACA,kBAAA,QACA,8BAAA,qBAEA,mBAAA,SACA,sBAAA,QACA,sBAAA,OACA,sBAAA,KACA,uBAAA,KACA,wBAAA,MAGA,gBAAA,QACA,sBAAA,QAEA,gBAAA,QAEA,kBAAA,QExDF,EC8DA,QADA,SD1DE,WAAA,WAeE,8CANJ,MAOM,gBAAA,QAcN,KACE,OAAA,EACA,YAAA,2BDmPI,UAAA,yBCjPJ,YAAA,2BACA,YAAA,2BACA,MAAA,qBACA,WAAA,0BACA,iBAAA,kBACA,yBAAA,KACA,4BAAA,YASF,GACE,OAAA,KAAA,EACA,MAAA,QACA,OAAA,EACA,WAAA,IAAA,MACA,QAAA,IAUF,GAAA,GAAA,GAAA,GAAA,GAAA,GACE,WAAA,EACA,cAAA,MAGA,YAAA,IACA,YAAA,IAIF,GD6MQ,UAAA,uBAlKJ,0BC3CJ,GDoNQ,UAAA,QC/MR,GDwMQ,UAAA,sBAlKJ,0BCtCJ,GD+MQ,UAAA,MC1MR,GDmMQ,UAAA,oBAlKJ,0BCjCJ,GD0MQ,UAAA,SCrMR,GD8LQ,UAAA,sBAlKJ,0BC5BJ,GDqMQ,UAAA,QChMR,GDqLM,UAAA,QChLN,GDgLM,UAAA,KCrKN,EACE,WAAA,EACA,cAAA,KAUF,YACE,wBAAA,UAAA,OAAA,gBAAA,UAAA,OACA,OAAA,KACA,iCAAA,KAAA,yBAAA,KAMF,QACE,cAAA,KACA,WAAA,OACA,YAAA,QAMF,GCqBA,GDnBE,aAAA,KCyBF,GDtBA,GCqBA,GDlBE,WAAA,EACA,cAAA,KAGF,MCsBA,MACA,MAFA,MDjBE,cAAA,EAGF,GACE,YAAA,IAKF,GACE,cAAA,MACA,YAAA,EAMF,WACE,OAAA,EAAA,EAAA,KAQF,ECWA,ODTE,YAAA,OAQF,MDmFM,UAAA,OC5EN,KACE,QAAA,QACA,iBAAA,uBASF,ICHA,IDKE,SAAA,SD+DI,UAAA,MC7DJ,YAAA,EACA,eAAA,SAGF,IAAM,OAAA,OACN,IAAM,IAAA,MAKN,EACE,MAAA,qBACA,gBAAA,UAEA,QACE,MAAA,2BAWF,2BAAA,iCAEE,MAAA,QACA,gBAAA,KCPJ,KACA,IDaA,ICZA,KDgBE,YAAA,yBDqBI,UAAA,ICbN,IACE,QAAA,MACA,WAAA,EACA,cAAA,KACA,SAAA,KDSI,UAAA,OCJJ,SDII,UAAA,QCFF,MAAA,QACA,WAAA,OAIJ,KDHM,UAAA,OCKJ,MAAA,qBACA,UAAA,WAGA,OACE,MAAA,QAIJ,IACE,QAAA,SAAA,QDfI,UAAA,OCiBJ,MAAA,kBACA,iBAAA,qBEpSE,cAAA,OFuSF,QACE,QAAA,EDtBE,UAAA,ICiCN,OACE,OAAA,EAAA,EAAA,KAMF,ICjCA,IDmCE,eAAA,OAQF,MACE,aAAA,OACA,gBAAA,SAGF,QACE,YAAA,MACA,eAAA,MACA,MAAA,QACA,WAAA,KAOF,GAEE,WAAA,QACA,WAAA,qBCxCF,MAGA,GAFA,MAGA,GDuCA,MCzCA,GD+CE,aAAA,QACA,aAAA,MACA,aAAA,EAQF,MACE,QAAA,aAMF,OAEE,cAAA,EAQF,iCACE,QAAA,ECtDF,OD2DA,MCzDA,SADA,OAEA,SD6DE,OAAA,EACA,YAAA,QDrHI,UAAA,QCuHJ,YAAA,QAIF,OC5DA,OD8DE,eAAA,KAKF,cACE,OAAA,QAGF,OAGE,UAAA,OAGA,gBACE,QAAA,EAOJ,0IACE,QAAA,eClEF,cACA,aACA,cDwEA,OAIE,mBAAA,OCxEF,6BACA,4BACA,6BDyEI,sBACE,OAAA,QAON,mBACE,QAAA,EACA,aAAA,KAKF,SACE,OAAA,SAUF,SACE,UAAA,EACA,QAAA,EACA,OAAA,EACA,OAAA,EAQF,OACE,MAAA,KACA,MAAA,KACA,QAAA,EACA,cAAA,MD1MM,UAAA,sBC6MN,YAAA,QD/WE,0BCwWJ,OD/LQ,UAAA,QCwMN,SACE,MAAA,KChFJ,kCDuFA,uCCxFA,mCADA,+BAGA,oCAJA,6BAKA,mCD4FE,QAAA,EAGF,4BACE,OAAA,KASF,cACE,eAAA,KACA,mBAAA,UAmBF,4BACE,mBAAA,KAKF,+BACE,QAAA,EAOF,6BACE,KAAA,QACA,mBAAA,OAFF,uBACE,KAAA,QACA,mBAAA,OAKF,OACE,QAAA,aAKF,OACE,OAAA,EAOF,QACE,QAAA,UACA,OAAA,QAQF,SACE,eAAA,SAQF,SACE,QAAA","sourcesContent":["@mixin bsBanner($file, $suffix:\"\") {\n /*!\n * Bootstrap #{$file} v5.2.0 (https://getbootstrap.com/)\n * Copyright 2011-2022 The Bootstrap Authors\n * Copyright 2011-2022 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n}\n\n",":root {\n // Note: Custom variable values only support SassScript inside `#{}`.\n\n // Colors\n //\n // Generate palettes for full colors, grays, and theme colors.\n\n @each $color, $value in $colors {\n --#{$prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $grays {\n --#{$prefix}gray-#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors-rgb {\n --#{$prefix}#{$color}-rgb: #{$value};\n }\n\n --#{$prefix}white-rgb: #{to-rgb($white)};\n --#{$prefix}black-rgb: #{to-rgb($black)};\n --#{$prefix}body-color-rgb: #{to-rgb($body-color)};\n --#{$prefix}body-bg-rgb: #{to-rgb($body-bg)};\n\n // Fonts\n\n // Note: Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --#{$prefix}font-sans-serif: #{inspect($font-family-sans-serif)};\n --#{$prefix}font-monospace: #{inspect($font-family-monospace)};\n --#{$prefix}gradient: #{$gradient};\n\n // Root and body\n // scss-docs-start root-body-variables\n @if $font-size-root != null {\n --#{$prefix}root-font-size: #{$font-size-root};\n }\n --#{$prefix}body-font-family: #{$font-family-base};\n @include rfs($font-size-base, --#{$prefix}body-font-size);\n --#{$prefix}body-font-weight: #{$font-weight-base};\n --#{$prefix}body-line-height: #{$line-height-base};\n --#{$prefix}body-color: #{$body-color};\n @if $body-text-align != null {\n --#{$prefix}body-text-align: #{$body-text-align};\n }\n --#{$prefix}body-bg: #{$body-bg};\n // scss-docs-end root-body-variables\n\n // scss-docs-start root-border-var\n --#{$prefix}border-width: #{$border-width};\n --#{$prefix}border-style: #{$border-style};\n --#{$prefix}border-color: #{$border-color};\n --#{$prefix}border-color-translucent: #{$border-color-translucent};\n\n --#{$prefix}border-radius: #{$border-radius};\n --#{$prefix}border-radius-sm: #{$border-radius-sm};\n --#{$prefix}border-radius-lg: #{$border-radius-lg};\n --#{$prefix}border-radius-xl: #{$border-radius-xl};\n --#{$prefix}border-radius-2xl: #{$border-radius-2xl};\n --#{$prefix}border-radius-pill: #{$border-radius-pill};\n // scss-docs-end root-border-var\n\n --#{$prefix}link-color: #{$link-color};\n --#{$prefix}link-hover-color: #{$link-hover-color};\n\n --#{$prefix}code-color: #{$code-color};\n\n --#{$prefix}highlight-bg: #{$mark-bg};\n}\n","// stylelint-disable property-blacklist, scss/dollar-variable-default\n\n// SCSS RFS mixin\n//\n// Automated responsive values for font sizes, paddings, margins and much more\n//\n// Licensed under MIT (https://github.com/twbs/rfs/blob/main/LICENSE)\n\n// Configuration\n\n// Base value\n$rfs-base-value: 1.25rem !default;\n$rfs-unit: rem !default;\n\n@if $rfs-unit != rem and $rfs-unit != px {\n @error \"`#{$rfs-unit}` is not a valid unit for $rfs-unit. Use `px` or `rem`.\";\n}\n\n// Breakpoint at where values start decreasing if screen width is smaller\n$rfs-breakpoint: 1200px !default;\n$rfs-breakpoint-unit: px !default;\n\n@if $rfs-breakpoint-unit != px and $rfs-breakpoint-unit != em and $rfs-breakpoint-unit != rem {\n @error \"`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.\";\n}\n\n// Resize values based on screen height and width\n$rfs-two-dimensional: false !default;\n\n// Factor of decrease\n$rfs-factor: 10 !default;\n\n@if type-of($rfs-factor) != number or $rfs-factor <= 1 {\n @error \"`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.\";\n}\n\n// Mode. Possibilities: \"min-media-query\", \"max-media-query\"\n$rfs-mode: min-media-query !default;\n\n// Generate enable or disable classes. Possibilities: false, \"enable\" or \"disable\"\n$rfs-class: false !default;\n\n// 1 rem = $rfs-rem-value px\n$rfs-rem-value: 16 !default;\n\n// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14\n$rfs-safari-iframe-resize-bug-fix: false !default;\n\n// Disable RFS by setting $enable-rfs to false\n$enable-rfs: true !default;\n\n// Cache $rfs-base-value unit\n$rfs-base-value-unit: unit($rfs-base-value);\n\n@function divide($dividend, $divisor, $precision: 10) {\n $sign: if($dividend > 0 and $divisor > 0 or $dividend < 0 and $divisor < 0, 1, -1);\n $dividend: abs($dividend);\n $divisor: abs($divisor);\n @if $dividend == 0 {\n @return 0;\n }\n @if $divisor == 0 {\n @error \"Cannot divide by 0\";\n }\n $remainder: $dividend;\n $result: 0;\n $factor: 10;\n @while ($remainder > 0 and $precision >= 0) {\n $quotient: 0;\n @while ($remainder >= $divisor) {\n $remainder: $remainder - $divisor;\n $quotient: $quotient + 1;\n }\n $result: $result * 10 + $quotient;\n $factor: $factor * .1;\n $remainder: $remainder * 10;\n $precision: $precision - 1;\n @if ($precision < 0 and $remainder >= $divisor * 5) {\n $result: $result + 1;\n }\n }\n $result: $result * $factor * $sign;\n $dividend-unit: unit($dividend);\n $divisor-unit: unit($divisor);\n $unit-map: (\n \"px\": 1px,\n \"rem\": 1rem,\n \"em\": 1em,\n \"%\": 1%\n );\n @if ($dividend-unit != $divisor-unit and map-has-key($unit-map, $dividend-unit)) {\n $result: $result * map-get($unit-map, $dividend-unit);\n }\n @return $result;\n}\n\n// Remove px-unit from $rfs-base-value for calculations\n@if $rfs-base-value-unit == px {\n $rfs-base-value: divide($rfs-base-value, $rfs-base-value * 0 + 1);\n}\n@else if $rfs-base-value-unit == rem {\n $rfs-base-value: divide($rfs-base-value, divide($rfs-base-value * 0 + 1, $rfs-rem-value));\n}\n\n// Cache $rfs-breakpoint unit to prevent multiple calls\n$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);\n\n// Remove unit from $rfs-breakpoint for calculations\n@if $rfs-breakpoint-unit-cache == px {\n $rfs-breakpoint: divide($rfs-breakpoint, $rfs-breakpoint * 0 + 1);\n}\n@else if $rfs-breakpoint-unit-cache == rem or $rfs-breakpoint-unit-cache == \"em\" {\n $rfs-breakpoint: divide($rfs-breakpoint, divide($rfs-breakpoint * 0 + 1, $rfs-rem-value));\n}\n\n// Calculate the media query value\n$rfs-mq-value: if($rfs-breakpoint-unit == px, #{$rfs-breakpoint}px, #{divide($rfs-breakpoint, $rfs-rem-value)}#{$rfs-breakpoint-unit});\n$rfs-mq-property-width: if($rfs-mode == max-media-query, max-width, min-width);\n$rfs-mq-property-height: if($rfs-mode == max-media-query, max-height, min-height);\n\n// Internal mixin used to determine which media query needs to be used\n@mixin _rfs-media-query {\n @if $rfs-two-dimensional {\n @if $rfs-mode == max-media-query {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}), (#{$rfs-mq-property-height}: #{$rfs-mq-value}) {\n @content;\n }\n }\n @else {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) and (#{$rfs-mq-property-height}: #{$rfs-mq-value}) {\n @content;\n }\n }\n }\n @else {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) {\n @content;\n }\n }\n}\n\n// Internal mixin that adds disable classes to the selector if needed.\n@mixin _rfs-rule {\n @if $rfs-class == disable and $rfs-mode == max-media-query {\n // Adding an extra class increases specificity, which prevents the media query to override the property\n &,\n .disable-rfs &,\n &.disable-rfs {\n @content;\n }\n }\n @else if $rfs-class == enable and $rfs-mode == min-media-query {\n .enable-rfs &,\n &.enable-rfs {\n @content;\n }\n }\n @else {\n @content;\n }\n}\n\n// Internal mixin that adds enable classes to the selector if needed.\n@mixin _rfs-media-query-rule {\n\n @if $rfs-class == enable {\n @if $rfs-mode == min-media-query {\n @content;\n }\n\n @include _rfs-media-query {\n .enable-rfs &,\n &.enable-rfs {\n @content;\n }\n }\n }\n @else {\n @if $rfs-class == disable and $rfs-mode == min-media-query {\n .disable-rfs &,\n &.disable-rfs {\n @content;\n }\n }\n @include _rfs-media-query {\n @content;\n }\n }\n}\n\n// Helper function to get the formatted non-responsive value\n@function rfs-value($values) {\n // Convert to list\n $values: if(type-of($values) != list, ($values,), $values);\n\n $val: '';\n\n // Loop over each value and calculate value\n @each $value in $values {\n @if $value == 0 {\n $val: $val + ' 0';\n }\n @else {\n // Cache $value unit\n $unit: if(type-of($value) == \"number\", unit($value), false);\n\n @if $unit == px {\n // Convert to rem if needed\n $val: $val + ' ' + if($rfs-unit == rem, #{divide($value, $value * 0 + $rfs-rem-value)}rem, $value);\n }\n @else if $unit == rem {\n // Convert to px if needed\n $val: $val + ' ' + if($rfs-unit == px, #{divide($value, $value * 0 + 1) * $rfs-rem-value}px, $value);\n }\n @else {\n // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n $val: $val + ' ' + $value;\n }\n }\n }\n\n // Remove first space\n @return unquote(str-slice($val, 2));\n}\n\n// Helper function to get the responsive value calculated by RFS\n@function rfs-fluid-value($values) {\n // Convert to list\n $values: if(type-of($values) != list, ($values,), $values);\n\n $val: '';\n\n // Loop over each value and calculate value\n @each $value in $values {\n @if $value == 0 {\n $val: $val + ' 0';\n }\n\n @else {\n // Cache $value unit\n $unit: if(type-of($value) == \"number\", unit($value), false);\n\n // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n @if not $unit or $unit != px and $unit != rem {\n $val: $val + ' ' + $value;\n }\n\n @else {\n // Remove unit from $value for calculations\n $value: divide($value, $value * 0 + if($unit == px, 1, divide(1, $rfs-rem-value)));\n\n // Only add the media query if the value is greater than the minimum value\n @if abs($value) <= $rfs-base-value or not $enable-rfs {\n $val: $val + ' ' + if($rfs-unit == rem, #{divide($value, $rfs-rem-value)}rem, #{$value}px);\n }\n @else {\n // Calculate the minimum value\n $value-min: $rfs-base-value + divide(abs($value) - $rfs-base-value, $rfs-factor);\n\n // Calculate difference between $value and the minimum value\n $value-diff: abs($value) - $value-min;\n\n // Base value formatting\n $min-width: if($rfs-unit == rem, #{divide($value-min, $rfs-rem-value)}rem, #{$value-min}px);\n\n // Use negative value if needed\n $min-width: if($value < 0, -$min-width, $min-width);\n\n // Use `vmin` if two-dimensional is enabled\n $variable-unit: if($rfs-two-dimensional, vmin, vw);\n\n // Calculate the variable width between 0 and $rfs-breakpoint\n $variable-width: #{divide($value-diff * 100, $rfs-breakpoint)}#{$variable-unit};\n\n // Return the calculated value\n $val: $val + ' calc(' + $min-width + if($value < 0, ' - ', ' + ') + $variable-width + ')';\n }\n }\n }\n }\n\n // Remove first space\n @return unquote(str-slice($val, 2));\n}\n\n// RFS mixin\n@mixin rfs($values, $property: font-size) {\n @if $values != null {\n $val: rfs-value($values);\n $fluidVal: rfs-fluid-value($values);\n\n // Do not print the media query if responsive & non-responsive values are the same\n @if $val == $fluidVal {\n #{$property}: $val;\n }\n @else {\n @include _rfs-rule {\n #{$property}: if($rfs-mode == max-media-query, $val, $fluidVal);\n\n // Include safari iframe resize fix if needed\n min-width: if($rfs-safari-iframe-resize-bug-fix, (0 * 1vw), null);\n }\n\n @include _rfs-media-query-rule {\n #{$property}: if($rfs-mode == max-media-query, $fluidVal, $val);\n }\n }\n }\n}\n\n// Shorthand helper mixins\n@mixin font-size($value) {\n @include rfs($value);\n}\n\n@mixin padding($value) {\n @include rfs($value, padding);\n}\n\n@mixin padding-top($value) {\n @include rfs($value, padding-top);\n}\n\n@mixin padding-right($value) {\n @include rfs($value, padding-right);\n}\n\n@mixin padding-bottom($value) {\n @include rfs($value, padding-bottom);\n}\n\n@mixin padding-left($value) {\n @include rfs($value, padding-left);\n}\n\n@mixin margin($value) {\n @include rfs($value, margin);\n}\n\n@mixin margin-top($value) {\n @include rfs($value, margin-top);\n}\n\n@mixin margin-right($value) {\n @include rfs($value, margin-right);\n}\n\n@mixin margin-bottom($value) {\n @include rfs($value, margin-bottom);\n}\n\n@mixin margin-left($value) {\n @include rfs($value, margin-left);\n}\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n @if $font-size-root != null {\n @include font-size(var(--#{$prefix}root-font-size));\n }\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\n// scss-docs-start reboot-body-rules\nbody {\n margin: 0; // 1\n font-family: var(--#{$prefix}body-font-family);\n @include font-size(var(--#{$prefix}body-font-size));\n font-weight: var(--#{$prefix}body-font-weight);\n line-height: var(--#{$prefix}body-line-height);\n color: var(--#{$prefix}body-color);\n text-align: var(--#{$prefix}body-text-align);\n background-color: var(--#{$prefix}body-bg); // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n// scss-docs-end reboot-body-rules\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n border: 0;\n border-top: $hr-border-width solid $hr-border-color;\n opacity: $hr-opacity;\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 2. Add explicit cursor to indicate changed behavior.\n// 3. Prevent the text-decoration to be skipped.\n\nabbr[title] {\n text-decoration: underline dotted; // 1\n cursor: help; // 2\n text-decoration-skip-ink: none; // 3\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n background-color: var(--#{$prefix}highlight-bg);\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: var(--#{$prefix}link-color);\n text-decoration: $link-decoration;\n\n &:hover {\n color: var(--#{$prefix}link-hover-color);\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: var(--#{$prefix}code-color);\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`