kuaifan 5 éve
szülő
commit
103b2b31b0

+ 110 - 0
app/Http/Controllers/Api/ChatController.php

@@ -0,0 +1,110 @@
+<?php
+
+namespace App\Http\Controllers\Api;
+
+use App\Http\Controllers\Controller;
+use App\Module\Base;
+use App\Module\Chat;
+use App\Module\Users;
+use DB;
+use Request;
+
+/**
+ * @apiDefine chat
+ *
+ * 聊天
+ */
+class ChatController extends Controller
+{
+    public function __invoke($method, $action = '')
+    {
+        $app = $method ? $method : 'main';
+        if ($action) {
+            $app .= "__" . $action;
+        }
+        return (method_exists($this, $app)) ? $this->$app() : Base::ajaxError("404 not found (" . str_replace("__", "/", $app) . ").");
+    }
+
+    /**
+     * 对话列表
+     */
+    public function dialog__lists()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $lists = Base::DBC2A(DB::table('chat_dialog')
+            ->where(function ($query) use ($user) {
+                return $query->where('user1', $user['username'])->orWhere('user2', $user['username']);
+            })
+            ->orderByDesc('lastdate')
+            ->take(200)
+            ->get());
+        if (count($lists) <= 0) {
+            return Base::retError('暂无对话记录');
+        }
+        foreach ($lists AS $key => $item) {
+            $lists[$key] = array_merge($item, Users::username2basic($item['user1'] == $user['username'] ? $item['user2'] : $item['user1']));
+            $lists[$key]['lastdate'] = $item['lastdate'] ?: $item['indate'];
+        }
+        return Base::retSuccess('success', $lists);
+    }
+
+
+    /**
+     * 添加/创建对话
+     *
+     * @apiParam {String} username             用户名
+     */
+    public function dialog__add()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $target = Users::username2basic(trim(Request::input('username')));
+        if (empty($target)) {
+            return Base::retError('用户不存在');
+        }
+        return Chat::openDialog($user['username'], $target['username']);
+    }
+
+    /**
+     * 消息列表
+     *
+     * @apiParam {String} username             用户名
+     */
+    public function message__lists()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $res = Chat::openDialog($user['username'], trim(Request::input('username')));
+        if (Base::isError($res)) {
+            return $res;
+        }
+        $lists = DB::table('chat_msg')
+            ->where('did', $res['data']['id'])
+            ->orderByDesc('indate')
+            ->orderByDesc('id')
+            ->paginate(Min(Max(Base::nullShow(Request::input('pagesize'), 10), 1), 100));
+        $lists = Base::getPageList($lists, false);
+        //
+        foreach ($lists['lists'] AS $key => $item) {
+            $lists['lists'][$key]['message'] = Base::string2array($item['message']);
+        }
+        //
+        return Base::retSuccess('success', $lists);
+    }
+}

+ 19 - 1
app/Http/Controllers/Api/UsersController.php

@@ -234,6 +234,13 @@ class UsersController extends Controller
 
     /**
      * 团队列表
+     *
+     * @apiParam {Object} [sorts]               排序方式,格式:{key:'', order:''}
+     * - key: username|id(默认)
+     * - order: asc|desc
+     * @apiParam {Number} [firstchart]          是否获取首字母,1:获取
+     * @apiParam {Number} [page]                当前页,默认:1
+     * @apiParam {Number} [pagesize]            每页显示数量,默认:10,最大:100
      */
     public function team__lists()
     {
@@ -244,13 +251,24 @@ class UsersController extends Controller
             $user = $user['data'];
         }
         //
-        $lists = DB::table('users')->select(['id', 'username', 'nickname', 'userimg', 'profession', 'regdate'])->orderByDesc('id')->paginate(Min(Max(Base::nullShow(Request::input('pagesize'), 10), 1), 100));
+        $orderBy = '`id` DESC';
+        $sorts = Base::json2array(Request::input('sorts'));
+        if (in_array($sorts['order'], ['asc', 'desc'])) {
+            switch ($sorts['key']) {
+                case 'username':
+                    $orderBy = '`' . $sorts['key'] . '` ' . $sorts['order'] . ',`id` DESC';
+                    break;
+            }
+        }
+        //
+        $lists = DB::table('users')->select(['id', 'username', 'nickname', 'userimg', 'profession', 'regdate'])->orderByRaw($orderBy)->paginate(Min(Max(Base::nullShow(Request::input('pagesize'), 10), 1), 100));
         $lists = Base::getPageList($lists);
         if ($lists['total'] == 0) {
             return Base::retError('未找到任何相关的团队成员');
         }
         foreach ($lists['lists'] AS $key => $item) {
             $lists['lists'][$key]['userimg'] = Users::userimg($item['userimg']);
+            $lists['lists'][$key]['firstchart'] = Base::getFirstCharter($item['username']);
         }
         return Base::retSuccess('success', $lists);
     }

+ 42 - 0
app/Module/Base.php

@@ -2279,6 +2279,48 @@ class Base
     }
 
     /**
+     * 获取中文字符拼音首字母
+     * @param $str
+     * @return string
+     */
+    public static function getFirstCharter($str)
+    {
+        if (empty($str)) {
+            return '';
+        }
+        $fchar = ord($str[0]);
+        if ($fchar >= ord('A') && $fchar <= ord('z')) return strtoupper($str{0});
+        $s1 = iconv('UTF-8', 'gb2312', $str);
+        $s2 = iconv('gb2312', 'UTF-8', $s1);
+        $s = $s2 == $str ? $s1 : $str;
+        $asc = ord($s[0]) * 256 + ord($s[1]) - 65536;
+        if ($asc >= -20319 && $asc <= -20284) return 'A';
+        if ($asc >= -20283 && $asc <= -19776) return 'B';
+        if ($asc >= -19775 && $asc <= -19219) return 'C';
+        if ($asc >= -19218 && $asc <= -18711) return 'D';
+        if ($asc >= -18710 && $asc <= -18527) return 'E';
+        if ($asc >= -18526 && $asc <= -18240) return 'F';
+        if ($asc >= -18239 && $asc <= -17923) return 'G';
+        if ($asc >= -17922 && $asc <= -17418) return 'H';
+        if ($asc >= -17417 && $asc <= -16475) return 'J';
+        if ($asc >= -16474 && $asc <= -16213) return 'K';
+        if ($asc >= -16212 && $asc <= -15641) return 'L';
+        if ($asc >= -15640 && $asc <= -15166) return 'M';
+        if ($asc >= -15165 && $asc <= -14923) return 'N';
+        if ($asc >= -14922 && $asc <= -14915) return 'O';
+        if ($asc >= -14914 && $asc <= -14631) return 'P';
+        if ($asc >= -14630 && $asc <= -14150) return 'Q';
+        if ($asc >= -14149 && $asc <= -14091) return 'R';
+        if ($asc >= -14090 && $asc <= -13319) return 'S';
+        if ($asc >= -13318 && $asc <= -12839) return 'T';
+        if ($asc >= -12838 && $asc <= -12557) return 'W';
+        if ($asc >= -12556 && $asc <= -11848) return 'X';
+        if ($asc >= -11847 && $asc <= -11056) return 'Y';
+        if ($asc >= -11055 && $asc <= -10247) return 'Z';
+        return '#';
+    }
+
+    /**
      * 缓存数据
      * @param $title
      * @param null $value

+ 110 - 0
app/Module/Chat.php

@@ -0,0 +1,110 @@
+<?php
+
+namespace App\Module;
+
+use App\Model\DBCache;
+use Cache;
+use DB;
+use Request;
+use Session;
+
+/**
+ * Class Chat
+ * @package App\Module
+ */
+class Chat
+{
+    /**
+     * 打开对话(创建对话)
+     * @param string $username      发送者用户名
+     * @param string $receive       接受者用户名
+     * @return mixed
+     */
+    public static function openDialog($username, $receive)
+    {
+        $cacheKey = $username . "@" . $receive;
+        $result = Cache::remember($cacheKey, now()->addMinutes(10), function() use ($receive, $username) {
+            $row = Base::DBC2A(DB::table('chat_dialog')->where([
+                'user1' => $username,
+                'user2' => $receive,
+            ])->first());
+            if ($row) {
+                return Base::retSuccess('already1', $row);
+            }
+            $row = Base::DBC2A(DB::table('chat_dialog')->where([
+                'user1' => $receive,
+                'user2' => $username,
+            ])->first());
+            if ($row) {
+                return Base::retSuccess('already2', $row);
+            }
+            //
+            DB::table('chat_dialog')->insert([
+                'user1' => $username,
+                'user2' => $receive,
+                'indate' => Base::time()
+            ]);
+            $row = Base::DBC2A(DB::table('chat_dialog')->where([
+                'user1' => $username,
+                'user2' => $receive,
+            ])->first());
+            if ($row) {
+                return Base::retSuccess('success', $row);
+            }
+            //
+            return Base::retError('系统繁忙,请稍后再试!');
+        });
+        if (Base::isError($result)) {
+            Cache::forget($cacheKey);
+        }
+        return $result;
+    }
+
+    /**
+     * 保存对话消息
+     * @param string $username      发送者用户名
+     * @param string $receive       接受者用户名
+     * @param array $message
+     * @return mixed
+     */
+    public static function saveMessage($username, $receive, $message)
+    {
+        $dialog = self::openDialog($username, $receive);
+        if (Base::isError($dialog)) {
+            return $dialog;
+        } else {
+            $dialog = $dialog['data'];
+        }
+        //
+        $indate = abs($message['indate'] - time()) > 30 ? time() : $message['indate'];
+        $inArray = [
+            'did' => $dialog['id'],
+            'username' => $username,
+            'receive' => $receive,
+            'message' => Base::array2string($message),
+            'indate' => $indate
+        ];
+        //
+        switch ($message['type']) {
+            case 'text':
+                $lastText = $message['text'];
+                break;
+            case 'image':
+                $lastText = '[图片]';
+                break;
+            default:
+                $lastText = '[未知类型]';
+                break;
+        }
+        if ($lastText) {
+            DB::table('chat_dialog')->where('id', $dialog['id'])->update([
+                'lasttext' => $lastText,
+                'lastdate' => $indate,
+            ]);
+        }
+        $inArray['id'] = DB::table('chat_msg')->insertGetId($inArray);
+        $inArray['message'] = $message;
+        //
+        return Base::retSuccess('success', $inArray);
+    }
+}

+ 34 - 3
app/Services/WebSocketService.php

@@ -3,6 +3,7 @@
 namespace App\Services;
 
 use App\Module\Base;
+use App\Module\Chat;
 use Cache;
 use DB;
 use Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface;
@@ -97,11 +98,30 @@ class WebSocketService implements WebSocketHandlerInterface
     public function onMessage(Server $server, Frame $frame)
     {
         $data = Base::json2array($frame->data);
+        $feedback = [
+            'status' => 1,
+            'message' => '',
+        ];
         switch ($data['type']) {
             case 'user':
-                $to = self::name2fs($data['target']);
+                $to = self::name2fd($data['target']);
                 if ($to) {
-                    $server->push($to, Base::array2json($data));
+                    $res = Chat::saveMessage(self::fd2name($frame->fd), $data['target'], $data['content']);
+                    if (Base::isError($res)) {
+                        $feedback = [
+                            'status' => 0,
+                            'message' => $res['msg'],
+                        ];
+                    } else {
+                        $data['content']['id'] = $res['data']['id'];
+                        $server->push($to, Base::array2json($data));
+                        $feedback['message'] = $res['data']['id'];
+                    }
+                } else {
+                    $feedback = [
+                        'status' => 0,
+                        'message' => '账号不存在!',
+                    ];
                 }
                 break;
 
@@ -112,6 +132,17 @@ class WebSocketService implements WebSocketHandlerInterface
                 }
                 break;
         }
+        if ($data['messageId']) {
+            $server->push($frame->fd, Base::array2json([
+                'messageType' => 'feedback',
+                'messageId' => $data['messageId'],
+                'type' => 'user',
+                'sender' => null,
+                'target' => null,
+                'content' => $feedback,
+                'time' => Base::time()
+            ]));
+        }
     }
 
     /**
@@ -171,7 +202,7 @@ class WebSocketService implements WebSocketHandlerInterface
      * @param $username
      * @return mixed
      */
-    public static function name2fs($username)
+    public static function name2fd($username)
     {
         return DB::table('users')->select(['wsid'])->where('username', $username)->value('wsid');
     }

+ 94 - 0
resources/assets/js/_components/ScrollerY.vue

@@ -0,0 +1,94 @@
+<template>
+    <div ref="scrollerView" class="app-scroller" :class="[static ? 'app-scroller-static' : '']">
+        <slot/>
+    </div>
+</template>
+
+<style lang="scss" scoped>
+    .app-scroller {
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        overflow-x: hidden;
+        overflow-y: auto;
+        -webkit-overflow-scrolling: touch;
+    }
+
+    .app-scroller-static {
+        position: static;
+        flex: 1;
+    }
+</style>
+<script>
+    export default {
+        name: 'ScrollerY',
+        props: {
+            static: {
+                type: Boolean,
+                default: false
+            },
+        },
+        data() {
+            return {
+                scrollY: 0,
+                scrollDiff: 0,
+                scrollInfo: {},
+            }
+        },
+        mounted() {
+            this.$nextTick(() => {
+                let scrollListener = typeof this.$listeners['on-scroll'] === "function";
+                let scrollerView = $A(this.$refs.scrollerView);
+                scrollerView.scroll(() => {
+                    let wInnerH = Math.round(scrollerView.innerHeight());
+                    let wScrollY = scrollerView.scrollTop();
+                    let bScrollH = this.$refs.scrollerView.scrollHeight;
+                    this.scrollY = wScrollY;
+                    if (scrollListener) {
+                        let direction = 'static';
+                        let directionreal = 'static';
+                        if (this.scrollDiff - wScrollY > 50) {
+                            this.scrollDiff = wScrollY;
+                            direction = 'down';
+                        } else if (this.scrollDiff - wScrollY < -100) {
+                            this.scrollDiff = wScrollY;
+                            direction = 'up';
+                        }
+                        if (this.scrollDiff - wScrollY > 1) {
+                            this.scrollDiff = wScrollY;
+                            directionreal = 'down';
+                        } else if (this.scrollDiff - wScrollY < -1) {
+                            this.scrollDiff = wScrollY;
+                            directionreal = 'up';
+                        }
+                        this.$emit('on-scroll', {
+                            scale: wScrollY / (bScrollH - wInnerH),     //已滚动比例
+                            scrollY: wScrollY,                          //滚动的距离
+                            scrollE: bScrollH - wInnerH - wScrollY,     //与底部距离
+                            direction: direction,                       //滚动方向
+                            directionreal: directionreal,               //滚动方向(即时)
+                        });
+                    }
+                });
+            });
+        },
+        activated() {
+            if (this.scrollY > 0) {
+                this.$nextTick(() => {
+                    this.scrollTo(this.scrollY);
+                });
+            }
+        },
+        methods: {
+            scrollTo(top, animate) {
+                if (animate === false) {
+                    $A(this.$refs.scrollerView).stop().scrollTop(top);
+                } else {
+                    $A(this.$refs.scrollerView).stop().animate({"scrollTop": top});
+                }
+            }
+        }
+    }
+</script>

+ 1 - 1
resources/assets/js/common.js

@@ -1317,7 +1317,7 @@
             //
             let loadText = "数据加载中.....";
             let busyNetwork = "网络繁忙,请稍后再试!";
-            if (typeof $A.app.$L === 'function') {
+            if (typeof $A.app === 'object' && typeof $A.app.$L === 'function') {
                 loadText = $A.app.$L(loadText);
                 busyNetwork = $A.app.$L(busyNetwork);
             }

+ 9 - 1
resources/assets/js/main/App.vue

@@ -5,17 +5,22 @@
                 <router-view class="child-view"></router-view>
             </keep-alive>
         </transition>
+        <Drawer v-model="chatDrawerShow" :closable="false" width="75%">
+            <chat-index></chat-index>
+        </Drawer>
         <w-spinner></w-spinner>
     </div>
 </template>
 
 <script>
     import WSpinner from "./components/WSpinner";
+    import ChatIndex from "./components/chat/Index";
     export default {
-        components: {WSpinner},
+        components: {ChatIndex, WSpinner},
         data () {
             return {
                 transitionName: null,
+                chatDrawerShow: true,
             }
         },
         mounted() {
@@ -155,6 +160,9 @@
                         if (msgDetail.sender == $A.getUserName()) {
                             return;
                         }
+                        if (msgDetail.messageType != 'send') {
+                            return;
+                        }
                         let content = $A.jsonParse(msgDetail.content)
                         if (content.type == 'task') {
                             $A.triggerTaskInfoListener(content.act, content.taskDetail, false);

+ 4 - 1
resources/assets/js/main/components/UserView.vue

@@ -1,6 +1,6 @@
 <template>
     <div class="user-view-inline">
-        <Tooltip :disabled="nickname === null" :delay="delay" :transfer="transfer" @on-popper-show="popperShow">
+        <Tooltip :disabled="nickname === null" :delay="delay" :transfer="transfer" :placement="placement" @on-popper-show="popperShow">
             {{nickname || username}}
             <div slot="content">
                 <div>用户名:{{username}}</div>
@@ -31,6 +31,9 @@
                 type: Boolean,
                 default: true
             },
+            placement: {
+                default: 'bottom'
+            },
         },
         data() {
             return {

+ 688 - 0
resources/assets/js/main/components/chat/Index.vue

@@ -0,0 +1,688 @@
+<template>
+    <div class="chat-index">
+
+        <!--左边选项-->
+        <ul class="chat-menu">
+            <li class="self">
+                <img :src="userInfo.userimg">
+            </li>
+            <li :class="{active:chatTap=='dialog'}" @click="chatTap='dialog'"><Icon type="md-text" /></li>
+            <li :class="{active:chatTap=='team'}" @click="chatTap='team'"><Icon type="md-person" /></li>
+        </ul>
+
+        <!--对话列表-->
+        <ul v-if="chatTap=='dialog'" class="chat-user">
+            <li class="sreach">
+                <Input placeholder="搜索" prefix="ios-search"/>
+            </li>
+            <li v-for="(dialog, index) in dialogLists"
+                :key="index"
+                :class="{active:dialog.username==dialogTarget.username}"
+                @click="openDialog(dialog)">
+                <img :src="dialog.userimg">
+                <div class="user-msg-box">
+                    <div class="user-msg-title">
+                        <span><user-view :username="dialog.username"/></span>
+                        <em>{{formatCDate(dialog.lastdate)}}</em>
+                    </div>
+                    <div class="user-msg-text">{{dialog.lasttext}}</div>
+                </div>
+            </li>
+            <li v-if="dialogLists.length == 0" class="chat-none">{{dialogNoDataText}}</li>
+        </ul>
+
+        <!--联系人列表-->
+        <ul v-else-if="chatTap=='team'" class="chat-team">
+            <li class="sreach">
+                <Input placeholder="搜索" prefix="ios-search"/>
+            </li>
+            <li v-for="(lists, key) in teamLists">
+                <div class="team-label">{{key}}</div>
+                <ul>
+                    <li v-for="(item, index) in lists" :key="index" @click="openDialog(item)">
+                        <img :src="item.userimg">
+                        <div class="team-username"><user-view :username="item.username"/></div>
+                    </li>
+                </ul>
+            </li>
+            <li v-if="Object.keys(teamLists).length == 0" class="chat-none">{{teamNoDataText}}</li>
+        </ul>
+
+        <!--对话窗口-->
+        <div v-if="chatTap=='dialog' && dialogTarget.username" class="chat-message">
+            <div class="manage-title">
+                <user-view :username="dialogTarget.username"/>
+                <Dropdown class="manage-title-right" placement="bottom-end" trigger="click" transfer>
+                    <Icon type="ios-more"/>
+                    <DropdownMenu slot="list">
+                        <DropdownItem name="clear">清除聊天记录</DropdownItem>
+                    </DropdownMenu>
+                </Dropdown>
+            </div>
+            <ScrollerY ref="manageLists" class="manage-lists" @on-scroll="messageListsScroll">
+                <div ref="manageBody" class="manage-body">
+                    <chat-message v-for="(info, index) in messageLists" :key="index" :info="info"></chat-message>
+                </div>
+                <div class="manage-lists-message-new" v-if="messageNew > 0" @click="messageBottomGo(true)">有{{messageNew}}条新消息</div>
+            </ScrollerY>
+            <div class="manage-send">
+                <textarea ref="textarea" class="manage-input" v-model="messageText" placeholder="请输入要发送的消息" @keydown="messageSend($event)"></textarea>
+            </div>
+            <div class="manage-quick">
+                <Button type="primary" size="small">表情</Button>
+                <Button type="warning" size="small">清除聊天记录</Button>
+            </div>
+        </div>
+
+    </div>
+</template>
+
+<style lang="scss" scoped>
+    .chat-index {
+        display: flex;
+        flex-direction: row;
+        align-items: flex-start;
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        .chat-none {
+            height: auto;
+            color: #666666;
+            padding: 22px 8px;
+            text-align: center;
+            justify-content: center;
+            &:before {
+                display: none;
+            }
+        }
+        .chat-menu {
+            background-color: rgba(28, 29, 31, 0.92);
+            width: 68px;
+            height: 100%;
+            padding-top: 20px;
+            li {
+                padding: 12px 0;
+                text-align: center;
+                font-size: 28px;
+                color: #919193;
+                background-color: transparent;
+                cursor: pointer;
+                &.self {
+                    img {
+                        width: 36px;
+                        height: 36px;
+                        border-radius: 3px;
+                    }
+                }
+                &.active {
+                    color: #ffffff;
+                    background-color: rgba(255, 255, 255, 0.06);
+                }
+            }
+        }
+        .chat-user {
+            width: 248px;
+            height: 100%;
+            background-color: #ffffff;
+            border-right: 1px solid #ededed;
+            li {
+                display: flex;
+                flex-direction: row;
+                align-items: center;
+                height: 70px;
+                padding: 0 12px;
+                position: relative;
+                cursor: pointer;
+                &:before {
+                    content: "";
+                    position: absolute;
+                    left: 0;
+                    right: 0;
+                    bottom: 0;
+                    height: 1px;
+                    background-color: rgba(0, 0, 0, 0.06);
+                }
+                &.sreach {
+                    height: 62px;
+                }
+                &.active {
+                    &:before {
+                        top: 0;
+                        height: 100%;
+                    }
+                }
+                img {
+                    width: 42px;
+                    height: 42px;
+                    border-radius: 4px;
+                }
+                .user-msg-box {
+                    flex: 1;
+                    display: flex;
+                    flex-direction: column;
+                    padding-left: 12px;
+                    .user-msg-title {
+                        display: flex;
+                        flex-direction: row;
+                        align-items: center;
+                        justify-content: space-between;
+                        line-height: 24px;
+                        span {
+                            flex: 1;
+                            max-width: 130px;
+                            color: #333333;
+                            font-size: 14px;
+                            white-space: nowrap;
+                            overflow: hidden;
+                            text-overflow: ellipsis;
+                        }
+                        em {
+                            color: #999999;
+                            font-size: 12px;
+                        }
+                    }
+                    .user-msg-text {
+                        max-width: 170px;
+                        color: #999999;
+                        font-size: 12px;
+                        line-height: 24px;
+                        white-space: nowrap;
+                        overflow: hidden;
+                        text-overflow: ellipsis;
+                    }
+                }
+            }
+        }
+        .chat-team {
+            width: 248px;
+            height: 100%;
+            background-color: #ffffff;
+            border-right: 1px solid #ededed;
+            > li {
+                margin-left: 24px;
+                position: relative;
+                &.sreach {
+                    display: flex;
+                    flex-direction: row;
+                    align-items: center;
+                    height: 62px;
+                    margin: 0;
+                    padding: 0 12px;
+                    position: relative;
+                    cursor: pointer;
+                    &:before {
+                        content: "";
+                        position: absolute;
+                        left: 0;
+                        right: 0;
+                        bottom: 0;
+                        height: 1px;
+                        background-color: rgba(0, 0, 0, 0.06);
+                    }
+                }
+                .team-label {
+                    padding-left: 4px;
+                    margin-top: 6px;
+                    margin-bottom: 6px;
+                    height: 34px;
+                    line-height: 34px;
+                    border-bottom: 1px solid #efefef;
+                }
+                > ul {
+                    > li {
+                        display: flex;
+                        flex-direction: row;
+                        align-items: center;
+                        height: 52px;
+                        cursor: pointer;
+                        img {
+                            width: 30px;
+                            height: 30px;
+                            border-radius: 3px;
+                        }
+                        .team-username {
+                            padding: 0 12px;
+                            font-size: 14px;
+                            white-space: nowrap;
+                            overflow: hidden;
+                            text-overflow: ellipsis;
+                        }
+                    }
+                }
+            }
+        }
+        .chat-message {
+            flex: 1;
+            height: 100%;
+            background-color: #F3F3F3;
+            position: relative;
+            .manage-title {
+                position: absolute;
+                top: 0;
+                left: 0;
+                z-index: 3;
+                width: 100%;
+                height: 62px;
+                padding: 0 20px;
+                line-height: 62px;
+                font-size: 16px;
+                font-weight: 500;
+                text-align: left;
+                background: #ffffff;
+                border-bottom: 1px solid #ededed;
+                .manage-title-right {
+                    position: absolute;
+                    top: 0;
+                    right: 0;
+                    z-index: 9;
+                    width: 62px;
+                    height: 62px;
+                    line-height: 62px;
+                    text-align: center;
+                    font-size: 22px;
+                    color: #242424;
+                }
+            }
+            .manage-lists {
+                position: absolute;
+                left: 0;
+                top: 62px;
+                z-index: 1;
+                bottom: 120px;
+                width: 100%;
+                overflow: auto;
+                padding: 8px 0;
+                background-color: #E8EBF2;
+                .manage-lists-message-new {
+                    position: fixed;
+                    bottom: 130px;
+                    right: 20px;
+                    color: #ffffff;
+                    background-color: rgba(0, 0, 0, 0.6);
+                    padding: 6px 12px;
+                    border-radius: 16px;
+                    font-size: 12px;
+                    cursor: pointer;
+                }
+            }
+            .manage-send {
+                position: absolute;
+                left: 0;
+                bottom: 0;
+                z-index: 2;
+                display: flex;
+                width: 100%;
+                height: 120px;
+                background-color: #ffffff;
+                border-top: 1px solid #e4e4e4;
+                .manage-input,.manage-input:focus {
+                    flex: 1;
+                    -webkit-appearance: none;
+                    font-size: 14px;
+                    box-sizing: border-box;
+                    padding: 0;
+                    margin: 38px 8px 6px;
+                    border: 0;
+                    line-height: 20px;
+                    box-shadow: none;
+                    resize:none;
+                    outline: 0;
+                }
+                .manage-join,
+                .manage-spin {
+                    position: absolute;
+                    top: 0;
+                    left: 0;
+                    right: 0;
+                    bottom: 0;
+                    background: #ffffff;
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                }
+            }
+            .manage-quick {
+                position: absolute;
+                z-index: 2;
+                left: 0;
+                right: 0;
+                bottom: 79px;
+                padding: 8px;
+                > button,
+                .quick-button {
+                    font-size: 12px;
+                    margin-right: 4px;
+                }
+            }
+            @media screen and (max-width: 768px) {
+                .manage-lists {
+                    bottom: 96px;
+                    .manage-lists-message-new {
+                        bottom: 106px;
+                    }
+                }
+                .manage-send {
+                    height: 96px;
+                }
+                .manage-quick {
+                    bottom: 54px;
+                    > button,
+                    .quick-button {
+                        margin-right: 2px;
+                    }
+                }
+            }
+        }
+    }
+</style>
+<script>
+    import DrawerTabsContainer from "../DrawerTabsContainer";
+    import ScrollerY from "../../../_components/ScrollerY";
+    import ChatMessage from "./message";
+    export default {
+        name: 'ChatIndex',
+        components: {ChatMessage, ScrollerY, DrawerTabsContainer},
+        data () {
+            return {
+                loadIng: 0,
+
+                userInfo: {},
+
+                chatTap: 'dialog',
+
+                dialogTarget: {},
+                dialogLists: [],
+                dialogNoDataText: '',
+
+                teamReady: false,
+                teamLists: {},
+                teamNoDataText: '',
+
+                autoBottom: true,
+                messageNew: 0,
+                messageText: '',
+                messageLists: [],
+                messageNoDataText: '',
+            }
+        },
+
+        created() {
+            this.dialogNoDataText = this.$L("数据加载中.....");
+            this.teamNoDataText = this.$L("数据加载中.....");
+            this.messageNoDataText = this.$L("数据加载中.....");
+        },
+
+        mounted() {
+            this.userInfo = $A.getUserInfo((res) => {
+                this.userInfo = res;
+            }, false);
+            //
+            $A.WS.setOnMsgListener("chat/index", (msgDetail) => {
+                if (msgDetail.sender == $A.getUserName()) {
+                    return;
+                }
+                if (msgDetail.messageType != 'send') {
+                    return;
+                }
+                //
+                let data = $A.jsonParse(msgDetail.content);
+                let lasttext;
+                switch (data.type) {
+                    case 'text':
+                        lasttext = data['text'];
+                        break;
+                    case 'image':
+                        lasttext = '[图片]';
+                        break;
+                    default:
+                        lasttext = '[未知类型]';
+                        break;
+                }
+                this.openDialog({
+                    username: data.username,
+                    userimg: data.userimg,
+                }, {
+                    lasttext: lasttext,
+                    lastdate: data.indate
+                });
+                if (msgDetail.sender == this.dialogTarget.username) {
+                    this.addMessageData(data, true);
+                }
+            })
+            //
+            this.getDialogLists();
+            this.messageBottomAuto();
+        },
+
+        watch: {
+            chatTap(val) {
+                if (val === 'team' && this.teamReady == false) {
+                    this.teamReady = true;
+                    this.getTeamLists();
+                }
+            },
+            dialogTarget: {
+                handler: function () {
+                    this.getDialogMessage();
+                },
+                deep: true
+            }
+        },
+
+        methods: {
+            formatCDate(v) {
+                let string = '';
+                if ($A.runNum(v) > 0) {
+                    if ($A.formatDate('Ymd') === $A.formatDate('Ymd', v)) {
+                        string = $A.formatDate('H:i', v)
+                    } else if ($A.formatDate('Y') === $A.formatDate('Y', v)) {
+                        string = $A.formatDate('m-d H:i', v)
+                    } else {
+                        string = $A.formatDate('Y-m-d H:i', v)
+                    }
+                }
+                return string || '';
+            },
+
+            getDialogLists() {
+                this.loadIng++;
+                this.dialogNoDataText = this.$L("数据加载中.....");
+                $A.aAjax({
+                    url: 'chat/dialog/lists',
+                    complete: () => {
+                        this.loadIng--;
+                    },
+                    error: () => {
+                        this.dialogNoDataText = this.$L("数据加载失败!");
+                    },
+                    success: (res) => {
+                        if (res.ret === 1) {
+                            this.dialogLists = res.data;
+                            this.dialogNoDataText = this.$L("没有相关的数据");
+                        } else {
+                            this.dialogLists = [];
+                            this.dialogNoDataText = res.msg
+                        }
+                    }
+                });
+            },
+
+            getDialogMessage() {
+                let username = this.dialogTarget.username;
+                if (username === this.__dialogTargetUsername) {
+                    return;
+                }
+                this.__dialogTargetUsername = username;
+                this.autoBottom = true;
+                this.messageNew = 0;
+                this.messageLists = [];
+                //
+                this.loadIng++;
+                this.messageNoDataText = this.$L("数据加载中.....");
+                $A.aAjax({
+                    url: 'chat/message/lists',
+                    data: {
+                        username: username,
+                        pagesize: 30
+                    },
+                    complete: () => {
+                        this.loadIng--;
+                    },
+                    error: () => {
+                        this.messageNoDataText = this.$L("数据加载失败!");
+                    },
+                    success: (res) => {
+                        if (username != this.dialogTarget.username) {
+                            return;
+                        }
+                        if (res.ret === 1) {
+                            res.data.lists.reverse().forEach((item) => {
+                                this.addMessageData(Object.assign(item.message, {
+                                    id: item.id,
+                                }));
+                            });
+                            this.messageNoDataText = this.$L("没有相关的数据");
+                        } else {
+                            this.messageNoDataText = res.msg
+                        }
+                    }
+                });
+            },
+
+            getTeamLists() {
+                this.loadIng++;
+                this.teamNoDataText = this.$L("数据加载中.....");
+                $A.aAjax({
+                    url: 'users/team/lists',
+                    data: {
+                        sorts: {
+                            key: 'username',
+                            order: 'asc'
+                        },
+                        firstchart: 1,
+                        pagesize: 100,
+                    },
+                    complete: () => {
+                        this.loadIng--;
+                    },
+                    error: () => {
+                        this.teamNoDataText = this.$L("数据加载失败!");
+                    },
+                    success: (res) => {
+                        if (res.ret === 1) {
+                            res.data.lists.forEach((item) => {
+                                if (typeof this.teamLists[item.firstchart] === "undefined") {
+                                    this.$set(this.teamLists, item.firstchart, []);
+                                }
+                                this.teamLists[item.firstchart].push(item);
+                                console.log(this.teamLists);
+                            });
+                            this.teamNoDataText = this.$L("没有相关的数据");
+                        } else {
+                            this.teamLists = {};
+                            this.teamNoDataText = res.msg
+                        }
+                    }
+                });
+            },
+
+            openDialog(user, lastMsg) {
+                if (typeof lastMsg === "object") {
+                    user = Object.assign(user, lastMsg);
+                    this.dialogLists = this.dialogLists.filter((item) => {return item.username != user.username});
+                }
+                let lists = this.dialogLists.filter((item) => {return item.username == user.username});
+                if (lists.length === 0) {
+                    this.dialogLists.unshift(user);
+                }
+                if (typeof lastMsg !== "object") {
+                    this.chatTap = 'dialog';
+                    this.dialogTarget = user;
+                }
+            },
+
+            messageListsScroll(res) {
+                if (res.directionreal === 'up') {
+                    if (res.scrollE < 10) {
+                        this.autoBottom = true;
+                    }
+                } else if (res.directionreal === 'down') {
+                    this.autoBottom = false;
+                }
+            },
+
+            messageBottomAuto() {
+                let randString = $A.randomString(8);
+                window.__messageBottomAuto = randString;
+                setTimeout(() => {
+                    if (randString === window.__messageBottomAuto) {
+                        window.__messageBottomAuto = null;
+                        if (this.autoBottom) {
+                            this.messageBottomGo();
+                        }
+                        this.messageBottomAuto();
+                    }
+                }, 1200);
+            },
+
+            messageBottomGo(animation = false) {
+                this.$nextTick(() => {
+                    this.messageNew = 0;
+                    if (typeof this.$refs.manageLists !== "undefined") {
+                        this.$refs.manageLists.scrollTo(this.$refs.manageBody.clientHeight, animation);
+                        this.autoBottom = true;
+                    }
+                });
+            },
+
+            addMessageData(data, animation = false) {
+                data.self = data.username === this.userInfo.username;
+                this.messageLists.push(data);
+                //
+                if (this.autoBottom) {
+                    this.messageBottomGo(animation);
+                } else {
+                    this.messageNew++;
+                }
+            },
+
+            messageSend(e) {
+                if (e.keyCode == 13) {
+                    if (e.shiftKey) {
+                        return;
+                    }
+                    e.preventDefault();
+                    this.messageSubmit();
+                }
+            },
+
+            messageSubmit() {
+                let text = this.messageText.trim();
+                if ($A.count(text) > 0) {
+                    let data = {
+                        id: 0,
+                        type: 'text',
+                        username: this.userInfo.username,
+                        userimg: this.userInfo.userimg,
+                        indate: Math.round(new Date().getTime() / 1000),
+                        text: text
+                    };
+                    $A.WS.sendTo('user', this.dialogTarget.username, data, (res) => {
+                        this.$set(data, res.status === 1 ? 'id' : 'error', res.message)
+                    });
+                    //
+                    this.openDialog(this.dialogTarget, {
+                        lasttext: text,
+                        lastdate: data.indate
+                    });
+                    this.addMessageData(data, true);
+                }
+                this.$nextTick(() => {
+                    this.messageText = "";
+                });
+            },
+        }
+    }
+</script>

+ 232 - 0
resources/assets/js/main/components/chat/message.vue

@@ -0,0 +1,232 @@
+<template>
+    <div :data-id="info.id">
+        <!--文本-->
+        <div v-if="info.type==='text'">
+            <div v-if="info.self===true" class="list-right">
+                <div v-if="info.error" class="item-error" @click="clickError(info.error)">
+                    <Icon type="md-alert" />
+                </div>
+                <div class="item-right">
+                    <div class="item-username" @click="clickUser">
+                        <em class="item-name"><user-view :username="info.username" placement="left"/></em>
+                        <em v-if="info.indate" class="item-date">{{formatCDate(info.indate)}}</em>
+                    </div>
+                    <div class="item-text">
+                        <div class="item-text-view" v-html="textMsg(info.text)"></div>
+                    </div>
+                </div>
+                <img class="item-userimg" @click="clickUser" :src="info.userimg" onerror="this.src=window.location.origin+'/images/other/avatar.png'"/>
+            </div>
+            <div v-else-if="info.self===false" class="list-item">
+                <img class="item-userimg" @click="clickUser" :src="info.userimg" onerror="this.src=window.location.origin+'/images/other/avatar.png'"/>
+                <div class="item-left">
+                    <div class="item-username" @click="clickUser">
+                        <em class="item-name"><user-view :username="info.username" placement="right"/></em>
+                        <em v-if="info.__usertag" class="item-tag">{{info.__usertag}}</em>
+                        <em v-if="info.indate" class="item-date">{{formatCDate(info.indate)}}</em>
+                    </div>
+                    <div class="item-text">
+                        <div class="item-text-view" v-html="textMsg(info.text)"></div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!--图片-->
+        <div v-else-if="info.type==='image'">
+            <div v-if="info.self===true" class="list-right">
+                <div v-if="info.error" class="item-error" @click="clickError(info.error)">
+                    <Icon type="md-alert" />
+                </div>
+                <div class="item-right">
+                    <div class="item-username" @click="clickUser">
+                        <em class="item-name"><user-view :username="info.username" placement="left"/></em>
+                        <em v-if="info.indate" class="item-date">{{formatCDate(info.indate)}}</em>
+                    </div>
+                    <a class="item-image" :href="info.url" target="_blank">
+                        <img class="item-image-view" :src="info.url"/>
+                    </a>
+                </div>
+                <img class="item-userimg" @click="clickUser" :src="info.userimg" onerror="this.src=window.location.origin+'/images/other/avatar.png'"/>
+            </div>
+            <div v-else-if="info.self===false" class="list-item">
+                <img class="item-userimg" @click="clickUser" :src="info.userimg" onerror="this.src=window.location.origin+'/images/other/avatar.png'"/>
+                <div class="item-left">
+                    <div class="item-username" @click="clickUser">
+                        <em class="item-name"><user-view :username="info.username" placement="right"/></em>
+                        <em v-if="info.__usertag" class="item-tag">{{info.__usertag}}</em>
+                        <em v-if="info.indate" class="item-date">{{formatCDate(info.indate)}}</em>
+                    </div>
+                    <a class="item-image" :href="info.url" target="_blank">
+                        <img class="item-image-view" :src="info.url"/>
+                    </a>
+                </div>
+            </div>
+        </div>
+
+        <!--通知-->
+        <div v-else-if="info.type==='notice'">
+            <div class="item-notice">{{info.notice}}</div>
+        </div>
+    </div>
+</template>
+
+<style lang="scss" scoped>
+    /*通用*/
+    .list-item, .list-right {
+        display: flex;
+        width: 100%;
+        padding-top: 7px;
+        padding-bottom: 7px;
+        background-color: #E8EBF2;
+        .item-left, .item-right {
+            display: flex;
+            flex-direction: column;
+            max-width: 80%;
+            .item-username {
+                font-size: 12px;
+                padding-top: 1px;
+                padding-bottom: 4px;
+                display: flex;
+                flex-direction: row;
+                align-items: center;
+                em {
+                    display: inline-block;
+                    font-style: normal;
+                    &.item-name {
+                        color: #888888;
+                    }
+                    &.item-tag {
+                        color: #ffffff;
+                        background-color: #ff0000;
+                        line-height: 16px;
+                        padding: 2px 4px;
+                        margin-left: 3px;
+                        border-radius: 2px;
+                        font-size: 12px;
+                        transform: scale(0.8);
+                        font-weight: 600;
+                    }
+                    &.item-date {
+                        margin-left: 4px;
+                        color: #aaaaaa;
+                    }
+                }
+            }
+        }
+        .item-left {
+            align-items: flex-start;
+        }
+        .item-right {
+            align-items: flex-end;
+            .item-username {
+                text-align: right;
+            }
+        }
+        .item-userimg {
+            width: 38px;
+            height: 38px;
+            margin-left: 8px;
+            margin-right: 8px;
+        }
+        .item-error {
+            cursor: pointer;
+            width: 48px;
+            position: relative;
+            > i {
+                color: #ff0000;
+                font-size: 18px;
+                position: absolute;
+                top: 50%;
+                left: 50%;
+                transform: translate(-50%, -50%);
+            }
+        }
+    }
+
+    .list-right {
+        justify-content: flex-end;
+    }
+
+    /*文本*/
+    .item-text {
+        display: inline-block;
+        border-radius: 6px;
+        padding: 8px;
+        background-color: #ffffff;
+        .item-text-view {
+            max-width: 520px;
+            color: #242424;
+            font-size: 14px;
+            line-height: 18px;
+            word-break: break-all;
+        }
+    }
+
+    /*图片*/
+    .item-image {
+        display: inline-block;
+        text-decoration: none;
+        .item-image-view {
+            max-width: 320px;
+            max-height: 320px;
+            border-radius: 6px;
+        }
+    }
+
+    /*通知*/
+    .item-notice {
+        color: #777777;
+        font-size: 12px;
+        text-align: center;
+        padding: 12px 24px;
+    }
+</style>
+
+<script>
+
+    export default {
+        name: 'ChatMessage',
+        props: {
+            info: {
+                type: Object,
+                default: {},
+            },
+        },
+
+        mounted() {
+
+        },
+
+        methods: {
+            textMsg(text) {
+                return (text + "").replace(/\n/, '<br/>');
+            },
+
+            formatCDate(v) {
+                let string = '';
+                if ($A.runNum(v) > 0) {
+                    if ($A.formatDate('Ymd') === $A.formatDate('Ymd', v)) {
+                        string = $A.formatDate('H:i', v)
+                    } else if ($A.formatDate('Y') === $A.formatDate('Y', v)) {
+                        string = $A.formatDate('m-d H:i', v)
+                    } else {
+                        string = $A.formatDate('Y-m-d H:i', v)
+                    }
+                }
+                return string ? '(' + string + ')' : '';
+            },
+
+            clickError(err) {
+                this.$Modal.error({
+                    title: "错误详情",
+                    content: err
+                });
+            },
+
+            clickUser(e) {
+                this.$emit('clickUser', this.info, e);
+            },
+        }
+    }
+</script>

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

@@ -290,6 +290,7 @@ import '../../sass/main.scss';
         WS: {
             __instance: null,
             __connected: false,
+            __callbackid: {},
             __autoLine() {
                 let tempId = $A.randomString(16);
                 this.__autoId = tempId;
@@ -331,6 +332,11 @@ import '../../sass/main.scss';
                     if (msgDetail.messageType === 'open') {
                         this.__connected = true;
                     }
+                    if (msgDetail.messageType === 'feedback') {
+                        typeof this.__callbackid[msgDetail.messageId] === "function" && this.__callbackid[msgDetail.messageId](msgDetail.content);
+                        delete this.__callbackid[msgDetail.messageId];
+                        return;
+                    }
                     this.triggerMsgListener(msgDetail);
                 };
 
@@ -386,8 +392,9 @@ import '../../sass/main.scss';
              * @param type      会话类型:user:指定target、all:所有会员
              * @param target    接收方的标识,type=all时此项无效
              * @param content   发送内容
+             * @param callback  发送回调
              */
-            sendTo(type, target, content) {
+            sendTo(type, target, content, callback) {
                 if (this.__instance === null) {
                     console.log("[WS] 未初始化连接");
                     return;
@@ -400,8 +407,14 @@ import '../../sass/main.scss';
                     console.log("[WS] 错误的消息类型-" + type);
                     return;
                 }
+                let messageId = '';
+                if (typeof callback === "function") {
+                    messageId = $A.randomString(16);
+                    this.__callbackid[messageId] = callback;
+                }
                 this.__instance.send(JSON.stringify({
                     messageType: 'send',
+                    messageId: messageId,
                     type: type,
                     sender: $A.getUserName(),
                     target: target,

+ 0 - 1
resources/assets/js/main/pages/docs.vue

@@ -229,7 +229,6 @@
 
         created() {
             this.bookNoDataText = this.$L("数据加载中.....");
-            this.bookNoDataText = this.$L("数据加载中.....");
             this.sectionNoDataText = this.$L("数据加载中.....");
             this.sectionTypeLists = [
                 {value: 'document', text: this.$L("文本")},

+ 3 - 0
routes/web.php

@@ -19,6 +19,9 @@ Route::prefix('api')->middleware(ApiMiddleware::class)->group(function () {
     //知识库
     Route::any('docs/{method}',                     'Api\DocsController');
     Route::any('docs/{method}/{action}',            'Api\DocsController');
+    //聊天
+    Route::any('chat/{method}',                     'Api\ChatController');
+    Route::any('chat/{method}/{action}',            'Api\ChatController');
 });