Ver código fonte

聊天/任务评论支持粘贴/拖拽上传图片及文件

kuaifan 5 anos atrás
pai
commit
92973336dd

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

@@ -154,4 +154,92 @@ class ChatController extends Controller
         //
         return Base::retSuccess('success', $lists);
     }
+
+    /**
+     * 文件 - 上传
+     *
+     * @apiParam {String} username            get-发给用户名(群组名)
+     * @apiParam {String} [filename]          post-文件名称
+     * @apiParam {String} [image64]           post-base64图片(二选一)
+     * @apiParam {File} [files]               post-文件对象(二选一)
+     */
+    public function files__upload()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $username = trim(Request::input('username'));
+        if (Base::leftExists($username, "group::")) {
+            $res = Chat::groupOpen($username);
+        } else {
+            $res = Chat::openDialog($user['username'], $username);
+        }
+        if (Base::isError($res)) {
+            return $res;
+        }
+        $did = $res['data']['id'];
+        //
+        $path = "uploads/chat/" . Users::token2userid() . "/";
+        $image64 = trim(Base::getPostValue('image64'));
+        $fileName = trim(Base::getPostValue('filename'));
+        if ($image64) {
+            $data = Base::image64save([
+                "image64" => $image64,
+                "path" => $path,
+                "fileName" => $fileName,
+            ]);
+        } else {
+            $data = Base::upload([
+                "file" => Request::file('files'),
+                "type" => 'file',
+                "path" => $path,
+                "fileName" => $fileName,
+            ]);
+        }
+        //
+        if (Base::isError($data)) {
+            return Base::retError($data['msg']);
+        } else {
+            $fileData = $data['data'];
+            $fileData['thumb'] = $fileData['thumb'] ?: 'images/files/file.png';
+            switch ($fileData['ext']) {
+                case "docx":
+                    $fileData['thumb'] = 'images/files/doc.png';
+                    break;
+                case "xlsx":
+                    $fileData['thumb'] = 'images/files/xls.png';
+                    break;
+                case "pptx":
+                    $fileData['thumb'] = 'images/files/ppt.png';
+                    break;
+                case "doc":
+                case "xls":
+                case "ppt":
+                case "txt":
+                case "esp":
+                case "gif":
+                    $fileData['thumb'] = 'images/files/' . $fileData['ext'] . '.png';
+                    break;
+            }
+            $array = [
+                'did' => $did,
+                'group' => Base::leftExists($username, 'group::') ? $username : '',
+                'name' => $fileData['name'],
+                'size' => $fileData['size'] * 1024,
+                'ext' => $fileData['ext'],
+                'path' => $fileData['path'],
+                'thumb' => $fileData['thumb'],
+                'username' => $user['username'],
+                'indate' => Base::time(),
+            ];
+            DB::table('chat_files')->insertGetId($array);
+            //
+            $fileData['thumb'] = Base::fillUrl($fileData['thumb']);
+            return Base::retSuccess('success', $fileData);
+        }
+    }
 }

+ 29 - 20
app/Http/Controllers/Api/ProjectController.php

@@ -2153,8 +2153,11 @@ class ProjectController extends Controller
     /**
      * 项目文件-上传
      *
-     * @apiParam {Number} [projectid]           项目ID
-     * @apiParam {Number} [taskid]              任务ID(如果项目ID为空时此参必须赋值且任务必须是自己负责人)
+     * @apiParam {Number} [projectid]           get-项目ID
+     * @apiParam {Number} [taskid]              get-任务ID(如果项目ID为空时此参必须赋值且任务必须是自己负责人)
+     * @apiParam {String} [filename]            post-文件名称
+     * @apiParam {String} [image64]             post-base64图片(二选一)
+     * @apiParam {File} [files]                 post-文件对象(二选一)
      */
     public function files__upload()
     {
@@ -2183,25 +2186,38 @@ class ProjectController extends Controller
             $projectid = $row['projectid'];
         }
         //
-        $data = Base::upload([
-            "file" => Request::file('files'),
-            "type" => 'file',
-            "path" => "uploads/projects/" . $projectid . "/",
-        ]);
+        $path = "uploads/projects/" . ($projectid ?: Users::token2userid()) . "/";
+        $image64 = trim(Base::getPostValue('image64'));
+        $fileName = trim(Base::getPostValue('filename'));
+        if ($image64) {
+            $data = Base::image64save([
+                "image64" => $image64,
+                "path" => $path,
+                "fileName" => $fileName,
+            ]);
+        } else {
+            $data = Base::upload([
+                "file" => Request::file('files'),
+                "type" => 'file',
+                "path" => $path,
+                "fileName" => $fileName,
+            ]);
+        }
+        //
         if (Base::isError($data)) {
             return Base::retError($data['msg']);
         } else {
             $fileData = $data['data'];
-            $thumb = 'images/files/file.png';
+            $fileData['thumb'] = $fileData['thumb'] ?: 'images/files/file.png';
             switch ($fileData['ext']) {
                 case "docx":
-                    $thumb = 'images/files/doc.png';
+                    $fileData['thumb'] = 'images/files/doc.png';
                     break;
                 case "xlsx":
-                    $thumb = 'images/files/xls.png';
+                    $fileData['thumb'] = 'images/files/xls.png';
                     break;
                 case "pptx":
-                    $thumb = 'images/files/ppt.png';
+                    $fileData['thumb'] = 'images/files/ppt.png';
                     break;
                 case "doc":
                 case "xls":
@@ -2209,14 +2225,7 @@ class ProjectController extends Controller
                 case "txt":
                 case "esp":
                 case "gif":
-                    $thumb = 'images/files/' . $fileData['ext'] . '.png';
-                    break;
-                case "jpg":
-                case "jpeg":
-                case "png":
-                    if (Base::imgThumb($fileData['file'], $fileData['file'] . "_thumb.jpg", 64, 0)) {
-                        $thumb = $fileData['path'] . "_thumb.jpg";
-                    }
+                    $fileData['thumb'] = 'images/files/' . $fileData['ext'] . '.png';
                     break;
             }
             $array = [
@@ -2226,7 +2235,7 @@ class ProjectController extends Controller
                 'size' => $fileData['size'] * 1024,
                 'ext' => $fileData['ext'],
                 'path' => $fileData['path'],
-                'thumb' => $thumb,
+                'thumb' => $fileData['thumb'],
                 'username' => $user['username'],
                 'indate' => Base::time(),
             ];

+ 17 - 8
app/Http/Controllers/Api/SystemController.php

@@ -137,15 +137,24 @@ class SystemController extends Controller
             $scale = [2160, 4160, -1];
         }
         $path = "uploads/picture/" . Users::token2userid() . "/" . date("Ym") . "/";
-        if (Request::input('from') == 'chat') {
-            $path = "uploads/chat/" . Users::token2userid() . "/" . date("Ym") . "/";
+        $image64 = trim(Base::getPostValue('image64'));
+        $fileName = trim(Base::getPostValue('filename'));
+        if ($image64) {
+            $data = Base::image64save([
+                "image64" => $image64,
+                "path" => $path,
+                "fileName" => $fileName,
+                "scale" => $scale
+            ]);
+        } else {
+            $data = Base::upload([
+                "file" => Request::file('image'),
+                "type" => 'image',
+                "path" => $path,
+                "fileName" => $fileName,
+                "scale" => $scale
+            ]);
         }
-        $data = Base::upload([
-            "file" => Request::file('image'),
-            "type" => 'image',
-            "path" => $path,
-            "scale" => $scale
-        ]);
         if (Base::isError($data)) {
             return Base::retError($data['msg']);
         } else {

+ 3 - 0
app/Http/Middleware/VerifyCsrfToken.php

@@ -18,6 +18,9 @@ class VerifyCsrfToken extends Middleware
         //上传项目文件
         'api/project/files/upload/',
 
+        //上传聊天文件
+        'api/chat/files/upload/',
+
         //修改项目任务
         'api/project/task/edit/',
 

+ 96 - 1
app/Module/Base.php

@@ -1950,6 +1950,101 @@ class Base
     }
 
     /**
+     * image64图片保存
+     * @param array $param [ image64=带前缀的base64, path=>文件路径, fileName=>文件名称, scale=>[压缩原图宽,高, 压缩方式] ]
+     * @return array [name=>文件名, size=>文件大小(单位KB),file=>绝对地址, path=>相对地址, url=>全路径地址, ext=>文件后缀名]
+     */
+    public static function image64save($param)
+    {
+        $imgBase64 = $param['image64'];
+        if (preg_match('/^(data:\s*image\/(\w+);base64,)/', $imgBase64, $res)) {
+            $extension = $res[2];
+            if (!in_array($extension, ['png', 'jpg', 'jpeg', 'gif'])) {
+                return Base::retError('图片格式错误!');
+            }
+            $scaleName = "";
+            if ($param['fileName']) {
+                $fileName = $param['fileName'];
+            }else{
+                if ($param['scale'] && is_array($param['scale'])) {
+                    list($width, $height) = $param['scale'];
+                    if ($width > 0 || $height > 0) {
+                        $scaleName = "_{WIDTH}x{HEIGHT}";
+                        if (isset($param['scale'][2])) {
+                            $scaleName.= $param['scale'][2];
+                        }
+                    }
+                }
+                $fileName = 'paste_' . md5($imgBase64) . '.' . $extension;
+                $scaleName = md5_file($imgBase64) . $scaleName . '.' . $extension;
+            }
+            $fileDir = $param['path'];
+            $filePath = public_path($fileDir);
+            Base::makeDir($filePath);
+            if (file_put_contents($filePath . $fileName, base64_decode(str_replace($res[1], '', $imgBase64)))) {
+                $fileSize = filesize($filePath . $fileName);
+                $array = [
+                    "name" => $fileName,                                                //原文件名
+                    "size" => Base::twoFloat($fileSize / 1024, true),         //大小KB
+                    "file" => $filePath . $fileName,                                    //目录的完整路径                "D:\www....KzZ.jpg"
+                    "path" => $fileDir . $fileName,                                     //相对路径                     "uploads/pic....KzZ.jpg"
+                    "url" => Base::fillUrl($fileDir . $fileName),                   //完整的URL                    "https://.....hhsKzZ.jpg"
+                    "thumb" => '',                                                      //缩略图(预览图)               "https://.....hhsKzZ.jpg_thumb.jpg"
+                    "width" => -1,                                                      //图片宽度
+                    "height" => -1,                                                     //图片高度
+                    "ext" => $extension,                                                //文件后缀名
+                ];
+                if (in_array($extension, ['png', 'jpg', 'jpeg', 'gif'])) {
+                    //图片尺寸
+                    $paramet = getimagesize($array['file']);
+                    $array['width'] = $paramet[0];
+                    $array['height'] = $paramet[1];
+                    //原图压缩
+                    if ($param['scale'] && is_array($param['scale'])) {
+                        list($width, $height) = $param['scale'];
+                        if (($width > 0 && $array['width'] > $width) || ($height > 0 && $array['height'] > $height)) {
+                            $cut = ($width > 0 && $height > 0) ? 1 : -1;
+                            $cut = $param['scale'][2] ?? $cut;
+                            //图片压缩
+                            $tmpFile = $array['file'] . '_tmp.jpg';
+                            if (Base::imgThumb($array['file'], $tmpFile, $width, $height, $cut)) {
+                                $tmpSize = filesize($tmpFile);
+                                if ($tmpSize > $fileSize) {
+                                    @unlink($tmpFile);
+                                }else{
+                                    @unlink($array['file']);
+                                    rename($tmpFile, $array['file']);
+                                }
+                            }
+                            //图片尺寸
+                            $paramet = getimagesize($array['file']);
+                            $array['width'] = $paramet[0];
+                            $array['height'] = $paramet[1];
+                            //重命名
+                            if ($scaleName) {
+                                $scaleName = str_replace(['{WIDTH}', '{HEIGHT}'], [$array['width'], $array['height']], $scaleName);
+                                if (rename($array['file'], Base::rightDelete($array['file'], $fileName) . $scaleName)) {
+                                    $array['file'] = Base::rightDelete($array['file'], $fileName) . $scaleName;
+                                    $array['path'] = Base::rightDelete($array['path'], $fileName) . $scaleName;
+                                    $array['url'] = Base::rightDelete($array['url'], $fileName) . $scaleName;
+                                }
+                            }
+                        }
+                    }
+                    //生成缩略图
+                    $array['thumb'] = $array['path'];
+                    if (Base::imgThumb($array['file'], $array['file']."_thumb.jpg", 180, 0)) {
+                        $array['thumb'].= "_thumb.jpg";
+                    }
+                    $array['thumb'] = Base::fillUrl($array['thumb']);
+                }
+                return Base::retSuccess('success', $array);
+            }
+        }
+        return Base::retError('图片保存失败!');
+    }
+
+    /**
      * 上传文件
      * @param array $param [ type=[文件类型], file=>Request::file, path=>文件路径, fileName=>文件名称, scale=>[压缩原图宽,高, 压缩方式], size=>限制大小KB, autoThumb=>false不要自动生成缩略图 ]
      * @return array [name=>原文件名, size=>文件大小(单位KB),file=>绝对地址, path=>相对地址, url=>全路径地址, ext=>文件后缀名]
@@ -2064,7 +2159,7 @@ class Base
                 }
             }
             //
-            if (in_array($param['type'], ['png', 'png+jpg', 'image'])) {
+            if (in_array($extension, ['jpg', 'jpeg', 'gif', 'png'])) {
                 //图片尺寸
                 $paramet = getimagesize($array['file']);
                 $array['width'] = $paramet[0];

+ 4 - 0
app/Module/Chat.php

@@ -104,6 +104,7 @@ class Chat
         if (isset($message['username'])) unset($message['username']);
         if (isset($message['userimg'])) unset($message['userimg']);
         if (isset($message['indate'])) unset($message['indate']);
+        if (isset($message['replaceId'])) unset($message['replaceId']);
         $inArray = [
             'did' => $dialog['id'],
             'username' => $username,
@@ -220,6 +221,9 @@ class Chat
             case 'image':
                 $lastText = '[图片]';
                 break;
+            case 'file':
+                $lastText = '[文件]';
+                break;
             case 'taskB':
                 $lastText = $message['text'] . " [来自关注任务]";
                 break;

+ 127 - 32
resources/assets/js/main/components/chat/Index.vue

@@ -66,7 +66,11 @@
         </ul>
 
         <!--对话窗口-->
-        <div class="chat-message" :style="{display:(chatTap=='dialog'&&dialogTarget.username)?'block':'none'}">
+        <div class="chat-message"
+             :style="{display:(chatTap=='dialog'&&dialogTarget.username)?'block':'none'}"
+             @drop.prevent="messagePasteDrag($event, 'drag')"
+             @dragover.prevent="messageDragOver(true)"
+             @dragleave.prevent="messageDragOver(false)">
             <div class="manage-title">
                 <user-view :username="dialogTarget.username"/>
                 <Dropdown class="manage-title-right" placement="bottom-end" trigger="click" @on-click="dialogDropdown" transfer>
@@ -87,7 +91,7 @@
                 <div class="manage-lists-message-new" v-if="messageNew > 0" @click="messageBottomGo(true)">{{$L('有%条新消息', messageNew)}}</div>
             </ScrollerY>
             <div class="manage-send" @click="clickDialog(dialogTarget.username)">
-                <textarea ref="textarea" class="manage-input" maxlength="20000" v-model="messageText" :placeholder="$L('请输入要发送的消息')" @keydown="messageSend($event)"></textarea>
+                <textarea ref="textarea" class="manage-input" maxlength="20000" v-model="messageText" :placeholder="$L('请输入要发送的消息')" @keydown="messageSend($event)" @paste="messagePasteDrag"></textarea>
             </div>
             <div class="manage-quick">
                 <emoji-picker @emoji="messageInsertText" :search="messageEmojiSearch">
@@ -110,9 +114,15 @@
                         </div>
                     </div>
                 </emoji-picker>
-                <Tooltip :content="$L('图片')" placement="top">
+                <Tooltip :content="$L('文件/图片')" placement="top">
                     <Icon class="quick-item" type="ios-photos-outline" @click="$refs.messageUpload.handleClick()"/>
-                    <img-upload ref="messageUpload" class="message-upload" type="callback" @on-callback="messageInsertImage" num="3" :otherParams="{from:'chat'}"></img-upload>
+                    <ChatLoad
+                        ref="messageUpload"
+                        class="message-upload"
+                        :target="dialogTarget.username"
+                        @on-progress="messageFile('progress', $event)"
+                        @on-success="messageFile('success', $event)"
+                        @on-error="messageFile('error', $event)"/>
                 </Tooltip>
                 <template v-if="systemConfig.callav=='open'">
                     <Tooltip :content="$L('语音聊天')" placement="top">
@@ -123,6 +133,9 @@
                     </Tooltip>
                 </template>
             </div>
+            <div v-if="dialogDragOver" class="manage-drag-over">
+                <div class="manage-drag-text">{{$L('拖动到这里发送给 %', dialogTarget.nickname || dialogTarget.username)}}</div>
+            </div>
         </div>
 
         <!--语音、视频通话-->
@@ -627,6 +640,33 @@
                     overflow: hidden;
                 }
             }
+            .manage-drag-over {
+                position: absolute;
+                top: 0;
+                left: 0;
+                right: 0;
+                bottom: 0;
+                z-index: 3;
+                background-color: rgba(255, 255, 255, 0.78);
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                &:before {
+                    content: "";
+                    position: absolute;
+                    top: 16px;
+                    left: 16px;
+                    right: 16px;
+                    bottom: 16px;
+                    border: 2px dashed #7b7b7b;
+                    border-radius: 12px;
+                }
+                .manage-drag-text {
+                    padding: 12px;
+                    font-size: 18px;
+                    color: #666666;
+                }
+            }
             @media screen and (max-width: 768px) {
                 .manage-lists {
                     bottom: 96px;
@@ -806,12 +846,13 @@
     import EmojiPicker from 'vue-emoji-picker'
     import DrawerTabsContainer from "../DrawerTabsContainer";
     import ScrollerY from "../../../_components/ScrollerY";
-    import ChatMessage from "./message";
+    import ChatMessage from "./Message";
     import ImgUpload from "../ImgUpload";
+    import ChatLoad from "./Upload";
 
     export default {
         name: 'ChatIndex',
-        components: {ImgUpload, ChatMessage, EmojiPicker, ScrollerY, DrawerTabsContainer},
+        components: {ChatLoad, ImgUpload, ChatMessage, EmojiPicker, ScrollerY, DrawerTabsContainer},
         props: {
             value: {
                 default: 0
@@ -838,6 +879,7 @@
                 dialogTarget: {},
                 dialogLists: [],
                 dialogNoDataText: '',
+                dialogDragOver: false,
 
                 teamSearch: '',
                 teamReady: false,
@@ -1417,39 +1459,41 @@
                 this.messageText+= emoji;
             },
 
-            messageInsertImage(lists) {
-                for (let i = 0; i < lists.length; i++) {
-                    let item = lists[i];
-                    if (typeof item === 'object' && typeof item.url === "string") {
-                        let data = {
-                            type: 'image',
-                            username: this.userInfo.username,
-                            userimg: this.userInfo.userimg,
-                            indate: Math.round(new Date().getTime() / 1000),
-                            url: item.url,
-                            width: $A.getObject(item, 'response.data.width'),
-                            height: $A.getObject(item, 'response.data.height'),
-                        };
-                        $A.WSOB.sendTo('user', this.dialogTarget.username, data, (res) => {
-                            this.$set(data, res.status === 1 ? 'id' : 'error', res.message)
-                        });
-                        //
-                        this.addDialog(Object.assign(this.dialogTarget, {
-                            lasttext: this.$L('[图片]'),
-                            lastdate: data.indate
-                        }));
-                        this.openDialog(this.dialogTarget);
-                        this.addMessageData(data, true);
-                    }
+            messageInsertFile(item) {
+                if (typeof item === 'object' && typeof item.url === "string") {
+                    let data = {
+                        type: ['jpg', 'jpeg', 'png', 'gif'].indexOf(item.ext) !== -1 ? 'image' : 'file',
+                        filename: item.name,
+                        filesize: item.size,
+                        filethumb: item.thumb,
+                        username: this.userInfo.username,
+                        userimg: this.userInfo.userimg,
+                        indate: Math.round(new Date().getTime() / 1000),
+                        url: item.url,
+                        width: $A.getObject(item, 'response.data.width'),
+                        height: $A.getObject(item, 'response.data.height'),
+                        replaceId: item.tempId,
+                    };
+                    $A.WSOB.sendTo('user', this.dialogTarget.username, data, (res) => {
+                        this.$set(data, res.status === 1 ? 'id' : 'error', res.message)
+                    });
+                    //
+                    this.addDialog(Object.assign(this.dialogTarget, {
+                        lasttext: this.$L(data.type == 'image' ? '[图片]' : '[文件]'),
+                        lastdate: data.indate
+                    }));
+                    this.openDialog(this.dialogTarget);
+                    this.addMessageData(data, true);
                 }
             },
 
             addMessageData(data, animation = false, isUnshift = false) {
                 data.self = data.username === this.userInfo.username;
                 let sikp = false;
-                if (data.id) {
+                if (data.id || data.replaceId) {
                     this.messageLists.some((item, index) => {
-                        if (item.id == data.id) {
+                        if (item.id == data.id || item.id == data.replaceId) {
+                            data.nickname = data.nickname || item.nickname;
                             this.messageLists.splice(index, 1, data);
                             return sikp = true;
                         }
@@ -1480,6 +1524,57 @@
                 }
             },
 
+            messageDragOver(show) {
+                let random = (this.__dialogDragOver = $A.randomString(8));
+                if (!show) {
+                    setTimeout(() => {
+                        if (random === this.__dialogDragOver) {
+                            this.dialogDragOver = show;
+                        }
+                    }, 150);
+                } else {
+                    this.dialogDragOver = show;
+                }
+            },
+
+            messagePasteDrag(e, type) {
+                this.dialogDragOver = false;
+                const files = type === 'drag' ? e.dataTransfer.files : e.clipboardData.files;
+                const postFiles = Array.prototype.slice.call(files);
+                if (postFiles.length > 0) {
+                    e.preventDefault();
+                    postFiles.forEach((file) => {
+                        this.$refs.messageUpload.upload(file);
+                    });
+                }
+            },
+
+            messageFile(type, file) {
+                switch (type) {
+                    case 'progress':
+                        this.addMessageData({
+                            id: file.tempId,
+                            type: 'image',
+                            username: this.userInfo.username,
+                            userimg: this.userInfo.userimg,
+                            indate: Math.round(new Date().getTime() / 1000),
+                            url: 'loading',
+                        }, true);
+                        break;
+                    case 'error':
+                        this.messageLists.some((item, index) => {
+                            if (item.id == file.tempId) {
+                                this.messageLists.splice(index, 1);
+                                return true;
+                            }
+                        });
+                        break;
+                    case 'success':
+                        this.messageInsertFile(file);
+                        break;
+                }
+            },
+
             messageSubmit() {
                 let dialogUser = this.dialogLists.filter((item) => { return item.username == this.dialogTarget.username });
                 if (dialogUser.length > 0) {

+ 71 - 9
resources/assets/js/main/components/chat/message.vue

@@ -53,8 +53,8 @@
             </div>
         </div>
 
-        <!--图片-->
-        <div v-else-if="info.type==='image'">
+        <!--图片、文件-->
+        <div v-else-if="info.type==='image' || info.type==='file'">
             <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" />
@@ -64,8 +64,18 @@
                         <em class="item-name"><user-view :username="userName" :info="info" 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"/>
+                    <div v-if="info.url==='loading'" class="item-loading">
+                        <WLoading/>
+                    </div>
+                    <a v-else class="item-file" :href="info.url" target="_blank">
+                        <div v-if="info.type==='file'" class="item-file-box">
+                            <img class="item-file-thumb" :src="info.filethumb"/>
+                            <div class="item-file-info">
+                                <div class="item-file-name">{{info.filename}}</div>
+                                <div class="item-file-size">{{$A.bytesToSize($A.runNum(info.filesize) * 1024)}}</div>
+                            </div>
+                        </div>
+                        <img v-else class="item-file-img" :src="info.url"/>
                     </a>
                 </div>
                 <UserImg :info="info" @click="clickUser" class="item-userimg"/>
@@ -78,8 +88,18 @@
                         <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"/>
+                    <div v-if="info.url==='loading'" class="item-loading">
+                        <WLoading/>
+                    </div>
+                    <a v-else class="item-file" :href="info.url" target="_blank">
+                        <div v-if="info.type==='file'" class="item-file-box">
+                            <img class="item-file-thumb" :src="info.filethumb"/>
+                            <div class="item-file-info">
+                                <div class="item-file-name">{{info.filename}}</div>
+                                <div class="item-file-size">{{$A.bytesToSize($A.runNum(info.filesize) * 1024)}}</div>
+                            </div>
+                        </div>
+                        <img v-else class="item-file-img" :src="info.url"/>
                     </a>
                 </div>
             </div>
@@ -235,11 +255,53 @@
         }
     }
 
-    /*图片*/
-    .item-image {
+    /*加载中*/
+    .item-loading {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        width: 28px;
+        height: 28px;
+        margin: 24px;
+    }
+
+    /*文件、图片*/
+    .item-file {
         display: inline-block;
         text-decoration: none;
-        .item-image-view {
+        .item-file-box {
+            background: #ffffff;
+            display: flex;
+            align-items: center;
+            padding: 10px 14px;
+            border-radius: 3px;
+            .item-file-thumb {
+                width: 36px;
+            }
+            .item-file-info {
+                margin-left: 12px;
+                display: flex;
+                flex-direction: column;
+                justify-content: center;
+                .item-file-name {
+                    color: #333333;
+                    font-size: 14px;
+                    max-width: 220px;
+                    word-break: break-all;
+                    text-overflow: ellipsis;
+                    overflow: hidden;
+                    display: -webkit-box;
+                    -webkit-line-clamp: 2;
+                    -webkit-box-orient: vertical;
+                }
+                .item-file-size {
+                    padding-top: 2px;
+                    color: #666666;
+                    font-size: 14px;
+                }
+            }
+        }
+        .item-file-img {
             max-width: 220px;
             max-height: 220px;
             border-radius: 6px;

+ 115 - 0
resources/assets/js/main/components/chat/Upload.vue

@@ -0,0 +1,115 @@
+<template>
+    <Upload
+        name="files"
+        ref="upload"
+        :action="actionUrl"
+        :data="params"
+        multiple
+        :format="uploadFormat"
+        :show-upload-list="false"
+        :max-size="maxSize"
+        :on-progress="handleProgress"
+        :on-success="handleSuccess"
+        :on-format-error="handleFormatError"
+        :on-exceeded-size="handleMaxSize"
+        :before-upload="handleBeforeUpload">
+    </Upload>
+</template>
+
+<script>
+export default {
+    name: 'ChatLoad',
+    props: {
+        target: {
+            default: ''
+        },
+        maxSize: {
+            type: Number,
+            default: 10240
+        }
+    },
+
+    data() {
+        return {
+            uploadFormat: ['jpg', 'jpeg', 'png', 'gif', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'],
+            actionUrl: $A.apiUrl('chat/files/upload'),
+            params: {
+                username: this.target,
+                token: $A.getToken(),
+            }
+        }
+    },
+
+    mounted() {
+
+    },
+
+    watch: {},
+
+    methods: {
+        handleProgress(event, file) {
+            //上传时
+            if (typeof file.tempId === "undefined") {
+                file.tempId = $A.randomString(8);
+                this.$emit('on-progress', file);
+            }
+        },
+
+        handleSuccess(res, file) {
+            //上传完成
+            if (res.ret === 1) {
+                for (let key in res.data) {
+                    if (res.data.hasOwnProperty(key)) {
+                        file[key] = res.data[key];
+                    }
+                }
+                this.$emit('on-success', file);
+            } else {
+                this.$Modal.warning({
+                    title: this.$L('上传失败'),
+                    content: this.$L('文件 % 上传失败,%', file.name, res.msg)
+                });
+                this.$emit('on-error', file);
+                this.$refs.upload.fileList.pop();
+            }
+        },
+
+        handleFormatError(file) {
+            //上传类型错误
+            this.$Modal.warning({
+                title: this.$L('文件格式不正确'),
+                content: this.$L('文件 % 格式不正确,仅支持上传:%', file.name, this.uploadFormat.join(','))
+            });
+        },
+
+        handleMaxSize(file) {
+            //上传大小错误
+            this.$Modal.warning({
+                title: this.$L('超出文件大小限制'),
+                content: this.$L('文件 % 太大,不能超过%。', file.name, this.maxSize + 'KB')
+            });
+        },
+
+        handleBeforeUpload() {
+            //上传前判断
+            this.params = {
+                username: this.target,
+                token: $A.getToken(),
+            };
+            return true;
+        },
+
+        handleClick() {
+            //手动上传
+            if (this.handleBeforeUpload()) {
+                this.$refs.upload.handleClick()
+            }
+        },
+
+        upload(file) {
+            //手动传file
+            this.$refs.upload.upload(file);
+        },
+    }
+}
+</script>

+ 395 - 0
resources/assets/js/main/components/iview/WInput.vue

@@ -0,0 +1,395 @@
+<template>
+    <div :class="wrapClasses">
+        <template v-if="type !== 'textarea'">
+            <div :class="[prefixCls + '-group-prepend']" v-if="prepend" v-show="slotReady"><slot name="prepend"></slot></div>
+            <i class="ivu-icon" :class="['ivu-icon-ios-close-circle', prefixCls + '-icon', prefixCls + '-icon-clear' , prefixCls + '-icon-normal']" v-if="clearable && currentValue && !itemDisabled" @click="handleClear"></i>
+            <i class="ivu-icon" :class="['ivu-icon-' + icon, prefixCls + '-icon', prefixCls + '-icon-normal']" v-else-if="icon" @click="handleIconClick"></i>
+            <i class="ivu-icon ivu-icon-ios-search" :class="[prefixCls + '-icon', prefixCls + '-icon-normal', prefixCls + '-search-icon']" v-else-if="search && enterButton === false" @click="handleSearch"></i>
+            <span class="ivu-input-suffix" v-else-if="showSuffix"><slot name="suffix"><i class="ivu-icon" :class="['ivu-icon-' + suffix]" v-if="suffix"></i></slot></span>
+            <span class="ivu-input-word-count" v-else-if="showWordLimit">{{ textLength }}/{{ upperLimit }}</span>
+            <span class="ivu-input-suffix" v-else-if="password" @click="handleToggleShowPassword">
+                <i class="ivu-icon ivu-icon-ios-eye-off-outline" v-if="showPassword"></i>
+                <i class="ivu-icon ivu-icon-ios-eye-outline" v-else></i>
+            </span>
+            <transition name="fade">
+                <i class="ivu-icon ivu-icon-ios-loading ivu-load-loop" :class="[prefixCls + '-icon', prefixCls + '-icon-validate']" v-if="!icon"></i>
+            </transition>
+            <input
+                :id="elementId"
+                :autocomplete="autocomplete"
+                :spellcheck="spellcheck"
+                ref="input"
+                :type="currentType"
+                :class="inputClasses"
+                :placeholder="placeholder"
+                :disabled="itemDisabled"
+                :maxlength="maxlength"
+                :readonly="readonly"
+                :name="name"
+                :value="currentValue"
+                :number="number"
+                :autofocus="autofocus"
+                @keyup.enter="handleEnter"
+                @keyup="handleKeyup"
+                @keypress="handleKeypress"
+                @keydown="handleKeydown"
+                @focus="handleFocus"
+                @blur="handleBlur"
+                @compositionstart="handleComposition"
+                @compositionupdate="handleComposition"
+                @compositionend="handleComposition"
+                @input="handleInput"
+                @change="handleChange"
+                @paste="handlePaste">
+            <div :class="[prefixCls + '-group-append']" v-if="append" v-show="slotReady"><slot name="append"></slot></div>
+            <div :class="[prefixCls + '-group-append', prefixCls + '-search']" v-else-if="search && enterButton" @click="handleSearch">
+                <i class="ivu-icon ivu-icon-ios-search" v-if="enterButton === true"></i>
+                <template v-else>{{ enterButton }}</template>
+            </div>
+            <span class="ivu-input-prefix" v-else-if="showPrefix"><slot name="prefix"><i class="ivu-icon" :class="['ivu-icon-' + prefix]" v-if="prefix"></i></slot></span>
+        </template>
+        <template v-else>
+            <textarea
+                :id="elementId"
+                :wrap="wrap"
+                :autocomplete="autocomplete"
+                :spellcheck="spellcheck"
+                ref="textarea"
+                :class="textareaClasses"
+                :style="textareaStyles"
+                :placeholder="placeholder"
+                :disabled="itemDisabled"
+                :rows="rows"
+                :maxlength="maxlength"
+                :readonly="readonly"
+                :name="name"
+                :value="currentValue"
+                :autofocus="autofocus"
+                @keyup.enter="handleEnter"
+                @keyup="handleKeyup"
+                @keypress="handleKeypress"
+                @keydown="handleKeydown"
+                @focus="handleFocus"
+                @blur="handleBlur"
+                @compositionstart="handleComposition"
+                @compositionupdate="handleComposition"
+                @compositionend="handleComposition"
+                @input="handleInput"
+                @paste="handlePaste">
+            </textarea>
+            <span class="ivu-input-word-count" v-if="showWordLimit">{{ textLength }}/{{ upperLimit }}</span>
+        </template>
+    </div>
+</template>
+<script>
+    import { oneOf, findComponentUpward } from 'view-design/src/utils/assist';
+    import calcTextareaHeight from 'view-design/src/utils/calcTextareaHeight';
+    import Emitter from 'view-design/src/mixins/emitter';
+    import mixinsForm from 'view-design/src/mixins/form';
+
+    const prefixCls = 'ivu-input';
+
+    export default {
+        name: 'WInput',
+        mixins: [ Emitter, mixinsForm ],
+        props: {
+            type: {
+                validator (value) {
+                    return oneOf(value, ['text', 'textarea', 'password', 'url', 'email', 'date', 'number', 'tel']);
+                },
+                default: 'text'
+            },
+            value: {
+                type: [String, Number],
+                default: ''
+            },
+            size: {
+                validator (value) {
+                    return oneOf(value, ['small', 'large', 'default']);
+                },
+                default () {
+                    return !this.$IVIEW || this.$IVIEW.size === '' ? 'default' : this.$IVIEW.size;
+                }
+            },
+            placeholder: {
+                type: String,
+                default: ''
+            },
+            maxlength: {
+                type: [String, Number]
+            },
+            disabled: {
+                type: Boolean,
+                default: false
+            },
+            icon: String,
+            autosize: {
+                type: [Boolean, Object],
+                default: false
+            },
+            rows: {
+                type: Number,
+                default: 2
+            },
+            readonly: {
+                type: Boolean,
+                default: false
+            },
+            name: {
+                type: String
+            },
+            number: {
+                type: Boolean,
+                default: false
+            },
+            autofocus: {
+                type: Boolean,
+                default: false
+            },
+            spellcheck: {
+                type: Boolean,
+                default: false
+            },
+            autocomplete: {
+                type: String,
+                default: 'off'
+            },
+            clearable: {
+                type: Boolean,
+                default: false
+            },
+            elementId: {
+                type: String
+            },
+            wrap: {
+                validator (value) {
+                    return oneOf(value, ['hard', 'soft']);
+                },
+                default: 'soft'
+            },
+            prefix: {
+                type: String,
+                default: ''
+            },
+            suffix: {
+                type: String,
+                default: ''
+            },
+            search: {
+                type: Boolean,
+                default: false
+            },
+            enterButton: {
+                type: [Boolean, String],
+                default: false
+            },
+            // 4.0.0
+            showWordLimit: {
+                type: Boolean,
+                default: false
+            },
+            // 4.0.0
+            password: {
+                type: Boolean,
+                default: false
+            }
+        },
+        data () {
+            return {
+                currentValue: this.value,
+                prefixCls: prefixCls,
+                slotReady: false,
+                textareaStyles: {},
+                isOnComposition: false,
+                showPassword: false
+            };
+        },
+        computed: {
+            currentType () {
+                let type = this.type;
+                if (type === 'password' && this.password && this.showPassword) type = 'text';
+                return type;
+            },
+            prepend () {
+                let state = false;
+                if (this.type !== 'textarea') state = this.$slots.prepend !== undefined;
+                return state;
+            },
+            append () {
+                let state = false;
+                if (this.type !== 'textarea') state = this.$slots.append !== undefined;
+                return state;
+            },
+            showPrefix () {
+                let state = false;
+                if (this.type !== 'textarea') state = this.prefix !== '' || this.$slots.prefix !== undefined;
+                return state;
+            },
+            showSuffix () {
+                let state = false;
+                if (this.type !== 'textarea') state = this.suffix !== '' || this.$slots.suffix !== undefined;
+                return state;
+            },
+            wrapClasses () {
+                return [
+                    `${prefixCls}-wrapper`,
+                    {
+                        [`${prefixCls}-wrapper-${this.size}`]: !!this.size,
+                        [`${prefixCls}-type-${this.type}`]: this.type,
+                        [`${prefixCls}-group`]: this.prepend || this.append || (this.search && this.enterButton),
+                        [`${prefixCls}-group-${this.size}`]: (this.prepend || this.append || (this.search && this.enterButton)) && !!this.size,
+                        [`${prefixCls}-group-with-prepend`]: this.prepend,
+                        [`${prefixCls}-group-with-append`]: this.append || (this.search && this.enterButton),
+                        [`${prefixCls}-hide-icon`]: this.append,  // #554
+                        [`${prefixCls}-with-search`]: (this.search && this.enterButton)
+                    }
+                ];
+            },
+            inputClasses () {
+                return [
+                    `${prefixCls}`,
+                    {
+                        [`${prefixCls}-${this.size}`]: !!this.size,
+                        [`${prefixCls}-disabled`]: this.itemDisabled,
+                        [`${prefixCls}-with-prefix`]: this.showPrefix,
+                        [`${prefixCls}-with-suffix`]: this.showSuffix || (this.search && this.enterButton === false)
+                    }
+                ];
+            },
+            textareaClasses () {
+                return [
+                    `${prefixCls}`,
+                    {
+                        [`${prefixCls}-disabled`]: this.itemDisabled
+                    }
+                ];
+            },
+            upperLimit () {
+                return this.maxlength;
+            },
+            textLength () {
+                if (typeof this.value === 'number') {
+                    return String(this.value).length;
+                }
+
+                return (this.value || '').length;
+            }
+        },
+        methods: {
+            handleEnter (event) {
+                this.$emit('on-enter', event);
+                if (this.search) this.$emit('on-search', this.currentValue);
+            },
+            handleKeydown (event) {
+                this.$emit('on-keydown', event);
+            },
+            handleKeypress(event) {
+                this.$emit('on-keypress', event);
+            },
+            handleKeyup (event) {
+                this.$emit('on-keyup', event);
+            },
+            handleIconClick (event) {
+                this.$emit('on-click', event);
+            },
+            handleFocus (event) {
+                this.$emit('on-focus', event);
+            },
+            handleBlur (event) {
+                this.$emit('on-blur', event);
+                if (!findComponentUpward(this, ['DatePicker', 'TimePicker', 'Cascader', 'Search'])) {
+                    this.dispatch('FormItem', 'on-form-blur', this.currentValue);
+                }
+            },
+            handleComposition(event) {
+                if (event.type === 'compositionstart') {
+                    this.isOnComposition = true;
+                }
+                if (event.type === 'compositionend') {
+                    this.isOnComposition = false;
+                    this.handleInput(event);
+                }
+            },
+            handleInput (event) {
+                if (this.isOnComposition) return;
+
+                let value = event.target.value;
+                if (this.number && value !== '') value = Number.isNaN(Number(value)) ? value : Number(value);
+                this.$emit('input', value);
+                this.setCurrentValue(value);
+                this.$emit('on-change', event);
+            },
+            handleChange (event) {
+                this.$emit('on-input-change', event);
+            },
+            handlePaste (event) {
+                this.$emit('on-input-paste', event);
+            },
+            setCurrentValue (value) {
+                if (value === this.currentValue) return;
+                this.$nextTick(() => {
+                    this.resizeTextarea();
+                });
+                this.currentValue = value;
+                if (!findComponentUpward(this, ['DatePicker', 'TimePicker', 'Cascader', 'Search'])) {
+                    this.dispatch('FormItem', 'on-form-change', value);
+                }
+            },
+            resizeTextarea () {
+                const autosize = this.autosize;
+                if (!autosize || this.type !== 'textarea') {
+                    return false;
+                }
+
+                const minRows = autosize.minRows;
+                const maxRows = autosize.maxRows;
+
+                this.textareaStyles = calcTextareaHeight(this.$refs.textarea, minRows, maxRows);
+            },
+            focus () {
+                if (this.type === 'textarea') {
+                    this.$refs.textarea.focus();
+                } else {
+                    this.$refs.input.focus();
+                }
+            },
+            blur () {
+                if (this.type === 'textarea') {
+                    this.$refs.textarea.blur();
+                } else {
+                    this.$refs.input.blur();
+                }
+            },
+            handleClear () {
+                const e = { target: { value: '' } };
+                this.$emit('input', '');
+                this.setCurrentValue('');
+                this.$emit('on-change', e);
+                this.$emit('on-clear');
+            },
+            handleSearch () {
+                if (this.itemDisabled) return false;
+                this.$refs.input.focus();
+                this.$emit('on-search', this.currentValue);
+            },
+            handleToggleShowPassword () {
+                if (this.itemDisabled) return false;
+                this.showPassword = !this.showPassword;
+                this.focus();
+                const len = this.currentValue.length;
+                setTimeout(() => {
+                    this.$refs.input.setSelectionRange(len, len);
+                }, 0);
+            }
+        },
+        watch: {
+            value (val) {
+                this.setCurrentValue(val);
+            }
+        },
+        mounted () {
+            this.slotReady = true;
+            this.resizeTextarea();
+        }
+    };
+</script>

+ 67 - 9
resources/assets/js/main/components/project/task/detail/detail.vue

@@ -1,6 +1,10 @@
 <template>
     <div class="project-task-detail-window" :class="{'task-detail-show': visible}" @click="$nextTick(()=>{visible=false})">
-        <div class="task-detail-main" @click.stop="">
+        <div class="task-detail-main"
+             @click.stop=""
+             @drop.prevent="commentPasteDrag($event, 'drag')"
+             @dragover.prevent="commentDragOver(true)"
+             @dragleave.prevent="commentDragOver(false)">
             <div class="detail-left">
                 <div class="detail-title-box detail-icon">
                     <Input v-model="detail.title"
@@ -122,14 +126,14 @@
                             <Button class="detail-button-btn" size="small" @click="handleTask('fileupload')">{{$L('添加附件')}}</Button>
                         </div>
                     </div>
-                    <project-task-files ref="upload" :taskid="taskid" :projectid="detail.projectid" :simple="true" @change="handleTask('filechange', $event)"></project-task-files>
+                    <project-task-files ref="projectUpload" :taskid="taskid" :projectid="detail.projectid" :simple="true" @change="handleTask('filechange', $event)"></project-task-files>
                 </div>
                 <div class="detail-h2 detail-comment-box detail-icon"><strong class="link" :class="{active:logType=='评论'}" @click="logType='评论'">{{$L('评论')}}</strong><em></em><strong class="link" :class="{active:logType=='日志'}" @click="logType='日志'">{{$L('操作记录')}}</strong></div>
                 <div class="detail-log-box">
                     <project-task-logs ref="log" :logtype="logType" :projectid="detail.projectid" :taskid="taskid" :pagesize="5"></project-task-logs>
                 </div>
                 <div class="detail-footer-box">
-                    <Input class="comment-input" v-model="commentText" type="textarea" :rows="1" :autosize="{ minRows: 1, maxRows: 3 }" :maxlength="255" @on-keydown="commentKeydown" :placeholder="$L('输入评论,Enter发表评论,Shift+Enter换行')" />
+                    <WInput class="comment-input" v-model="commentText" type="textarea" :rows="1" :autosize="{ minRows: 1, maxRows: 3 }" :maxlength="255" @on-keydown="commentKeydown" @on-input-paste="commentPasteDrag" :placeholder="$L('输入评论,Enter发表评论,Shift+Enter换行')" />
                     <Button :loading="!!loadData.comment" :disabled="!commentText" type="primary" @click="handleTask('comment')">评 论</Button>
                 </div>
             </div>
@@ -193,6 +197,9 @@
                 <Button v-else :loading="!!loadData.unarchived" icon="md-filing" class="btn" @click="handleTask('unarchived')">{{$L('取消归档')}}</Button>
                 <Button :loading="!!loadData.delete" icon="md-trash" class="btn" type="error" ghost @click="handleTask('deleteb')">{{$L('删除')}}</Button>
             </div>
+            <div v-if="detailDragOver" class="detail-drag-over">
+                <div class="detail-drag-text">{{$L('拖动到这里添加附件至 %', detail.title)}}</div>
+            </div>
         </div>
     </div>
 </template>
@@ -202,13 +209,15 @@
     import ProjectTaskFiles from "../files";
     import draggable from 'vuedraggable'
     import cloneDeep from "lodash/cloneDeep";
+    import WInput from "../../../iview/WInput";
 
     export default {
-        components: {ProjectTaskFiles, ProjectTaskLogs, draggable},
+        components: {WInput, ProjectTaskFiles, ProjectTaskLogs, draggable},
         data() {
             return {
                 taskid: 0,
                 detail: {},
+                detailDragOver: false,
 
                 visible: false,
 
@@ -353,7 +362,6 @@
             },
 
             titleKeydown(e) {
-                e = e || event;
                 if (e.keyCode == 13) {
                     e.preventDefault();
                     e.target.blur();
@@ -361,7 +369,6 @@
             },
 
             descKeydown(e) {
-                e = e || event;
                 if (e.keyCode == 13) {
                     if (e.shiftKey) {
                         return;
@@ -372,7 +379,6 @@
             },
 
             commentKeydown(e) {
-                e = e || event;
                 if (e.keyCode == 13) {
                     if (e.shiftKey) {
                         return;
@@ -382,8 +388,32 @@
                 }
             },
 
+            commentDragOver(show) {
+                let random = (this.__detailDragOver = $A.randomString(8));
+                if (!show) {
+                    setTimeout(() => {
+                        if (random === this.__detailDragOver) {
+                            this.detailDragOver = show;
+                        }
+                    }, 150);
+                } else {
+                    this.detailDragOver = show;
+                }
+            },
+
+            commentPasteDrag(e, type) {
+                this.detailDragOver = false;
+                const files = type === 'drag' ? e.dataTransfer.files : e.clipboardData.files;
+                const postFiles = Array.prototype.slice.call(files);
+                if (postFiles.length > 0) {
+                    e.preventDefault();
+                    postFiles.forEach((file) => {
+                        this.$refs.projectUpload.upload(file);
+                    });
+                }
+            },
+
             subtaskKeydown(subindex, e) {
-                e = e || event;
                 if (e.keyCode == 13) {
                     if (e.shiftKey) {
                         return;
@@ -584,7 +614,7 @@
                         break;
 
                     case 'fileupload':
-                        this.$refs.upload.uploadHandleClick();
+                        this.$refs.projectUpload.uploadHandleClick();
                         return;
 
                     case 'filechange':
@@ -1242,6 +1272,34 @@
                     text-overflow: ellipsis;
                 }
             }
+            .detail-drag-over {
+                position: absolute;
+                top: 0;
+                left: 0;
+                right: 0;
+                bottom: 0;
+                z-index: 3;
+                background-color: rgba(255, 255, 255, 0.78);
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                border-radius: 4px;
+                &:before {
+                    content: "";
+                    position: absolute;
+                    top: 16px;
+                    left: 16px;
+                    right: 16px;
+                    bottom: 16px;
+                    border: 2px dashed #7b7b7b;
+                    border-radius: 12px;
+                }
+                .detail-drag-text {
+                    padding: 12px;
+                    font-size: 18px;
+                    color: #666666;
+                }
+            }
         }
     }
 </style>

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

@@ -16,7 +16,7 @@
                 </div>
                 <div class="item item-button">
                     <Button type="text" v-if="$A.objImplode(keys)!=''" @click="sreachTab(true)">{{$L('取消筛选')}}</Button>
-                    <Button type="primary" icon="md-search" :loading="loadIng > 0" @click="sreachTab">{{$L('搜索')}}</Button type="primary">
+                    <Button type="primary" icon="md-search" :loading="loadIng > 0" @click="sreachTab">{{$L('搜索')}}</Button>
                 </div>
             </Row>
 
@@ -557,6 +557,10 @@
                 };
                 return true;
             },
+
+            upload(file) {
+                this.$refs.upload.upload(file);
+            },
         }
     }
 </script>

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

@@ -919,6 +919,9 @@ import '../../sass/main.scss';
                     case 'image':
                         desc = $A.app.$L('[图片]');
                         break;
+                    case 'file':
+                        desc = $A.app.$L('[文件]');
+                        break;
                     case 'taskB':
                         desc = content.text + " " + $A.app.$L("[来自关注任务]");
                         break;

+ 1 - 0
resources/lang/en/general.js

@@ -361,6 +361,7 @@ export default {
     "有%条新消息": "There are new messages%",
     "请输入要发送的消息": "Please enter the message to be sent",
     "[图片]": "[image]",
+    "[文件]": "[file]",
     "[来自关注任务]": "[Attention from the task]",
     "[来自工作报告]": "[From work report]",
     "[未知类型]": "[Unknown type]",

+ 3 - 0
resources/notify/chrome/js/base.js

@@ -214,6 +214,9 @@
             case 'image':
                 desc = '[图片]';
                 break;
+            case 'file':
+                desc = '[文件]';
+                break;
             case 'taskB':
                 desc = content.text + " [来自关注任务]";
                 break;