kuaifan 5 лет назад
Родитель
Сommit
f8ff3beb03

+ 1 - 1
_ide_helper.php

@@ -3,7 +3,7 @@
 
 /**
  * A helper file for Laravel, to provide autocomplete information to your IDE
- * Generated for Laravel 7.10.3 on 2020-05-09 00:04:59.
+ * Generated for Laravel 7.10.3 on 2020-05-20 22:34:58.
  *
  * This file should not be included in your code, only analyzed by your IDE!
  *

+ 178 - 0
app/Services/WebSocketService.php

@@ -0,0 +1,178 @@
+<?php
+
+namespace App\Services;
+
+use App\Module\Base;
+use Cache;
+use DB;
+use Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface;
+use Swoole\Http\Request;
+use Swoole\WebSocket\Frame;
+use Swoole\WebSocket\Server;
+
+/**
+ * @see https://wiki.swoole.com/#/start/start_ws_server
+ */
+class WebSocketService implements WebSocketHandlerInterface
+{
+    /**
+     * 声明没有参数的构造函数
+     * WebSocketService constructor.
+     */
+    public function __construct()
+    {
+
+    }
+
+    /**
+     * 连接建立时触发
+     * @param Server $server
+     * @param Request $request
+     */
+    public function onOpen(Server $server, Request $request)
+    {
+        $to = $request->fd;
+        if (!isset($request->get['token'])) {
+            $server->push($to, Base::array2json([
+                'messageType' => 'error',
+                'type' => 'user',
+                'sender' => null,
+                'target' => null,
+                'content' => [
+                    'error' => '参数错误'
+                ],
+                'time' => Base::time()
+            ]));
+            $server->close($to);
+            self::forgetUser($to);
+            return;
+        }
+        //
+        $token = $request->get['token'];
+        $cacheKey = "ws-token:" . md5($token);
+        $username = Cache::remember($cacheKey, now()->addSeconds(1), function () use ($token) {
+            list($id, $username, $encrypt, $timestamp) = explode("@", base64_decode($token) . "@@@@");
+            if (intval($id) > 0 && intval($timestamp) + 2592000 > Base::time()) {
+                if (DB::table('users')->where(['id' => $id, 'username' => $username, 'encrypt' => $encrypt])->exists()) {
+                    return $username;
+                }
+            }
+            return null;
+        });
+        if (empty($username)) {
+            Cache::forget($cacheKey);
+            $server->push($to, Base::array2json([
+                'messageType' => 'error',
+                'type' => 'user',
+                'sender' => null,
+                'target' => null,
+                'content' => [
+                    'error' => '会员不存在',
+                ],
+                'time' => Base::time()
+            ]));
+            $server->close($to);
+            self::forgetUser($to);
+            return;
+        }
+        //
+        self::saveUser($to, $username);
+        $server->push($to, Base::array2json([
+            'messageType' => 'open',
+            'type' => 'user',
+            'sender' => null,
+            'target' => null,
+            'content' => [
+                'swid' => $to,
+            ],
+            'time' => Base::time()
+        ]));
+    }
+
+    /**
+     * 收到消息时触发
+     * @param Server $server
+     * @param Frame $frame
+     */
+    public function onMessage(Server $server, Frame $frame)
+    {
+        $data = Base::json2array($frame->data);
+        switch ($data['type']) {
+            case 'user':
+                $to = self::name2fs($data['target']);
+                if ($to) {
+                    $server->push($to, Base::array2json($data));
+                }
+                break;
+
+            case 'all':
+                foreach (self::getUsers() as $user) {
+                    $data['target'] = $user['username'];
+                    $server->push($user['wsid'], Base::array2json($data));
+                }
+                break;
+        }
+    }
+
+    /**
+     * 关闭连接时触发
+     * @param Server $server
+     * @param $fd
+     * @param $reactorId
+     */
+    public function onClose(Server $server, $fd, $reactorId)
+    {
+        self::forgetUser($fd);
+    }
+
+    /** ****************************************************************************** */
+    /** ****************************************************************************** */
+    /** ****************************************************************************** */
+
+    /**
+     * 缓存用户信息
+     * @param $fd
+     * @param $username
+     */
+    public static function saveUser($fd, $username)
+    {
+        DB::table('users')->where('wsid', $fd)->update(['wsid' => 0]);
+        DB::table('users')->where('username', $username)->update(['wsid' => $fd]);
+    }
+
+    /**
+     * 清除用户缓存
+     * @param $fd
+     */
+    public static function forgetUser($fd)
+    {
+        DB::table('users')->where('wsid', $fd)->update(['wsid' => 0]);
+    }
+
+    /**
+     * 获取当前用户
+     * @return array|string
+     */
+    public static function getUsers()
+    {
+        return Base::DBC2A(DB::table('users')->select(['wsid', 'username'])->where('wsid', '>', 0)->get());
+    }
+
+    /**
+     * @param $fd
+     * @return mixed
+     */
+    public static function fd2name($fd)
+    {
+        return DB::table('users')->select(['username'])->where('wsid', $fd)->value('username');
+    }
+
+    /**
+     * @param $username
+     * @return mixed
+     */
+    public static function name2fs($username)
+    {
+        return DB::table('users')->select(['wsid'])->where('username', $username)->value('wsid');
+    }
+}

+ 26 - 0
bin/fswatch

@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+WORK_DIR=$1
+if [ ! -n "${WORK_DIR}" ] ;then
+    WORK_DIR="."
+fi
+
+echo "Restarting LaravelS..."
+./bin/laravels restart -d -i
+
+echo "Starting fswatch..."
+LOCKING=0
+fswatch -e ".*" -i "\\.php$" -r ${WORK_DIR} | while read file
+do
+    if [[ ! ${file} =~ .php$ ]] ;then
+        continue
+    fi
+    if [ ${LOCKING} -eq 1 ] ;then
+        echo "Reloading, skipped."
+        continue
+    fi
+    echo "File ${file} has been modified."
+    LOCKING=1
+    ./bin/laravels reload
+    LOCKING=0
+done
+exit 0

+ 28 - 0
bin/inotify

@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+WORK_DIR=$1
+if [ ! -n "${WORK_DIR}" ] ;then
+    WORK_DIR="."
+fi
+
+echo "Restarting LaravelS..."
+./bin/laravels restart -d -i
+
+echo "Starting inotifywait..."
+LOCKING=0
+
+inotifywait --event modify --event create --event move --event delete -mrq   ${WORK_DIR}  | while read file
+
+do
+    if [[ ! ${file} =~ .php$ ]] ;then
+        continue
+    fi
+    if [ ${LOCKING} -eq 1 ] ;then
+        echo "Reloading, skipped."
+        continue
+    fi
+    echo "File ${file} has been modified."
+    LOCKING=1
+    ./bin/laravels reload
+    LOCKING=0
+done
+exit 0

+ 165 - 0
bin/laravels

@@ -0,0 +1,165 @@
+#!/usr/bin/env php
+<?php
+
+/**
+ * This autoloader is only used to pull laravel-s.
+ * Class Psr4Autoloader
+ */
+class Psr4Autoloader
+{
+    /**
+     * An associative array where the key is a namespace prefix and the value
+     * is an array of base directories for classes in that namespace.
+     *
+     * @var array
+     */
+    protected $prefixes = array();
+
+    /**
+     * Register loader with SPL autoloader stack.
+     *
+     * @return void
+     */
+    public function register()
+    {
+        spl_autoload_register(array($this, 'loadClass'));
+    }
+
+    /**
+     * Adds a base directory for a namespace prefix.
+     *
+     * @param string $prefix The namespace prefix.
+     * @param string $base_dir A base directory for class files in the
+     * namespace.
+     * @param bool $prepend If true, prepend the base directory to the stack
+     * instead of appending it; this causes it to be searched first rather
+     * than last.
+     * @return void
+     */
+    public function addNamespace($prefix, $base_dir, $prepend = false)
+    {
+        // normalize namespace prefix
+        $prefix = trim($prefix, '\\') . '\\';
+
+        // normalize the base directory with a trailing separator
+        $base_dir = rtrim($base_dir, DIRECTORY_SEPARATOR) . '/';
+
+        // initialize the namespace prefix array
+        if (isset($this->prefixes[$prefix]) === false) {
+            $this->prefixes[$prefix] = array();
+        }
+
+        // retain the base directory for the namespace prefix
+        if ($prepend) {
+            array_unshift($this->prefixes[$prefix], $base_dir);
+        } else {
+            array_push($this->prefixes[$prefix], $base_dir);
+        }
+    }
+
+    /**
+     * Loads the class file for a given class name.
+     *
+     * @param string $class The fully-qualified class name.
+     * @return mixed The mapped file name on success, or boolean false on
+     * failure.
+     */
+    public function loadClass($class)
+    {
+        // the current namespace prefix
+        $prefix = $class;
+
+        // work backwards through the namespace names of the fully-qualified
+        // class name to find a mapped file name
+        while (false !== $pos = strrpos($prefix, '\\')) {
+
+            // retain the trailing namespace separator in the prefix
+            $prefix = substr($class, 0, $pos + 1);
+
+            // the rest is the relative class name
+            $relative_class = substr($class, $pos + 1);
+
+            // try to load a mapped file for the prefix and relative class
+            $mapped_file = $this->loadMappedFile($prefix, $relative_class);
+            if ($mapped_file) {
+                return $mapped_file;
+            }
+
+            // remove the trailing namespace separator for the next iteration
+            // of strrpos()
+            $prefix = rtrim($prefix, '\\');
+        }
+
+        // never found a mapped file
+        return false;
+    }
+
+    /**
+     * Load the mapped file for a namespace prefix and relative class.
+     *
+     * @param string $prefix The namespace prefix.
+     * @param string $relative_class The relative class name.
+     * @return mixed Boolean false if no mapped file can be loaded, or the
+     * name of the mapped file that was loaded.
+     */
+    protected function loadMappedFile($prefix, $relative_class)
+    {
+        // are there any base directories for this namespace prefix?
+        if (isset($this->prefixes[$prefix]) === false) {
+            return false;
+        }
+
+        // look through base directories for this namespace prefix
+        foreach ($this->prefixes[$prefix] as $base_dir) {
+
+            // replace the namespace prefix with the base directory,
+            // replace namespace separators with directory separators
+            // in the relative class name, append with .php
+            $file = $base_dir
+                . str_replace('\\', '/', $relative_class)
+                . '.php';
+
+            // if the mapped file exists, require it
+            if ($this->requireFile($file)) {
+                // yes, we're done
+                return $file;
+            }
+        }
+
+        // never found it
+        return false;
+    }
+
+    /**
+     * If a file exists, require it from the file system.
+     *
+     * @param string $file The file to require.
+     * @return bool True if the file exists, false if not.
+     */
+    protected function requireFile($file)
+    {
+        if (file_exists($file)) {
+            require $file;
+            return true;
+        }
+        return false;
+    }
+}
+
+$basePath = realpath(__DIR__ . '/../');
+$loader = new Psr4Autoloader();
+$loader->register();
+
+// Register laravel-s
+$loader->addNamespace('Hhxsv5\LaravelS', $basePath . '/vendor/hhxsv5/laravel-s/src');
+
+// Register laravel-s dependencies
+$loader->addNamespace('Symfony\Component\Console', $basePath . '/vendor/symfony/console');
+$loader->addNamespace('Symfony\Contracts\Service', $basePath . '/vendor/symfony/service-contracts');
+$loader->addNamespace('Symfony\Contracts', $basePath . '/vendor/symfony/contracts');
+
+$command = new Hhxsv5\LaravelS\Console\Portal($basePath);
+$input = new Symfony\Component\Console\Input\ArgvInput();
+$output = new Symfony\Component\Console\Output\ConsoleOutput();
+$code = $command->run($input, $output);
+exit($code);

+ 1 - 0
composer.json

@@ -12,6 +12,7 @@
         "fideloper/proxy": "^4.2",
         "fruitcake/laravel-cors": "^1.0",
         "guzzlehttp/guzzle": "^6.3",
+        "hhxsv5/laravel-s": "^3.7",
         "laravel/framework": "^7.10.3",
         "laravel/tinker": "^2.0",
         "maatwebsite/excel": "^3.1",

+ 110 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "da9601e98760848dfbf3b0d7737025c8",
+    "content-hash": "af4ee4c97e2d6a53979854c7482f60c4",
     "packages": [
         {
             "name": "asm89/stack-cors",
@@ -690,6 +690,81 @@
             "time": "2019-07-01T23:21:34+00:00"
         },
         {
+            "name": "hhxsv5/laravel-s",
+            "version": "v3.7.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/hhxsv5/laravel-s.git",
+                "reference": "e5a7ad6bbd726a0a7b91f84af622d2f619e18d4a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/hhxsv5/laravel-s/zipball/e5a7ad6bbd726a0a7b91f84af622d2f619e18d4a",
+                "reference": "e5a7ad6bbd726a0a7b91f84af622d2f619e18d4a",
+                "shasum": ""
+            },
+            "require": {
+                "ext-curl": "*",
+                "ext-json": "*",
+                "ext-pcntl": "*",
+                "php": ">=5.5.9",
+                "swoole/ide-helper": "@dev",
+                "symfony/console": ">=2.7.0"
+            },
+            "suggest": {
+                "ext-inotify": "Inotify, used to real-time reload.",
+                "ext-swoole": "Coroutine based Async PHP programming framework, require >= 1.7.19."
+            },
+            "bin": [
+                "bin/fswatch"
+            ],
+            "type": "library",
+            "extra": {
+                "laravel": {
+                    "providers": [
+                        "Hhxsv5\\LaravelS\\Illuminate\\LaravelSServiceProvider"
+                    ]
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Hhxsv5\\LaravelS\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Xie Biao",
+                    "email": "hhxsv5@sina.com"
+                }
+            ],
+            "description": "🚀 LaravelS is an out-of-the-box adapter between Swoole and Laravel/Lumen.",
+            "homepage": "https://github.com/hhxsv5/laravel-s",
+            "keywords": [
+                "LaravelS",
+                "async",
+                "coroutine",
+                "http",
+                "inotify",
+                "laravel",
+                "laravel-s",
+                "lumen",
+                "performance",
+                "process",
+                "server",
+                "swoole",
+                "task",
+                "tcp",
+                "timer",
+                "udp",
+                "websocket"
+            ],
+            "time": "2020-05-14T08:52:22+00:00"
+        },
+        {
             "name": "laravel/framework",
             "version": "v7.10.3",
             "source": {
@@ -2315,6 +2390,40 @@
             "time": "2019-11-12T09:31:26+00:00"
         },
         {
+            "name": "swoole/ide-helper",
+            "version": "4.5.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/swoole/ide-helper.git",
+                "reference": "4020851800932578c51eb472b1ad6148bf311ed5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/swoole/ide-helper/zipball/4020851800932578c51eb472b1ad6148bf311ed5",
+                "reference": "4020851800932578c51eb472b1ad6148bf311ed5",
+                "shasum": ""
+            },
+            "require-dev": {
+                "guzzlehttp/guzzle": "~6.5.0",
+                "laminas/laminas-code": "~3.4.0",
+                "squizlabs/php_codesniffer": "~3.5.0",
+                "symfony/filesystem": "~4.0"
+            },
+            "type": "library",
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "Team Swoole",
+                    "email": "team@swoole.com"
+                }
+            ],
+            "description": "IDE help files for Swoole.",
+            "time": "2020-05-11T20:47:58+00:00"
+        },
+        {
             "name": "symfony/console",
             "version": "v5.0.8",
             "source": {

+ 95 - 0
config/laravels.php

@@ -0,0 +1,95 @@
+<?php
+/**
+ * @see https://github.com/hhxsv5/laravel-s/blob/master/Settings-CN.md  Chinese
+ * @see https://github.com/hhxsv5/laravel-s/blob/master/Settings.md  English
+ */
+return [
+    'listen_ip'                => env('LARAVELS_LISTEN_IP', '127.0.0.1'),
+    'listen_port'              => env('LARAVELS_LISTEN_PORT', 5200),
+    'socket_type'              => defined('SWOOLE_SOCK_TCP') ? SWOOLE_SOCK_TCP : 1,
+    'enable_coroutine_runtime' => false,
+    'server'                   => env('LARAVELS_SERVER', 'LaravelS'),
+    'handle_static'            => env('LARAVELS_HANDLE_STATIC', false),
+    'laravel_base_path'        => env('LARAVEL_BASE_PATH', base_path()),
+    'inotify_reload'           => [
+        'enable'        => env('LARAVELS_INOTIFY_RELOAD', false),
+        'watch_path'    => base_path(),
+        'file_types'    => ['.php'],
+        'excluded_dirs' => [],
+        'log'           => true,
+    ],
+    'event_handlers'           => [],
+    'websocket'                => [
+        'enable' => true,
+        'handler' => \App\Services\WebSocketService::class,
+    ],
+    'sockets'                  => [],
+    'processes'                => [
+        //[
+        //    'class'    => \App\Processes\TestProcess::class,
+        //    'redirect' => false, // Whether redirect stdin/stdout, true or false
+        //    'pipe'     => 0 // The type of pipeline, 0: no pipeline 1: SOCK_STREAM 2: SOCK_DGRAM
+        //    'enable'   => true // Whether to enable, default true
+        //],
+    ],
+    'timer'                    => [
+        'enable'        => env('LARAVELS_TIMER', false),
+        'jobs'          => [
+            // Enable LaravelScheduleJob to run `php artisan schedule:run` every 1 minute, replace Linux Crontab
+            //\Hhxsv5\LaravelS\Illuminate\LaravelScheduleJob::class,
+            // Two ways to configure parameters:
+            // [\App\Jobs\XxxCronJob::class, [1000, true]], // Pass in parameters when registering
+            // \App\Jobs\XxxCronJob::class, // Override the corresponding method to return the configuration
+        ],
+        'max_wait_time' => 5,
+    ],
+    'swoole_tables'            => [],
+    'register_providers'       => [],
+    'cleaners'                 => [
+        // See LaravelS's built-in cleaners: https://github.com/hhxsv5/laravel-s/blob/master/Settings.md#cleaners
+    ],
+    'destroy_controllers'      => [
+        'enable'        => false,
+        'excluded_list' => [
+            //\App\Http\Controllers\TestController::class,
+        ],
+    ],
+    'swoole'                   => [
+        'daemonize'          => env('LARAVELS_DAEMONIZE', false),
+        'dispatch_mode'      => 2,
+        'reactor_num'        => env('LARAVELS_REACTOR_NUM', function_exists('swoole_cpu_num') ? swoole_cpu_num() * 2 : 4),
+        'worker_num'         => env('LARAVELS_WORKER_NUM', function_exists('swoole_cpu_num') ? swoole_cpu_num() * 2 : 8),
+        //'task_worker_num'    => env('LARAVELS_TASK_WORKER_NUM', function_exists('swoole_cpu_num') ? swoole_cpu_num() * 2 : 8),
+        'task_ipc_mode'      => 1,
+        'task_max_request'   => env('LARAVELS_TASK_MAX_REQUEST', 8000),
+        'task_tmpdir'        => @is_writable('/dev/shm/') ? '/dev/shm' : '/tmp',
+        'max_request'        => env('LARAVELS_MAX_REQUEST', 8000),
+        'open_tcp_nodelay'   => true,
+        'pid_file'           => storage_path('laravels.pid'),
+        'log_file'           => storage_path(sprintf('logs/swoole-%s.log', date('Y-m'))),
+        'log_level'          => 4,
+        'document_root'      => base_path('public'),
+        'buffer_output_size' => 2 * 1024 * 1024,
+        'socket_buffer_size' => 128 * 1024 * 1024,
+        'package_max_length' => 4 * 1024 * 1024,
+        'reload_async'       => true,
+        'max_wait_time'      => 60,
+        'enable_reuse_port'  => true,
+        'enable_coroutine'   => false,
+        'http_compression'   => false,
+
+        // Slow log
+        // 'request_slowlog_timeout' => 2,
+        // 'request_slowlog_file'    => storage_path(sprintf('logs/slow-%s.log', date('Y-m'))),
+        // 'trace_event_worker'      => true,
+
+        'heartbeat_idle_time'      => 600,
+        'heartbeat_check_interval' => 60,
+
+        /**
+         * More settings of Swoole
+         * @see https://wiki.swoole.com/#/server/setting  Chinese
+         * @see https://www.swoole.co.uk/docs/modules/swoole-server/configuration  English
+         */
+    ],
+];

+ 3 - 0
resources/assets/js/common.js

@@ -1308,6 +1308,7 @@
             if (typeof params.dataType === 'undefined') params.dataType = 'json';
             if (typeof params.beforeSend === 'undefined') params.beforeSend = () => { };
             if (typeof params.complete === 'undefined') params.complete = () => { };
+            if (typeof params.afterComplete === 'undefined') params.afterComplete = () => { };
             if (typeof params.success === 'undefined') params.success = () => { };
             if (typeof params.error === 'undefined') params.error = () => { };
             //
@@ -1385,10 +1386,12 @@
                 success: function(data, status, xhr) {
                     params.complete();
                     params.success(data, status, xhr);
+                    params.afterComplete(true);
                 },
                 error: function(xhr, status) {
                     params.complete();
                     params.error(xhr, status);
+                    params.afterComplete(false);
                 }
             });
         }

+ 19 - 0
resources/assets/js/main/App.vue

@@ -32,6 +32,9 @@
             setInterval(() => {
                 this.searchEnter();
             }, 1000);
+            //
+            this.handleWebSocket();
+            $A.setOnUserInfoListener("app", () => { this.handleWebSocket() });
         },
         watch: {
             '$route' (To, From) {
@@ -142,6 +145,22 @@
                         }
                     }
                 });
+            },
+
+            handleWebSocket(force) {
+                if ($A.getToken() === false) {
+                    $A.WS.close();
+                } else {
+                    $A.WS.setOnMsgListener("app", (msgDetail) => {
+                        if (msgDetail.sender == $A.getUserName()) {
+                            return;
+                        }
+                        let content = $A.jsonParse(msgDetail.content)
+                        if (content.type == 'task') {
+                            $A.triggerTaskInfoListener(content.act, content.taskDetail, false);
+                        }
+                    }).connection(force);
+                }
             }
         }
     }

+ 1 - 1
resources/assets/js/main/components/project/task/logs.vue

@@ -22,7 +22,7 @@
                 </li>
                 <li v-if="loadIng > 0" class="logs-loading"><w-loading></w-loading></li>
                 <li v-else-if="hasMorePages" class="logs-more" @click="getMore">加载更多</li>
-                <li v-else-if="totalNum == 0" class="logs-none" @click="getLists(true)">没有相关{{logtype.indexOf(['日志', '评论'])===-1?logtype:'数据'}}</li>
+                <li v-else-if="totalNum == 0" class="logs-none" @click="getLists(true)">没有相关{{['日志', '评论'].indexOf(logtype)===-1?'数据':logtype}}</li>
             </ul>
         </div>
     </drawer-tabs-container>

+ 147 - 6
resources/assets/js/main/main.js

@@ -167,13 +167,14 @@ import '../../sass/main.scss';
                             $A.storage("userInfo", res.data);
                             $A.setToken(res.data.token);
                             $A.triggerUserInfoListener(res.data);
-                            typeof callback === "function" && callback(res.data);
+                            typeof callback === "function" && callback(res.data, $A.getToken() !== false);
                         }
-                        //
+                    },
+                    afterComplete: () => {
                         if (typeof continueListenerName == "string" && continueListenerName) {
                             $A.setOnUserInfoListener(continueListenerName, callback);
                         }
-                    }
+                    },
                 });
             }
             return $A.jsonParse($A.storage("userInfo"));
@@ -231,7 +232,7 @@ import '../../sass/main.scss';
                 if (!$A.__userInfoListenerObject.hasOwnProperty(key)) continue;
                 item = $A.__userInfoListenerObject[key];
                 if (typeof item.callback === "function") {
-                    item.callback(userInfo);
+                    item.callback(userInfo, $A.getToken() !== false);
                 }
             }
         },
@@ -254,21 +255,161 @@ import '../../sass/main.scss';
                 }
             }
         },
-        triggerTaskInfoListener(act, taskDetail) {
+        triggerTaskInfoListener(act, taskDetail, sendTo = true) {
             let key, item;
             for (key in $A.__taskInfoListenerObject) {
                 if (!$A.__taskInfoListenerObject.hasOwnProperty(key)) continue;
                 item = $A.__taskInfoListenerObject[key];
                 if (typeof item.callback === "function") {
-                    if (act.indexOf(['deleteproject', 'deletelabel', 'leveltask']) === -1 || item.special === true) {
+                    if (['deleteproject', 'deletelabel', 'leveltask'].indexOf(act) === -1 || item.special === true) {
                         item.callback(act, taskDetail);
                     }
                 }
             }
+            if (sendTo === true) {
+                $A.WS.sendTo('all', null, {
+                    type: "task",
+                    act: act,
+                    taskDetail: taskDetail
+                });
+            }
         },
         __taskInfoListenerObject: {},
 
     });
 
+    /**
+     * =============================================================================
+     * *****************************   websocket assist   ****************************
+     * =============================================================================
+     */
+    $.extend({
+        WS: {
+            __instance: null,
+            __connected: false,
+
+            /**
+             * 连接
+             */
+            connection(force = false) {
+                let url = $A.getObject(window.webSocketConfig, 'URL');
+                url += ($A.strExists(url, "?") ? "&" : "?") + "token=" + $A.getToken();
+                if (!$A.leftExists(url, "ws://") && !$A.leftExists(url, "wss://")) {
+                    return;
+                }
+
+                if (this.__instance !== null && force !== true) {
+                    return;
+                }
+
+                // 初始化客户端套接字并建立连接
+                this.__instance = new WebSocket(url);
+
+                // 连接建立时触发
+                this.__instance.onopen = (event) => {
+                    // console.log("Connection open ...");
+                }
+
+                // 接收到服务端推送时执行
+                this.__instance.onmessage = (event) => {
+                    let msgDetail = $A.jsonParse(event.data);
+                    if (msgDetail.messageType === 'open') {
+                        this.__connected = true;
+                    }
+                    this.triggerMsgListener(msgDetail);
+                };
+
+                // 连接关闭时触发
+                this.__instance.onclose = (event) => {
+                    // console.log("Connection closed ...");
+                    this.__connected = false;
+                    this.__instance = null;
+                }
+
+                // 连接出错
+                this.__instance.onerror = (event) => {
+                    // console.log("Connection error ...");
+                    this.__connected = false;
+                    this.__instance = null;
+                }
+
+                return this;
+            },
+
+            /**
+             * 添加消息监听
+             * @param listenerName
+             * @param callback
+             */
+            setOnMsgListener(listenerName, callback) {
+                if (typeof listenerName != "string") {
+                    return;
+                }
+                if (typeof callback === "function") {
+                    this.__msgListenerObject[listenerName] = {
+                        callback: callback,
+                    }
+                }
+                return this;
+            },
+            triggerMsgListener(msgDetail) {
+                let key, item;
+                for (key in this.__msgListenerObject) {
+                    if (!this.__msgListenerObject.hasOwnProperty(key)) continue;
+                    item = this.__msgListenerObject[key];
+                    if (typeof item.callback === "function") {
+                        item.callback(msgDetail);
+                    }
+                }
+            },
+            __msgListenerObject: {},
+
+            /**
+             * 发送消息
+             * @param type      会话类型:user:指定target、all:所有会员
+             * @param target    接收方的标识,type=all时此项无效
+             * @param content   发送内容
+             */
+            sendTo(type, target, content) {
+                if (this.__instance === null) {
+                    console.log("ws:未初始化连接");
+                    return;
+                }
+                if (this.__connected === false) {
+                    console.log("ws:未连接成功");
+                    return;
+                }
+                if (['user', 'all'].indexOf(type) === -1) {
+                    console.log("ws:错误的消息类型-" + type);
+                    return;
+                }
+                this.__instance.send(JSON.stringify({
+                    messageType: 'send',
+                    type: type,
+                    sender: $A.getUserName(),
+                    target: target,
+                    content: content,
+                    time: Math.round(new Date().getTime() / 1000),
+                }));
+                return this;
+            },
+
+            /**
+             * 关闭连接
+             */
+            close() {
+                if (this.__instance === null) {
+                    console.log("ws:未初始化连接");
+                    return;
+                }
+                if (this.__connected === false) {
+                    console.log("ws:未连接成功");
+                    return;
+                }
+                this.__instance.close();
+            }
+        }
+    });
+
     window.$A = $;
 })(window);

+ 1 - 1
resources/assets/js/main/pages/doc.vue

@@ -30,7 +30,7 @@
         components: {WContent, WHeader},
         data () {
             return {
-
+                loadIng: 0,
             }
         },
         mounted() {

+ 4 - 0
resources/assets/js/main/pages/project.vue

@@ -336,6 +336,10 @@
         },
         mounted() {
             this.getLists(true);
+            $A.getUserInfo((res, isLogin) => {
+                isLogin && this.getLists(true);
+            }, false);
+            //
             $A.setOnTaskInfoListener('pages/project',(act, detail) => {
                 switch (act) {
                     case 'deleteproject':   // 删除项目

+ 16 - 11
resources/assets/js/main/pages/project-panel.vue

@@ -309,16 +309,16 @@
 <script>
     import draggable from 'vuedraggable'
 
-    import WHeader from "../components/WHeader";
-    import WContent from "../components/WContent";
-    import WLoading from "../components/WLoading";
-    import ProjectAddTask from "../components/project/task/add";
-    import ProjectTaskLists from "../components/project/task/lists";
-    import ProjectTaskFiles from "../components/project/task/files";
-    import ProjectTaskLogs from "../components/project/task/logs";
-    import ProjectArchived from "../components/project/archived";
-    import ProjectUsers from "../components/project/users";
-    import ProjectStatistics from "../components/project/statistics";
+    import WHeader from "../../components/WHeader";
+    import WContent from "../../components/WContent";
+    import WLoading from "../../components/WLoading";
+    import ProjectAddTask from "../../components/project/task/add";
+    import ProjectTaskLists from "../../components/project/task/lists";
+    import ProjectTaskFiles from "../../components/project/task/files";
+    import ProjectTaskLogs from "../../components/project/task/logs";
+    import ProjectArchived from "../../components/project/archived";
+    import ProjectUsers from "../../components/project/users";
+    import ProjectStatistics from "../../components/project/statistics";
 
     export default {
         components: {
@@ -383,12 +383,14 @@
                     case "unarchived":      // 取消归档
                         this.projectLabel.forEach((label) => {
                             if (label.id == detail.labelid) {
+                                let index = label.taskLists.length;
                                 label.taskLists.some((task, i) => {
                                     if (detail.inorder > task.inorder || (detail.inorder == task.inorder && detail.id > task.id)) {
-                                        label.taskLists.splice(i, 0, detail);
+                                        index = i;
                                         return true;
                                     }
                                 });
+                                label.taskLists.splice(index, 0, detail);
                             }
                         });
                         this.projectSortData = this.getProjectSort();
@@ -403,6 +405,9 @@
             }
         },
         deactivated() {
+            if ($A.getToken() === false) {
+                this.projectid = 0;
+            }
             this.projectDrawerShow = false;
             this.projectSettingDrawerShow = false;
         },

+ 3 - 0
resources/assets/js/main/pages/team.vue

@@ -238,6 +238,9 @@
         },
         mounted() {
             this.getLists(true);
+            $A.getUserInfo((res, isLogin) => {
+                isLogin && this.getLists(true);
+            }, false);
         },
         deactivated() {
             this.addShow = false;

+ 11 - 8
resources/assets/js/main/pages/todo.vue

@@ -325,8 +325,6 @@
             return {
                 loadIng: 0,
 
-                userInfo: {},
-
                 taskDatas: {
                     "1": {lists: [], hasMorePages: false},
                     "2": {lists: [], hasMorePages: false},
@@ -342,10 +340,11 @@
             }
         },
         mounted() {
-            this.userInfo = $A.getUserInfo((res) => {
-                this.userInfo = res;
-                this.refreshTask();
-            }, true);
+            this.refreshTask();
+            $A.getUserInfo((res, isLogin) => {
+                isLogin && this.refreshTask();
+            }, false);
+            //
             $A.setOnTaskInfoListener('pages/todo',(act, detail) => {
                 switch (act) {
                     case 'deleteproject':   // 删除项目
@@ -385,12 +384,14 @@
                                 }
                             });
                             if (level == detail.level) {
+                                let index = this.taskDatas[level].lists.length;
                                 this.taskDatas[level].lists.some((task, i) => {
                                     if (detail.userorder > task.userorder || (detail.userorder == task.userorder && detail.id > task.id)) {
-                                        this.taskDatas[level].lists.splice(i, 0, detail);
+                                        index = i;
                                         return true;
                                     }
                                 });
+                                this.taskDatas[level].lists.splice(index, 0, detail);
                             }
                         }
                         this.taskSortData = this.getTaskSort();
@@ -413,12 +414,14 @@
                     case "unarchived":      // 取消归档
                         for (let level in this.taskDatas) {
                             if (level == detail.level) {
+                                let index = this.taskDatas[level].lists.length;
                                 this.taskDatas[level].lists.some((task, i) => {
                                     if (detail.userorder > task.userorder || (detail.userorder == task.userorder && detail.id > task.id)) {
-                                        this.taskDatas[level].lists.splice(i, 0, detail);
+                                        index = i;
                                         return true;
                                     }
                                 });
+                                this.taskDatas[level].lists.splice(index, 0, detail);
                             }
                         }
                         this.taskSortData = this.getTaskSort();

+ 1 - 1
resources/assets/js/main/routes.js

@@ -17,7 +17,7 @@ export default [
         path: '/project/panel/:projectid',
         name: 'project-panel',
         meta: { slide: false },
-        component: resolve => require(['./pages/project-panel.vue'], resolve)
+        component: resolve => require(['./pages/project/panel.vue'], resolve)
     }, {
         path: '/doc',
         name: 'doc',

+ 1 - 0
resources/views/main.blade.php

@@ -16,6 +16,7 @@
     <script src="//cdn.jsdelivr.net/npm/bootstrap@4.4.1/dist/js/bootstrap.min.js"></script>
     <script>
         window.csrfToken = { csrfToken : "{{ csrf_token() }}" };
+        window.webSocketConfig = { URL: "{{ env('LARAVELS_PROXY_URL') }}" };
     </script>
 </head>
 <body>

+ 4 - 0
storage/.gitignore

@@ -0,0 +1,4 @@
+laravels.json
+laravels.pid
+laravels-timer-process.pid
+laravels-custom-processes.pid

+ 47 - 0
test.php

@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="UTF-8">
+    <title>Chat Client</title>
+</head>
+<body>
+<script>
+    window.onload = function () {
+        var nick = prompt("Enter your nickname");
+        var input = document.getElementById("input");
+        input.focus();
+
+        // 初始化客户端套接字并建立连接
+        var socket = new WebSocket("ws://127.0.0.1:5200");
+
+        // 连接建立时触发
+        socket.onopen = function (event) {
+            console.log("Connection open ...");
+        }
+
+        // 接收到服务端推送时执行
+        socket.onmessage = function (event) {
+            var msg = event.data;
+            var node = document.createTextNode(msg);
+            var div = document.createElement("div");
+            div.appendChild(node);
+            document.body.insertBefore(div, input);
+            input.scrollIntoView();
+        };
+
+        // 连接关闭时触发
+        socket.onclose = function (event) {
+            console.log("Connection closed ...");
+        }
+
+        input.onchange = function () {
+            var msg = nick + ": " + input.value;
+            // 将输入框变更信息通过 send 方法发送到服务器
+            socket.send(msg);
+            input.value = "";
+        };
+    }
+</script>
+<input id="input" style="width: 100%;">
+</body>
+</html>