| ';
18 |
19 | return strip_tags($content, $allowed_tags);
20 | }
21 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/app/model/SysConfig.php:
--------------------------------------------------------------------------------
1 | 'array', // 将 JSON 字段转换为 PHP 数组
10 | ];
11 | protected $guarded = ['id'];
12 | /**
13 | * The table associated with the model.
14 | *
15 | * @var string
16 | */
17 | protected $table = 't_sys_config';
18 |
19 | /**
20 | * The primary key associated with the table.
21 | *
22 | * @var string
23 | */
24 | protected $primaryKey = 'id';
25 |
26 | /**
27 | * Indicates if the model should be timestamped.
28 | *
29 | * @var bool
30 | */
31 | public $timestamps = true;
32 |
33 | }
--------------------------------------------------------------------------------
/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' => '127.0.0.1',
18 | 'password' => null,
19 | 'port' => 6379,
20 | 'database' => 0,
21 | ],
22 | ];
23 |
--------------------------------------------------------------------------------
/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/model/Like.php:
--------------------------------------------------------------------------------
1 | belongsTo('app\model\User','user_id','id');
32 | }
33 | public function memo() {
34 | return $this->belongsTo('app\model\Memo','memo_id','id');
35 | }
36 | }
--------------------------------------------------------------------------------
/app/model/Comment.php:
--------------------------------------------------------------------------------
1 | belongsTo('app\model\User','user_id','id');
32 | }
33 |
34 | public function memo() {
35 | return $this->belongsTo('app\model\Memo','memo_id','id');
36 | }
37 | }
--------------------------------------------------------------------------------
/app/controller/UploadController.php:
--------------------------------------------------------------------------------
1 | file($filePath);
29 | } else {
30 | return response('not found');
31 | }
32 | }
33 |
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/app/model/User.php:
--------------------------------------------------------------------------------
1 | 'array', // 将 JSON 字段转换为 PHP 数组
14 | ];
15 | /**
16 | * The table associated with the model.
17 | *
18 | * @var string
19 | */
20 | protected $table = 't_users';
21 |
22 | /**
23 | * The primary key associated with the table.
24 | *
25 | * @var string
26 | */
27 | protected $primaryKey = 'id';
28 |
29 | /**
30 | * Indicates if the model should be timestamped.
31 | *
32 | * @var bool
33 | */
34 | public $timestamps = true;
35 |
36 | public function memos() {
37 | return $this->hasMany('app\model\Memo','user_id','id');
38 | }
39 | }
--------------------------------------------------------------------------------
/public/icons/hot.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 | ];
--------------------------------------------------------------------------------
/app/middleware/Auth.php:
--------------------------------------------------------------------------------
1 | path());
17 | if (session('user')) {
18 | View::assign('currentUser', session('user'));
19 | }
20 |
21 | $controller = new ReflectionClass($request->controller);
22 | $noNeedLogin = $controller->getDefaultProperties()['noNeedLogin'] ?? [];
23 |
24 | if (!in_array($request->action, $noNeedLogin) && !session('user')) {
25 | Log::info("redirect");
26 | return redirect('/user/login');
27 | }
28 |
29 | return $handler($request);
30 | }
31 | }
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | db:
5 | image: postgres:14-alpine
6 | restart: always
7 | environment:
8 | POSTGRES_USER: m-moments
9 | POSTGRES_PASSWORD: m-moments
10 | POSTGRES_DB: m-moments
11 | networks:
12 | - moments_network
13 | volumes:
14 | - ./data:/var/lib/postgresql/data/
15 | - ./sql/schema.sql:/docker-entrypoint-initdb.d/init.sql
16 | web:
17 | image: kingwrcy/m-moments:latest
18 | restart: always
19 | environment:
20 | - DB_HOST=db
21 | - DB_PORT=5432
22 | - DB_NAME=m-moments
23 | - DB_USER=m-moments
24 | - DB_PASSWORD=m-moments
25 | networks:
26 | - moments_network
27 | ports:
28 | - '8787:8787'
29 | depends_on:
30 | - db
31 | volumes:
32 | - ./upload:/app/upload
33 |
34 | networks:
35 | moments_network:
36 |
--------------------------------------------------------------------------------
/app/model/Memo.php:
--------------------------------------------------------------------------------
1 | belongsTo('app\model\User','user_id','id');
32 | }
33 |
34 |
35 | public function comments() {
36 | return $this->hasMany('app\model\Comment','memo_id','id');
37 | }
38 |
39 | public function likes() {
40 | return $this->hasMany('app\model\Like','memo_id','id');
41 | }
42 | }
--------------------------------------------------------------------------------
/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' => true,
19 | 'error_reporting' => E_ALL,
20 | 'default_timezone' => 'Asia/Shanghai',
21 | 'request_class' => Request::class,
22 | 'public_path' => base_path() . DIRECTORY_SEPARATOR . 'public',
23 | 'runtime_path' => base_path(false) . DIRECTORY_SEPARATOR . 'runtime',
24 | 'controller_suffix' => 'Controller',
25 | 'controller_reuse' => false,
26 | ];
27 |
--------------------------------------------------------------------------------
/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 |
15 | return [
16 | 'default' => 'pgsql',
17 | 'connections' => [
18 | 'pgsql' => [
19 | 'driver' => 'pgsql',
20 | 'host' => getenv('DB_HOST'),
21 | 'port' => getenv('DB_PORT'),
22 | 'database' => getenv('DB_NAME'),
23 | 'username' => getenv('DB_USER'),
24 | 'password' => getenv('DB_PASSWORD'),
25 | 'charset' => 'utf8',
26 | 'prefix' => '',
27 | 'schema' => 'public',
28 | 'sslmode' => 'prefer',
29 | ],
30 | ]
31 |
32 | ];
33 |
--------------------------------------------------------------------------------
/app/functions.php:
--------------------------------------------------------------------------------
1 | diff($inputTime);
19 |
20 | // 获取差值的总秒数
21 | $seconds = $interval->s;
22 | $minutes = $interval->i;
23 | $hours = $interval->h;
24 | $days = $interval->d;
25 | $months = $interval->m;
26 | $years = $interval->y;
27 |
28 | // 返回相对时间字符串
29 | if ($interval->y > 0) {
30 | return $years . '年前';
31 | } elseif ($interval->m > 0) {
32 | return $months . '月前';
33 | } elseif ($interval->d > 0) {
34 | return $days . '天前';
35 | } elseif ($interval->h > 0) {
36 | return $hours . '小时前';
37 | } elseif ($interval->i > 0) {
38 | return $minutes . '分钟前';
39 | } elseif ($interval->s > 10) {
40 | return $seconds . '秒前';
41 | } else {
42 | return '刚刚';
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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' => 'http://0.0.0.0:8787',
17 | 'transport' => 'tcp',
18 | 'context' => [],
19 | 'name' => 'webman',
20 | 'count' => cpu_count() * 4,
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 |
--------------------------------------------------------------------------------
/public/css/main.css:
--------------------------------------------------------------------------------
1 | * {
2 | font-family: Helvetica, Tahoma, Arial, "Microsoft YaHei", "Hiragino Sans GB", "WenQuanYi Micro Hei", sans-serif !important;
3 | }
4 |
5 | :root {
6 | --bulma-body-font-size: 14px;
7 | }
8 |
9 | html, body {
10 | min-height: 100%;
11 | margin: 0;
12 | padding: 0;
13 | }
14 |
15 | button {
16 | margin: 0;
17 | padding: 0;
18 | border: none;
19 | outline: none;
20 | background-color: transparent;
21 | color:white;
22 | }
23 |
24 | [un-cloak] {
25 | display: none;
26 | }
27 |
28 | a {
29 | text-decoration: none !important;
30 | }
31 |
32 | .markdown-body {
33 | font-size: 14px !important;
34 | }
35 |
36 | .markdown-body img {
37 | max-height: 300px;
38 | border-radius: 5px;
39 | cursor: pointer;
40 | }
41 |
42 | .markdown-body pre {
43 | border-radius: 5px !important;
44 | margin: 10px 0 !important;
45 | }
46 |
47 |
48 | .markdown-body p > code,
49 | .markdown-body li > code {
50 | background-color: #64748b;
51 | color: white;
52 | padding: 2px 6px;
53 | border: 1px solid #ccc;
54 | border-radius: 4px;
55 | margin: 0 2px;
56 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # 用于将项目构建成镜像
2 | ARG WEBMAN_DOCKER_VERSION=8.2-cli-alpine
3 |
4 | # https://github.com/krissss/docker-webman
5 | FROM krisss/docker-webman:$WEBMAN_DOCKER_VERSION
6 |
7 | # 增加额外的扩展
8 | #RUN apk add --no-cache git
9 | RUN install-php-extensions pgsql pdo_pgsql
10 |
11 | # 设置配置文件
12 | # 自定义 php 配置文件,如果需要的话
13 | # 覆盖镜像自带的
14 | #COPY environments/docker/php.ini "$PHP_INI_DIR/conf.d/app.ini"
15 | # 扩展额外的
16 | #COPY environments/docker/my_php.ini "$PHP_INI_DIR/conf.d/my_php.ini"
17 | # 自定义 supervisor 配置,如果需要的话
18 | # 覆盖镜像自带的
19 | #COPY environments/docker/supervisord.conf /etc/supervisor/supervisord.conf
20 | # 扩展额外的
21 | #COPY environments/docker/my_supervisord.conf /etc/supervisor/conf.d/my_supervisord.conf
22 |
23 | # 预先加载 Composer 包依赖,优化 Docker 构建镜像的速度
24 | COPY ./composer.json /app/
25 | COPY ./composer.lock /app/
26 | RUN composer install --no-interaction --no-dev --no-autoloader --no-scripts
27 |
28 | # 复制项目代码
29 | COPY . /app
30 |
31 | ENV UPLOAD_DIR /app/upload
32 | ENV DEFAULT_USER_AVATAR_CDN https://gravatar.cooluc.com/avatar/
33 |
34 | # 执行 Composer 自动加载和相关脚本
35 | RUN composer install --no-interaction --no-dev && composer dump-autoload
36 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/view/default/index.Twig:
--------------------------------------------------------------------------------
1 | {% extends 'default/layout/base.Twig' %}
2 |
3 | {% block title %} {{sysConfig.website_title}} {% endblock %}
4 | {% block content %}
5 |
6 |
7 | {% for memo in memos %}
8 | {% include 'default/component/memo.Twig' %}
9 | {% endfor %}
10 |
11 |
12 | {% endblock %}
13 |
14 | {% block afterScript %}
15 |
16 |
20 |
22 |
23 |
36 | {% endblock %}
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 极简微博 - 多人版的[极简朋友圈](https://m.mblog.club)
2 |
3 | 目前功能比较简单
4 |
5 | 1. 支持markdown语法
6 | 2. 支持点赞/评论
7 | 3. 支持注册用户
8 |
9 | ## 部署
10 |
11 | [镜像地址](https://hub.docker.com/repository/docker/kingwrcy/m-moments/)
12 |
13 | 1. 新增postgres数据库`m-moments`.
14 | 2. 修改根目录下的`docker-compose.yml`文件里的数据库部分,然后使用`docker-compose up -d`一键启动.
15 | 3. docker容器里的`/app/upload`目录则是上传的图片目录,需要映射出来,目前只支持上传头像,发言里的图片暂不支持上传.
16 |
17 | ## 本地开发
18 |
19 | 1. 克隆本项目到本地.
20 | 2. 提前安装好php和postgres环境,注意php需要安装`pgsql`和`pdo_pgsql`扩展.
21 | 3. 执行`composer install`安装依赖.
22 | 4. 新建`.env`文件,内容如下:
23 |
24 | ```shell
25 | DB_HOST=postgres
26 | DB_PORT=5432
27 | DB_NAME=m-moments
28 | DB_USER=postgres
29 | DB_PASSWORD=postgres
30 | #图片上传目录
31 | UPLOAD_DIR=/opt/moments/upload
32 | DEFAULT_USER_AVATAR_CDN=https://gravatar.cooluc.com/avatar/
33 | ```
34 |
35 | 5. 执行`php start.php start`启动服务
36 |
37 | ## 支持的环境变量
38 |
39 | | 变量名 | 说明 | 默认值 |
40 | |-------------------------|---------------|----------------------------------------------|
41 | | DEFAULT_USER_AVATAR_CDN | 用户头像的avatar镜像 | https://gravatar.cooluc.com/avatar/ |
42 | | STATIC_ASSET_CDN | 静态资源cdn前缀 | 无,读取本地,使用的话需要把项目根目录的`public`文件夹底下的所有文件上传到CDN |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/view/default/user/login.Twig:
--------------------------------------------------------------------------------
1 | {% extends 'default/layout/base.Twig' %}
2 | {% block title %} 用户登录 {% endblock %}
3 |
4 | {% block content %}
5 |
6 |
43 |
44 | {% endblock %}
--------------------------------------------------------------------------------
/public/css/toastify.min.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Minified by jsDelivr using clean-css v5.3.0.
3 | * Original file: /npm/toastify-js@1.12.0/src/toastify.css
4 | *
5 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
6 | */
7 | /*!
8 | * Toastify js 1.12.0
9 | * https://github.com/apvarun/toastify-js
10 | * @license MIT licensed
11 | *
12 | * Copyright (C) 2018 Varun A P
13 | */
14 | .toastify{padding:12px 20px;color:#fff;display:inline-block;box-shadow:0 3px 6px -1px rgba(0,0,0,.12),0 10px 36px -4px rgba(77,96,232,.3);background:-webkit-linear-gradient(315deg,#73a5ff,#5477f5);background:linear-gradient(135deg,#73a5ff,#5477f5);position:fixed;opacity:0;transition:all .4s cubic-bezier(.215, .61, .355, 1);border-radius:2px;cursor:pointer;text-decoration:none;max-width:calc(50% - 20px);z-index:2147483647}.toastify.on{opacity:1}.toast-close{background:0 0;border:0;color:#fff;cursor:pointer;font-family:inherit;font-size:1em;opacity:.4;padding:0 5px}.toastify-right{right:15px}.toastify-left{left:15px}.toastify-top{top:-150px}.toastify-bottom{bottom:-150px}.toastify-rounded{border-radius:25px}.toastify-avatar{width:1.5em;height:1.5em;margin:-7px 5px;border-radius:2px}.toastify-center{margin-left:auto;margin-right:auto;left:0;right:0;max-width:fit-content;max-width:-moz-fit-content}@media only screen and (max-width:360px){.toastify-left,.toastify-right{margin-left:auto;margin-right:auto;left:0;right:0;max-width:fit-content}}
15 | /*# sourceMappingURL=/sm/cb4335d1b03e933ed85cb59fffa60cf51f07567ed09831438c60f59afd166464.map */
--------------------------------------------------------------------------------
/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 | global $argv;
16 |
17 | return [
18 | // File update detection and automatic reload
19 | 'monitor' => [
20 | 'handler' => process\Monitor::class,
21 | 'reloadable' => false,
22 | 'constructor' => [
23 | // Monitor these directories
24 | 'monitorDir' => array_merge([
25 | app_path(),
26 | config_path(),
27 | base_path() . '/process',
28 | base_path() . '/support',
29 | base_path() . '/resource',
30 | base_path() . '/.env',
31 | ], glob(base_path() . '/plugin/*/app'), glob(base_path() . '/plugin/*/config'), glob(base_path() . '/plugin/*/api')),
32 | // Files with these suffixes will be monitored
33 | 'monitorExtensions' => [
34 | 'php', 'html', 'htm', 'env','Twig'
35 | ],
36 | 'options' => [
37 | 'enable_file_monitor' => !in_array('-d', $argv) && DIRECTORY_SEPARATOR === '/',
38 | 'enable_memory_monitor' => DIRECTORY_SEPARATOR === '/',
39 | ]
40 | ]
41 | ]
42 | ];
43 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: deploy Docker image
2 | on:
3 | push:
4 | tags:
5 | - 'v*'
6 | jobs:
7 | build:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v4
11 | - name: Extract Version
12 | id: version_step
13 | run: |
14 | echo "##[set-output name=version;]VERSION=${GITHUB_REF#$"refs/tags/v"}"
15 | echo "##[set-output name=version_tag;]kingwrcy/m-moments:${GITHUB_REF#$"refs/tags/v"}"
16 | echo "##[set-output name=latest_tag;]kingwrcy/m-moments:latest"
17 | - name: Set up QEMU
18 | uses: docker/setup-qemu-action@v3
19 | - name: Set up Docker Buildx
20 | uses: docker/setup-buildx-action@v3
21 |
22 | - name: Login to DockerHub
23 | uses: docker/login-action@v3
24 | with:
25 | username: ${{ secrets.DOCKER_USER_NAME }}
26 | password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
27 |
28 | - name: PrepareReg Names
29 | id: read-docker-image-identifiers
30 | run: |
31 | echo VERSION_TAG=$(echo ${{ steps.version_step.outputs.version_tag }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
32 | echo LASTEST_TAG=$(echo ${{ steps.version_step.outputs.latest_tag }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
33 |
34 | - name: Build and push Docker images
35 | id: docker_build
36 | uses: docker/build-push-action@v5
37 | with:
38 | push: true
39 | context: .
40 | tags: |
41 | ${{env.VERSION_TAG}}
42 | ${{env.LASTEST_TAG}}
43 | build-args: |
44 | ${{steps.version_step.outputs.version}}
45 |
--------------------------------------------------------------------------------
/app/view/default/layout/base.Twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {% block title %} {% endblock %}
7 |
8 |
9 |
10 |
11 |
12 | {% include 'default/layout/header.Twig' %}
13 |
14 |
15 | {% block beforeScript %} {% endblock %}
16 | {% if currentUser and currentUser.config.css %}
17 |
20 | {% endif %}
21 |
22 |
23 |
24 | {% if currentUser and currentUser.config.selfHomePage == "on" and path == '/' %}
25 | {% else %}
26 | {% include 'default/layout/nav.Twig' %}
27 | {% endif %}
28 |
29 |
30 | {% block content %} {% endblock %}
31 |
32 | {% include 'default/layout/footer.Twig' %}
33 |
34 |
35 | {% block afterScript %} {% endblock %}
36 | {% if currentUser and currentUser.config.js %}
37 |
40 | {% endif %}
41 |
42 |
--------------------------------------------------------------------------------
/app/view/default/user/mine.Twig:
--------------------------------------------------------------------------------
1 | {% extends 'default/layout/base.Twig' %}
2 | {% block title %} {{ user.nickname }}-个人主页 {% endblock %}
3 |
4 | {% block content %}
5 |
18 |
19 |
20 | {% for memo in memos %}
21 | {% include 'default/component/memo.Twig' %}
22 | {% endfor %}
23 |
24 |
25 | {% endblock %}
26 |
27 | {% block afterScript %}
28 |
29 |
33 |
35 |
48 |
49 | {% endblock %}
--------------------------------------------------------------------------------
/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 | use Webman\Session\FileSessionHandler;
16 | use Webman\Session\RedisSessionHandler;
17 | use Webman\Session\RedisClusterSessionHandler;
18 |
19 | return [
20 |
21 | 'type' => 'file', // or redis or redis_cluster
22 |
23 | 'handler' => FileSessionHandler::class,
24 |
25 | 'config' => [
26 | 'file' => [
27 | 'save_path' => runtime_path() . '/sessions',
28 | ],
29 | 'redis' => [
30 | 'host' => '127.0.0.1',
31 | 'port' => 6379,
32 | 'auth' => '',
33 | 'timeout' => 2,
34 | 'database' => '',
35 | 'prefix' => 'redis_session_',
36 | ],
37 | 'redis_cluster' => [
38 | 'host' => ['127.0.0.1:7000', '127.0.0.1:7001', '127.0.0.1:7001'],
39 | 'timeout' => 2,
40 | 'auth' => '',
41 | 'prefix' => 'redis_session_',
42 | ]
43 | ],
44 |
45 | 'session_name' => 'PHPSID',
46 |
47 | 'auto_update_timestamp' => false,
48 |
49 | 'lifetime' => 7*24*60*60,
50 |
51 | 'cookie_lifetime' => 365*24*60*60,
52 |
53 | 'cookie_path' => '/',
54 |
55 | 'domain' => '',
56 |
57 | 'http_only' => true,
58 |
59 | 'secure' => false,
60 |
61 | 'same_site' => '',
62 |
63 | 'gc_probability' => [1, 1000],
64 |
65 | ];
66 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "workerman/webman",
3 | "type": "project",
4 | "keywords": [
5 | "high performance",
6 | "http service"
7 | ],
8 | "homepage": "https://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": "https://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": "https://wenda.workerman.net/",
23 | "wiki": "https://workerman.net/doc/webman",
24 | "source": "https://github.com/walkor/webman"
25 | },
26 | "require": {
27 | "php": ">=7.2",
28 | "workerman/webman-framework": "^1.5.0",
29 | "monolog/monolog": "^2.0",
30 | "illuminate/database": "^11.10",
31 | "illuminate/pagination": "^11.11",
32 | "illuminate/events": "^11.11",
33 | "symfony/var-dumper": "^7.1",
34 | "twig/twig": "^3.10",
35 | "workerman/validation": "^3.1",
36 | "vlucas/phpdotenv": "^5.5",
37 | "ext-fileinfo": "*",
38 | "webman-tech/docker": "^2.3",
39 | "robmorgan/phinx": "^0.16.1"
40 | },
41 | "suggest": {
42 | "ext-event": "For better performance. "
43 | },
44 | "autoload": {
45 | "psr-4": {
46 | "": "./",
47 | "app\\": "./app",
48 | "App\\": "./app",
49 | "app\\View\\Components\\": "./app/view/components"
50 | },
51 | "files": [
52 | "./support/helpers.php"
53 | ]
54 | },
55 | "scripts": {
56 | "post-package-install": [
57 | "support\\Plugin::install"
58 | ],
59 | "post-package-update": [
60 | "support\\Plugin::install"
61 | ],
62 | "pre-package-uninstall": [
63 | "support\\Plugin::uninstall"
64 | ]
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/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 app\controller\IndexController;
16 | use app\controller\MemoController;
17 | use app\controller\UploadController;
18 | use app\controller\UserController;
19 | use Webman\Route;
20 |
21 | Route::get('/hot', [IndexController::class, 'hot']);
22 | Route::get('/square', [IndexController::class, 'square']);
23 | Route::get('/settings', [IndexController::class, 'settings']);
24 | Route::post('/saveSettings', [IndexController::class, 'saveSettings']);
25 |
26 | Route::get('/user/login', [UserController::class, 'login']);
27 | Route::post('/user/doLogin', [UserController::class, 'doLogin']);
28 | Route::post('/user/doReg', [UserController::class, 'doReg']);
29 | Route::get('/user/reg', [UserController::class, 'reg']);
30 | Route::get('/user/logout', [UserController::class, 'logout']);
31 | Route::get('/user/settings', [UserController::class, 'settings']);
32 | Route::post('/user/saveSettings', [UserController::class, 'saveSettings']);
33 | Route::get('/user/{username}', [UserController::class, 'specifyUser']);
34 |
35 | Route::get('/upload/[{file:.+}]', [UploadController::class, 'file']);
36 |
37 |
38 |
39 | Route::get('/memo/add', [MemoController::class, 'add']);
40 | Route::post('/memo/save', [MemoController::class, 'save']);
41 | Route::get('/memo/edit/{id}', [MemoController::class, 'toEdit']);
42 | Route::get('/memo/remove/{id}', [MemoController::class, 'remove']);
43 | Route::post('/memo/like/{id}', [MemoController::class, 'like']);
44 | Route::post('/memo/comment/{id}', [MemoController::class, 'comment']);
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/app/view/default/user/reg.Twig:
--------------------------------------------------------------------------------
1 | {% extends 'default/layout/base.Twig' %}
2 | {% block title %} 注册用户 {% endblock %}
3 | {% block content %}
4 | {% if sysConfig.config.disable_register != "on" %}
5 |
65 | {% else %}
66 | 不开放注册!
67 | {% endif %}
68 | {% endblock %}
--------------------------------------------------------------------------------
/sql/schema.sql:
--------------------------------------------------------------------------------
1 | -- t_comments definition
2 |
3 | -- Drop table
4 |
5 | -- DROP TABLE t_comments;
6 |
7 | CREATE TABLE t_comments
8 | (
9 | id serial4 NOT NULL,
10 | username varchar NULL,
11 | user_id int4 NULL,
12 | website varchar NULL,
13 | email varchar NULL,
14 | "content" varchar NOT NULL,
15 | created_at timestamp NULL,
16 | updated_at timestamp NULL,
17 | memo_id int4 NOT NULL,
18 | reply_user_id int4 NULL,
19 | reply_username varchar NULL,
20 | CONSTRAINT t_comments_pk PRIMARY KEY (id)
21 | );
22 |
23 |
24 | -- t_likes definition
25 |
26 | -- Drop table
27 |
28 | -- DROP TABLE t_likes;
29 |
30 | CREATE TABLE t_likes
31 | (
32 | id serial4 NOT NULL,
33 | user_id int4 NOT NULL,
34 | memo_id int4 NOT NULL,
35 | created_at timestamp NULL,
36 | updated_at timestamp NULL,
37 | CONSTRAINT t_likes_pk PRIMARY KEY (id),
38 | CONSTRAINT t_likes_pk_2 UNIQUE (user_id, memo_id)
39 | );
40 |
41 |
42 | -- t_memos definition
43 |
44 | -- Drop table
45 |
46 | -- DROP TABLE t_memos;
47 |
48 | CREATE TABLE t_memos
49 | (
50 | id serial4 NOT NULL,
51 | content_html varchar NULL,
52 | imgs _text NULL,
53 | fav_count int4 DEFAULT 0 NULL,
54 | comment_count int4 DEFAULT 0 NULL,
55 | user_id int4 NULL,
56 | pinned bool NULL,
57 | "permission" int4 NULL,
58 | ext jsonb NULL,
59 | created_at timestamp NULL,
60 | updated_at timestamp NULL,
61 | tags _text NULL,
62 | content_md varchar NULL,
63 | CONSTRAINT t_memos_pk PRIMARY KEY (id)
64 | );
65 |
66 |
67 | -- t_sys_config definition
68 |
69 | -- Drop table
70 |
71 | -- DROP TABLE t_sys_config;
72 |
73 | CREATE TABLE t_sys_config
74 | (
75 | id serial4 NOT NULL,
76 | theme varchar NULL,
77 | website_title varchar NULL,
78 | favicon varchar NULL,
79 | config json NULL,
80 | created_at timestamp NULL,
81 | updated_at timestamp NULL,
82 | CONSTRAINT t_sys_config_pk PRIMARY KEY (id)
83 | );
84 |
85 |
86 | -- t_users definition
87 |
88 | -- Drop table
89 |
90 | -- DROP TABLE t_users;
91 |
92 | CREATE TABLE t_users
93 | (
94 | id serial4 NOT NULL,
95 | username varchar NOT NULL,
96 | nickname varchar NOT NULL,
97 | "password" varchar NULL,
98 | avatar_url varchar NULL,
99 | slogan varchar NULL,
100 | cover_url varchar NULL,
101 | config json NULL,
102 | created_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
103 | updated_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
104 | email varchar NULL,
105 | CONSTRAINT t_users_pk PRIMARY KEY (id),
106 | CONSTRAINT t_users_unique_nickname UNIQUE (nickname),
107 | CONSTRAINT t_users_unique_username UNIQUE (username)
108 | );
--------------------------------------------------------------------------------
/app/controller/IndexController.php:
--------------------------------------------------------------------------------
1 | SysConfig::find(1)
18 | ]);
19 | }
20 |
21 | public function saveSettings(Request $request): Response {
22 | $data = $request->post();
23 | $config = [
24 | 'theme' => $data['theme'],
25 | 'website_title' => $data['website_title'],
26 | 'favicon' => $data['favicon'],
27 | 'config' => [
28 | 'ak' => $data['ak'],
29 | 'sk' => $data['sk'],
30 | 'domain' => $data['domain'],
31 | 'endpoint' => $data['endpoint'],
32 | 'bucket' => $data['bucket'],
33 | 'suffix' => $data['suffix'],
34 | 'disable_register' => $data['disable_register'] ?? "off",
35 | 'anonymous_comment' => $data['anonymous_comment'] ?? "off",
36 | ],
37 | ];
38 | $exist = SysConfig::find(1);
39 | if ($exist) {
40 | $exist->update($config);
41 | } else {
42 | SysConfig::create($config);
43 | }
44 | return redirect("/settings");
45 | }
46 |
47 |
48 | public function square(Request $request): Response {
49 | $per_page = 50;
50 | $current_page = $request->input('page', 1);
51 |
52 | $memos = Memo::with(['likes' => function ($query) {
53 | $query->take(5);
54 | }])->orderBy("created_at", "desc")
55 | ->paginate($per_page, ['*'], 'page', $current_page);
56 |
57 | return view('default/index', [
58 | 'memos' => $memos,
59 | 'sysConfig' => SysConfig::find(1)
60 | ]);
61 | }
62 |
63 | public function index(Request $request): Response {
64 | $per_page = 50;
65 | $current_page = $request->input('page', 1);
66 |
67 | $user = session('user');
68 | if ($user && isset($user->config['selfHomePage']) && $user->config['selfHomePage'] == "on") {
69 | $memos = Memo::where("user_id", $user->id)->with(['likes' => function ($query) {
70 | $query->take(5);
71 | }])->orderBy("created_at", "desc")
72 | ->paginate($per_page, ['*'], 'page', $current_page);
73 |
74 | return view('default/user/mine', [
75 | 'memos' => $memos,
76 | 'user' => $user,
77 | 'sysConfig' => SysConfig::find(1),
78 | ]);
79 | }
80 |
81 | $memos = Memo::with(['likes' => function ($query) {
82 | $query->take(5);
83 | }])->orderBy("created_at", "desc")
84 | ->paginate($per_page, ['*'], 'page', $current_page);
85 |
86 | return view('default/index', [
87 | 'memos' => $memos,
88 | 'sysConfig' => SysConfig::find(1)
89 | ]);
90 | }
91 |
92 | public function hot(Request $request): Response {
93 | $per_page = 50;
94 | $current_page = $request->input('page', 1);
95 | $memos = Memo::where("comment_count", ">", "0")->orWhere("fav_count", ">", "0")->with(['likes' => function ($query) {
96 | $query->take(5);
97 | }])->orderBy("comment_count", "desc")->orderBy("fav_count", "desc")
98 | ->paginate($per_page, ['*'], 'page', $current_page);
99 |
100 | return view('default/index', [
101 | 'memos' => $memos,
102 | 'sysConfig' => SysConfig::find(1)
103 | ]);
104 | }
105 |
106 | // public function json(Request $request)
107 | // {
108 | // return json(['code' => 0, 'msg' => 'ok']);
109 | // }
110 |
111 | }
112 |
--------------------------------------------------------------------------------
/windows.php:
--------------------------------------------------------------------------------
1 | load();
18 | } else {
19 | Dotenv::createMutable(base_path())->load();
20 | }
21 | }
22 |
23 | App::loadAllConfig(['route']);
24 |
25 | $errorReporting = config('app.error_reporting');
26 | if (isset($errorReporting)) {
27 | error_reporting($errorReporting);
28 | }
29 |
30 | $runtimeProcessPath = runtime_path() . DIRECTORY_SEPARATOR . '/windows';
31 | if (!is_dir($runtimeProcessPath)) {
32 | mkdir($runtimeProcessPath);
33 | }
34 | $processFiles = [
35 | __DIR__ . DIRECTORY_SEPARATOR . 'start.php'
36 | ];
37 | foreach (config('process', []) as $processName => $config) {
38 | $processFiles[] = write_process_file($runtimeProcessPath, $processName, '');
39 | }
40 |
41 | foreach (config('plugin', []) as $firm => $projects) {
42 | foreach ($projects as $name => $project) {
43 | if (!is_array($project)) {
44 | continue;
45 | }
46 | foreach ($project['process'] ?? [] as $processName => $config) {
47 | $processFiles[] = write_process_file($runtimeProcessPath, $processName, "$firm.$name");
48 | }
49 | }
50 | foreach ($projects['process'] ?? [] as $processName => $config) {
51 | $processFiles[] = write_process_file($runtimeProcessPath, $processName, $firm);
52 | }
53 | }
54 |
55 | function write_process_file($runtimeProcessPath, $processName, $firm): string
56 | {
57 | $processParam = $firm ? "plugin.$firm.$processName" : $processName;
58 | $configParam = $firm ? "config('plugin.$firm.process')['$processName']" : "config('process')['$processName']";
59 | $fileContent = << true]);
101 | if (!$resource) {
102 | exit("Can not execute $cmd\r\n");
103 | }
104 | return $resource;
105 | }
106 |
107 | $resource = popen_processes($processFiles);
108 | echo "\r\n";
109 | while (1) {
110 | sleep(1);
111 | if (!empty($monitor) && $monitor->checkAllFilesChange()) {
112 | $status = proc_get_status($resource);
113 | $pid = $status['pid'];
114 | shell_exec("taskkill /F /T /PID $pid");
115 | proc_close($resource);
116 | $resource = popen_processes($processFiles);
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/public/css/md.css:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 | .markdown-body body {
3 | color: #595959;
4 | font-size: 14px;
5 | font-family: -apple-system, system-ui, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
6 | background-image: linear-gradient(90deg, rgba(60, 10, 30, 0.04) 3%, rgba(0, 0, 0, 0) 3%), linear-gradient(360deg, rgba(60, 10, 30, 0.04) 3%, rgba(0, 0, 0, 0) 3%);
7 | background-size: 20px 20px;
8 | background-position: center center;
9 | min-width: 200px;
10 | max-width: 760px;
11 | margin: 0 auto;
12 | padding: 1em;
13 | }
14 |
15 | /* 段落 */
16 | .markdown-body p,
17 | .markdown-body ul,
18 | .markdown-body ol{
19 | color: #595959;
20 | font-size: 14px;
21 | line-height: 1.4rem;
22 | font-weight: 400;
23 | }
24 |
25 | /* 段落间距控制 */
26 | .markdown-body p + p {
27 | margin-top: 12px;
28 | }
29 |
30 | /* 标题的通用设置 */
31 | .markdown-body h2,
32 | .markdown-body h3,
33 | .markdown-body h4,
34 | .markdown-body h5,
35 | .markdown-body h6 {
36 | padding: 30px 0;
37 | margin: 0;
38 | color: #135ce0;
39 | }
40 |
41 | /* 二级标题 */
42 | .markdown-body h2 {
43 | position: relative;
44 | font-size: 20px;
45 | border-left: 4px solid;
46 | padding: 0 0 0 10px;
47 | margin: 30px 0;
48 | }
49 |
50 | /* 三级标题 */
51 | .markdown-body h3 {
52 | font-size: 16px;
53 | }
54 |
55 | /* 无序列表 */
56 | .markdown-body ul {
57 | list-style: disc outside;
58 | margin-left: 2em;
59 | margin-top: 1em;
60 | }
61 |
62 | /* 无序列表 */
63 | .markdown-body ol {
64 | list-style: decimal outside;
65 | margin-left: 2em;
66 | margin-top: 1em;
67 | }
68 |
69 | /* 无序列表内容 */
70 | .markdown-body li {
71 | line-height: 2;
72 | color: #595959;
73 | margin-bottom: 0;
74 | list-style: inherit;
75 | }
76 |
77 | .markdown-body img {
78 | max-width: 100%;
79 | }
80 |
81 | /* 已加载图片 */
82 | .markdown-body img.loaded {
83 | margin: 0 auto;
84 | display: block;
85 | }
86 |
87 | /* 引用 */
88 | .markdown-body blockquote {
89 | background: #fff9f9;
90 | margin: 2em 0;
91 | padding: 2px 20px;
92 | border-left: 4px solid #b2aec5;
93 | }
94 |
95 | /* 引用文字 */
96 | .markdown-body blockquote p {
97 | color: #666;
98 | line-height: 2;
99 | }
100 |
101 | /* 链接 */
102 | .markdown-body a {
103 | color: #036aca;
104 | font-weight: 400;
105 | text-decoration: none;
106 | }
107 |
108 | /* 加粗 */
109 | .markdown-body strong {
110 | color: #036aca;
111 | }
112 |
113 | /* 加粗斜体 */
114 | .markdown-body em strong {
115 | color: #036aca;
116 | }
117 |
118 | /* 分隔线 */
119 | .markdown-body hr {
120 | border-top: 1px solid #135ce0;
121 | }
122 |
123 | /* 代码 */
124 | .markdown-body pre {
125 | overflow: auto;
126 | }
127 |
128 | .markdown-body pre,
129 | .markdown-body code {
130 | overflow: auto;
131 | position: relative;
132 | line-height: 1.75;
133 | font-family: Menlo, Monaco, Consolas, Courier New, monospace;
134 | border-radius: 5px;
135 | }
136 |
137 | .markdown-body pre > code {
138 | /* font-size: 12px; */
139 | padding: 15px 12px;
140 | margin: 0;
141 | word-break: normal;
142 | display: block;
143 | overflow-x: auto;
144 | }
145 |
146 | .markdown-body code {
147 | word-break: break-word;
148 | border-radius: 4px;
149 | overflow-x: auto;
150 |
151 | font-size: 0.87em;
152 | padding: 0.065em 0.5em;
153 | /*border:1px solid #ccc;*/
154 | margin:0 2px;
155 | }
156 |
157 | /* 表格 */
158 | .markdown-body table {
159 | border-collapse: collapse;
160 | margin: 1rem 0;
161 | overflow-x: auto;
162 | }
163 |
164 | .markdown-body table th,
165 | .markdown-body table td {
166 | border: 1px solid #dfe2e5;
167 | padding: 0.6em 1em;
168 | }
169 |
170 | .markdown-body table tr {
171 | border-top: 1px solid #dfe2e5;
172 | }
173 |
174 | .markdown-body table tr:nth-child(2n) {
175 | background-color: #f6f8fa;
176 | }
--------------------------------------------------------------------------------
/app/controller/MemoController.php:
--------------------------------------------------------------------------------
1 | config['anonymous_comment'] == "on") {
21 | $memo = Memo::find($id);
22 | if (!$memo) {
23 | return response("not found");
24 | }
25 | $data = $request->post();
26 | $content = $data['content'];
27 | $uid = $currentUser ? $currentUser->id : null;
28 | if (strlen(trim($content)) == 0) {
29 | return response("评论不能为空");
30 | }
31 | Comment::create([
32 | 'user_id' => $uid,
33 | 'memo_id' => $id,
34 | 'content' => $content,
35 | 'username' => $request->post('username') ?: '匿名',
36 | 'email' => $request->post('email', ''),
37 | 'website' => $request->post('website', ''),
38 | 'reply_user_id' => $request->post('reply_user_id'),
39 | 'reply_username' => $request->post('reply_username', ''),
40 | ]);
41 | $memo->increment('comment_count');
42 | $memo->save();
43 | return redirect("/");
44 | }
45 | return redirect("/user/login");
46 | }
47 |
48 | public function like(Request $request, $id): Response {
49 | $sysConfig = SysConfig::find(1);
50 | $currentUser = session('user');
51 | if ($currentUser || $sysConfig->config['anonymous_comment'] == "on") {
52 | $memo = Memo::find($id);
53 |
54 | if ($currentUser) {
55 | if ($memo->author->id === $currentUser->id) {
56 | return json(['fav_count' => $memo->fav_count]);
57 | }
58 | if (Like::where('user_id', $currentUser->id)
59 | ->where('memo_id', $id)->count() == 0) {
60 | Like::create([
61 | 'user_id' => $currentUser->id,
62 | 'memo_id' => $id,
63 | ]);
64 | $memo->increment('fav_count');
65 | }
66 | } else {
67 | $memo->increment('fav_count');
68 | }
69 |
70 | $memo->refresh();
71 | return json(['fav_count' => $memo->fav_count, 'like_persons' => $memo->likes->take(5)->map(function ($item) {
72 | return $item->author->nickname;
73 | })->join(",")]);
74 | }
75 | return redirect("/user/login");
76 | }
77 |
78 | public function remove(Request $request, $id): Response {
79 | $currentUser = session('user');
80 | $memo = Memo::find($id);
81 | if (!$memo) {
82 | return response("not found");
83 | }
84 | if ($currentUser->id === 1 || $memo->author->id === $currentUser->id) {
85 | $memo->delete();
86 | }
87 | return redirect("/");
88 | }
89 |
90 | public function add(Request $request): Response {
91 | return view('default/memo/save');
92 | }
93 |
94 | public function toEdit(Request $request, $id): Response {
95 | $memo = Memo::find($id);
96 | if (!$memo) {
97 | return response("not found");
98 | }
99 | return view('default/memo/save', ['data' => $memo]);
100 | }
101 |
102 | public function save(Request $request): Response {
103 | $session = $request->session();
104 | $contentHtml = $request->post('contentHtml');
105 | $contentMD = $request->post('contentMD');
106 | if (strlen(trim($contentHtml)) == 0) {
107 | return view('default/memo/save', [
108 | 'exception' => '内容不能为空',
109 | 'data' => $request->post()
110 | ]);
111 | }
112 |
113 | $data = [
114 | 'fav_count' => 0,
115 | 'comment_count' => 0,
116 | 'user_id' => $session->get('user')->id,
117 | 'pinned' => false,
118 | 'permission' => 0,
119 | 'content_html' => trim(str_replace(" ", "", $contentHtml)),
120 | 'content_md' => trim($contentMD),
121 | ];
122 | $id = $request->post('id');
123 | if ($id) {
124 | $data['id'] = $id;
125 | }
126 | Memo::upsert($data, ['id']);
127 | return redirect('/');
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/public/js/textarea.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license Rangy Inputs, a jQuery plug-in for selection and caret manipulation within textareas and text inputs.
3 | *
4 | * https://github.com/timdown/rangyinputs
5 | *
6 | * For range and selection features for contenteditable, see Rangy.
7 |
8 | * http://code.google.com/p/rangy/
9 | *
10 | * Depends on jQuery 1.0 or later.
11 | *
12 | * Copyright 2014, Tim Down
13 | * Licensed under the MIT license.
14 | * Version: 1.2.0
15 | * Build date: 30 November 2014
16 | */
17 | !function(e){function t(e,t){var n=typeof e[t];return"function"===n||!("object"!=n||!e[t])||"unknown"==n}function n(e,t){return typeof e[t]!=x}function r(e,t){return!("object"!=typeof e[t]||!e[t])}function o(e){window.console&&window.console.log&&window.console.log("RangyInputs not supported in your browser. Reason: "+e)}function a(e,t,n){return 0>t&&(t+=e.value.length),typeof n==x&&(n=t),0>n&&(n+=e.value.length),{start:t,end:n}}function c(e,t,n){return{start:t,end:n,length:n-t,text:e.value.slice(t,n)}}function l(){return r(document,"body")?document.body:document.getElementsByTagName("body")[0]}var i,u,s,d,f,v,p,m,g,x="undefined";e(document).ready(function(){function h(e,t){var n=e.value,r=i(e),o=r.start;return{value:n.slice(0,o)+t+n.slice(r.end),index:o,replaced:r.text}}function y(e,t){e.focus();var n=i(e);return u(e,n.start,n.end),""==t?document.execCommand("delete",!1,null):document.execCommand("insertText",!1,t),{replaced:n.text,index:n.start}}function T(e,t){e.focus();var n=h(e,t);return e.value=n.value,n}function E(e,t){return function(){var n=this.jquery?this[0]:this,r=n.nodeName.toLowerCase();if(1==n.nodeType&&("textarea"==r||"input"==r&&/^(?:text|email|number|search|tel|url|password)$/i.test(n.type))){var o=[n].concat(Array.prototype.slice.call(arguments)),a=e.apply(this,o);if(!t)return a}return t?this:void 0}}var S=document.createElement("textarea");if(l().appendChild(S),n(S,"selectionStart")&&n(S,"selectionEnd"))i=function(e){var t=e.selectionStart,n=e.selectionEnd;return c(e,t,n)},u=function(e,t,n){var r=a(e,t,n);e.selectionStart=r.start,e.selectionEnd=r.end},g=function(e,t){t?e.selectionEnd=e.selectionStart:e.selectionStart=e.selectionEnd};else{if(!(t(S,"createTextRange")&&r(document,"selection")&&t(document.selection,"createRange")))return l().removeChild(S),void o("No means of finding text input caret position");i=function(e){var t,n,r,o,a=0,l=0,i=document.selection.createRange();return i&&i.parentElement()==e&&(r=e.value.length,t=e.value.replace(/\r\n/g,"\n"),n=e.createTextRange(),n.moveToBookmark(i.getBookmark()),o=e.createTextRange(),o.collapse(!1),n.compareEndPoints("StartToEnd",o)>-1?a=l=r:(a=-n.moveStart("character",-r),a+=t.slice(0,a).split("\n").length-1,n.compareEndPoints("EndToEnd",o)>-1?l=r:(l=-n.moveEnd("character",-r),l+=t.slice(0,l).split("\n").length-1))),c(e,a,l)};var w=function(e,t){return t-(e.value.slice(0,t).split("\r\n").length-1)};u=function(e,t,n){var r=a(e,t,n),o=e.createTextRange(),c=w(e,r.start);o.collapse(!0),r.start==r.end?o.move("character",c):(o.moveEnd("character",w(e,r.end)),o.moveStart("character",c)),o.select()},g=function(e,t){var n=document.selection.createRange();n.collapse(t),n.select()}}l().removeChild(S);var b=function(e,t){var n=h(e,t);try{var r=y(e,t);if(e.value==n.value)return b=y,r}catch(o){}return b=T,e.value=n.value,n};d=function(e,t,n,r){t!=n&&(u(e,t,n),b(e,"")),r&&u(e,t)},s=function(e){u(e,b(e,"").index)},m=function(e){var t=b(e,"");return u(e,t.index),t.replaced};var R=function(e,t,n,r){var o=t+n.length;if(r="string"==typeof r?r.toLowerCase():"",("collapsetoend"==r||"select"==r)&&/[\r\n]/.test(n)){var a=n.replace(/\r\n/g,"\n").replace(/\r/g,"\n");o=t+a.length;var c=t+a.indexOf("\n");"\r\n"==e.value.slice(c,c+2)&&(o+=a.match(/\n/g).length)}switch(r){case"collapsetostart":u(e,t,t);break;case"collapsetoend":u(e,o,o);break;case"select":u(e,t,o)}};f=function(e,t,n,r){u(e,n),b(e,t),"boolean"==typeof r&&(r=r?"collapseToEnd":""),R(e,n,t,r)},v=function(e,t,n){var r=b(e,t);R(e,r.index,t,n||"collapseToEnd")},p=function(e,t,n,r){typeof n==x&&(n=t);var o=i(e),a=b(e,t+o.text+n);R(e,a.index+t.length,o.text,r||"select")},e.fn.extend({getSelection:E(i,!1),setSelection:E(u,!0),collapseSelection:E(g,!0),deleteSelectedText:E(s,!0),deleteText:E(d,!0),extractSelectedText:E(m,!1),insertText:E(f,!0),replaceSelectedText:E(v,!0),surroundSelectedText:E(p,!0)})})}(jQuery);
--------------------------------------------------------------------------------
/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\Log;
17 | use Webman\Bootstrap;
18 | use Webman\Config;
19 | use Webman\Middleware;
20 | use Webman\Route;
21 | use Webman\Util;
22 |
23 | $worker = $worker ?? null;
24 |
25 | set_error_handler(function ($level, $message, $file = '', $line = 0) {
26 | if (error_reporting() & $level) {
27 | throw new ErrorException($message, 0, $level, $file, $line);
28 | }
29 | });
30 |
31 | if ($worker) {
32 | register_shutdown_function(function ($startTime) {
33 | if (time() - $startTime <= 0.1) {
34 | sleep(1);
35 | }
36 | }, time());
37 | }
38 |
39 | if (class_exists('Dotenv\Dotenv') && file_exists(base_path(false) . '/.env')) {
40 | if (method_exists('Dotenv\Dotenv', 'createUnsafeMutable')) {
41 | Dotenv::createUnsafeMutable(base_path(false))->load();
42 | } else {
43 | Dotenv::createMutable(base_path(false))->load();
44 | }
45 | }
46 |
47 | Config::clear();
48 | support\App::loadAllConfig(['route']);
49 | if ($timezone = config('app.default_timezone')) {
50 | date_default_timezone_set($timezone);
51 | }
52 |
53 | foreach (config('autoload.files', []) as $file) {
54 | include_once $file;
55 | }
56 | foreach (config('plugin', []) as $firm => $projects) {
57 | foreach ($projects as $name => $project) {
58 | if (!is_array($project)) {
59 | continue;
60 | }
61 | foreach ($project['autoload']['files'] ?? [] as $file) {
62 | include_once $file;
63 | }
64 | }
65 | foreach ($projects['autoload']['files'] ?? [] as $file) {
66 | include_once $file;
67 | }
68 | }
69 |
70 | Middleware::load(config('middleware', []));
71 | foreach (config('plugin', []) as $firm => $projects) {
72 | foreach ($projects as $name => $project) {
73 | if (!is_array($project) || $name === 'static') {
74 | continue;
75 | }
76 | Middleware::load($project['middleware'] ?? []);
77 | }
78 | Middleware::load($projects['middleware'] ?? [], $firm);
79 | if ($staticMiddlewares = config("plugin.$firm.static.middleware")) {
80 | Middleware::load(['__static__' => $staticMiddlewares], $firm);
81 | }
82 | }
83 | Middleware::load(['__static__' => config('static.middleware', [])]);
84 |
85 | foreach (config('bootstrap', []) as $className) {
86 | if (!class_exists($className)) {
87 | $log = "Warning: Class $className setting in config/bootstrap.php not found\r\n";
88 | echo $log;
89 | Log::error($log);
90 | continue;
91 | }
92 | /** @var Bootstrap $className */
93 | $className::start($worker);
94 | }
95 |
96 | foreach (config('plugin', []) as $firm => $projects) {
97 | foreach ($projects as $name => $project) {
98 | if (!is_array($project)) {
99 | continue;
100 | }
101 | foreach ($project['bootstrap'] ?? [] as $className) {
102 | if (!class_exists($className)) {
103 | $log = "Warning: Class $className setting in config/plugin/$firm/$name/bootstrap.php not found\r\n";
104 | echo $log;
105 | Log::error($log);
106 | continue;
107 | }
108 | /** @var Bootstrap $className */
109 | $className::start($worker);
110 | }
111 | }
112 | foreach ($projects['bootstrap'] ?? [] as $className) {
113 | /** @var string $className */
114 | if (!class_exists($className)) {
115 | $log = "Warning: Class $className setting in plugin/$firm/config/bootstrap.php not found\r\n";
116 | echo $log;
117 | Log::error($log);
118 | continue;
119 | }
120 | /** @var Bootstrap $className */
121 | $className::start($worker);
122 | }
123 | }
124 |
125 | $directory = base_path() . '/plugin';
126 | $paths = [config_path()];
127 | foreach (Util::scanDir($directory) as $path) {
128 | if (is_dir($path = "$path/config")) {
129 | $paths[] = $path;
130 | }
131 | }
132 | Route::load($paths);
133 |
134 |
--------------------------------------------------------------------------------
/public/js/main.js:
--------------------------------------------------------------------------------
1 | function copyToClipboard(text) {
2 | // 创建一个临时的可编辑元素
3 | const textarea = document.createElement('textarea');
4 | // 将样式设置为隐藏,避免在页面上显示
5 | textarea.style.position = 'fixed';
6 | textarea.style.top = 0;
7 | textarea.style.left = 0;
8 | textarea.style.width = '2em';
9 | textarea.style.height = '2em';
10 | textarea.style.padding = 0;
11 | textarea.style.border = 'none';
12 | textarea.style.outline = 'none';
13 | textarea.style.boxShadow = 'none';
14 | textarea.style.background = 'transparent';
15 | // 将文本设置到创建的元素中
16 | textarea.value = text;
17 | // 将元素添加到文档中
18 | document.body.appendChild(textarea);
19 | // 选中文本
20 | textarea.select();
21 | // 执行复制操作
22 | const successful = document.execCommand('copy');
23 | // 从文档中移除元素
24 | document.body.removeChild(textarea);
25 | // 返回复制操作是否成功
26 | return successful;
27 | }
28 |
29 | $(function () {
30 |
31 | $(".markdown-body img").each(function () {
32 | $(this).attr('data-fancybox', '')
33 | })
34 | Fancybox.bind("[data-fancybox]", {
35 | // Your custom options
36 | });
37 |
38 | //评论工具栏
39 | $("div[data-trigger-comment]").click(function () {
40 | const id = $(this).data('trigger-comment')
41 | const $target = $("form[data-comment-box='" + id + "']")
42 | const $commentBox = $("form[data-comment-box]")
43 | $commentBox.not($target).removeClass('flex').addClass('hidden');
44 | $commentBox.not($target).each(function () {
45 | if ($(this).is(":only-child")) {
46 | $(this).parent().removeClass('flex').addClass('hidden');
47 | }
48 | })
49 | if ($target.hasClass('flex')) {
50 | $target.removeClass('flex').addClass('hidden')
51 | } else {
52 | $target.removeClass('hidden').addClass('flex')
53 | $("div[data-comment-area='" + id + "']").removeClass('hidden').addClass('flex')
54 | }
55 | })
56 |
57 | // 互动工具栏
58 | $(document.body).on('click', function (event) {
59 | const $target = $(event.target); // 获取被点击的元素
60 | const $trigger = $($target.closest('[data-trigger]'))
61 | const $triggerItem = $($target.closest('[data-trigger-item]'))
62 | if ($trigger.length > 0) {
63 | $("div[data-trigger-item]").hide()
64 | $($trigger).next().toggle()
65 | } else if ($triggerItem.length > 0) {
66 | $($triggerItem).hide();
67 | } else {
68 | $("div[data-trigger-item]").hide()
69 | }
70 | })
71 |
72 | // 发起点赞
73 | $("form[data-like-form]").submit(function () {
74 | const likeMemos = JSON.parse(localStorage.getItem("likeMemos") || "[]")
75 | const id = $(this).data('like-form')
76 | const commentAreaSel = `div[data-comment-area='${id}']`
77 | if (likeMemos.includes(id)) {
78 | return false;
79 | }
80 | $(this).ajaxSubmit(function (data) {
81 | $(commentAreaSel)
82 | .removeClass('hidden').addClass('flex')
83 | $(commentAreaSel).find("> div[data-fav-count-box]")
84 | .removeClass('hidden').addClass('flex')
85 | $(commentAreaSel).find(".fav_count").text(data.fav_count)
86 | $(commentAreaSel).find("span[data-like-persons]").text(data.like_persons)
87 | likeMemos.push(id)
88 | localStorage.setItem("likeMemos", JSON.stringify(likeMemos))
89 | })
90 | return false;
91 | })
92 |
93 |
94 | setTimeout(() => {
95 | $(".markdown-body pre code").each(function () {
96 | const $pre = $(this).parent()
97 | const copyBtn = $(``)
98 | $pre.css("position", "relative")
99 | $pre.append(copyBtn).hover(() => {
100 | copyBtn.removeClass('text-gray-300').text('复制').toggle()
101 | })
102 | copyBtn.bind('click', () => {
103 | copyBtn.addClass('text-gray-300').text('已复制!')
104 | copyToClipboard($pre.find("code").text())
105 | })
106 | })
107 |
108 | const maxHeight = 400
109 | const maxHeightClass = `max-h-[${maxHeight}px]`
110 |
111 | $("div[data-content]").each(function () {
112 | const showAllBtn = $("div[data-show-all='" + $(this).data('content') + "']")
113 | const contentDiv = $(this)
114 | // 检查是否有垂直滚动条
115 | if (contentDiv[0].scrollHeight > maxHeight) {
116 | showAllBtn.show(); // 如果有滚动条,则显示“全部”按钮
117 | } else {
118 | showAllBtn.hide(); // 如果没有滚动条,则隐藏“全部”按钮
119 | }
120 |
121 | // 监听“全部”按钮的点击事件
122 | showAllBtn.click(function () {
123 | contentDiv.toggleClass(maxHeightClass)
124 | if (contentDiv.hasClass(maxHeightClass)) {
125 | showAllBtn.text('全文')
126 | } else {
127 | showAllBtn.text('收起')
128 | }
129 | });
130 | })
131 | }, 300)
132 |
133 |
134 | })
--------------------------------------------------------------------------------
/app/view/default/user/settings.Twig:
--------------------------------------------------------------------------------
1 | {% extends 'default/layout/base.Twig' %}
2 | {% block title %} 个人设置 {% endblock %}
3 |
4 | {% block content %}
5 |
112 | {% endblock %}
113 |
114 | {% block afterScript %}
115 |
148 | {% endblock %}
--------------------------------------------------------------------------------
/app/view/default/sysSettings.Twig:
--------------------------------------------------------------------------------
1 | {% extends 'default/layout/base.Twig' %}
2 | {% block title %} 系统设置 {% endblock %}
3 |
4 | {% block content %}
5 |
119 | {% endblock %}
120 |
121 | {% block afterScript %}
122 |
163 | {% endblock %}
--------------------------------------------------------------------------------
/app/controller/UserController.php:
--------------------------------------------------------------------------------
1 | session()->forget('user');
20 | return redirect('/');
21 | }
22 |
23 | public function specifyUser(Request $request, $username): Response {
24 | $user = User::firstWhere("username", $username);
25 | $per_page = 10;
26 | $current_page = $request->input('page', 1);
27 | $memos = Memo::where("user_id", $user->id)->with(['likes' => function ($query) {
28 | $query->take(5);
29 | }])->orderBy("created_at", "desc")
30 | ->paginate($per_page, ['*'], 'page', $current_page);
31 |
32 | return view('default/user/mine', [
33 | 'memos' => $memos,
34 | 'user' => $user
35 | ]);
36 | }
37 |
38 |
39 | public function saveSettings(Request $request): Response {
40 | $data = $request->post();
41 | $uid = $request->session()->get('user')->id;
42 | Log::info("data is saveSettings", $data);
43 |
44 | if (User::where("username", $data['username'])->where("id", "<>", $uid)->first()) {
45 | Log::info("username is exists");
46 |
47 | return view('default/user/settings', [
48 | 'message' => '用户名 已存在,保存失败',
49 | 'data' => $data
50 | ]);
51 | }
52 | if (User::where("nickname", $data['nickname'])->where("id", "<>", $uid)->first()) {
53 | Log::info("nickname is exists");
54 |
55 | return view('default/user/settings', [
56 | 'message' => '昵称 已存在,保存失败',
57 | 'data' => $data
58 | ]);
59 | }
60 | $avatar_url = $data['avatar_url'];
61 | $avatar_url_file = $request->file('avatar_url');
62 | if ($avatar_url_file && $avatar_url_file->isValid()) {
63 | $destination = date("Ymd") . DIRECTORY_SEPARATOR . uniqid() . "." . $avatar_url_file->getUploadExtension();
64 | $avatar_url_file->move(getenv('UPLOAD_DIR') . DIRECTORY_SEPARATOR . $destination);
65 | $avatar_url = '/upload/' . $destination;
66 | }
67 |
68 | $cover_url = $data['cover_url'];
69 | $cover_url_file = $request->file('cover_url');
70 | if ($cover_url_file && $cover_url_file->isValid()) {
71 | $destination = date("Ymd") . DIRECTORY_SEPARATOR . uniqid() . "." . $cover_url_file->getUploadExtension();
72 | $cover_url_file->move(getenv('UPLOAD_DIR') . DIRECTORY_SEPARATOR . $destination);
73 | $cover_url = '/upload/' . $destination;
74 | }
75 |
76 | User::find($uid)->update([
77 | 'username' => $data['username'],
78 | 'nickname' => $data['nickname'],
79 | 'email' => $data['email'],
80 | 'avatar_url' => $avatar_url,
81 | 'cover_url' => $cover_url,
82 | 'slogan' => $data['slogan'],
83 | 'config' => [
84 | 'css' => $data['css'],
85 | 'js' => $data['js'],
86 | 'selfHomePage' => $data['selfHomePage'] ?? "off"
87 | ],
88 | ]);
89 |
90 | if ($data['password']) {
91 | User::find($uid)->update([
92 | 'password' => password_hash($data['password'], PASSWORD_BCRYPT),
93 | ]);
94 | $request->session()->forget('user');
95 | return redirect('/user/login');
96 | }
97 | session(['user' => User::find($uid)]);
98 | return redirect('/user/settings');
99 | }
100 |
101 | public function settings(Request $request): Response {
102 | return view('default/user/settings', ['data' => $request->session()->get('user')]);
103 | }
104 |
105 | public function reg(Request $request): Response {
106 | return view('default/user/reg', [
107 | 'sysConfig' => SysConfig::find(1)
108 | ]);
109 | }
110 |
111 | public function doReg(Request $request): Response {
112 | try {
113 | v::input($request->post(), [
114 | 'username' => v::alnum()->length(3, 12)->setName('用户名'),
115 | 'password' => v::stringVal()->length(5, 20)->setName('密码'),
116 | 'email' => v::email()->length(5, 64)->setName('邮箱')
117 | ]);
118 | } catch (ValidationException $exception) {
119 | return view('default/user/reg', [
120 | 'exception' => $exception->getMessage(),
121 | 'data' => $request->post()
122 | ]);
123 | }
124 | $data = $request->post();
125 | if ($data['password'] !== $data['repeatPassword']) {
126 | return view('default/user/reg', [
127 | 'exception' => '密码 两次密码不一致',
128 | 'data' => $request->post()
129 | ]);
130 | }
131 | $user = User::where('username', $data['username'])->first();
132 | if ($user) {
133 | return view('default/user/reg', [
134 | 'exception' => '用户名 已存在',
135 | 'data' => $data
136 | ]);
137 | }
138 | User::create([
139 | 'username' => $data['username'],
140 | 'password' => password_hash($data['password'], PASSWORD_BCRYPT),
141 | 'email' => $data['email'],
142 | 'nickname' => $data['username'],
143 | 'avatar_url' => getenv('DEFAULT_USER_AVATAR_CDN') . hash("sha256", $data['email']),
144 | 'cover_url' => '/images/cover.webp',
145 | 'sloan' => '天道酬勤',
146 | ]);
147 |
148 | return view('default/user/reg', ['message' => '注册成功,快去登录吧!']);
149 | }
150 |
151 | public function login(Request $request): Response {
152 | return view('default/user/login');
153 | }
154 |
155 | public function doLogin(Request $request): Response {
156 | try {
157 | v::input($request->post(), [
158 | 'username' => v::alnum()->length(3, 12)->setName('用户名'),
159 | 'password' => v::stringVal()->length(5, 20)->setName('密码'),
160 | ]);
161 | } catch (ValidationException $exception) {
162 | return view('default/user/login', [
163 | 'exception' => $exception->getMessage(),
164 | 'data' => $request->post()
165 | ]);
166 | }
167 | $data = $request->post();
168 | $user = User::where('username', $data['username'])->first();
169 | if (!$user) {
170 | return view('default/user/login', [
171 | 'exception' => '用户名 / 密码 不正确',
172 | 'data' => $data
173 | ]);
174 | }
175 |
176 | if (password_verify($data['password'], $user->password)) {
177 | $request->session()->set('user', $user);
178 | return redirect('/');
179 | }
180 | return view('default/user/login', [
181 | 'exception' => '用户名 / 密码 不正确',
182 | 'data' => $data
183 | ]);
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/public/js/toastify-js.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Minified by jsDelivr using Terser v5.19.2.
3 | * Original file: /npm/toastify-js@1.12.0/src/toastify.js
4 | *
5 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
6 | */
7 | /*!
8 | * Toastify js 1.12.0
9 | * https://github.com/apvarun/toastify-js
10 | * @license MIT licensed
11 | *
12 | * Copyright (C) 2018 Varun A P
13 | */
14 | !function(t,o){"object"==typeof module&&module.exports?module.exports=o():t.Toastify=o()}(this,(function(t){var o=function(t){return new o.lib.init(t)};function i(t,o){return o.offset[t]?isNaN(o.offset[t])?o.offset[t]:o.offset[t]+"px":"0px"}function s(t,o){return!(!t||"string"!=typeof o)&&!!(t.className&&t.className.trim().split(/\s+/gi).indexOf(o)>-1)}return o.defaults={oldestFirst:!0,text:"Toastify is awesome!",node:void 0,duration:3e3,selector:void 0,callback:function(){},destination:void 0,newWindow:!1,close:!1,gravity:"toastify-top",positionLeft:!1,position:"",backgroundColor:"",avatar:"",className:"",stopOnFocus:!0,onClick:function(){},offset:{x:0,y:0},escapeMarkup:!0,ariaLive:"polite",style:{background:""}},o.lib=o.prototype={toastify:"1.12.0",constructor:o,init:function(t){return t||(t={}),this.options={},this.toastElement=null,this.options.text=t.text||o.defaults.text,this.options.node=t.node||o.defaults.node,this.options.duration=0===t.duration?0:t.duration||o.defaults.duration,this.options.selector=t.selector||o.defaults.selector,this.options.callback=t.callback||o.defaults.callback,this.options.destination=t.destination||o.defaults.destination,this.options.newWindow=t.newWindow||o.defaults.newWindow,this.options.close=t.close||o.defaults.close,this.options.gravity="bottom"===t.gravity?"toastify-bottom":o.defaults.gravity,this.options.positionLeft=t.positionLeft||o.defaults.positionLeft,this.options.position=t.position||o.defaults.position,this.options.backgroundColor=t.backgroundColor||o.defaults.backgroundColor,this.options.avatar=t.avatar||o.defaults.avatar,this.options.className=t.className||o.defaults.className,this.options.stopOnFocus=void 0===t.stopOnFocus?o.defaults.stopOnFocus:t.stopOnFocus,this.options.onClick=t.onClick||o.defaults.onClick,this.options.offset=t.offset||o.defaults.offset,this.options.escapeMarkup=void 0!==t.escapeMarkup?t.escapeMarkup:o.defaults.escapeMarkup,this.options.ariaLive=t.ariaLive||o.defaults.ariaLive,this.options.style=t.style||o.defaults.style,t.backgroundColor&&(this.options.style.background=t.backgroundColor),this},buildToast:function(){if(!this.options)throw"Toastify is not initialized";var t=document.createElement("div");for(var o in t.className="toastify on "+this.options.className,this.options.position?t.className+=" toastify-"+this.options.position:!0===this.options.positionLeft?(t.className+=" toastify-left",console.warn("Property `positionLeft` will be depreciated in further versions. Please use `position` instead.")):t.className+=" toastify-right",t.className+=" "+this.options.gravity,this.options.backgroundColor&&console.warn('DEPRECATION NOTICE: "backgroundColor" is being deprecated. Please use the "style.background" property.'),this.options.style)t.style[o]=this.options.style[o];if(this.options.ariaLive&&t.setAttribute("aria-live",this.options.ariaLive),this.options.node&&this.options.node.nodeType===Node.ELEMENT_NODE)t.appendChild(this.options.node);else if(this.options.escapeMarkup?t.innerText=this.options.text:t.innerHTML=this.options.text,""!==this.options.avatar){var s=document.createElement("img");s.src=this.options.avatar,s.className="toastify-avatar","left"==this.options.position||!0===this.options.positionLeft?t.appendChild(s):t.insertAdjacentElement("afterbegin",s)}if(!0===this.options.close){var e=document.createElement("button");e.type="button",e.setAttribute("aria-label","Close"),e.className="toast-close",e.innerHTML="✖",e.addEventListener("click",function(t){t.stopPropagation(),this.removeElement(this.toastElement),window.clearTimeout(this.toastElement.timeOutValue)}.bind(this));var n=window.innerWidth>0?window.innerWidth:screen.width;("left"==this.options.position||!0===this.options.positionLeft)&&n>360?t.insertAdjacentElement("afterbegin",e):t.appendChild(e)}if(this.options.stopOnFocus&&this.options.duration>0){var a=this;t.addEventListener("mouseover",(function(o){window.clearTimeout(t.timeOutValue)})),t.addEventListener("mouseleave",(function(){t.timeOutValue=window.setTimeout((function(){a.removeElement(t)}),a.options.duration)}))}if(void 0!==this.options.destination&&t.addEventListener("click",function(t){t.stopPropagation(),!0===this.options.newWindow?window.open(this.options.destination,"_blank"):window.location=this.options.destination}.bind(this)),"function"==typeof this.options.onClick&&void 0===this.options.destination&&t.addEventListener("click",function(t){t.stopPropagation(),this.options.onClick()}.bind(this)),"object"==typeof this.options.offset){var l=i("x",this.options),r=i("y",this.options),p="left"==this.options.position?l:"-"+l,d="toastify-top"==this.options.gravity?r:"-"+r;t.style.transform="translate("+p+","+d+")"}return t},showToast:function(){var t;if(this.toastElement=this.buildToast(),!(t="string"==typeof this.options.selector?document.getElementById(this.options.selector):this.options.selector instanceof HTMLElement||"undefined"!=typeof ShadowRoot&&this.options.selector instanceof ShadowRoot?this.options.selector:document.body))throw"Root element is not defined";var i=o.defaults.oldestFirst?t.firstChild:t.lastChild;return t.insertBefore(this.toastElement,i),o.reposition(),this.options.duration>0&&(this.toastElement.timeOutValue=window.setTimeout(function(){this.removeElement(this.toastElement)}.bind(this),this.options.duration)),this},hideToast:function(){this.toastElement.timeOutValue&&clearTimeout(this.toastElement.timeOutValue),this.removeElement(this.toastElement)},removeElement:function(t){t.className=t.className.replace(" on",""),window.setTimeout(function(){this.options.node&&this.options.node.parentNode&&this.options.node.parentNode.removeChild(this.options.node),t.parentNode&&t.parentNode.removeChild(t),this.options.callback.call(t),o.reposition()}.bind(this),400)}},o.reposition=function(){for(var t,o={top:15,bottom:15},i={top:15,bottom:15},e={top:15,bottom:15},n=document.getElementsByClassName("toastify"),a=0;a0?window.innerWidth:screen.width)<=360?(n[a].style[t]=e[t]+"px",e[t]+=l+15):!0===s(n[a],"toastify-left")?(n[a].style[t]=o[t]+"px",o[t]+=l+15):(n[a].style[t]=i[t]+"px",i[t]+=l+15)}return this},o.lib.init.prototype=o.lib,o}));
15 | //# sourceMappingURL=/sm/e1ebbfe1bf0b0061f0726ebc83434e1c2f8308e6354c415fd05ecccdaad47617.map
--------------------------------------------------------------------------------
/app/view/default/layout/nav.Twig:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 FilesystemIterator;
18 | use RecursiveDirectoryIterator;
19 | use RecursiveIteratorIterator;
20 | use SplFileInfo;
21 | use Workerman\Timer;
22 | use Workerman\Worker;
23 |
24 | /**
25 | * Class FileMonitor
26 | * @package process
27 | */
28 | class Monitor
29 | {
30 | /**
31 | * @var array
32 | */
33 | protected $paths = [];
34 |
35 | /**
36 | * @var array
37 | */
38 | protected $extensions = [];
39 |
40 | /**
41 | * @var string
42 | */
43 | public static $lockFile = __DIR__ . '/../runtime/monitor.lock';
44 |
45 | /**
46 | * Pause monitor
47 | * @return void
48 | */
49 | public static function pause()
50 | {
51 | file_put_contents(static::$lockFile, time());
52 | }
53 |
54 | /**
55 | * Resume monitor
56 | * @return void
57 | */
58 | public static function resume(): void
59 | {
60 | clearstatcache();
61 | if (is_file(static::$lockFile)) {
62 | unlink(static::$lockFile);
63 | }
64 | }
65 |
66 | /**
67 | * Whether monitor is paused
68 | * @return bool
69 | */
70 | public static function isPaused(): bool
71 | {
72 | clearstatcache();
73 | return file_exists(static::$lockFile);
74 | }
75 |
76 | /**
77 | * FileMonitor constructor.
78 | * @param $monitorDir
79 | * @param $monitorExtensions
80 | * @param array $options
81 | */
82 | public function __construct($monitorDir, $monitorExtensions, array $options = [])
83 | {
84 | static::resume();
85 | $this->paths = (array)$monitorDir;
86 | $this->extensions = $monitorExtensions;
87 | if (!Worker::getAllWorkers()) {
88 | return;
89 | }
90 | $disableFunctions = explode(',', ini_get('disable_functions'));
91 | if (in_array('exec', $disableFunctions, true)) {
92 | echo "\nMonitor file change turned off because exec() has been disabled by disable_functions setting in " . PHP_CONFIG_FILE_PATH . "/php.ini\n";
93 | } else {
94 | if ($options['enable_file_monitor'] ?? true) {
95 | Timer::add(1, function () {
96 | $this->checkAllFilesChange();
97 | });
98 | }
99 | }
100 |
101 | $memoryLimit = $this->getMemoryLimit($options['memory_limit'] ?? null);
102 | if ($memoryLimit && ($options['enable_memory_monitor'] ?? true)) {
103 | Timer::add(60, [$this, 'checkMemory'], [$memoryLimit]);
104 | }
105 | }
106 |
107 | /**
108 | * @param $monitorDir
109 | * @return bool
110 | */
111 | public function checkFilesChange($monitorDir): bool
112 | {
113 | static $lastMtime, $tooManyFilesCheck;
114 | if (!$lastMtime) {
115 | $lastMtime = time();
116 | }
117 | clearstatcache();
118 | if (!is_dir($monitorDir)) {
119 | if (!is_file($monitorDir)) {
120 | return false;
121 | }
122 | $iterator = [new SplFileInfo($monitorDir)];
123 | } else {
124 | // recursive traversal directory
125 | $dirIterator = new RecursiveDirectoryIterator($monitorDir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS);
126 | $iterator = new RecursiveIteratorIterator($dirIterator);
127 | }
128 | $count = 0;
129 | foreach ($iterator as $file) {
130 | $count ++;
131 | /** var SplFileInfo $file */
132 | if (is_dir($file->getRealPath())) {
133 | continue;
134 | }
135 | // check mtime
136 | if (in_array($file->getExtension(), $this->extensions, true) && $lastMtime < $file->getMTime()) {
137 | $var = 0;
138 | exec('"'.PHP_BINARY . '" -l ' . $file, $out, $var);
139 | $lastMtime = $file->getMTime();
140 | if ($var) {
141 | continue;
142 | }
143 | echo $file . " update and reload\n";
144 | // send SIGUSR1 signal to master process for reload
145 | if (DIRECTORY_SEPARATOR === '/') {
146 | posix_kill(posix_getppid(), SIGUSR1);
147 | } else {
148 | return true;
149 | }
150 | break;
151 | }
152 | }
153 | if (!$tooManyFilesCheck && $count > 1000) {
154 | echo "Monitor: There are too many files ($count files) in $monitorDir which makes file monitoring very slow\n";
155 | $tooManyFilesCheck = 1;
156 | }
157 | return false;
158 | }
159 |
160 | /**
161 | * @return bool
162 | */
163 | public function checkAllFilesChange(): bool
164 | {
165 | if (static::isPaused()) {
166 | return false;
167 | }
168 | foreach ($this->paths as $path) {
169 | if ($this->checkFilesChange($path)) {
170 | return true;
171 | }
172 | }
173 | return false;
174 | }
175 |
176 | /**
177 | * @param $memoryLimit
178 | * @return void
179 | */
180 | public function checkMemory($memoryLimit)
181 | {
182 | if (static::isPaused() || $memoryLimit <= 0) {
183 | return;
184 | }
185 | $ppid = posix_getppid();
186 | $childrenFile = "/proc/$ppid/task/$ppid/children";
187 | if (!is_file($childrenFile) || !($children = file_get_contents($childrenFile))) {
188 | return;
189 | }
190 | foreach (explode(' ', $children) as $pid) {
191 | $pid = (int)$pid;
192 | $statusFile = "/proc/$pid/status";
193 | if (!is_file($statusFile) || !($status = file_get_contents($statusFile))) {
194 | continue;
195 | }
196 | $mem = 0;
197 | if (preg_match('/VmRSS\s*?:\s*?(\d+?)\s*?kB/', $status, $match)) {
198 | $mem = $match[1];
199 | }
200 | $mem = (int)($mem / 1024);
201 | if ($mem >= $memoryLimit) {
202 | posix_kill($pid, SIGINT);
203 | }
204 | }
205 | }
206 |
207 | /**
208 | * Get memory limit
209 | * @return float
210 | */
211 | protected function getMemoryLimit($memoryLimit)
212 | {
213 | if ($memoryLimit === 0) {
214 | return 0;
215 | }
216 | $usePhpIni = false;
217 | if (!$memoryLimit) {
218 | $memoryLimit = ini_get('memory_limit');
219 | $usePhpIni = true;
220 | }
221 |
222 | if ($memoryLimit == -1) {
223 | return 0;
224 | }
225 | $unit = strtolower($memoryLimit[strlen($memoryLimit) - 1]);
226 | if ($unit === 'g') {
227 | $memoryLimit = 1024 * (int)$memoryLimit;
228 | } else if ($unit === 'm') {
229 | $memoryLimit = (int)$memoryLimit;
230 | } else if ($unit === 'k') {
231 | $memoryLimit = ((int)$memoryLimit / 1024);
232 | } else {
233 | $memoryLimit = ((int)$memoryLimit / (1024 * 1024));
234 | }
235 | if ($memoryLimit < 30) {
236 | $memoryLimit = 30;
237 | }
238 | if ($usePhpIni) {
239 | $memoryLimit = (int)(0.8 * $memoryLimit);
240 | }
241 | return $memoryLimit;
242 | }
243 | }
244 |
--------------------------------------------------------------------------------
/app/view/default/memo/save.Twig:
--------------------------------------------------------------------------------
1 | {% extends 'default/layout/base.Twig' %}
2 | {% block title %} 发言 {% endblock %}
3 |
4 | {% block content %}
5 |
6 |
57 |
58 | {% endblock %}
59 |
60 | {% block beforeScript %}
61 |
62 |
63 | {% endblock %}
64 |
65 | {% block afterScript %}
66 |
151 | {% endblock %}
--------------------------------------------------------------------------------
/app/view/default/component/memo.Twig:
--------------------------------------------------------------------------------
1 |
2 |
3 | 
5 |
6 |
7 | {{ memo.author.nickname }}
8 |
9 |
10 |
11 | {{ memo.content_html|raw }}
12 |
13 |
14 | 全文
15 |
16 |
17 |
18 | {{ memo.created_at|timeAgo }}
19 |
20 | {% if currentUser or sysConfig.config.anonymous_comment == 'on' %}
21 |
28 |
30 |
31 |
32 |
42 |
43 |
44 |
45 |
53 | 评论
54 |
55 |
56 | {% if currentUser.id == memo.author.id or currentUser.id == 1 %}
57 |
58 |
60 |
67 | 编辑
68 |
69 |
70 |
71 |
73 |
82 | 删除
83 |
84 | {% endif %}
85 |
86 |
87 | {% endif %}
88 |
89 |
90 |
91 |
166 |
167 |
168 |
169 |
--------------------------------------------------------------------------------
/support/helpers.php:
--------------------------------------------------------------------------------
1 |
11 | * @copyright walkor
12 | * @link http://www.workerman.net/
13 | * @license http://www.opensource.org/licenses/mit-license.php MIT License
14 | */
15 |
16 | use support\Container;
17 | use support\Request;
18 | use support\Response;
19 | use support\Translation;
20 | use support\view\Blade;
21 | use support\view\Raw;
22 | use support\view\ThinkPHP;
23 | use support\view\Twig;
24 | use Twig\Error\LoaderError;
25 | use Twig\Error\RuntimeError;
26 | use Twig\Error\SyntaxError;
27 | use Webman\App;
28 | use Webman\Config;
29 | use Webman\Route;
30 | use Workerman\Protocols\Http\Session;
31 | use Workerman\Worker;
32 |
33 | // Project base path
34 | define('BASE_PATH', dirname(__DIR__));
35 |
36 | /**
37 | * return the program execute directory
38 | * @param string $path
39 | * @return string
40 | */
41 | function run_path(string $path = ''): string
42 | {
43 | static $runPath = '';
44 | if (!$runPath) {
45 | $runPath = is_phar() ? dirname(Phar::running(false)) : BASE_PATH;
46 | }
47 | return path_combine($runPath, $path);
48 | }
49 |
50 | /**
51 | * if the param $path equal false,will return this program current execute directory
52 | * @param string|false $path
53 | * @return string
54 | */
55 | function base_path($path = ''): string
56 | {
57 | if (false === $path) {
58 | return run_path();
59 | }
60 | return path_combine(BASE_PATH, $path);
61 | }
62 |
63 | /**
64 | * App path
65 | * @param string $path
66 | * @return string
67 | */
68 | function app_path(string $path = ''): string
69 | {
70 | return path_combine(BASE_PATH . DIRECTORY_SEPARATOR . 'app', $path);
71 | }
72 |
73 | /**
74 | * Public path
75 | * @param string $path
76 | * @return string
77 | */
78 | function public_path(string $path = ''): string
79 | {
80 | static $publicPath = '';
81 | if (!$publicPath) {
82 | $publicPath = \config('app.public_path') ?: run_path('public');
83 | }
84 | return path_combine($publicPath, $path);
85 | }
86 |
87 | /**
88 | * Config path
89 | * @param string $path
90 | * @return string
91 | */
92 | function config_path(string $path = ''): string
93 | {
94 | return path_combine(BASE_PATH . DIRECTORY_SEPARATOR . 'config', $path);
95 | }
96 |
97 | /**
98 | * Runtime path
99 | * @param string $path
100 | * @return string
101 | */
102 | function runtime_path(string $path = ''): string
103 | {
104 | static $runtimePath = '';
105 | if (!$runtimePath) {
106 | $runtimePath = \config('app.runtime_path') ?: run_path('runtime');
107 | }
108 | return path_combine($runtimePath, $path);
109 | }
110 |
111 | /**
112 | * Generate paths based on given information
113 | * @param string $front
114 | * @param string $back
115 | * @return string
116 | */
117 | function path_combine(string $front, string $back): string
118 | {
119 | return $front . ($back ? (DIRECTORY_SEPARATOR . ltrim($back, DIRECTORY_SEPARATOR)) : $back);
120 | }
121 |
122 | /**
123 | * Response
124 | * @param int $status
125 | * @param array $headers
126 | * @param string $body
127 | * @return Response
128 | */
129 | function response(string $body = '', int $status = 200, array $headers = []): Response
130 | {
131 | return new Response($status, $headers, $body);
132 | }
133 |
134 | /**
135 | * Json response
136 | * @param $data
137 | * @param int $options
138 | * @return Response
139 | */
140 | function json($data, int $options = JSON_UNESCAPED_UNICODE): Response
141 | {
142 | return new Response(200, ['Content-Type' => 'application/json'], json_encode($data, $options));
143 | }
144 |
145 | /**
146 | * Xml response
147 | * @param $xml
148 | * @return Response
149 | */
150 | function xml($xml): Response
151 | {
152 | if ($xml instanceof SimpleXMLElement) {
153 | $xml = $xml->asXML();
154 | }
155 | return new Response(200, ['Content-Type' => 'text/xml'], $xml);
156 | }
157 |
158 | /**
159 | * Jsonp response
160 | * @param $data
161 | * @param string $callbackName
162 | * @return Response
163 | */
164 | function jsonp($data, string $callbackName = 'callback'): Response
165 | {
166 | if (!is_scalar($data) && null !== $data) {
167 | $data = json_encode($data);
168 | }
169 | return new Response(200, [], "$callbackName($data)");
170 | }
171 |
172 | /**
173 | * Redirect response
174 | * @param string $location
175 | * @param int $status
176 | * @param array $headers
177 | * @return Response
178 | */
179 | function redirect(string $location, int $status = 302, array $headers = []): Response
180 | {
181 | $response = new Response($status, ['Location' => $location]);
182 | if (!empty($headers)) {
183 | $response->withHeaders($headers);
184 | }
185 | return $response;
186 | }
187 |
188 | /**
189 | * View response
190 | * @param string $template
191 | * @param array $vars
192 | * @param string|null $app
193 | * @param string|null $plugin
194 | * @return Response
195 | */
196 | function view(string $template, array $vars = [], string $app = null, string $plugin = null): Response
197 | {
198 | $request = \request();
199 | $plugin = $plugin === null ? ($request->plugin ?? '') : $plugin;
200 | $handler = \config($plugin ? "plugin.$plugin.view.handler" : 'view.handler');
201 | return new Response(200, [], $handler::render($template, $vars, $app, $plugin));
202 | }
203 |
204 | /**
205 | * Raw view response
206 | * @param string $template
207 | * @param array $vars
208 | * @param string|null $app
209 | * @return Response
210 | * @throws Throwable
211 | */
212 | function raw_view(string $template, array $vars = [], string $app = null): Response
213 | {
214 | return new Response(200, [], Raw::render($template, $vars, $app));
215 | }
216 |
217 | /**
218 | * Blade view response
219 | * @param string $template
220 | * @param array $vars
221 | * @param string|null $app
222 | * @return Response
223 | */
224 | function blade_view(string $template, array $vars = [], string $app = null): Response
225 | {
226 | return new Response(200, [], Blade::render($template, $vars, $app));
227 | }
228 |
229 | /**
230 | * Think view response
231 | * @param string $template
232 | * @param array $vars
233 | * @param string|null $app
234 | * @return Response
235 | */
236 | function think_view(string $template, array $vars = [], string $app = null): Response
237 | {
238 | return new Response(200, [], ThinkPHP::render($template, $vars, $app));
239 | }
240 |
241 | /**
242 | * Twig view response
243 | * @param string $template
244 | * @param array $vars
245 | * @param string|null $app
246 | * @return Response
247 | * @throws LoaderError
248 | * @throws RuntimeError
249 | * @throws SyntaxError
250 | */
251 | function twig_view(string $template, array $vars = [], string $app = null): Response
252 | {
253 | return new Response(200, [], Twig::render($template, $vars, $app));
254 | }
255 |
256 | /**
257 | * Get request
258 | * @return \Webman\Http\Request|Request|null
259 | */
260 | function request()
261 | {
262 | return App::request();
263 | }
264 |
265 | /**
266 | * Get config
267 | * @param string|null $key
268 | * @param $default
269 | * @return array|mixed|null
270 | */
271 | function config(string $key = null, $default = null)
272 | {
273 | return Config::get($key, $default);
274 | }
275 |
276 | /**
277 | * Create url
278 | * @param string $name
279 | * @param ...$parameters
280 | * @return string
281 | */
282 | function route(string $name, ...$parameters): string
283 | {
284 | $route = Route::getByName($name);
285 | if (!$route) {
286 | return '';
287 | }
288 |
289 | if (!$parameters) {
290 | return $route->url();
291 | }
292 |
293 | if (is_array(current($parameters))) {
294 | $parameters = current($parameters);
295 | }
296 |
297 | return $route->url($parameters);
298 | }
299 |
300 | /**
301 | * Session
302 | * @param mixed $key
303 | * @param mixed $default
304 | * @return mixed|bool|Session
305 | */
306 | function session($key = null, $default = null)
307 | {
308 | $session = \request()->session();
309 | if (null === $key) {
310 | return $session;
311 | }
312 | if (is_array($key)) {
313 | $session->put($key);
314 | return null;
315 | }
316 | if (strpos($key, '.')) {
317 | $keyArray = explode('.', $key);
318 | $value = $session->all();
319 | foreach ($keyArray as $index) {
320 | if (!isset($value[$index])) {
321 | return $default;
322 | }
323 | $value = $value[$index];
324 | }
325 | return $value;
326 | }
327 | return $session->get($key, $default);
328 | }
329 |
330 | /**
331 | * Translation
332 | * @param string $id
333 | * @param array $parameters
334 | * @param string|null $domain
335 | * @param string|null $locale
336 | * @return string
337 | */
338 | function trans(string $id, array $parameters = [], string $domain = null, string $locale = null): string
339 | {
340 | $res = Translation::trans($id, $parameters, $domain, $locale);
341 | return $res === '' ? $id : $res;
342 | }
343 |
344 | /**
345 | * Locale
346 | * @param string|null $locale
347 | * @return string
348 | */
349 | function locale(string $locale = null): string
350 | {
351 | if (!$locale) {
352 | return Translation::getLocale();
353 | }
354 | Translation::setLocale($locale);
355 | return $locale;
356 | }
357 |
358 | /**
359 | * 404 not found
360 | * @return Response
361 | */
362 | function not_found(): Response
363 | {
364 | return new Response(404, [], file_get_contents(public_path() . '/404.html'));
365 | }
366 |
367 | /**
368 | * Copy dir
369 | * @param string $source
370 | * @param string $dest
371 | * @param bool $overwrite
372 | * @return void
373 | */
374 | function copy_dir(string $source, string $dest, bool $overwrite = false)
375 | {
376 | if (is_dir($source)) {
377 | if (!is_dir($dest)) {
378 | mkdir($dest);
379 | }
380 | $files = scandir($source);
381 | foreach ($files as $file) {
382 | if ($file !== "." && $file !== "..") {
383 | copy_dir("$source/$file", "$dest/$file", $overwrite);
384 | }
385 | }
386 | } else if (file_exists($source) && ($overwrite || !file_exists($dest))) {
387 | copy($source, $dest);
388 | }
389 | }
390 |
391 | /**
392 | * Remove dir
393 | * @param string $dir
394 | * @return bool
395 | */
396 | function remove_dir(string $dir): bool
397 | {
398 | if (is_link($dir) || is_file($dir)) {
399 | return unlink($dir);
400 | }
401 | $files = array_diff(scandir($dir), array('.', '..'));
402 | foreach ($files as $file) {
403 | (is_dir("$dir/$file") && !is_link($dir)) ? remove_dir("$dir/$file") : unlink("$dir/$file");
404 | }
405 | return rmdir($dir);
406 | }
407 |
408 | /**
409 | * Bind worker
410 | * @param $worker
411 | * @param $class
412 | */
413 | function worker_bind($worker, $class)
414 | {
415 | $callbackMap = [
416 | 'onConnect',
417 | 'onMessage',
418 | 'onClose',
419 | 'onError',
420 | 'onBufferFull',
421 | 'onBufferDrain',
422 | 'onWorkerStop',
423 | 'onWebSocketConnect',
424 | 'onWorkerReload'
425 | ];
426 | foreach ($callbackMap as $name) {
427 | if (method_exists($class, $name)) {
428 | $worker->$name = [$class, $name];
429 | }
430 | }
431 | if (method_exists($class, 'onWorkerStart')) {
432 | call_user_func([$class, 'onWorkerStart'], $worker);
433 | }
434 | }
435 |
436 | /**
437 | * Start worker
438 | * @param $processName
439 | * @param $config
440 | * @return void
441 | */
442 | function worker_start($processName, $config)
443 | {
444 | $worker = new Worker($config['listen'] ?? null, $config['context'] ?? []);
445 | $propertyMap = [
446 | 'count',
447 | 'user',
448 | 'group',
449 | 'reloadable',
450 | 'reusePort',
451 | 'transport',
452 | 'protocol',
453 | ];
454 | $worker->name = $processName;
455 | foreach ($propertyMap as $property) {
456 | if (isset($config[$property])) {
457 | $worker->$property = $config[$property];
458 | }
459 | }
460 |
461 | $worker->onWorkerStart = function ($worker) use ($config) {
462 | require_once base_path('/support/bootstrap.php');
463 | if (isset($config['handler'])) {
464 | if (!class_exists($config['handler'])) {
465 | echo "process error: class {$config['handler']} not exists\r\n";
466 | return;
467 | }
468 |
469 | $instance = Container::make($config['handler'], $config['constructor'] ?? []);
470 | worker_bind($worker, $instance);
471 | }
472 | };
473 | }
474 |
475 | /**
476 | * Get realpath
477 | * @param string $filePath
478 | * @return string
479 | */
480 | function get_realpath(string $filePath): string
481 | {
482 | if (strpos($filePath, 'phar://') === 0) {
483 | return $filePath;
484 | } else {
485 | return realpath($filePath);
486 | }
487 | }
488 |
489 | /**
490 | * Is phar
491 | * @return bool
492 | */
493 | function is_phar(): bool
494 | {
495 | return class_exists(Phar::class, false) && Phar::running();
496 | }
497 |
498 | /**
499 | * Get cpu count
500 | * @return int
501 | */
502 | function cpu_count(): int
503 | {
504 | // Windows does not support the number of processes setting.
505 | if (DIRECTORY_SEPARATOR === '\\') {
506 | return 1;
507 | }
508 | $count = 4;
509 | if (is_callable('shell_exec')) {
510 | if (strtolower(PHP_OS) === 'darwin') {
511 | $count = (int)shell_exec('sysctl -n machdep.cpu.core_count');
512 | } else {
513 | $count = (int)shell_exec('nproc');
514 | }
515 | }
516 | return $count > 0 ? $count : 4;
517 | }
518 |
519 | /**
520 | * Get request parameters, if no parameter name is passed, an array of all values is returned, default values is supported
521 | * @param string|null $param param's name
522 | * @param mixed|null $default default value
523 | * @return mixed|null
524 | */
525 | function input(string $param = null, $default = null)
526 | {
527 | return is_null($param) ? request()->all() : request()->input($param, $default);
528 | }
529 |
--------------------------------------------------------------------------------
/public/js/jquery.form.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * jQuery Form Plugin
3 | * version: 4.3.0
4 | * Requires jQuery v1.7.2 or later
5 | * Project repository: https://github.com/jquery-form/form
6 |
7 | * Copyright 2017 Kevin Morris
8 | * Copyright 2006 M. Alsup
9 |
10 | * Dual licensed under the LGPL-2.1+ or MIT licenses
11 | * https://github.com/jquery-form/form#license
12 |
13 | * This library is free software; you can redistribute it and/or
14 | * modify it under the terms of the GNU Lesser General Public
15 | * License as published by the Free Software Foundation; either
16 | * version 2.1 of the License, or (at your option) any later version.
17 | * This library is distributed in the hope that it will be useful,
18 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 | * Lesser General Public License for more details.
21 | */
22 | !function(r){"function"==typeof define&&define.amd?define(["jquery"],r):"object"==typeof module&&module.exports?module.exports=function(e,t){return void 0===t&&(t="undefined"!=typeof window?require("jquery"):require("jquery")(e)),r(t),t}:r(jQuery)}(function(q){"use strict";var m=/\r?\n/g,S={};S.fileapi=void 0!==q('').get(0).files,S.formdata=void 0!==window.FormData;var _=!!q.fn.prop;function o(e){var t=e.data;e.isDefaultPrevented()||(e.preventDefault(),q(e.target).closest("form").ajaxSubmit(t))}function i(e){var t=e.target,r=q(t);if(!r.is("[type=submit],[type=image]")){var a=r.closest("[type=submit]");if(0===a.length)return;t=a[0]}var n,o=t.form;"image"===(o.clk=t).type&&(void 0!==e.offsetX?(o.clk_x=e.offsetX,o.clk_y=e.offsetY):"function"==typeof q.fn.offset?(n=r.offset(),o.clk_x=e.pageX-n.left,o.clk_y=e.pageY-n.top):(o.clk_x=e.pageX-t.offsetLeft,o.clk_y=e.pageY-t.offsetTop)),setTimeout(function(){o.clk=o.clk_x=o.clk_y=null},100)}function N(){var e;q.fn.ajaxSubmit.debug&&(e="[jquery.form] "+Array.prototype.join.call(arguments,""),window.console&&window.console.log?window.console.log(e):window.opera&&window.opera.postError&&window.opera.postError(e))}q.fn.attr2=function(){if(!_)return this.attr.apply(this,arguments);var e=this.prop.apply(this,arguments);return e&&e.jquery||"string"==typeof e?e:this.attr.apply(this,arguments)},q.fn.ajaxSubmit=function(M,e,t,r){if(!this.length)return N("ajaxSubmit: skipping submit process - no element selected"),this;var O,a,n,o,X=this;"function"==typeof M?M={success:M}:"string"==typeof M||!1===M&&0',s)).css({position:"absolute",top:"-1000px",left:"-1000px"}),m=d[0],p={aborted:0,responseText:null,responseXML:null,status:0,statusText:"n/a",getAllResponseHeaders:function(){},getResponseHeader:function(){},setRequestHeader:function(){},abort:function(e){var t="timeout"===e?"timeout":"aborted";N("aborting upload... "+t),this.aborted=1;try{m.contentWindow.document.execCommand&&m.contentWindow.document.execCommand("Stop")}catch(e){}d.attr("src",l.iframeSrc),p.error=t,l.error&&l.error.call(l.context,p,t,e),f&&q.event.trigger("ajaxError",[p,l,t]),l.complete&&l.complete.call(l.context,p,t)}},(f=l.global)&&0==q.active++&&q.event.trigger("ajaxStart"),f&&q.event.trigger("ajaxSend",[p,l]),l.beforeSend&&!1===l.beforeSend.call(l.context,p,l))return l.global&&q.active--,g.reject(),g;if(p.aborted)return g.reject(),g;(a=i.clk)&&(n=a.name)&&!a.disabled&&(l.extraData=l.extraData||{},l.extraData[n]=a.value,"image"===a.type&&(l.extraData[n+".x"]=i.clk_x,l.extraData[n+".y"]=i.clk_y));var x=1,y=2;function b(t){var r=null;try{t.contentWindow&&(r=t.contentWindow.document)}catch(e){N("cannot get iframe.contentWindow document: "+e)}if(r)return r;try{r=t.contentDocument?t.contentDocument:t.document}catch(e){N("cannot get iframe.contentDocument: "+e),r=t.document}return r}var c=q("meta[name=csrf-token]").attr("content"),T=q("meta[name=csrf-param]").attr("content");function j(){var e=X.attr2("target"),t=X.attr2("action"),r=X.attr("enctype")||X.attr("encoding")||"multipart/form-data";i.setAttribute("target",o),O&&!/post/i.test(O)||i.setAttribute("method","POST"),t!==l.url&&i.setAttribute("action",l.url),l.skipEncodingOverride||O&&!/post/i.test(O)||X.attr({encoding:"multipart/form-data",enctype:"multipart/form-data"}),l.timeout&&(v=setTimeout(function(){h=!0,A(x)},l.timeout));var a=[];try{if(l.extraData)for(var n in l.extraData)l.extraData.hasOwnProperty(n)&&(q.isPlainObject(l.extraData[n])&&l.extraData[n].hasOwnProperty("name")&&l.extraData[n].hasOwnProperty("value")?a.push(q('',s).val(l.extraData[n].value).appendTo(i)[0]):a.push(q('',s).val(l.extraData[n]).appendTo(i)[0]));l.iframeTarget||d.appendTo(u),m.attachEvent?m.attachEvent("onload",A):m.addEventListener("load",A,!1),setTimeout(function e(){try{var t=b(m).readyState;N("state = "+t),t&&"uninitialized"===t.toLowerCase()&&setTimeout(e,50)}catch(e){N("Server abort: ",e," (",e.name,")"),A(y),v&&clearTimeout(v),v=void 0}},15);try{i.submit()}catch(e){document.createElement("form").submit.apply(i)}}finally{i.setAttribute("action",t),i.setAttribute("enctype",r),e?i.setAttribute("target",e):X.removeAttr("target"),q(a).remove()}}T&&c&&(l.extraData=l.extraData||{},l.extraData[T]=c),l.forceSync?j():setTimeout(j,10);var w,S,k,D=50;function A(e){if(!p.aborted&&!k){if((S=b(m))||(N("cannot access response document"),e=y),e===x&&p)return p.abort("timeout"),void g.reject(p,"timeout");if(e===y&&p)return p.abort("server abort"),void g.reject(p,"error","server abort");if(S&&S.location.href!==l.iframeSrc||h){m.detachEvent?m.detachEvent("onload",A):m.removeEventListener("load",A,!1);var t,r="success";try{if(h)throw"timeout";var a="xml"===l.dataType||S.XMLDocument||q.isXMLDoc(S);if(N("isXml="+a),!a&&window.opera&&(null===S.body||!S.body.innerHTML)&&--D)return N("requeing onLoad callback, DOM not available"),void setTimeout(A,250);var n=S.body?S.body:S.documentElement;p.responseText=n?n.innerHTML:null,p.responseXML=S.XMLDocument?S.XMLDocument:S,a&&(l.dataType="xml"),p.getResponseHeader=function(e){return{"content-type":l.dataType}[e.toLowerCase()]},n&&(p.status=Number(n.getAttribute("status"))||p.status,p.statusText=n.getAttribute("statusText")||p.statusText);var o,i,s,u=(l.dataType||"").toLowerCase(),c=/(json|script|text)/.test(u);c||l.textarea?(o=S.getElementsByTagName("textarea")[0])?(p.responseText=o.value,p.status=Number(o.getAttribute("status"))||p.status,p.statusText=o.getAttribute("statusText")||p.statusText):c&&(i=S.getElementsByTagName("pre")[0],s=S.getElementsByTagName("body")[0],i?p.responseText=i.textContent?i.textContent:i.innerText:s&&(p.responseText=s.textContent?s.textContent:s.innerText)):"xml"===u&&!p.responseXML&&p.responseText&&(p.responseXML=F(p.responseText));try{w=E(p,u,l)}catch(e){r="parsererror",p.error=t=e||r}}catch(e){N("error caught: ",e),r="error",p.error=t=e||r}p.aborted&&(N("upload aborted"),r=null),p.status&&(r=200<=p.status&&p.status<300||304===p.status?"success":"error"),"success"===r?(l.success&&l.success.call(l.context,w,"success",p),g.resolve(p.responseText,"success",p),f&&q.event.trigger("ajaxSuccess",[p,l])):r&&(void 0===t&&(t=p.statusText),l.error&&l.error.call(l.context,p,r,t),g.reject(p,"error",t),f&&q.event.trigger("ajaxError",[p,l,t])),f&&q.event.trigger("ajaxComplete",[p,l]),f&&!--q.active&&q.event.trigger("ajaxStop"),l.complete&&l.complete.call(l.context,p,r),k=!0,l.timeout&&clearTimeout(v),setTimeout(function(){l.iframeTarget?d.attr("src",l.iframeSrc):d.remove(),p.responseXML=null},100)}}}var F=q.parseXML||function(e,t){return window.ActiveXObject?((t=new ActiveXObject("Microsoft.XMLDOM")).async="false",t.loadXML(e)):t=(new DOMParser).parseFromString(e,"text/xml"),t&&t.documentElement&&"parsererror"!==t.documentElement.nodeName?t:null},L=q.parseJSON||function(e){return window.eval("("+e+")")},E=function(e,t,r){var a=e.getResponseHeader("content-type")||"",n=("xml"===t||!t)&&0<=a.indexOf("xml"),o=n?e.responseXML:e.responseText;return n&&"parsererror"===o.documentElement.nodeName&&q.error&&q.error("parsererror"),r&&r.dataFilter&&(o=r.dataFilter(o,t)),"string"==typeof o&&(("json"===t||!t)&&0<=a.indexOf("json")?o=L(o):("script"===t||!t)&&0<=a.indexOf("javascript")&&q.globalEval(o)),o};return g}},q.fn.ajaxForm=function(e,t,r,a){if(("string"==typeof e||!1===e&&0.fancybox__content{padding:0;background:rgba(0,0,0,0);min-height:1px;background-repeat:no-repeat;background-size:contain;background-position:center center;transition:none;transform:translate3d(0, 0, 0);backface-visibility:hidden}.fancybox__slide.has-image>.fancybox__content>picture>img{width:100%;height:auto;max-height:100%}.is-animating .fancybox__content,.is-dragging .fancybox__content{will-change:transform,width,height}.fancybox-image{margin:auto;display:block;width:100%;height:100%;min-height:0;object-fit:contain;user-select:none;filter:blur(0px)}.fancybox__caption{align-self:center;max-width:100%;flex-shrink:0;margin:0;padding:14px 0 4px 0;overflow-wrap:anywhere;line-height:1.375;color:var(--fancybox-color, currentColor);opacity:var(--fancybox-opacity, 1);cursor:auto;visibility:visible}.is-loading .fancybox__caption,.is-closing .fancybox__caption{opacity:0;visibility:hidden}.is-compact .fancybox__caption{padding-bottom:0}.f-button.is-close-btn{--f-button-svg-stroke-width: 2;position:absolute;top:0;right:8px;z-index:40}.fancybox__content>.f-button.is-close-btn{--f-button-width: 34px;--f-button-height: 34px;--f-button-border-radius: 4px;--f-button-color: var(--fancybox-color, #fff);--f-button-hover-color: var(--fancybox-color, #fff);--f-button-bg: transparent;--f-button-hover-bg: transparent;--f-button-active-bg: transparent;--f-button-svg-width: 22px;--f-button-svg-height: 22px;position:absolute;top:-38px;right:0;opacity:.75}.is-loading .fancybox__content>.f-button.is-close-btn{visibility:hidden}.is-zooming-out .fancybox__content>.f-button.is-close-btn{visibility:hidden}.fancybox__content>.f-button.is-close-btn:hover{opacity:1}.fancybox__footer{padding:0;margin:0;position:relative}.fancybox__footer .fancybox__caption{width:100%;padding:24px;opacity:var(--fancybox-opacity, 1);transition:all .25s ease}.is-compact .fancybox__footer{position:absolute;bottom:0;left:0;right:0;z-index:20;background:rgba(24,24,27,.5)}.is-compact .fancybox__footer .fancybox__caption{padding:12px}.is-compact .fancybox__content>.f-button.is-close-btn{--f-button-border-radius: 50%;--f-button-color: #fff;--f-button-hover-color: #fff;--f-button-outline-color: #000;--f-button-bg: rgba(0, 0, 0, 0.6);--f-button-active-bg: rgba(0, 0, 0, 0.6);--f-button-hover-bg: rgba(0, 0, 0, 0.6);--f-button-svg-width: 18px;--f-button-svg-height: 18px;--f-button-svg-filter: none;top:5px;right:5px}.fancybox__nav{--f-button-width: 50px;--f-button-height: 50px;--f-button-border: 0;--f-button-border-radius: 50%;--f-button-color: var(--fancybox-color);--f-button-hover-color: var(--fancybox-hover-color);--f-button-bg: transparent;--f-button-hover-bg: rgba(24, 24, 27, 0.3);--f-button-active-bg: rgba(24, 24, 27, 0.5);--f-button-shadow: none;--f-button-transition: all 0.15s ease;--f-button-transform: none;--f-button-svg-width: 26px;--f-button-svg-height: 26px;--f-button-svg-stroke-width: 2.5;--f-button-svg-fill: none;--f-button-svg-filter: drop-shadow(1px 1px 1px rgba(24, 24, 27, 0.5));--f-button-svg-disabled-opacity: 0.65;--f-button-next-pos: 1rem;--f-button-prev-pos: 1rem;opacity:var(--fancybox-opacity, 1)}.fancybox__nav .f-button:before{position:absolute;content:"";top:-30px;right:-20px;left:-20px;bottom:-30px;z-index:1}.is-idle .fancybox__nav{animation:.15s ease-out both f-fadeOut}.is-idle.is-compact .fancybox__footer{pointer-events:none;animation:.15s ease-out both f-fadeOut}.fancybox__slide>.f-spinner{position:absolute;top:50%;left:50%;margin:var(--f-spinner-top, calc(var(--f-spinner-width) * -0.5)) 0 0 var(--f-spinner-left, calc(var(--f-spinner-height) * -0.5));z-index:30;cursor:pointer}.fancybox-protected{position:absolute;top:0;left:0;right:0;bottom:0;z-index:40;user-select:none}.fancybox-ghost{position:absolute;top:0;left:0;width:100%;height:100%;min-height:0;object-fit:contain;z-index:40;user-select:none;pointer-events:none}.fancybox-focus-guard{outline:none;opacity:0;position:fixed;pointer-events:none}.fancybox__container:not([aria-hidden]){opacity:0}.fancybox__container.is-animated[aria-hidden=false]>*:not(.fancybox__backdrop,.fancybox__carousel),.fancybox__container.is-animated[aria-hidden=false] .fancybox__carousel>*:not(.fancybox__viewport),.fancybox__container.is-animated[aria-hidden=false] .fancybox__slide>*:not(.fancybox__content){animation:var(--f-interface-enter-duration, 0.25s) ease .1s backwards f-fadeIn}.fancybox__container.is-animated[aria-hidden=false] .fancybox__backdrop{animation:var(--f-backdrop-enter-duration, 0.35s) ease backwards f-fadeIn}.fancybox__container.is-animated[aria-hidden=true]>*:not(.fancybox__backdrop,.fancybox__carousel),.fancybox__container.is-animated[aria-hidden=true] .fancybox__carousel>*:not(.fancybox__viewport),.fancybox__container.is-animated[aria-hidden=true] .fancybox__slide>*:not(.fancybox__content){animation:var(--f-interface-exit-duration, 0.15s) ease forwards f-fadeOut}.fancybox__container.is-animated[aria-hidden=true] .fancybox__backdrop{animation:var(--f-backdrop-exit-duration, 0.35s) ease forwards f-fadeOut}.has-iframe .fancybox__content,.has-map .fancybox__content,.has-pdf .fancybox__content,.has-youtube .fancybox__content,.has-vimeo .fancybox__content,.has-html5video .fancybox__content{max-width:100%;flex-shrink:1;min-height:1px;overflow:visible}.has-iframe .fancybox__content,.has-map .fancybox__content,.has-pdf .fancybox__content{width:calc(100% - 120px);height:90%}.fancybox__container.is-compact .has-iframe .fancybox__content,.fancybox__container.is-compact .has-map .fancybox__content,.fancybox__container.is-compact .has-pdf .fancybox__content{width:100%;height:100%}.has-youtube .fancybox__content,.has-vimeo .fancybox__content,.has-html5video .fancybox__content{width:960px;height:540px;max-width:100%;max-height:100%}.has-map .fancybox__content,.has-pdf .fancybox__content,.has-youtube .fancybox__content,.has-vimeo .fancybox__content,.has-html5video .fancybox__content{padding:0;background:rgba(24,24,27,.9);color:#fff}.has-map .fancybox__content{background:#e5e3df}.fancybox__html5video,.fancybox__iframe{border:0;display:block;height:100%;width:100%;background:rgba(0,0,0,0)}.fancybox-placeholder{border:0 !important;clip:rect(1px, 1px, 1px, 1px) !important;-webkit-clip-path:inset(50%) !important;clip-path:inset(50%) !important;height:1px !important;margin:-1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important;white-space:nowrap !important}.f-carousel__thumbs{--f-thumb-width: 96px;--f-thumb-height: 72px;--f-thumb-outline: 0;--f-thumb-outline-color: #5eb0ef;--f-thumb-opacity: 1;--f-thumb-hover-opacity: 1;--f-thumb-selected-opacity: 1;--f-thumb-border-radius: 2px;--f-thumb-offset: 0px;--f-button-next-pos: 0;--f-button-prev-pos: 0}.f-carousel__thumbs.is-classic{--f-thumb-gap: 8px;--f-thumb-opacity: 0.5;--f-thumb-hover-opacity: 1;--f-thumb-selected-opacity: 1}.f-carousel__thumbs.is-modern{--f-thumb-gap: 4px;--f-thumb-extra-gap: 16px;--f-thumb-clip-width: 46px}.f-thumbs{position:relative;flex:0 0 auto;margin:0;overflow:hidden;-webkit-tap-highlight-color:rgba(0,0,0,0);user-select:none;perspective:1000px;transform:translateZ(0)}.f-thumbs .f-spinner{position:absolute;top:0;left:0;width:100%;height:100%;border-radius:2px;background-image:linear-gradient(#ebeff2, #e2e8f0);z-index:-1}.f-thumbs .f-spinner svg{display:none}.f-thumbs.is-vertical{height:100%}.f-thumbs__viewport{width:100%;height:auto;overflow:hidden;transform:translate3d(0, 0, 0)}.f-thumbs__track{display:flex}.f-thumbs__slide{position:relative;flex:0 0 auto;box-sizing:content-box;display:flex;align-items:center;justify-content:center;padding:0;margin:0;width:var(--f-thumb-width);height:var(--f-thumb-height);overflow:visible;cursor:pointer}.f-thumbs__slide.is-loading img{opacity:0}.is-classic .f-thumbs__viewport{height:100%}.is-modern .f-thumbs__track{width:max-content}.is-modern .f-thumbs__track::before{content:"";position:absolute;top:0;bottom:0;left:calc((var(--f-thumb-clip-width, 0))*-0.5);width:calc(var(--width, 0)*1px + var(--f-thumb-clip-width, 0));cursor:pointer}.is-modern .f-thumbs__slide{width:var(--f-thumb-clip-width);transform:translate3d(calc(var(--shift, 0) * -1px), 0, 0);transition:none;pointer-events:none}.is-modern.is-resting .f-thumbs__slide{transition:transform .33s ease}.is-modern.is-resting .f-thumbs__slide__button{transition:clip-path .33s ease}.is-using-tab .is-modern .f-thumbs__slide:focus-within{filter:drop-shadow(-1px 0px 0px var(--f-thumb-outline-color)) drop-shadow(2px 0px 0px var(--f-thumb-outline-color)) drop-shadow(0px -1px 0px var(--f-thumb-outline-color)) drop-shadow(0px 2px 0px var(--f-thumb-outline-color))}.f-thumbs__slide__button{appearance:none;width:var(--f-thumb-width);height:100%;margin:0 -100% 0 -100%;padding:0;border:0;position:relative;border-radius:var(--f-thumb-border-radius);overflow:hidden;background:rgba(0,0,0,0);outline:none;cursor:pointer;pointer-events:auto;touch-action:manipulation;opacity:var(--f-thumb-opacity);transition:opacity .2s ease}.f-thumbs__slide__button:hover{opacity:var(--f-thumb-hover-opacity)}.f-thumbs__slide__button:focus:not(:focus-visible){outline:none}.f-thumbs__slide__button:focus-visible{outline:none;opacity:var(--f-thumb-selected-opacity)}.is-modern .f-thumbs__slide__button{--clip-path: inset( 0 calc( ((var(--f-thumb-width, 0) - var(--f-thumb-clip-width, 0))) * (1 - var(--progress, 0)) * 0.5 ) round var(--f-thumb-border-radius, 0) );clip-path:var(--clip-path)}.is-classic .is-nav-selected .f-thumbs__slide__button{opacity:var(--f-thumb-selected-opacity)}.is-classic .is-nav-selected .f-thumbs__slide__button::after{content:"";position:absolute;top:0;left:0;right:0;height:auto;bottom:0;border:var(--f-thumb-outline, 0) solid var(--f-thumb-outline-color, transparent);border-radius:var(--f-thumb-border-radius);animation:f-fadeIn .2s ease-out;z-index:10}.f-thumbs__slide__img{overflow:hidden;position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;margin:0;padding:var(--f-thumb-offset);box-sizing:border-box;pointer-events:none;object-fit:cover;border-radius:var(--f-thumb-border-radius)}.f-thumbs.is-horizontal .f-thumbs__track{padding:8px 0 12px 0}.f-thumbs.is-horizontal .f-thumbs__slide{margin:0 var(--f-thumb-gap) 0 0}.f-thumbs.is-vertical .f-thumbs__track{flex-wrap:wrap;padding:0 8px}.f-thumbs.is-vertical .f-thumbs__slide{margin:0 0 var(--f-thumb-gap) 0}.fancybox__thumbs{--f-thumb-width: 96px;--f-thumb-height: 72px;--f-thumb-border-radius: 2px;--f-thumb-outline: 2px;--f-thumb-outline-color: #ededed;position:relative;opacity:var(--fancybox-opacity, 1);transition:max-height .35s cubic-bezier(0.23, 1, 0.32, 1)}.fancybox__thumbs.is-classic{--f-thumb-gap: 8px;--f-thumb-opacity: 0.5;--f-thumb-hover-opacity: 1}.fancybox__thumbs.is-classic .f-spinner{background-image:linear-gradient(rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05))}.fancybox__thumbs.is-modern{--f-thumb-gap: 4px;--f-thumb-extra-gap: 16px;--f-thumb-clip-width: 46px;--f-thumb-opacity: 1;--f-thumb-hover-opacity: 1}.fancybox__thumbs.is-modern .f-spinner{background-image:linear-gradient(rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05))}.fancybox__thumbs.is-horizontal{padding:0 var(--f-thumb-gap)}.fancybox__thumbs.is-vertical{padding:var(--f-thumb-gap) 0}.is-compact .fancybox__thumbs{--f-thumb-width: 64px;--f-thumb-clip-width: 32px;--f-thumb-height: 48px;--f-thumb-extra-gap: 10px}.fancybox__thumbs.is-masked{max-height:0px !important}.is-closing .fancybox__thumbs{transition:none !important}.fancybox__toolbar{--f-progress-color: var(--fancybox-color, rgba(255, 255, 255, 0.94));--f-button-width: 46px;--f-button-height: 46px;--f-button-color: var(--fancybox-color);--f-button-hover-color: var(--fancybox-hover-color);--f-button-bg: rgba(24, 24, 27, 0.65);--f-button-hover-bg: rgba(70, 70, 73, 0.65);--f-button-active-bg: rgba(90, 90, 93, 0.65);--f-button-border-radius: 0;--f-button-svg-width: 24px;--f-button-svg-height: 24px;--f-button-svg-stroke-width: 1.5;--f-button-svg-filter: drop-shadow(1px 1px 1px rgba(24, 24, 27, 0.15));--f-button-svg-fill: none;--f-button-svg-disabled-opacity: 0.65;display:flex;flex-direction:row;justify-content:space-between;margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI Adjusted","Segoe UI","Liberation Sans",sans-serif;color:var(--fancybox-color, currentColor);opacity:var(--fancybox-opacity, 1);text-shadow:var(--fancybox-toolbar-text-shadow, 1px 1px 1px rgba(0, 0, 0, 0.5));pointer-events:none;z-index:20}.fancybox__toolbar :focus-visible{z-index:1}.fancybox__toolbar.is-absolute,.is-compact .fancybox__toolbar{position:absolute;top:0;left:0;right:0}.is-idle .fancybox__toolbar{pointer-events:none;animation:.15s ease-out both f-fadeOut}.fancybox__toolbar__column{display:flex;flex-direction:row;flex-wrap:wrap;align-content:flex-start}.fancybox__toolbar__column.is-left,.fancybox__toolbar__column.is-right{flex-grow:1;flex-basis:0}.fancybox__toolbar__column.is-right{display:flex;justify-content:flex-end;flex-wrap:nowrap}.fancybox__infobar{padding:0 5px;line-height:var(--f-button-height);text-align:center;font-size:17px;font-variant-numeric:tabular-nums;-webkit-font-smoothing:subpixel-antialiased;cursor:default;user-select:none}.fancybox__infobar span{padding:0 5px}.fancybox__infobar:not(:first-child):not(:last-child){background:var(--f-button-bg)}[data-fancybox-toggle-slideshow]{position:relative}[data-fancybox-toggle-slideshow] .f-progress{height:100%;opacity:.3}[data-fancybox-toggle-slideshow] svg g:first-child{display:flex}[data-fancybox-toggle-slideshow] svg g:last-child{display:none}.has-slideshow [data-fancybox-toggle-slideshow] svg g:first-child{display:none}.has-slideshow [data-fancybox-toggle-slideshow] svg g:last-child{display:flex}[data-fancybox-toggle-fullscreen] svg g:first-child{display:flex}[data-fancybox-toggle-fullscreen] svg g:last-child{display:none}:fullscreen [data-fancybox-toggle-fullscreen] svg g:first-child{display:none}:fullscreen [data-fancybox-toggle-fullscreen] svg g:last-child{display:flex}.f-progress{position:absolute;top:0;left:0;right:0;height:3px;transform:scaleX(0);transform-origin:0;transition-property:transform;transition-timing-function:linear;background:var(--f-progress-color, var(--f-carousel-theme-color, #0091ff));z-index:30;user-select:none;pointer-events:none}
--------------------------------------------------------------------------------
|
|---|