kuaifan 5 anos atrás
pai
commit
aa369a1d73

+ 230 - 111
app/Http/Controllers/Api/ProjectController.php

@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api;
 
 use App\Http\Controllers\Controller;
 use App\Module\Base;
+use App\Module\Project;
 use App\Module\Users;
 use DB;
 use Request;
@@ -75,6 +76,51 @@ class ProjectController extends Controller
     }
 
     /**
+     * 项目详情
+     *
+     * @apiParam {Number} projectid     项目ID
+     */
+    public function detail()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $projectid = trim(Request::input('projectid'));
+        $projectDetail = Base::DBC2A(DB::table('project_lists')->where('id', $projectid)->where('delete', 0)->first());
+        if (empty($projectDetail)) {
+            return Base::retError('项目不存在或已被删除!');
+        }
+        $inRes = Project::inThe($projectid, $user['username']);
+        if (Base::isError($inRes)) {
+            return $inRes;
+        }
+        //子分类
+        $label = Base::DBC2A(DB::table('project_label')->where('projectid', $projectid)->orderBy('inorder')->orderByDesc('id')->get());
+        //任务
+        $task = Base::DBC2A(DB::table('project_task')->where([ 'projectid' => $projectid, 'delete' => 0, 'complete' => 0 ])->orderBy('level')->orderBy('id')->get());
+        //任务归类
+        foreach ($label AS $index => $temp) {
+            $taskLists = [];
+            foreach ($task AS $info) {
+                if ($temp['id'] == $info['labelid']) {
+                    $info['overdue'] = Project::taskIsOverdue($info);
+                    $taskLists[] = array_merge($info, Users::username2basic($info['username']));
+                }
+            }
+            $label[$index]['taskLists'] = Project::sortTask($taskLists);
+        }
+        //
+        return Base::retSuccess('success', [
+            'project' => $projectDetail,
+            'label' => $label,
+        ]);
+    }
+
+    /**
      * 添加项目
      *
      * @apiParam {String} title         项目名称
@@ -123,6 +169,9 @@ class ProjectController extends Controller
             'indate' => Base::time()
         ]);
         if ($projectid) {
+            foreach ($insertLabels AS $key => $label) {
+                $insertLabels[$key]['projectid'] = $projectid;
+            }
             DB::table('project_label')->insert($insertLabels);
             DB::table('project_log')->insert([
                 'type' => '日志',
@@ -164,21 +213,21 @@ class ProjectController extends Controller
         }
         //
         $projectid = trim(Request::input('projectid'));
-        $item = Base::DBC2A(DB::table('project_lists')->where('id', $projectid)->first());
-        if (empty($item)) {
+        $projectDetail = Base::DBC2A(DB::table('project_lists')->where('id', $projectid)->where('delete', 0)->first());
+        if (empty($projectDetail)) {
             return Base::retError('项目不存在或已被删除!');
         }
-        return DB::transaction(function () use ($item, $user) {
+        return DB::transaction(function () use ($projectDetail, $user) {
             switch (Request::input('act')) {
                 case 'cancel': {
                     if (DB::table('project_users')->where([
                         'type' => '收藏',
-                        'projectid' => $item['id'],
+                        'projectid' => $projectDetail['id'],
                         'username' => $user['username'],
                     ])->delete()) {
                         DB::table('project_log')->insert([
                             'type' => '日志',
-                            'projectid' => $item['id'],
+                            'projectid' => $projectDetail['id'],
                             'username' => $user['username'],
                             'detail' => '取消收藏',
                             'indate' => Base::time()
@@ -190,20 +239,20 @@ class ProjectController extends Controller
                 default: {
                     $row = Base::DBC2A(DB::table('project_users')->where([
                         'type' => '收藏',
-                        'projectid' => $item['id'],
+                        'projectid' => $projectDetail['id'],
                         'username' => $user['username'],
                     ])->lockForUpdate()->first());
                     if (empty($row)) {
                         DB::table('project_users')->insert([
                             'type' => '收藏',
-                            'projectid' => $item['id'],
-                            'isowner' => $item['username'] == $user['username'] ? 1 : 0,
+                            'projectid' => $projectDetail['id'],
+                            'isowner' => $projectDetail['username'] == $user['username'] ? 1 : 0,
                             'username' => $user['username'],
                             'indate' => Base::time()
                         ]);
                         DB::table('project_log')->insert([
                             'type' => '日志',
-                            'projectid' => $item['id'],
+                            'projectid' => $projectDetail['id'],
                             'username' => $user['username'],
                             'detail' => '收藏项目',
                             'indate' => Base::time()
@@ -232,11 +281,11 @@ class ProjectController extends Controller
         }
         //
         $projectid = trim(Request::input('projectid'));
-        $item = Base::DBC2A(DB::table('project_lists')->where('id', $projectid)->first());
-        if (empty($item)) {
+        $projectDetail = Base::DBC2A(DB::table('project_lists')->where('id', $projectid)->where('delete', 0)->first());
+        if (empty($projectDetail)) {
             return Base::retError('项目不存在或已被删除!');
         }
-        if ($item['username'] != $user['username']) {
+        if ($projectDetail['username'] != $user['username']) {
             return Base::retError('你不是项目负责人!');
         }
         //
@@ -247,14 +296,14 @@ class ProjectController extends Controller
             return Base::retError('项目名称最多只能设置32个字!');
         }
         //
-        DB::table('project_lists')->where('id', $item['id'])->update([
+        DB::table('project_lists')->where('id', $projectDetail['id'])->update([
             'title' => $title
         ]);
         DB::table('project_log')->insert([
             'type' => '日志',
-            'projectid' => $item['id'],
+            'projectid' => $projectDetail['id'],
             'username' => $user['username'],
-            'detail' => '【' . $item['title'] . '】重命名【' . $title . '】',
+            'detail' => '【' . $projectDetail['title'] . '】重命名【' . $title . '】',
             'indate' => Base::time()
         ]);
         //
@@ -279,64 +328,60 @@ class ProjectController extends Controller
         }
         //
         $projectid = trim(Request::input('projectid'));
-        $item = Base::DBC2A(DB::table('project_lists')->where('id', $projectid)->first());
-        if (empty($item)) {
+        $projectDetail = Base::DBC2A(DB::table('project_lists')->where('id', $projectid)->where('delete', 0)->first());
+        if (empty($projectDetail)) {
             return Base::retError('项目不存在或已被删除!');
         }
-        if ($item['username'] != $user['username']) {
+        if ($projectDetail['username'] != $user['username']) {
             return Base::retError('你不是项目负责人!');
         }
         //
         $username = trim(Request::input('username'));
-        if ($username == $item['username']) {
+        if ($username == $projectDetail['username']) {
             return Base::retError('你已是项目负责人!');
         }
         $count = DB::table('users')->where('username', $username)->count();
         if ($count <= 0) {
             return Base::retError('成员用户名(' . $username . ')不存在!');
         }
-        //判断是否已在项目
-        $count = DB::table('project_users')->where([
-            'type' => '成员',
-            'projectid' => $item['id'],
-            'username' => $username,
-        ])->count();
-        if ($count <= 0) {
+        //判断是否已在项目成员内
+        $inRes = Project::inThe($projectDetail['id'], $username);
+        if (Base::isError($inRes)) {
             DB::table('project_users')->insert([
                 'type' => '成员',
-                'projectid' => $item['id'],
+                'projectid' => $projectDetail['id'],
                 'isowner' => 0,
                 'username' => $username,
                 'indate' => Base::time()
             ]);
             DB::table('project_log')->insert([
                 'type' => '日志',
-                'projectid' => $item['id'],
+                'projectid' => $projectDetail['id'],
                 'username' => $username,
                 'detail' => '移交项目,自动加入项目',
                 'indate' => Base::time()
             ]);
         }
         //开始移交
-        return DB::transaction(function () use ($user, $username, $item) {
-            DB::table('project_lists')->where('id', $item['id'])->update([
+        return DB::transaction(function () use ($user, $username, $projectDetail) {
+            DB::table('project_lists')->where('id', $projectDetail['id'])->update([
                 'username' => $username
             ]);
             DB::table('project_log')->insert([
                 'type' => '日志',
-                'projectid' => $item['id'],
+                'projectid' => $projectDetail['id'],
                 'username' => $user['username'],
-                'detail' => '【' . $item['username'] . '】移交给【' . $username . '】',
+                'detail' => '【' . $projectDetail['username'] . '】移交给【' . $username . '】',
                 'indate' => Base::time()
             ]);
             DB::table('project_users')->where([
-                'projectid' => $item['id'],
-                'username' => $item['username'],
+                'projectid' => $projectDetail['id'],
+                'username' => $projectDetail['username'],
             ])->update([
                 'isowner' => 0
             ]);
             DB::table('project_users')->where([
-                'projectid' => $item['id'],
+                'projectid' => $projectDetail['id'],
                 'username' => $username,
             ])->update([
                 'isowner' => 1
@@ -360,21 +405,21 @@ class ProjectController extends Controller
         }
         //
         $projectid = trim(Request::input('projectid'));
-        $item = Base::DBC2A(DB::table('project_lists')->where('id', $projectid)->first());
-        if (empty($item)) {
+        $projectDetail = Base::DBC2A(DB::table('project_lists')->where('id', $projectid)->where('delete', 0)->first());
+        if (empty($projectDetail)) {
             return Base::retError('项目不存在或已被删除!');
         }
-        if ($item['username'] != $user['username']) {
+        if ($projectDetail['username'] != $user['username']) {
             return Base::retError('你不是项目负责人!');
         }
         //
-        DB::table('project_lists')->where('id', $item['id'])->update([
+        DB::table('project_lists')->where('id', $projectDetail['id'])->update([
             'delete' => 1,
             'deletedate' => Base::time()
         ]);
         DB::table('project_log')->insert([
             'type' => '日志',
-            'projectid' => $item['id'],
+            'projectid' => $projectDetail['id'],
             'username' => $user['username'],
             'detail' => '删除项目',
             'indate' => Base::time()
@@ -398,30 +443,26 @@ class ProjectController extends Controller
         }
         //
         $projectid = trim(Request::input('projectid'));
-        $item = Base::DBC2A(DB::table('project_lists')->where('id', $projectid)->first());
-        if (empty($item)) {
+        $projectDetail = Base::DBC2A(DB::table('project_lists')->where('id', $projectid)->where('delete', 0)->first());
+        if (empty($projectDetail)) {
             return Base::retError('项目不存在或已被删除!');
         }
-        if ($item['username'] == $user['username']) {
+        if ($projectDetail['username'] == $user['username']) {
             return Base::retError('你是项目负责人,不可退出项目!');
         }
-        $count = DB::table('project_users')->where([
-            'type' => '成员',
-            'projectid' => $projectid,
-            'username' => $user['username'],
-        ])->count();
-        if ($count <= 0) {
-            return Base::retError('你不在项目成员内!');
+        $inRes = Project::inThe($projectid, $user['username']);
+        if (Base::isError($inRes)) {
+            return $inRes;
         }
         //
         DB::table('project_users')->where([
             'type' => '成员',
-            'projectid' => $item['id'],
+            'projectid' => $projectDetail['id'],
             'username' => $user['username'],
         ])->delete();
         DB::table('project_log')->insert([
             'type' => '日志',
-            'projectid' => $item['id'],
+            'projectid' => $projectDetail['id'],
             'username' => $user['username'],
             'detail' => '退出项目',
             'indate' => Base::time()
@@ -447,13 +488,9 @@ class ProjectController extends Controller
         }
         //
         $projectid = intval(Request::input('projectid'));
-        $count = DB::table('project_users')->where([
-            'type' => '成员',
-            'projectid' => $projectid,
-            'username' => $user['username'],
-        ])->count();
-        if ($count <= 0) {
-            return Base::retError('你不在项目成员内!');
+        $inRes = Project::inThe($projectid, $user['username']);
+        if (Base::isError($inRes)) {
+            return $inRes;
         }
         //
         $lists = DB::table('project_lists')
@@ -469,8 +506,8 @@ class ProjectController extends Controller
         if ($lists['total'] == 0) {
             return Base::retError('未找到任何相关的成员');
         }
-        foreach ($lists['lists'] AS $key => $item) {
-            $userInfo = Users::username2basic($item['username']);
+        foreach ($lists['lists'] AS $key => $projectDetail) {
+            $userInfo = Users::username2basic($projectDetail['username']);
             $lists['lists'][$key]['userimg'] = $userInfo['userimg'];
             $lists['lists'][$key]['nickname'] = $userInfo['nickname'];
             $lists['lists'][$key]['profession'] = $userInfo['profession'];
@@ -497,11 +534,11 @@ class ProjectController extends Controller
         }
         //
         $projectid = trim(Request::input('projectid'));
-        $item = Base::DBC2A(DB::table('project_lists')->where('id', $projectid)->first());
-        if (empty($item)) {
+        $projectDetail = Base::DBC2A(DB::table('project_lists')->where('id', $projectid)->where('delete', 0)->first());
+        if (empty($projectDetail)) {
             return Base::retError('项目不存在或已被删除!');
         }
-        if ($item['username'] != $user['username']) {
+        if ($projectDetail['username'] != $user['username']) {
             return Base::retError('你是不是项目负责人!');
         }
         $usernames = Request::input('username');
@@ -518,14 +555,10 @@ class ProjectController extends Controller
         //
         $logArray = [];
         foreach ($usernames AS $username) {
-            $count = DB::table('project_users')->where([
-                'type' => '成员',
-                'projectid' => $projectid,
-                'username' => $username,
-            ])->count();
+            $inRes = Project::inThe($projectid, $username);
             switch (Request::input('act')) {
                 case 'delete': {
-                    if ($count > 0 && $item['username'] != $username) {
+                    if (!Base::isError($inRes) && $projectDetail['username'] != $username) {
                         DB::table('project_users')->where([
                             'type' => '成员',
                             'projectid' => $projectid,
@@ -533,7 +566,7 @@ class ProjectController extends Controller
                         ])->delete();
                         $logArray[] = [
                             'type' => '日志',
-                            'projectid' => $item['id'],
+                            'projectid' => $projectDetail['id'],
                             'username' => $user['username'],
                             'detail' => '将成员【' . $username . '】移出项目',
                             'indate' => Base::time()
@@ -542,7 +575,7 @@ class ProjectController extends Controller
                     break;
                 }
                 default: {
-                    if ($count == 0) {
+                    if (Base::isError($inRes)) {
                         DB::table('project_users')->insert([
                             'type' => '成员',
                             'projectid' => $projectid,
@@ -552,7 +585,7 @@ class ProjectController extends Controller
                         ]);
                         $logArray[] = [
                             'type' => '日志',
-                            'projectid' => $item['id'],
+                            'projectid' => $projectDetail['id'],
                             'username' => $username,
                             'detail' => '将成员【' . $username . '】加入项目',
                             'indate' => Base::time()
@@ -569,10 +602,13 @@ class ProjectController extends Controller
      * 项目任务-列表
      *
      * @apiParam {Number} projectid             项目ID
-     * @apiParam {Number} [archived]            是否归档
-     * - 0: 未归档
-     * - 1: 已归档
+     * @apiParam {Number} [labelid]             项目子分类ID
+     * @apiParam {String} [archived]            是否归档
+     * - 未归档 (默认)
+     * - 已归档
+     * - 全部
      * @apiParam {String} [type]                任务类型
+     * - 全部(默认)
      * - 未完成
      * - 已超期
      * - 已完成
@@ -590,64 +626,151 @@ class ProjectController extends Controller
         }
         //
         $projectid = intval(Request::input('projectid'));
-        $count = DB::table('project_users')->where([
-            'type' => '成员',
-            'projectid' => $projectid,
-            'username' => $user['username'],
-        ])->count();
-        if ($count <= 0) {
-            return Base::retError('你不在项目成员内!');
+        $inRes = Project::inThe($projectid, $user['username']);
+        if (Base::isError($inRes)) {
+            return $inRes;
         }
         //
-        $whereFunc = null;
+        $orderBy = '`id` ASC';
         $whereArray = [];
         $whereArray[] = ['project_lists.id', '=', $projectid];
         $whereArray[] = ['project_lists.delete', '=', 0];
-        if (in_array(Request::input('archived'), [0, 1])) {
-            $whereArray[] = ['project_task.archived', '=', intval(Request::input('archived'))];
+        if (intval(Request::input('labelid')) > 0) {
+            $whereArray[] = ['project_task.labelid', '=', intval(Request::input('labelid'))];
+        }
+        $archived = trim(Request::input('archived'));
+        switch ($archived) {
+            case '已归档':
+                $whereArray[] = ['project_task.archived', '=', 1];
+                $orderBy = '`archiveddate` DESC';
+                break;
+            case '未归档':
+            default:
+                $whereArray[] = ['project_task.archived', '=', 0];
+                break;
         }
         $type = trim(Request::input('type'));
         switch ($type) {
             case '未完成':
-                $whereArray[] = ['project_task.status', '=', '进行中'];
-                $whereFunc = function($query) {
-                    $query->where('project_task.enddate', '=', 0)->orWhere('project_task.enddate', '>', Base::time());
-                };
+                $whereArray[] = ['project_task.complete', '=', 0];
                 break;
             case '已超期':
-                $whereArray[] = ['project_task.status', '=', '进行中'];
+                $whereArray[] = ['project_task.complete', '=', 0];
                 $whereArray[] = ['project_task.enddate', '>', 0];
                 $whereArray[] = ['project_task.enddate', '<=', Base::time()];
                 break;
             case '已完成':
-                $whereArray[] = ['project_task.status', '=', '已完成'];
+                $whereArray[] = ['project_task.complete', '=', 1];
                 break;
         }
         //
-        $orderBy = 'project_task.id';
-        if (intval(Request::input('archived')) === 1) {
-            $orderBy = 'project_task.archiveddate';
-        }
-        //
         $lists = DB::table('project_lists')
             ->join('project_task', 'project_lists.id', '=', 'project_task.projectid')
             ->select(['project_task.*'])
             ->where($whereArray)
-            ->where($whereFunc)
-            ->orderByDesc($orderBy)->paginate(Min(Max(Base::nullShow(Request::input('pagesize'), 10), 1), 100));
+            ->orderByRaw($orderBy)->paginate(Min(Max(Base::nullShow(Request::input('pagesize'), 10), 1), 100));
         $lists = Base::getPageList($lists);
+        if (intval(Request::input('statistics')) == 1) {
+            $lists['statistics_unfinished'] = $type === '未完成' ? $lists['total'] : DB::table('project_task')->where('projectid', $projectid)->where('complete', 0)->count();
+            $lists['statistics_overdue'] = $type === '已超期' ? $lists['total'] : DB::table('project_task')->where('projectid', $projectid)->where('complete', 0)->whereBetween('enddate', [1, Base::time()])->count();
+            $lists['statistics_complete'] = $type === '已完成' ? $lists['total'] : DB::table('project_task')->where('projectid', $projectid)->where('complete', 1)->count();
+        }
         if ($lists['total'] == 0) {
-            return Base::retError('未找到任何相关的任务');
+            return Base::retError('未找到任何相关的任务', $lists);
         }
-        if (intval(Request::input('statistics')) == 1) {
-            $lists['statistics_unfinished'] = $type === '未完成' ? $lists['total'] : DB::table('project_task')->where('status', '进行中')->where(function($query) { $query->where('enddate', '=', 0)->orWhere('enddate', '>', Base::time()); })->count();
-            $lists['statistics_overdue'] = $type === '已超期' ? $lists['total'] : DB::table('project_task')->where('status', '进行中')->whereBetween('enddate', [0, Base::time()])->count();
-            $lists['statistics_complete'] = $type === '已完成' ? $lists['total'] : DB::table('project_task')->where('status', '已完成')->count();
+        foreach ($lists['lists'] AS $key => $info) {
+            $info['overdue'] = Project::taskIsOverdue($info);
+            $lists['lists'][$key] = array_merge($info, Users::username2basic($info['username']));
         }
         return Base::retSuccess('success', $lists);
     }
 
     /**
+     * 项目任务-添加任务
+     *
+     * @apiParam {Number} projectid             项目ID
+     * @apiParam {Number} labelid               项目子分类ID
+     * @apiParam {String} title                 任务标题
+     * @apiParam {Number} [level]               任务紧急级别(1~4,默认:2)
+     * @apiParam {String} [username]            任务负责人用户名
+     * - 0: 未归档
+     * - 1: 已归档
+     *
+     * @throws \Throwable
+     */
+    public function task__add()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $projectid = intval(Request::input('projectid'));
+        $projectDetail = Base::DBC2A(DB::table('project_lists')->where('id', $projectid)->where('delete', 0)->first());
+        if (empty($projectDetail)) {
+            return Base::retError('项目不存在或已被删除!');
+        }
+        //
+        $labelid = intval(Request::input('labelid'));
+        $labelDetail = Base::DBC2A(DB::table('project_label')->where('id', $labelid)->where('projectid', $projectid)->first());
+        if (empty($labelDetail)) {
+            return Base::retError('项目子分类不存在或已被删除!');
+        }
+        //
+        $inRes = Project::inThe($projectid, $user['username']);
+        if (Base::isError($inRes)) {
+            return $inRes;
+        }
+        //
+        $username = trim(Request::input('username'));
+        if (empty($username)) {
+            $username = $user['username'];
+        }
+        if ($username != $user['username']) {
+            $inRes = Project::inThe($projectid, $username);
+            if (Base::isError($inRes)) {
+                return Base::retError('负责人不在项目成员内!');
+            }
+        }
+        //
+        $title = trim(Request::input('title'));
+        if (empty($title)) {
+            return Base::retError('任务标题不能为空!');
+        }
+        //
+        $inArray = [
+            'projectid' => $projectid,
+            'labelid' => $labelid,
+            'createuser' => $user['username'],
+            'username' => $username,
+            'title' => $title,
+            'level' => max(1, min(4, intval(Request::input('level')))),
+            'indate' => Base::time(),
+            'subtask' => Base::array2string([]),
+            'files' => Base::array2string([]),
+            'follower' => Base::array2string([]),
+        ];
+        return DB::transaction(function () use ($inArray) {
+            $taskid = DB::table('project_task')->insertGetId($inArray);
+            if (empty($taskid)) {
+                return Base::retError('系统繁忙,请稍后再试!');
+            }
+            DB::table('project_log')->insert([
+                'type' => '日志',
+                'projectid' => $inArray['projectid'],
+                'taskid' => $taskid,
+                'username' => $inArray['createuser'],
+                'detail' => '添加任务【' . $inArray['title'] . '】',
+                'indate' => Base::time()
+            ]);
+            Project::updateNum($inArray['projectid']);
+            return Base::retSuccess('添加成功!');
+        });
+    }
+
+    /**
      * 项目任务-归档、取消归档
      *
      * @apiParam {String} act
@@ -676,13 +799,9 @@ class ProjectController extends Controller
         if (empty($task)) {
             return Base::retError('任务不存在!');
         }
-        $count = DB::table('project_users')->where([
-            'type' => '成员',
-            'projectid' => $task['projectid'],
-            'username' => $user['username'],
-        ])->count();
-        if ($count <= 0) {
-            return Base::retError('你不在项目成员内!');
+        $inRes = Project::inThe($task['projectid'], $user['username']);
+        if (Base::isError($inRes)) {
+            return $inRes;
         }
         //
         switch (Request::input('act')) {

+ 69 - 0
app/Module/Project.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace App\Module;
+
+use DB;
+
+/**
+ * Class Project
+ * @package App\Module
+ */
+class Project
+{
+    /**
+     * 是否在项目里
+     * @param int $projectid
+     * @param string $username
+     * @return array
+     */
+    public static function inThe($projectid, $username)
+    {
+        $count = DB::table('project_users')->where([
+            'type' => '成员',
+            'projectid' => $projectid,
+            'username' => $username,
+        ])->count();
+        if ($count <= 0) {
+            return Base::retError('你不在项目成员内!');
+        } else {
+            return Base::retSuccess('你在项目内');
+        }
+    }
+
+    /**
+     * 更新项目(complete、unfinished)
+     * @param int $projectid
+     */
+    public static function updateNum($projectid)
+    {
+        DB::table('project_lists')->where('id', $projectid)->update([
+            'unfinished' => DB::table('project_task')->where('projectid', $projectid)->where('complete', 0)->count(),
+            'complete' => DB::table('project_task')->where('projectid', $projectid)->where('complete', 1)->count(),
+        ]);
+    }
+
+    /**
+     * 任务是否过期
+     * @param array $task
+     * @return int
+     */
+    public static function taskIsOverdue($task)
+    {
+        return $task['complete'] == 0 && $task['enddate'] > 0 && $task['enddate'] <= Base::time() ? 1 : 0;
+    }
+
+    /**
+     * 过期的排在前
+     * @param array $taskLists
+     * @return mixed
+     */
+    public static function sortTask($taskLists)
+    {
+        $inOrder = [];
+        foreach ($taskLists as $key => $oitem) {
+            $inOrder[$key] = $oitem['overdue'] ? -1 : $key;
+        }
+        array_multisort($inOrder, SORT_ASC, $taskLists);
+        return $taskLists;
+    }
+}

+ 13 - 0
package-lock.json

@@ -9081,6 +9081,11 @@
                 }
             }
         },
+        "sortablejs": {
+            "version": "1.10.2",
+            "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz",
+            "integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A=="
+        },
         "source-list-map": {
             "version": "2.0.1",
             "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
@@ -10331,6 +10336,14 @@
             "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
             "dev": true
         },
+        "vuedraggable": {
+            "version": "2.23.2",
+            "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.23.2.tgz",
+            "integrity": "sha512-PgHCjUpxEAEZJq36ys49HfQmXglattf/7ofOzUrW2/rRdG7tu6fK84ir14t1jYv4kdXewTEa2ieKEAhhEMdwkQ==",
+            "requires": {
+                "sortablejs": "^1.10.1"
+            }
+        },
         "watchpack": {
             "version": "1.6.1",
             "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.1.tgz",

+ 2 - 1
package.json

@@ -29,7 +29,8 @@
         "vue-template-compiler": "^2.6.11"
     },
     "dependencies": {
+        "tinymce": "^5.2.2",
         "view-design": "^4.2.0",
-        "tinymce": "^5.2.2"
+        "vuedraggable": "^2.23.2"
     }
 }

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

@@ -482,7 +482,7 @@
          * @param myObj
          * @returns {*}
          */
-        clone(myObj) {
+        cloneData(myObj) {
             if(typeof(myObj) !== 'object') return myObj;
             if(myObj === null) return myObj;
             //

+ 1 - 1
resources/assets/js/main/components/UseridInput.vue

@@ -187,7 +187,7 @@
                 if (this.multiple) {
                     return;
                 }
-                this.userName = $A.clone(val)
+                this.userName = $A.cloneData(val)
             },
 
             userName (val) {

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

@@ -158,7 +158,7 @@
                         page: Math.max(this.listPage, 1),
                         pagesize: Math.max($A.runNum(this.listPageSize), 10),
                         projectid: this.projectid,
-                        archived: 1,
+                        archived: '已归档',
                     },
                     complete: () => {
                         this.loadIng--;

+ 7 - 7
resources/assets/js/main/components/project/statistics.vue

@@ -45,7 +45,6 @@
 
                 > div {
                     position: relative;
-                    -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
                     box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
                     transition: all 0.2s;
                     border-radius: 6px;
@@ -223,6 +222,7 @@
                     return;
                 }
                 this.loadIng++;
+                let tempType = this.taskType;
                 $A.aAjax({
                     url: 'project/task/lists',
                     data: {
@@ -236,20 +236,20 @@
                         this.loadIng--;
                     },
                     success: (res) => {
+                        if (tempType != this.taskType) {
+                            return;
+                        }
                         if (res.ret === 1) {
                             this.lists = res.data.lists;
                             this.listTotal = res.data.total;
-                            this.statistics_unfinished = res.data.statistics_unfinished;
-                            this.statistics_overdue = res.data.statistics_overdue;
-                            this.statistics_complete = res.data.statistics_complete;
                         } else {
                             this.lists = [];
                             this.listTotal = 0;
                             this.noDataText = res.msg;
-                            this.statistics_unfinished = 0;
-                            this.statistics_overdue = 0;
-                            this.statistics_complete = 0;
                         }
+                        this.statistics_unfinished = res.data.statistics_unfinished || 0;
+                        this.statistics_overdue = res.data.statistics_overdue || 0;
+                        this.statistics_complete = res.data.statistics_complete || 0;
                     }
                 });
             },

+ 226 - 0
resources/assets/js/main/components/project/task/add.vue

@@ -0,0 +1,226 @@
+<template>
+    <div class="task-input-box">
+        <div v-if="!addText" class="input-placeholder">
+            <Icon type="md-create" size="18"/>&nbsp;{{placeholder}}
+        </div>
+        <div class="input-enter">
+            <Input
+                v-model="addText"
+                type="textarea"
+                class="input-enter-textarea"
+                :class="{bright:addFocus===true,highlight:!!addText}"
+                element-id="project-panel-enter-textarea"
+                @on-focus="addFocus=true"
+                @on-blur="addFocus=false"
+                :autosize="{ minRows: 1, maxRows: 6 }"
+                :maxlength="255"/>
+            <div v-if="!!addText" class="input-enter-module">
+                <Tooltip content="重要且紧急" placement="bottom" transfer><div @click="addLevel=1" class="enter-module-icon p1"><Icon v-if="addLevel=='1'" type="md-checkmark" /></div></Tooltip>
+                <Tooltip content="重要不紧急" placement="bottom" transfer><div @click="addLevel=2" class="enter-module-icon p2"><Icon v-if="addLevel=='2'" type="md-checkmark" /></div></Tooltip>
+                <Tooltip content="紧急不重要" placement="bottom" transfer><div @click="addLevel=3" class="enter-module-icon p3"><Icon v-if="addLevel=='3'" type="md-checkmark" /></div></Tooltip>
+                <Tooltip content="不重要不紧急" placement="bottom" transfer><div @click="addLevel=4" class="enter-module-icon p4"><Icon v-if="addLevel=='4'" type="md-checkmark" /></div></Tooltip>
+                <div class="enter-module-flex"></div>
+                <Poptip placement="bottom" @on-popper-show="nameTipDisabled=true" @on-popper-hide="nameTipDisabled=false" transfer>
+                    <Tooltip :content="`负责人: ${addUsername||'自己'}`" placement="bottom" :disabled="nameTipDisabled">
+                        <div class="enter-module-icon user">
+                            <img v-if="addUserimg" @error="addUserimg=''" :src="addUserimg"/>
+                            <Icon v-else type="md-person" />
+                        </div>
+                    </Tooltip>
+                    <div slot="content">
+                        <div style="width:240px">
+                            选择负责人
+                            <UseridInput v-model="addUsername" @change="changeUser" placeholder="留空默认: 自己" style="margin:5px 0 3px"></UseridInput>
+                        </div>
+                    </div>
+                </Poptip>
+                <Button class="enter-module-btn" type="info" size="small" @click="clickAdd">添加任务</Button>
+            </div>
+        </div>
+        <div v-if="loadIng > 0" class="load-box" @click.stop="">
+            <div class="load-box-main"><w-loading></w-loading></div>
+        </div>
+    </div>
+</template>
+
+<style lang="scss" scoped>
+    .task-input-box {
+        position: relative;
+        margin-top: 5px;
+        .input-placeholder,
+        .input-enter {
+            position: absolute;
+            top: 0;
+            left: 0;
+            right: 0;
+            width: 100%;
+        }
+        .input-placeholder {
+            z-index: 1;
+            height: 40px;
+            line-height: 40px;
+            color: rgba(0, 0, 0, .36);
+            padding-left: 12px;
+            padding-right: 12px;
+        }
+        .input-enter {
+            z-index: 2;
+            position: relative;
+            background-color: transparent;
+            .input-enter-textarea {
+                border-radius: 4px;
+                padding-left: 12px;
+                padding-right: 12px;
+                color: rgba(0, 0, 0, 0.85);
+                &.bright {
+                    background-color: rgba(46, 73, 136, .08);
+                }
+                &.highlight {
+                    background-color: #ffffff;
+                }
+            }
+            .input-enter-module {
+                display: flex;
+                width: 100%;
+                margin-top: 8px;
+                .enter-module-icon {
+                    display: inline-block;
+                    width: 16px;
+                    height: 16px;
+                    margin-right: 5px;
+                    border-radius: 4px;
+                    vertical-align: middle;
+                    cursor: pointer;
+                    &.p1 {
+                        background-color: #ff0000;
+                    }
+                    &.p2 {
+                        background-color: #BB9F35;
+                    }
+                    &.p3 {
+                        background-color: #449EDD;
+                    }
+                    &.p4 {
+                        background-color: #84A83B;
+                    }
+                    &.user {
+                        background-color: #98CD75;
+                        border-radius: 50%;
+                        width: 24px;
+                        height: 24px;
+                        margin-left: 10px;
+                        margin-right: 10px;
+                        img {
+                            width: 100%;
+                            height: 100%;
+                            border-radius: 50%;
+                        }
+                        i {
+                            line-height: 24px;
+                            font-size: 16px;
+                        }
+                    }
+                    i {
+                        width: 100%;
+                        height: 100%;
+                        color: #ffffff;
+                        line-height: 16px;
+                        font-size: 14px;
+                        transform: scale(0.85);
+                        vertical-align: 0;
+                    }
+                }
+                .enter-module-flex {
+                    flex: 1;
+                }
+                .enter-module-btn {
+                    font-size: 12px;
+                }
+            }
+        }
+        .load-box {
+            position: absolute;
+            top: 0;
+            left: 0;
+            right: 0;
+            bottom: 0;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            z-index: 9;
+            .load-box-main {
+                width: 24px;
+                height: 24px;
+            }
+        }
+    }
+</style>
+<script>
+    import WLoading from "../../WLoading";
+    export default {
+        name: 'ProjectAddTask',
+        components: {WLoading},
+        props: {
+            placeholder: {
+                type: String,
+                default: ''
+            },
+            projectid: {
+                type: Number,
+                default: 0
+            },
+            labelid: {
+                type: Number,
+                default: 0
+            },
+        },
+        data() {
+            return {
+                loadIng: 0,
+
+                addText: '',
+                addLevel: 2,
+                addUsername: '',
+
+                addUserimg: '',
+                addFocus: false,
+
+                nameTipDisabled: false,
+            }
+        },
+        methods: {
+            changeUser(user) {
+                this.addUserimg = user.userimg;
+            },
+            clickAdd() {
+                this.loadIng++;
+                $A.aAjax({
+                    url: 'project/task/add',
+                    data: {
+                        projectid: this.projectid,
+                        labelid: this.labelid,
+                        title: this.addText,
+                        level: this.addLevel,
+                        username: this.addUsername,
+                    },
+                    complete: () => {
+                        this.loadIng--;
+                    },
+                    error: () => {
+                        this.$Message.error(this.$L('网络繁忙,请稍后再试!'));
+                    },
+                    success: (res) => {
+                        if (res.ret === 1) {
+                            this.addText = '';
+                            this.addFocus = false;
+                            this.$Message.success(res.msg);
+                            this.$emit('on-add-success');
+                        } else {
+                            this.$Modal.error({title: this.$L('温馨提示'), content: res.msg});
+                        }
+                    }
+                });
+            }
+        }
+    }
+</script>

+ 290 - 9
resources/assets/js/main/pages/project-panel.vue

@@ -1,40 +1,275 @@
 <template>
-    <div class="w-main doc">
+    <div class="w-main project-panel">
 
-        <v-title>{{$L('知识库')}}-{{$L('轻量级的团队在线协作')}}</v-title>
+        <v-title>{{$L('项目面板')}}-{{$L('轻量级的团队在线协作')}}</v-title>
 
-        <w-header value="doc"></w-header>
+        <w-header value="project"></w-header>
 
         <div class="w-nav">
             <div class="nav-row">
                 <div class="w-nav-left">
-                    <span class="new ft hover"><i class="ft icon"></i> {{$L('新建知识库')}}</span>
+                    <div class="project-title">
+                        <div v-if="loadIng > 0" class="project-title-loading"><w-loading></w-loading></div>
+                        <h1>{{projectDetail.title}}</h1>
+                    </div>
                 </div>
                 <div class="w-nav-flex"></div>
+                <div class="w-nav-right">
+                    <span class="ft hover"><i class="ft icon">&#xE6E7;</i> 任务看板</span>
+                    <span class="ft hover"><i class="ft icon">&#xE89E;</i> 任务列表</span>
+                    <span class="ft hover"><i class="ft icon">&#xE705;</i> 甘特图</span>
+                    <span class="ft hover"><i class="ft icon">&#xE7A7;</i> 设置</span>
+                </div>
             </div>
         </div>
 
-        <w-content></w-content>
+        <w-content>
+            <draggable v-if="projectLabel.length > 0" v-model="projectLabel" class="label-box" draggable=".label-draggable" :animation="150">
+                <div v-for="label in projectLabel" :key="label.id" class="label-item label-draggable">
+                    <div class="label-body">
+                        <div class="title-box">
+                            <h2>{{label.title}}</h2>
+                            <Dropdown trigger="click" transfer>
+                                <Icon type="ios-more"/>
+                                <DropdownMenu slot="list">
+                                    <DropdownItem>重命名</DropdownItem>
+                                    <DropdownItem>删除</DropdownItem>
+                                </DropdownMenu>
+                            </Dropdown>
+                        </div>
+                        <draggable v-model="label.taskLists" class="task-box" group="task" :sort="false" :animation="150" draggable=".task-draggable">
+                            <div v-for="task in label.taskLists" :key="task.id" class="task-item task-draggable" :class="['p'+task.level,task.complete?'complete':'',task.overdue?'overdue':'']">
+                                <div class="task-title">{{task.title}}</div>
+                                <div class="task-more">
+                                    <div v-if="task.overdue" class="task-status">已超期</div>
+                                    <div v-else-if="task.complete" class="task-status">已完成</div>
+                                    <div v-else class="task-status">未完成</div>
+                                    <Tooltip class="task-userimg" :content="task.nickname || task.username"><img :src="task.userimg"/></Tooltip>
+                                </div>
+                            </div>
+                            <div slot="footer">
+                                <project-add-task :placeholder='`添加任务至"${label.title}"`' :projectid="label.projectid" :labelid="label.id" @on-add-success="addTaskSuccess(label)"></project-add-task>
+                            </div>
+                        </draggable>
+                    </div>
+                </div>
+                <div v-if="loadDetailed" slot="footer" class="label-item label-create">
+                    <div class="label-body">
+                        <div class="trigger-box ft hover"><i class="ft icon">&#xE8C8;</i>添加一个新列表</div>
+                    </div>
+                </div>
+            </draggable>
+        </w-content>
 
     </div>
 </template>
 
+<style lang="scss">
+    #project-panel-enter-textarea {
+        background: transparent;
+        background: none;
+        outline: none;
+        border: 0;
+        resize: none;
+        padding: 0;
+        margin: 8px 0;
+        line-height: 22px;
+        border-radius: 0;
+        color: rgba(0, 0, 0, 0.85);
+        &:focus {
+            border-color: transparent;
+            box-shadow: none;
+        }
+    }
+</style>
 <style lang="scss" scoped>
-    .doc {
+    .project-panel {
+        .project-title {
+            display: flex;
+            flex-direction: row;
+            align-items: center;
+            .project-title-loading {
+                width: 18px;
+                height: 18px;
+                margin-right: 6px;
+                display: flex;
+            }
+            h1 {
+                font-size: 14px;
+                font-weight: 500;
+            }
+        }
+        .label-box {
+            display: flex;
+            flex-direction: row;
+            align-items: flex-start;
+            justify-content: flex-start;
+            flex-wrap: nowrap;
+            overflow-x: auto;
+            overflow-y: hidden;
+            -webkit-overflow-scrolling: touch;
+            width: 100%;
+            height: 100%;
+            padding: 15px;
+            .label-item {
+                flex-grow: 1;
+                flex-shrink: 0;
+                flex-basis: auto;
+                height: 100%;
+                padding-right: 15px;
+                &.label-create {
+                    cursor: pointer;
+                }
+                .label-body {
+                    width: 300px;
+                    height: 100%;
+                    border-radius: 0.15rem;
+                    background-color: #ebecf0;
+                    overflow: hidden;
+                    position: relative;
+                    display: flex;
+                    flex-direction: column;
+                    .title-box {
+                        padding: 0 12px;
+                        font-weight: bold;
+                        color: #666666;
+                        position: relative;
+                        cursor: move;
+                        display: flex;
+                        align-items: center;
+                        width: 100%;
+                        height: 42px;
+                        h2 {
+                            flex: 1;
+                            font-size: 16px;
+                            overflow: hidden;
+                            text-overflow: ellipsis;
+                            white-space: nowrap;
+                        }
+                        i {
+                            font-weight: 500;
+                            font-size: 18px;
+                            height: 100%;
+                            line-height: 42px;
+                            width: 42px;
+                            text-align: right;
+                            cursor: pointer;
+                        }
+                    }
+                    .task-box {
+                        flex: 1;
+                        width: 100%;
+                        overflow: auto;
+                        display: flex;
+                        flex-direction: column;
+                        padding: 0 12px 2px;
+                        .task-item {
+                            width: 100%;
+                            margin: 5px 0 8px;
+                            padding: 8px;
+                            background-color: #ffffff;
+                            border-left: 2px solid #BF9F03;
+                            border-right: 2px solid #ffffff;
+                            color: #091e42;
+                            border-radius: 3px;
+                            box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
+                            transition: all 0.2s;
+                            cursor: pointer;
+                            &:hover{
+                                box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.38);
+                            }
+                            &.p1 {
+                                border-left-color: #ff0000;
+                            }
+                            &.p2 {
+                                border-left-color: #BB9F35;
+                            }
+                            &.p3 {
+                                border-left-color: #449EDD;
+                            }
+                            &.p4 {
+                                border-left-color: #84A83B;
+                            }
+                            &.complete {
+                                .task-more {
+                                    .task-status {
+                                        color: #666666;
+                                    }
+                                }
+                            }
+                            &.overdue {
+                                .task-more {
+                                    .task-status {
+                                        color: #ff0000;
+                                    }
+                                }
+                            }
+                            .task-title {
+                                font-size: 12px;
+                                color: #091e42;
+                            }
+                            .task-more {
+                                min-height: 30px;
+                                display: flex;
+                                align-items: flex-end;
+                                .task-status {
+                                    color: #19be6b;
+                                    font-size: 12px;
+                                    flex: 1;
+                                }
+                                .task-userimg {
+                                    width: 26px;
+                                    height: 26px;
+                                    img {
+                                        object-fit: cover;
+                                        width: 100%;
+                                        height: 100%;
+                                        border-radius: 50%;
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    .trigger-box {
+                        text-align: center;
+                        font-size: 16px;
+                        color: #666;
+                        width: 100%;
+                        position: absolute;
+                        top: 50%;
+                        transform: translate(0, -50%);
+                    }
+                }
+            }
+        }
     }
 </style>
 <script>
+    import draggable from 'vuedraggable'
+
     import WHeader from "../components/WHeader";
     import WContent from "../components/WContent";
+    import WLoading from "../components/WLoading";
+    import ProjectAddTask from "../components/project/task/add";
+
     export default {
-        components: {WContent, WHeader},
+        components: {ProjectAddTask, draggable, WLoading, WContent, WHeader},
         data () {
             return {
+                loadIng: 0,
+                loadDetailed: false,
 
+                projectid: this.$route.params.id,
+                projectDetail: {},
+                projectLabel: [],
             }
         },
         mounted() {
-
+            if ($A.runNum(this.projectid) <= 0) {
+                this.goBack();
+                return;
+            }
+            this.getDetail();
         },
         computed: {
 
@@ -43,7 +278,53 @@
 
         },
         methods: {
-
+            getDetail() {
+                this.loadIng++;
+                $A.aAjax({
+                    url: 'project/detail',
+                    data: {
+                        projectid: this.projectid,
+                    },
+                    complete: () => {
+                        this.loadIng--;
+                        this.loadDetailed = true;
+                    },
+                    error: () => {
+                        this.$Message.error(this.$L('网络繁忙,请稍后再试!'));
+                        this.goBack();
+                    },
+                    success: (res) => {
+                        if (res.ret === 1) {
+                            this.projectDetail = res.data.project;
+                            this.projectLabel = res.data.label;
+                        } else {
+                            this.$Modal.error({title: this.$L('温馨提示'), content: res.msg});
+                            this.goBack();
+                        }
+                    }
+                });
+            },
+            addTaskSuccess(label) {
+                $A.aAjax({
+                    url: 'project/task/lists',
+                    data: {
+                        projectid: this.projectid,
+                        labelid: label.id,
+                    },
+                    complete: () => {
+                    },
+                    error: () => {
+                        window.location.reload();
+                    },
+                    success: (res) => {
+                        if (res.ret === 1) {
+                            this.$set(label, 'taskLists', res.data.lists);
+                        } else {
+                            window.location.reload();
+                        }
+                    }
+                });
+            }
         },
     }
 </script>