ソースを参照

任务描述支持富文本、图片粘贴;
修复关注者无法查看任务详情的问题;
子任务添加负责人;
项目面板任务列表已完成任务放在最下面

kuaifan 5 年 前
コミット
1575246afd

+ 145 - 47
app/Http/Controllers/Api/ProjectController.php

@@ -113,6 +113,7 @@ class ProjectController extends Controller
             $taskLists = [];
             foreach ($task AS $info) {
                 if ($temp['id'] == $info['labelid']) {
+                    $info['persons'] = Project::taskPersons($info);
                     $info['overdue'] = Project::taskIsOverdue($info);
                     $info['subtask'] = Base::string2array($info['subtask']);
                     $info['follower'] = Base::string2array($info['follower']);
@@ -1078,7 +1079,6 @@ class ProjectController extends Controller
      * 项目任务-列表
      *
      * @apiParam {Number} [projectid]           项目ID
-     * @apiParam {Number} [taskid]              任务ID (1、填写此参数时projectid强制为此任务的projectid;2、赋值返回详细数据,不返回列表数据)
      * @apiParam {Number} [labelid]             项目子分类ID
      * @apiParam {String} [username]            负责人用户名(如果项目ID为空时此参数无效只获取自己的任务)
      * @apiParam {Number} [level]               任务等级(1~4)
@@ -1114,22 +1114,8 @@ class ProjectController extends Controller
             $user = $user['data'];
         }
         //
-        $taskid = intval(Request::input('taskid'));
-        $isOwner = false;
-        if ($taskid > 0) {
-            $taskDetail = Base::DBC2A(DB::table('project_task')->where('id', $taskid)->first());
-            if (empty($taskDetail)) {
-                return Base::retError('任务不存在!');
-            }
-            if ($taskDetail['username'] == $user['username']) {
-                $isOwner = true;
-            }
-            $projectid = $taskDetail['projectid'];
-        } else {
-            $projectid = intval(Request::input('projectid'));
-        }
-        //
-        if ($projectid > 0 && !$isOwner) {
+        $projectid = intval(Request::input('projectid'));
+        if ($projectid > 0) {
             $inRes = Project::inThe($projectid, $user['username']);
             if (Base::isError($inRes)) {
                 return $inRes;
@@ -1156,6 +1142,7 @@ class ProjectController extends Controller
             }
         }
         //
+        $builder = DB::table('project_task');
         $selectArray = ['project_task.*'];
         $whereRaw = null;
         $whereFunc = null;
@@ -1183,12 +1170,17 @@ class ProjectController extends Controller
                     $whereArray[] = ['project_task.username', '=', trim(Request::input('username'))];
                 }
             } else {
-                $whereArray[] = ['project_task.username', '=', $user['username']];
+                $builder->where(function ($query) use ($user) {
+                    $query->where('project_task.username', $user['username']);
+                    $query->orWhereIn('project_task.id', function ($inQuery) use ($user) {
+                        $inQuery->from('project_users')
+                            ->select('taskid')
+                            ->where('username', $user['username'])
+                            ->where('type', '负责人');
+                    });
+                });
             }
         }
-        if ($taskid > 0) {
-            $whereArray[] = ['project_task.id', '=', intval(Request::input('taskid'))];
-        }
         if (intval(Request::input('labelid')) > 0) {
             $whereArray[] = ['project_task.labelid', '=', intval(Request::input('labelid'))];
         }
@@ -1230,7 +1222,6 @@ class ProjectController extends Controller
             $orderBy = '`startdate` DESC';
         }
         //
-        $builder = DB::table('project_task');
         if ($projectid > 0) {
             $builder->join('project_lists', 'project_lists.id', '=', 'project_task.projectid');
         }
@@ -1249,7 +1240,7 @@ class ProjectController extends Controller
             ->where($whereArray)
             ->orderByRaw($orderBy)
             ->paginate(Base::getPaginate(100, 20));
-        $lists = Base::getPageList($lists, $taskid > 0 ? false : true);
+        $lists = Base::getPageList($lists);
         if (intval(Request::input('statistics')) == 1) {
             $lists['statistics_unfinished'] = $type === '未完成' ? $lists['total'] : DB::table('project_task')->where('projectid', $projectid)->where('delete', 0)->where('archived', 0)->where('complete', 0)->count();
             $lists['statistics_overdue'] = $type === '已超期' ? $lists['total'] : DB::table('project_task')->where('projectid', $projectid)->where('delete', 0)->where('archived', 0)->where('complete', 0)->whereBetween('enddate', [1, Base::time()])->count();
@@ -1259,21 +1250,73 @@ class ProjectController extends Controller
             return Base::retError('未找到任何相关的任务!', $lists);
         }
         foreach ($lists['lists'] AS $key => $info) {
+            $info['persons'] = Project::taskPersons($info);
             $info['overdue'] = Project::taskIsOverdue($info);
             $info['subtask'] = Base::string2array($info['subtask']);
             $info['follower'] = Base::string2array($info['follower']);
             $lists['lists'][$key] = array_merge($info, Users::username2basic($info['username']));
         }
-        if ($taskid > 0) {
-            if (count($lists['lists']) == 0) {
-                return Base::retError('未能找到此任务或无法管理此任务!');
-            }
-            $data = $lists['lists'][0];
-            $data['projectTitle'] = $data['projectid'] > 0 ? DB::table('project_lists')->where('id', $data['projectid'])->value('title') : '';
-            return Base::retSuccess('success', $data);
+        return Base::retSuccess('success', $lists);
+    }
+
+    /**
+     * 项目任务-详情(与任务有关系的用户(关注的、在项目里的、负责人、创建者)都可以查到)
+     *
+     * @apiParam {Number} taskid              任务ID
+     */
+    public function task__detail()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
         } else {
-            return Base::retSuccess('success', $lists);
+            $user = $user['data'];
+        }
+        //
+        $taskid = intval(Request::input('taskid'));
+        $tmpLists = Project::taskSomeUsers($taskid);
+        if (!in_array($user['username'], $tmpLists)) {
+            return Base::retError('未能找到此任务或无法管理此任务!');
         }
+        //
+        $task = Base::DBC2A(DB::table('project_task')->where('id', $taskid)->first());
+        $task['persons'] = Project::taskPersons($task);
+        $task['overdue'] = Project::taskIsOverdue($task);
+        $task['subtask'] = Base::string2array($task['subtask']);
+        $task['follower'] = Base::string2array($task['follower']);
+        $task = array_merge($task, Users::username2basic($task['username']));
+        $task['projectTitle'] = $task['projectid'] > 0 ? DB::table('project_lists')->where('id', $task['projectid'])->value('title') : '';
+        return Base::retSuccess('success', $task);
+    }
+
+    /**
+     * 项目任务-描述(任务有关系的用户(关注的、在项目里的、负责人、创建者)都可以查到)
+     *
+     * @apiParam {Number} taskid              任务ID
+     */
+    public function task__desc()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $taskid = intval(Request::input('taskid'));
+        $tmpLists = Project::taskSomeUsers($taskid);
+        if (!in_array($user['username'], $tmpLists)) {
+            return Base::retError('未能找到此任务或无法管理此任务!');
+        }
+        //
+        $desc = DB::table('project_content')->where('taskid', $taskid)->value('content');
+        if (empty($desc)) {
+            $desc = DB::table('project_task')->where('id', $taskid)->value('desc');
+        }
+        return Base::retSuccess('success', [
+            'taskid' => $taskid,
+            'desc' => $desc
+        ]);
     }
 
     /**
@@ -1420,6 +1463,7 @@ class ProjectController extends Controller
             Project::updateNum($inArray['projectid']);
             //
             $task = Base::DBC2A(DB::table('project_task')->where('id', $taskid)->first());
+            $task['persons'] = Project::taskPersons($task);
             $task['overdue'] = Project::taskIsOverdue($task);
             $task['subtask'] = Base::string2array($task['subtask']);
             $task['follower'] = Base::string2array($task['follower']);
@@ -1472,7 +1516,7 @@ class ProjectController extends Controller
             return Base::retError('任务不存在!');
         }
         if ($task['projectid'] > 0) {
-            if ($task['username'] != $user['username']) {
+            if (!Project::isPersons($task, $user['username'])) {
                 $inRes = Project::inThe($task['projectid'], $user['username']);
                 if (Base::isError($inRes)) {
                     return $inRes;
@@ -1507,7 +1551,7 @@ class ProjectController extends Controller
                 }
             }
         } else {
-            if ($task['username'] != $user['username']) {
+            if (!Project::isPersons($task, $user['username'])) {
                 return Base::retError('此操作只允许任务负责人!');
             }
         }
@@ -1546,10 +1590,26 @@ class ProjectController extends Controller
              * 修改描述
              */
             case 'desc': {
-                if ($content == $task['desc']) {
-                    return Base::retError('描述未做改变!');
+                preg_match_all("/<img\s*src=\"data:image\/(png|jpg|jpeg);base64,(.*?)\"/s", $content, $matchs);
+                foreach ($matchs[2] as $key => $text) {
+                    $p = "uploads/projects/" . ($task['projectid'] ?: Users::token2userid()) . "/";
+                    Base::makeDir(public_path($p));
+                    $p.= md5($text) . "." . $matchs[1][$key];
+                    $r = file_put_contents(public_path($p), base64_decode($text));
+                    if ($r) {
+                        $content = str_replace($matchs[0][$key], '<img src="' . Base::fillUrl($p) . '"', $content);
+                    }
                 }
-                $upArray['desc'] = $content;
+                Base::DBUPIN('project_content', [
+                    'taskid' => $task['id'],
+                ], [
+                    'content' => $content,
+                ], [
+                    'projectid' => $task['projectid'],
+                    'content' => $content,
+                    'indate' => Base::time()
+                ]);
+                $upArray['desc'] = Base::time();
                 $logArray[] = [
                     'type' => '日志',
                     'projectid' => $task['projectid'],
@@ -1932,6 +1992,12 @@ class ProjectController extends Controller
                 if (!is_array($content)) {
                     $content = [];
                 }
+                $subNames = [];
+                foreach ($content AS $tmp) {
+                    if ($tmp['uname'] && !in_array($tmp['uname'], $subNames)) {
+                        $subNames[] = $tmp['uname'];
+                    }
+                }
                 $content = Base::array2string($content);
                 if ($content == $task['subtask']) {
                     return Base::retError('子任务未做改变!');
@@ -1950,6 +2016,37 @@ class ProjectController extends Controller
                     $subtype = 'del';
                 }
                 //
+                if ($subNames) {
+                    DB::transaction(function() use ($task, $subNames) {
+                        foreach ($subNames AS $uname) {
+                            $row = Base::DBC2A(DB::table('project_users')->where([
+                                'type' => '负责人',
+                                'taskid' => $task['id'],
+                                'username' => $uname,
+                            ])->lockForUpdate()->first());
+                            if (empty($row)) {
+                                DB::table('project_users')->insert([
+                                    'type' => '负责人',
+                                    'projectid' => $task['projectid'],
+                                    'taskid' => $task['id'],
+                                    'isowner' => $task['username'] == $uname ? 1 : 0,
+                                    'username' => $uname,
+                                    'indate' => Base::time()
+                                ]);
+                            }
+                        }
+                        DB::table('project_users')->where([
+                            'type' => '负责人',
+                            'taskid' => $task['id'],
+                        ])->whereNotIn('username', $subNames)->delete();
+                    });
+                } else {
+                    DB::table('project_users')->where([
+                        'type' => '负责人',
+                        'taskid' => $task['id'],
+                    ])->delete();
+                }
+                //
                 $logArray[] = [
                     'type' => '日志',
                     'projectid' => $task['projectid'],
@@ -1961,6 +2058,7 @@ class ProjectController extends Controller
                         'type' => 'task',
                         'subtype' => $subtype,
                         'id' => $task['id'],
+                        'title' => $task['title'],
                         'subtask' => $content,
                         'old_subtask' => $task['subtask'],
                     ])
@@ -1970,7 +2068,6 @@ class ProjectController extends Controller
 
             default: {
                 return Base::retError('参数错误!');
-                break;
             }
         }
         //
@@ -1986,6 +2083,7 @@ class ProjectController extends Controller
         }
         //
         $task = array_merge($task, $upArray);
+        $task['persons'] = Project::taskPersons($task);
         $task['overdue'] = Project::taskIsOverdue($task);
         $task['subtask'] = Base::string2array($task['subtask']);
         $task['follower'] = Base::string2array($task['follower']);
@@ -2118,9 +2216,9 @@ class ProjectController extends Controller
             if ($taskid <= 0) {
                 return Base::retError('参数错误!');
             }
-            $count = DB::table('project_task')->where([ 'id' => $taskid, 'username' => $user['username']])->count();
-            if ($count <= 0) {
-                return Base::retError('你不是任务负责人!');
+            $tmpLists = Project::taskSomeUsers($taskid);
+            if (!in_array($user['username'], $tmpLists)) {
+                return Base::retError('未能找到此任务或无法管理此任务!');
             }
             $whereArray[] = ['taskid', '=', $taskid];
         }
@@ -2179,11 +2277,11 @@ class ProjectController extends Controller
             if ($taskid <= 0) {
                 return Base::retError('参数错误!');
             }
-            $row = Base::DBC2A(DB::table('project_task')->select(['projectid'])->where([ 'id' => $taskid, 'username' => $user['username']])->first());
-            if (empty($row)) {
-                return Base::retError('你不是任务负责人!');
+            $tmpLists = Project::taskSomeUsers($taskid);
+            if (!in_array($user['username'], $tmpLists)) {
+                return Base::retError('未能找到此任务或无法管理此任务!');
             }
-            $projectid = $row['projectid'];
+            $projectid = DB::table('project_task')->where('id', $taskid)->value('projectid');
         }
         //
         $path = "uploads/projects/" . ($projectid ?: Users::token2userid()) . "/";
@@ -2433,9 +2531,9 @@ class ProjectController extends Controller
             if ($taskid < 0) {
                 return Base::retError('参数错误!');
             }
-            $count = DB::table('project_task')->where([ 'id' => $taskid, 'username' => $user['username']])->count();
-            if ($count <= 0) {
-                return Base::retError('你不是任务负责人!');
+            $tmpLists = Project::taskSomeUsers($taskid);
+            if (!in_array($user['username'], $tmpLists)) {
+                return Base::retError('未能找到此任务或无法管理此任务!');
             }
             $whereArray[] = ['taskid', '=', $taskid];
         }

+ 49 - 3
app/Module/Project.php

@@ -50,6 +50,47 @@ class Project
     }
 
     /**
+     * 任务负责人组
+     * @param $task
+     * @return array
+     */
+    public static function taskPersons($task)
+    {
+        $array = [];
+        $array[] = Users::username2basic($task['username']);
+        $persons = [$task['username']];
+        $subtask = Base::string2array($task['subtask']);
+        foreach ($subtask AS $item) {
+            if ($item['uname'] && !in_array($item['uname'], $persons)) {
+                $persons[] = $item['uname'];
+                $basic = Users::username2basic($item['uname']);
+                if ($basic) {
+                    $array[] = $basic;
+                }
+            }
+        }
+        return $array;
+    }
+
+    /**
+     * 是否负责人(任务负责人、子任务负责人)
+     * @param $task
+     * @param $username
+     * @return bool
+     */
+    public static function isPersons($task, $username)
+    {
+        $persons = [$task['username']];
+        $subtask = Base::string2array($task['subtask']);
+        foreach ($subtask AS $item) {
+            if ($item['uname'] && !in_array($item['uname'], $persons)) {
+                $persons[] = $item['uname'];
+            }
+        }
+        return in_array($username, $persons) ? true : false;
+    }
+
+    /**
      * 任务是否过期
      * @param array $task
      * @return int
@@ -75,18 +116,23 @@ class Project
     }
 
     /**
-     * 获取任务有关系的用户(关注的、在项目里的、负责人、创建者)
+     * 获取任务有关系的用户(关注的、在项目里的、负责人、创建者)
      * @param $taskId
      * @return array
      */
     public static function taskSomeUsers($taskId)
     {
-        $taskDeatil = Base::DBC2A(DB::table('project_task')->select(['follower', 'createuser', 'username', 'projectid'])->where('id', $taskId)->first());
+        $taskDeatil = Base::DBC2A(DB::table('project_task')->select(['follower', 'subtask', 'createuser', 'username', 'projectid'])->where('id', $taskId)->first());
         if (empty($taskDeatil)) {
             return [];
         }
         //关注的用户
         $userArray = Base::string2array($taskDeatil['follower']);
+        //子任务负责人
+        $subtask = Base::string2array($taskDeatil['subtask']);
+        foreach ($subtask AS $item) {
+            $userArray[] = $item['uname'];
+        }
         //创建者
         $userArray[] = $taskDeatil['createuser'];
         //负责人
@@ -99,7 +145,7 @@ class Project
             }
         }
         //
-        return $userArray;
+        return array_values(array_filter(array_unique($userArray)));
     }
 
     /**

+ 34 - 0
database/migrations/2020_08_26_124253_create_project_content_table.php

@@ -0,0 +1,34 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateProjectContentTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('project_content', function (Blueprint $table) {
+            $table->increments('id');
+            $table->integer('projectid')->nullable()->default(0)->comment('项目ID');
+            $table->integer('taskid')->nullable()->default(0)->unique('IDEX_taskid')->comment('任务ID');
+            $table->text('content')->nullable()->comment('内容');
+            $table->bigInteger('indate')->nullable()->default(0);
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('project_content');
+    }
+}

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
     "name": "wookteam",
-    "version": "1.5.4",
+    "version": "1.5.5",
     "description": "WookTeam是一款轻量级的开源在线团队协作工具,提供各类文档工具、在线思维导图、在线流程图、项目管理、任务分发、即时IM,知识库管理等工具。",
     "scripts": {
         "ide-helper": "php artisan ide-helper:generate",

+ 1 - 0
resources/assets/js/main/components/TEditor.vue

@@ -46,6 +46,7 @@
         }
         .ivu-modal-close {
             top: 7px;
+            z-index: 2;
         }
         .teditor-transfer-body {
             position: absolute;

+ 26 - 6
resources/assets/js/main/components/UserView.vue

@@ -1,8 +1,8 @@
 <template>
     <div class="user-view-inline">
         <div class="user-view-info">
-            <UserImg v-if="showimg" class="user-view-img" :info="userInfo"/>
-            <Tooltip :disabled="loadIng" :delay="delay" :transfer="transfer" :placement="placement" maxWidth="auto" @on-popper-show="popperShow">
+            <UserImg v-if="showimg" class="user-view-img" :info="userInfo" :style="imgStyle"/>
+            <Tooltip v-if="showname" :disabled="loadIng" :delay="delay" :transfer="transfer" :placement="placement" maxWidth="auto" @on-popper-show="getUserData(30)">
                 {{nickname || username}}
                 <div slot="content" style="white-space:normal">
                     <div>{{$L('用户名')}}: {{username}}</div>
@@ -81,6 +81,16 @@
                 type: Boolean,
                 default: false
             },
+            imgsize: {
+
+            },
+            imgfontsize: {
+
+            },
+            showname: {
+                type: Boolean,
+                default: true
+            },
             info: {
                 default: null
             },
@@ -112,6 +122,20 @@
             userInfo() {
                 const {username, nickname, userimg} = this;
                 return {username, nickname, userimg}
+            },
+            imgStyle() {
+                const {imgsize, imgfontsize} = this;
+                const myStyle = {};
+                if (imgsize) {
+                    const size = /^\d+$/.test(imgsize) ? (imgsize + 'px') : imgsize;
+                    myStyle.width = size;
+                    myStyle.height = size;
+                    myStyle.lineHeight = size;
+                }
+                if (imgfontsize) {
+                    myStyle.fontSize = /^\d+$/.test(imgfontsize) ? (imgfontsize + 'px') : imgfontsize;
+                }
+                return myStyle;
             }
         },
         methods: {
@@ -126,10 +150,6 @@
                 }
             },
 
-            popperShow() {
-                this.getUserData(30)
-            },
-
             getUserData(cacheTime) {
                 $A.getUserBasic(this.username, (data, success) => {
                     if (success) {

+ 485 - 0
resources/assets/js/main/components/project/task/detail/DescEditor.vue

@@ -0,0 +1,485 @@
+<template>
+    <div>
+        <div class="desc-editor-box">
+            <div class="desc-editor-tool">
+                <Button class="tool-button" size="small" @click="openFull">{{$L('全屏')}}</Button>
+            </div>
+            <div v-if="loadIng > 0" class="desc-editor-load">
+                <WLoading/>
+            </div>
+            <div
+                ref="myTextarea"
+                class="desc-editor-content"
+                :id="id"
+                :placeholder="placeholder"
+                v-html="content"
+                @blur="handleBlur"></div>
+            <ImgUpload ref="myUpload" class="desc-editor-upload" type="callback" @on-callback="editorImage" num="50" style="margin-top:5px;height:26px;"></ImgUpload>
+        </div>
+        <Modal v-model="transfer" class="desc-editor-transfer" @on-visible-change="transferChange" footer-hide fullscreen transfer>
+            <div slot="close">
+                <Button type="primary" size="small">{{$L('完成')}}</Button>
+            </div>
+            <div class="desc-editor-transfer-body">
+                <textarea :id="'T_' + id" :placeholder="placeholder">{{content}}</textarea>
+            </div>
+        </Modal>
+    </div>
+</template>
+
+<style lang="scss">
+.desc-editor-box {
+    .desc-editor-content {
+        img {
+            max-width: 100%;
+            max-height: 100%;
+        }
+        &:before {
+            left: 8px !important;
+            color: #cccccc !important;
+        }
+    }
+}
+.desc-editor-transfer {
+    background-color: #ffffff;
+    .ivu-modal-header {
+        display: none;
+    }
+    .ivu-modal-close {
+        top: 7px;
+        z-index: 2;
+    }
+    .desc-editor-transfer-body {
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        padding: 0;
+        margin: 0;
+        textarea {
+            opacity: 0;
+        }
+        .tox-tinymce {
+            border: 0;
+            .tox-statusbar {
+                span.tox-statusbar__branding {
+                    a {
+                        display: none;
+                    }
+                }
+            }
+        }
+    }
+}
+</style>
+<style lang="scss" scoped>
+.desc-editor-box {
+    position: relative;
+    &:hover {
+        .desc-editor-tool {
+            display: flex;
+        }
+    }
+    .desc-editor-tool {
+        display: none;
+        flex-direction: row;
+        align-items: center;
+        position: absolute;
+        top: 5px;
+        right: 5px;
+        z-index: 2;
+        .tool-button {
+            font-size: 12px;
+            opacity: 0.9;
+            transition: all 0.3s;
+            margin-left: 5px;
+            background-color: #ffffff;
+            &:hover {
+                opacity: 1;
+            }
+        }
+    }
+    .desc-editor-load {
+        position: absolute;
+        right: 5px;
+        bottom: 5px;
+        z-index: 2;
+        width: 16px;
+        height: 16px;
+    }
+}
+.desc-editor-content {
+    position: relative;
+    margin: 10px 0 6px;
+    border: 2px solid transparent;
+    padding: 5px 8px;
+    color: #172b4d;
+    line-height: 1.5;
+    border-radius: 4px;
+    min-height: 56px;
+    max-height: 182px;
+    background: rgba(9, 30, 66, 0.04);
+    overflow: auto;
+    &:focus {
+        box-shadow: 0 0 0 2px rgba(45, 140, 240, .2);
+    }
+}
+.desc-editor-upload {
+    display: none;
+    width: 0;
+    height: 0;
+    overflow: hidden;
+}
+</style>
+
+<script>
+import tinymce from 'tinymce/tinymce';
+import ImgUpload from "../../../ImgUpload";
+
+export default {
+    name: 'DescEditor',
+    components: {ImgUpload},
+    props: {
+        taskid: {
+            default: ''
+        },
+        desc: {
+            default: ''
+        },
+        placeholder: {
+            type: String,
+            default: ''
+        }
+    },
+    data() {
+        return {
+            loadIng: 0,
+
+            id: "tinymce_" + Math.round(Math.random() * 10000),
+            content: '',
+            submitContent: '',
+
+            editor: null,
+            editorT: null,
+            cTinyMce: null,
+            checkerTimeout: null,
+            isTyping: false,
+
+            transfer: false,
+        };
+    },
+    mounted() {
+        this.loadData((val) => {
+            this.submitContent = val;
+            this.content = val;
+            this.init();
+        });
+    },
+    beforeDestroy() {
+        if (this.editor !== null) {
+            this.editor.destroy();
+        }
+        if (this.editorT !== null) {
+            this.editorT.destroy();
+        }
+    },
+    watch: {
+        desc() {
+            this.loadData((val) => {
+                this.submitContent = val;
+                this.content = val;
+                this.getEditor().setContent(val);
+            });
+        }
+    },
+    methods: {
+        loadData(callback) {
+            this.loadIng++;
+            $A.apiAjax({
+                url: 'project/task/desc',
+                data: {
+                    taskid: this.taskid,
+                },
+                complete: () => {
+                    this.loadIng--;
+                },
+                success: (res) => {
+                    if (res.ret === 1) {
+                        callback(res.data.desc);
+                    } else {
+                        callback('');
+                    }
+                }
+            });
+        },
+
+        init() {
+            this.$nextTick(() => {
+                tinymce.init(this.options(false));
+            });
+        },
+
+        initTransfer() {
+            this.$nextTick(() => {
+                tinymce.init(this.options(true));
+            });
+        },
+
+        options(isFull) {
+            let toolbar;
+            if (isFull) {
+                toolbar = 'undo redo | styleselect | uploadImages | bold italic underline forecolor backcolor | alignleft aligncenter alignright | outdent indent | link image emoticons media codesample | preview screenload';
+            } else {
+                toolbar = false;
+            }
+            return {
+                selector: (isFull ? '#T_' : '#') + this.id,
+                base_url: $A.serverUrl('js/build'),
+                auto_focus: false,
+                language: "zh_CN",
+                toolbar: toolbar,
+                plugins: [
+                    'advlist autolink lists link image charmap print preview hr anchor pagebreak imagetools',
+                    'searchreplace visualblocks code',
+                    'insertdatetime media nonbreaking save table contextmenu directionality',
+                    'emoticons paste textcolor colorpicker imagetools codesample'
+                ],
+                menubar: isFull,
+                inline: !isFull,
+                inline_boundaries: false,
+                paste_data_images: true,
+                menu: {
+                    view: {
+                        title: 'View',
+                        items: 'code | visualaid visualchars visualblocks | spellchecker | preview fullscreen screenload | showcomments'
+                    },
+                    insert: {
+                        title: "Insert",
+                        items: "image link media addcomment pageembed template codesample inserttable | charmap emoticons hr | pagebreak nonbreaking anchor toc | insertdatetime | uploadImages browseImages"
+                    }
+                },
+                codesample_languages: [
+                    {text: "HTML/VUE/XML", value: "markup"},
+                    {text: "JavaScript", value: "javascript"},
+                    {text: "CSS", value: "css"},
+                    {text: "PHP", value: "php"},
+                    {text: "Ruby", value: "ruby"},
+                    {text: "Python", value: "python"},
+                    {text: "Java", value: "java"},
+                    {text: "C", value: "c"},
+                    {text: "C#", value: "csharp"},
+                    {text: "C++", value: "cpp"}
+                ],
+                height: isFull ? '100%' : ($A.rightExists(this.height, '%') ? this.height : ($A.runNum(this.height) || 360)),
+                resize: !isFull,
+                convert_urls: false,
+                toolbar_mode: 'sliding',
+                toolbar_drawer: 'floating',
+                setup: (editor) => {
+                    editor.ui.registry.addMenuButton('uploadImages', {
+                        text: this.$L('图片'),
+                        tooltip: this.$L('上传/浏览 图片'),
+                        fetch: (callback) => {
+                            let items = [{
+                                type: 'menuitem',
+                                text: this.$L('上传图片'),
+                                onAction: () => {
+                                    this.$refs.myUpload.handleClick();
+                                }
+                            }, {
+                                type: 'menuitem',
+                                text: this.$L('浏览图片'),
+                                onAction: () => {
+                                    this.$refs.myUpload.browsePicture();
+                                }
+                            }];
+                            callback(items);
+                        }
+                    });
+                    editor.ui.registry.addMenuItem('uploadImages', {
+                        text: this.$L('上传图片'),
+                        onAction: () => {
+                            this.$refs.myUpload.handleClick();
+                        }
+                    });
+                    editor.ui.registry.addMenuItem('browseImages', {
+                        text: this.$L('浏览图片'),
+                        onAction: () => {
+                            this.$refs.myUpload.browsePicture();
+                        }
+                    });
+                    if (isFull) {
+                        editor.ui.registry.addButton('screenload', {
+                            icon: 'fullscreen',
+                            tooltip: this.$L('退出全屏'),
+                            onAction: () => {
+                                this.closeFull();
+                            }
+                        });
+                        editor.ui.registry.addMenuItem('screenload', {
+                            text: this.$L('退出全屏'),
+                            onAction: () => {
+                                this.closeFull();
+                            }
+                        });
+                        editor.on('Init', (e) => {
+                            this.editorT = editor;
+                            this.editorT.setContent(this.content);
+                        });
+                    } else {
+                        editor.ui.registry.addButton('screenload', {
+                            icon: 'fullscreen',
+                            tooltip: this.$L('全屏'),
+                            onAction: () => {
+                                this.openFull();
+                            }
+                        });
+                        editor.ui.registry.addMenuItem('screenload', {
+                            text: this.$L('全屏'),
+                            onAction: () => {
+                                this.openFull();
+                            }
+                        });
+                        editor.on('Init', (e) => {
+                            this.editor = editor;
+                            this.editor.setContent(this.content);
+                            this.$emit('editorInit', this.editor);
+                        });
+                        editor.on('KeyUp', (e) => {
+                            if (this.editor !== null) {
+                                this.submitNewContent();
+                            }
+                        });
+                        editor.on('Change', (e) => {
+                            if (this.editor !== null) {
+                                if (this.getContent() !== this.value) {
+                                    this.submitNewContent();
+                                }
+                                this.$emit('editorChange', e);
+                            }
+                        });
+                    }
+                },
+            };
+        },
+
+        openFull() {
+            this.content = this.getContent();
+            this.transfer = true;
+            this.initTransfer();
+        },
+
+        closeFull() {
+            this.content = this.getContent();
+            this.editor.setContent(this.content);
+            this.transfer = false;
+            if (this.editorT != null) {
+                this.editorT.destroy();
+                this.editorT = null;
+            }
+        },
+
+        transferChange(visible) {
+            if (!visible) {
+                this.$refs.myTextarea.focus();
+                if (this.editorT != null) {
+                    this.content = this.editorT.getContent();
+                    this.editor.setContent(this.content);
+                    this.editorT.destroy();
+                    this.editorT = null;
+                }
+            }
+        },
+
+        getEditor() {
+            return this.transfer ? this.editorT : this.editor;
+        },
+
+        getContent() {
+            if (this.getEditor() === null) {
+                return "";
+            }
+            return this.getEditor().getContent();
+        },
+
+        submitNewContent() {
+            this.isTyping = true;
+            if (this.checkerTimeout !== null) {
+                clearTimeout(this.checkerTimeout);
+            }
+            this.checkerTimeout = setTimeout(() => {
+                this.isTyping = false;
+            }, 300);
+        },
+
+        insertContent(content) {
+            if (this.getEditor() !== null) {
+                this.getEditor().insertContent(content);
+            } else {
+                this.content += content;
+            }
+        },
+
+        insertImage(src) {
+            this.insertContent('<img src="' + src + '">');
+        },
+
+        editorImage(lists) {
+            for (let i = 0; i < lists.length; i++) {
+                let item = lists[i];
+                if (typeof item === 'object' && typeof item.url === "string") {
+                    this.insertImage(item.url);
+                }
+            }
+        },
+
+        handleBlur() {
+            this.loadIng++;
+            setTimeout(() => {
+                this.handleSave();
+                this.loadIng--;
+            }, 300)
+        },
+
+        handleSave() {
+            if (this.transfer) {
+                return;
+            }
+            if (this.submitContent != this.getContent()) {
+                const bakContent = this.submitContent;
+                this.submitContent = this.getContent();
+                //
+                this.loadIng++;
+                $A.apiAjax({
+                    url: 'project/task/edit',
+                    method: 'post',
+                    data: {
+                        act: 'desc',
+                        taskid: this.taskid,
+                        content: this.submitContent,
+                    },
+                    complete: () => {
+                        this.loadIng--;
+                    },
+                    error: () => {
+                        this.getEditor().setContent(bakContent);
+                        alert(this.$L('网络繁忙,请稍后再试!'));
+                    },
+                    success: (res) => {
+                        if (res.ret === 1) {
+                            $A.triggerTaskInfoListener('desc', res.data);
+                            $A.triggerTaskInfoChange(this.taskid);
+                            this.$Message.success(res.msg);
+                            this.$emit('save-success');
+                        } else {
+                            this.$Modal.error({title: this.$L('温馨提示'), content: res.msg});
+                            this.getEditor().setContent(bakContent);
+                        }
+                    }
+                })
+            }
+        },
+    }
+}
+</script>

+ 49 - 40
resources/assets/js/main/components/project/task/detail/detail.vue

@@ -30,17 +30,7 @@
                 </div>
                 <div class="detail-desc-box detail-icon">
                     <div class="detail-h2"><strong class="active">{{$L('描述')}}</strong></div>
-                    <Input v-model="detail.desc"
-                           :disabled="!!loadData.desc"
-                           type="textarea"
-                           class="detail-desc-input"
-                           ref="descInput"
-                           :rows="2"
-                           :autosize="{minRows:2,maxRows:8}"
-                           maxlength="500"
-                           :placeholder="$L('添加详细描述...')"
-                           @on-keydown="descKeydown"
-                           @on-blur="handleTask('desc')"/>
+                    <DescEditor :taskid="detail.id" :desc="detail.desc" :placeholder="$L('添加详细描述...')" @save-success="handleTask('desc')"/>
                 </div>
                 <ul class="detail-text-box">
                     <li v-if="detail.startdate > 0 && detail.enddate > 0" class="text-time detail-icon">
@@ -76,7 +66,7 @@
                     </div>
                 </div>
                 <div class="detail-subtask-box">
-                    <div v-if="detail.subtask.length == 0" class="detail-subtask-none" @click="handleTask('subtaskAdd')">{{$L('暂无子任务')}}</div>
+                    <div v-if="detail.subtask.length == 0" class="detail-subtask-none">{{$L('暂无子任务')}}</div>
                     <div v-else>
                         <Progress class="detail-subtask-progress" :percent="subtaskProgress" :stroke-width="5" status="active" />
                         <draggable
@@ -90,6 +80,12 @@
                                           true-value="complete"
                                           false-value="unfinished"
                                           @on-change="handleTask('subtaskBlur')"></Checkbox>
+                                <UserView v-if="subitem.uname"
+                                          :username="subitem.uname"
+                                          imgsize="20"
+                                          imgfontsize="14"
+                                          :showname="false"
+                                          showimg/>
                                 <Input v-model="subitem.detail"
                                        type="textarea"
                                        class="detail-subtask-input"
@@ -102,8 +98,26 @@
                                        :placeholder="$L('子任务描述...')"
                                        @on-keydown="subtaskKeydown(subindex, $event)"
                                        @on-blur="handleTask('subtaskBlur')"/>
-                                <div class="detail-subtask-right" :style="subitem.showPoptip===true?{opacity:1}:{}">
+                                <div class="detail-subtask-right" :style="subitem.stip==='show'?{opacity:1}:{}">
                                     <Icon type="md-menu" class="detail-subtask-ricon detail-subtask-rmenu"/>
+                                    <Poptip
+                                        class="detail-subtask-ricon"
+                                        transfer
+                                        @on-popper-show="$set(subitem, 'stip', 'show')"
+                                        @on-popper-hide="[$set(subitem, 'stip', ''), handleTask('subtaskBlur')]">
+                                        <Icon type="md-person" />
+                                        <div slot="content">
+                                            <div style="width:280px">
+                                                {{$L('子任务负责人')}}
+                                                <UserInput
+                                                    v-model="subitem.uname"
+                                                    :projectid="detail.projectid"
+                                                    :transfer="false"
+                                                    :placeholder="$L('输入关键词搜索')"
+                                                    style="margin:5px 0 3px"></UserInput>
+                                            </div>
+                                        </div>
+                                    </Poptip>
                                     <div v-if="subitem.detail==''" class="detail-subtask-ricon">
                                         <Icon type="md-trash" @click="handleTask('subtaskDelete', subindex)"/>
                                     </div>
@@ -113,8 +127,8 @@
                                         confirm
                                         :title="$L('你确定你要删除这个子任务吗?')"
                                         @on-ok="handleTask('subtaskDelete', subindex)"
-                                        @on-popper-show="$set(subitem, 'showPoptip', true)"
-                                        @on-popper-hide="$set(subitem, 'showPoptip', false)"><Icon type="md-trash" /></Poptip>
+                                        @on-popper-show="$set(subitem, 'stip', 'show')"
+                                        @on-popper-hide="$set(subitem, 'stip', '')"><Icon type="md-trash" /></Poptip>
                                 </div>
                             </div>
                         </draggable>
@@ -211,9 +225,10 @@
     import draggable from 'vuedraggable'
     import cloneDeep from "lodash/cloneDeep";
     import WInput from "../../../iview/WInput";
+    import DescEditor from "./DescEditor";
 
     export default {
-        components: {WInput, ProjectTaskFiles, ProjectTaskLogs, draggable},
+        components: {DescEditor, WInput, ProjectTaskFiles, ProjectTaskLogs, draggable},
         data() {
             return {
                 taskid: 0,
@@ -450,10 +465,9 @@
 
             getTaskDetail() {
                 $A.apiAjax({
-                    url: 'project/task/lists',
+                    url: 'project/task/detail',
                     data: {
                         taskid: this.taskid,
-                        archived: '全部'
                     },
                     error: () => {
                         alert(this.$L('网络繁忙,请稍后再试!'));
@@ -465,7 +479,6 @@
                             this.bakData = cloneDeep(this.detail);
                             this.$nextTick(() => {
                                 this.$refs.titleInput.resizeTextarea();
-                                this.$refs.descInput.resizeTextarea();
                                 this.detail.subtask.forEach((temp, index) => {
                                     this.$refs['subtaskInput_' + (index)][0].resizeTextarea();
                                 })
@@ -520,10 +533,11 @@
                                 detail = detail.trim();
                                 detail && this.detail.subtask.push({
                                     id: $A.randomString(6),
-                                    uname: $A.getUserName(),
+                                    uname: '',
                                     time: Math.round(new Date().getTime()/1000),
                                     status: 'unfinished',
-                                    detail: detail
+                                    detail: detail,
+                                    stip: ''
                                 });
                             });
                             this.handleTask('subtask', () => {
@@ -545,7 +559,6 @@
                 //
                 switch (act) {
                     case 'title':
-                    case 'desc':
                         if (this.detail[act] == this.bakData[act]) {
                             return;
                         }
@@ -561,16 +574,21 @@
                         };
                         break;
 
+                    case 'desc':
+                        this.logType == '日志' && this.$refs.log.getLists(true, true);
+                        return;
+
                     case 'subtaskAdd':
                         if (!$A.isArray(this.detail.subtask)) {
                             this.detail.subtask = [];
                         }
                         this.detail.subtask.push({
                             id: $A.randomString(6),
-                            uname: $A.getUserName(),
+                            uname: '',
                             time: Math.round(new Date().getTime()/1000),
                             status: 'unfinished',
-                            detail: ''
+                            detail: '',
+                            stip: ''
                         });
                         this.$nextTick(() => {
                             this.$refs['subtaskInput_' + (this.detail.subtask.length  - 1)][0].focus();
@@ -821,10 +839,11 @@
                                 tempArray.splice(tempArray.length - 1, 1);
                                 this.detail.subtask.push({
                                     id: $A.randomString(6),
-                                    uname: $A.getUserName(),
+                                    uname: '',
                                     time: Math.round(new Date().getTime()/1000),
                                     status: 'unfinished',
-                                    detail: ''
+                                    detail: '',
+                                    stip: ''
                                 });
                             }
                             $A.triggerTaskInfoListener(ajaxData.act, res.data);
@@ -879,18 +898,6 @@
                 }
             }
         }
-        .detail-desc-box {
-            .detail-desc-input {
-                margin: 10px 0 6px;
-                textarea {
-                    border: 2px solid #F4F5F7;
-                    padding: 5px 8px;
-                    color: #172b4d;
-                    background: rgba(9, 30, 66, 0.04);
-                    resize: none;
-                }
-            }
-        }
         .detail-subtask-input {
             flex: 1;
             border: 0;
@@ -990,6 +997,9 @@
                         background: #cccccc;
                     }
                     .detail-button {
+                        display: flex;
+                        flex-direction: row;
+                        align-items: center;
                         position: absolute;
                         right: 12px;
                         top: 50%;
@@ -1004,7 +1014,7 @@
                             font-size: 12px;
                             opacity: 0.9;
                             transition: all 0.3s;
-                            margin-left: 3px;
+                            margin-left: 5px;
                             &:hover {
                                 opacity: 1;
                             }
@@ -1200,7 +1210,6 @@
                     }
                     .detail-subtask-none {
                         color: #666666;
-                        cursor: pointer;
                         padding: 0 12px;
                     }
                 }

+ 49 - 13
resources/assets/js/main/pages/project/panel.vue

@@ -67,7 +67,11 @@
                                 :disabled="projectSortDisabled"
                                 @sort="projectSortUpdate(false)"
                                 @remove="projectSortUpdate(false)">
-                                <div v-for="task in label.taskLists" :key="task.id" class="task-item task-draggable">
+                                <div v-for="task in label.taskLists"
+                                     :key="task.id"
+                                     :slot="task.complete ? 'footer' : 'default'"
+                                     class="task-item"
+                                     :class="{'task-draggable': !task.complete}">
                                     <div class="task-shadow" :class="[
                                         'p'+task.level,
                                         task.complete ? 'complete' : '',
@@ -80,7 +84,16 @@
                                             <div v-if="task.overdue" class="task-status">{{$L('已超期')}}</div>
                                             <div v-else-if="task.complete" class="task-status">{{$L('已完成')}}</div>
                                             <div v-else class="task-status">{{$L('未完成')}}</div>
-                                            <Tooltip class="task-userimg" :content="task.nickname || task.username" transfer><UserImg :info="task" class="avatar"/></Tooltip>
+                                            <div class="task-persons" :class="{'persons-more':task.persons.length > 1}">
+                                                <Tooltip
+                                                    v-for="(person, iper) in task.persons"
+                                                    class="task-userimg"
+                                                    :key="iper"
+                                                    :content="person.nickname || person.username"
+                                                    transfer>
+                                                    <UserImg :info="person" class="avatar"/>
+                                                </Tooltip>
+                                            </div>
                                         </div>
                                     </div>
                                 </div>
@@ -250,6 +263,14 @@
                         }
                         .task-item {
                             width: 100%;
+                            &.task-draggable {
+                                .task-shadow {
+                                    cursor: pointer;
+                                    &:hover{
+                                        box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.38);
+                                    }
+                                }
+                            }
                             .task-shadow {
                                 margin: 5px 0 4px;
                                 padding: 8px 10px 8px 8px;
@@ -258,13 +279,9 @@
                                 border-right: 0;
                                 color: #091e42;
                                 border-radius: 3px;
-                                cursor: pointer;
                                 box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
                                 transition: all 0.3s;
                                 transform: scale(1);
-                                &:hover{
-                                    box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.38);
-                                }
                                 &.p1 {
                                     border-left-color: #ff0000;
                                 }
@@ -323,15 +340,34 @@
                                         font-size: 12px;
                                         flex: 1;
                                     }
-                                    .task-userimg {
-                                        width: 26px;
-                                        height: 26px;
-                                        .avatar {
+                                    .task-persons {
+                                        max-width: 150px;
+                                        &.persons-more {
+                                            text-align: right;
+                                            .task-userimg {
+                                                width: 20px;
+                                                height: 20px;
+                                                margin-left: 4px;
+                                                margin-top: 4px;
+                                                .avatar {
+                                                    width: 20px;
+                                                    height: 20px;
+                                                    font-size: 12px;
+                                                    line-height: 20px;
+                                                }
+                                            }
+                                        }
+                                        .task-userimg {
                                             width: 26px;
                                             height: 26px;
-                                            font-size: 14px;
-                                            line-height: 26px;
-                                            border-radius: 13px;
+                                            vertical-align: bottom;
+                                            .avatar {
+                                                width: 26px;
+                                                height: 26px;
+                                                font-size: 14px;
+                                                line-height: 26px;
+                                                border-radius: 13px;
+                                            }
                                         }
                                     }
                                 }