kuaifan 5 年 前
コミット
df90ca7036

+ 89 - 5
app/Http/Controllers/Api/ProjectController.php

@@ -98,11 +98,16 @@ class ProjectController extends Controller
         if (Base::isError($inRes)) {
             return $inRes;
         }
+        $projectSetting = Base::string2array($projectDetail['setting']);
         //子分类
         $label = Base::DBC2A(DB::table('project_label')->where('projectid', $projectid)->orderBy('inorder')->orderBy('id')->get());
         $simpleLabel = [];
         //任务
-        $task = Base::DBC2A(DB::table('project_task')->where([ 'projectid' => $projectid, 'delete' => 0, 'complete' => 0 ])->orderByDesc('inorder')->orderByDesc('id')->get());
+        $whereArray = [ 'projectid' => $projectid, 'delete' => 0, 'complete' => 0 ];
+        if ($projectSetting['complete_show'] == 'show') {
+            unset($whereArray['complete']);
+        }
+        $task = Base::DBC2A(DB::table('project_task')->where($whereArray)->orderByDesc('inorder')->orderByDesc('id')->get());
         //任务归类
         foreach ($label AS $index => $temp) {
             $taskLists = [];
@@ -225,6 +230,58 @@ class ProjectController extends Controller
         }
     }
 
+
+    /**
+     * 设置项目
+     *
+     * @apiParam {String} act           类型
+     * - save: 保存
+     * - other: 读取
+     * @apiParam {Number} projectid     项目ID
+     * @apiParam {Object} ...           其他保存参数
+     *
+     * @throws \Throwable
+     */
+    public function setting()
+    {
+        $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('项目不存在或已被删除!');
+        }
+        //
+        $setting = Base::string2array($projectDetail['setting']);
+        $act = trim(Request::input('act'));
+        if ($act == 'save') {
+            if ($projectDetail['username'] != $user['username']) {
+                return Base::retError('你不是项目负责人!');
+            }
+            foreach (Request::input() AS $key => $value) {
+                if (in_array($key, ['add_role', 'edit_role', 'complete_role', 'archived_role', 'del_role', 'complete_show'])) {
+                    $setting[$key] = $value;
+                }
+            }
+            DB::table('project_lists')->where('id', $projectDetail['id'])->update([
+                'setting' => Base::string2array($setting)
+            ]);
+        }
+        //
+        foreach (['edit_role', 'complete_role', 'archived_role', 'del_role'] AS $key) {
+            $setting[$key] = is_array($setting[$key]) ? $setting[$key] : ['__', 'owner'];
+        }
+        $setting['add_role'] = is_array($setting['add_role']) ? $setting['add_role'] : ['__', 'member'];
+        $setting['complete_show'] = $setting['complete_show'] ?: 'hide';
+        //
+        return Base::retSuccess($act == 'save' ? '修改成功!' : 'success', $setting ?: json_decode('{}'));
+    }
+
     /**
      * 收藏项目
      *
@@ -1282,6 +1339,10 @@ class ProjectController extends Controller
             if (Base::isError($inRes)) {
                 return $inRes;
             }
+            $checkRole = Project::role('add_role', $projectid, 0);
+            if (Base::isError($checkRole)) {
+                return $checkRole;
+            }
             //
             $username = trim(Request::input('username'));
             if (empty($username)) {
@@ -1395,10 +1456,33 @@ class ProjectController extends Controller
             if (Base::isError($inRes)) {
                 return $inRes;
             }
-            if (!$inRes['data']['isowner']
-                && $task['username'] != $user['username']
-                && !in_array($act, ['comment', 'attention'])) {
-                return Base::retError('此操作只允许项目管理员或者任务负责人!');
+            if (!in_array($act, ['comment', 'attention'])) {
+                $checkRole = Project::role('edit_role', $task['projectid'], $task['id']);
+                if (Base::isError($checkRole)) {
+                    return $checkRole;
+                }
+                switch ($act) {
+                    case 'complete':
+                    case 'unfinished':
+                        $checkRole = Project::role('complete_role', $task['projectid'], $task['id']);
+                        if (Base::isError($checkRole)) {
+                            return $checkRole;
+                        }
+                        break;
+                    case 'archived':
+                    case 'unarchived':
+                        $checkRole = Project::role('archived_role', $task['projectid'], $task['id']);
+                        if (Base::isError($checkRole)) {
+                            return $checkRole;
+                        }
+                        break;
+                    case 'delete':
+                        $checkRole = Project::role('del_role', $task['projectid'], $task['id']);
+                        if (Base::isError($checkRole)) {
+                            return $checkRole;
+                        }
+                        break;
+                }
             }
         } else {
             if ($task['username'] != $user['username']) {

+ 1 - 1
app/Http/Controllers/IndexController.php

@@ -14,7 +14,7 @@ use Redirect;
 class IndexController extends Controller
 {
 
-    private $version = '1.3.3';
+    private $version = '1.4';
 
     public function __invoke($method, $action = '', $child = '')
     {

+ 64 - 0
app/Module/Project.php

@@ -101,4 +101,68 @@ class Project
         //
         return $userArray;
     }
+
+    /**
+     * 项目(任务)权限
+     * @param $type
+     * @param $projectid
+     * @param int $taskid
+     * @return array|mixed
+     */
+    public static function role($type, $projectid, $taskid = 0)
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $project = Base::DBC2A(DB::table('project_lists')->select(['username', 'setting'])->where('id', $projectid)->where('delete', 0)->first());
+        if (empty($project)) {
+            return Base::retError('项目不存在或已被删除!');
+        }
+        // 项目负责人最高权限
+        if ($project['username'] == $user['username']) {
+            return Base::retSuccess('success');
+        }
+        //
+        $setting = Base::string2array($project['setting']);
+        foreach (['edit_role', 'complete_role', 'archived_role', 'del_role'] AS $key) {
+            $setting[$key] = is_array($setting[$key]) ? $setting[$key] : ['__', 'owner'];
+        }
+        $setting['add_role'] = is_array($setting['add_role']) ? $setting['add_role'] : ['__', 'member'];
+        //
+        $role = $setting[$type];
+        if (empty($role) || !is_array($role)) {
+            return Base::retError('操作权限不足!');
+        }
+        if (in_array('member', $role)) {
+            $inRes = Project::inThe($projectid, $user['username']);
+            if (Base::isError($inRes)) {
+                return $inRes;
+            }
+        } elseif (in_array('owner', $role)) {
+            if (empty($taskid)) {
+                return Base::retError('任务不存在!');
+            }
+            $task = Base::DBC2A(DB::table('project_task')
+                ->select(['username'])
+                ->where([
+                    ['delete', '=', 0],
+                    ['id', '=', $taskid],
+                ])
+                ->first());
+            if (empty($task)) {
+                return Base::retError('任务不存在!');
+            }
+            if ($task['username'] != $user['username']) {
+                return Base::retError('此操作只允许项目管理员或者任务负责人!');
+            }
+        } else {
+            return Base::retError('此操作仅限项目负责人!');
+        }
+        //
+        return Base::retSuccess('success');
+    }
 }

+ 32 - 0
database/migrations/2020_07_16_175154_alter_pre_project_lists_table.php

@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class AlterPreProjectListsTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('project_lists', function (Blueprint $table) {
+            $table->text('setting')->after('deletedate')->nullable();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('project_lists', function (Blueprint $table) {
+            $table->dropColumn('setting');
+        });
+    }
+}

+ 2 - 0
install/BT.md

@@ -156,6 +156,8 @@ $ systemctl restart supervisord
 
 ## 升级更新
 
+**注意:在升级之前请备份好你的数据!**
+
 - 将最新的代码上传至站点;
 - 进入服务器,切换至站点目录,然后依次运行以下命令:
 

+ 2 - 0
install/DOCKER.md

@@ -82,6 +82,8 @@ $ ./cmd mysql "your command"            // 运行 mysql 命令
 
 ## 升级更新
 
+**注意:在升级之前请备份好你的数据!**
+
 - 进入目录,依次运行以下命令:
 
 ```bash

+ 2 - 0
install/SERVER.md

@@ -148,6 +148,8 @@ server {
 
 ## 升级更新
 
+**注意:在升级之前请备份好你的数据!**
+
 - 进入目录,依次运行以下命令:
 
 ```bash

+ 2 - 0
install/en/DOCKER.md

@@ -82,6 +82,8 @@ $ ./cmd mysql "your command"            // To run a mysql command
 
 ## Upgrade
 
+**Note: Please backup your data before upgrading!**
+
 - Go to the directory and run the following commands in turn:
 
 ```bash

+ 2 - 0
install/en/SERVER.md

@@ -148,6 +148,8 @@ server {
 
 ## Upgrade
 
+**Note: Please backup your data before upgrading!**
+
 - Go to the directory and run the following commands in turn:
 
 ```bash

+ 3 - 2
package.json

@@ -26,12 +26,13 @@
         "vue-template-compiler": "^2.6.11"
     },
     "dependencies": {
+        "gantt-schedule-timeline-calendar": "^2.6.8",
         "tinymce": "^5.4.1",
-        "view-design": "^4.3.1",
-        "vuedraggable": "^2.23.2",
+        "view-design": "^4.3.2",
         "vue-clipboard2": "^0.3.1",
         "vue-emoji-picker": "^1.0.1",
         "vue-kityminder-gg": "^1.3.1",
+        "vuedraggable": "^2.24.0",
         "x-data-spreadsheet": "^1.1.4",
         "xlsx": "^0.16.3"
     }

+ 119 - 0
resources/assets/js/main/components/project/gantt/GSTC.vue

@@ -0,0 +1,119 @@
+<template>
+    <div class="gstc" ref="gstc"></div>
+</template>
+
+<style lang="scss">
+    .gantt-schedule-timeline-calendar__list-column-row {
+        .gantt-schedule-timeline-calendar__list-column-row-content {
+            display: flex;
+            cursor: pointer;
+            &:hover {
+                .gantt-schedule-timeline-calendar-goto {
+                    display: flex;
+                }
+            }
+            .gantt-schedule-timeline-calendar-overdue {
+                display: flex;
+                align-items: center;
+                margin-right: 2px;
+                em {
+                    font-size: 12px;
+                    height: 22px;
+                    line-height: 22px;
+                    color: #ffffff;
+                    padding: 0 4px;
+                    border-radius: 3px;
+                    background: #ff0000;
+                    transform: scale(0.9);
+                    transform-origin: 0 center;
+                }
+            }
+            .gantt-schedule-timeline-calendar-title {
+                flex: 1;
+                padding-right: 6px;
+            }
+            .gantt-schedule-timeline-calendar-goto {
+                font-family: Ionicons;
+                font-weight: 400;
+                text-align: center;
+                display: none;
+                justify-content: center;
+                align-items: center;
+                width: 32px;
+                margin-right: 8px;
+                font-size: 16px;
+                color: #888888;
+                &:before {
+                    content: "\F216";
+                }
+                &:hover {
+                    color: #333333;
+                }
+            }
+        }
+    }
+</style>
+<script>
+    import GSTC from "gantt-schedule-timeline-calendar";
+    import "gantt-schedule-timeline-calendar/dist/style.css";
+
+    export default {
+        name: "GSTC",
+        props: ["config"],
+        data() {
+            return {
+                state: null,
+                gstc: null,
+            }
+        },
+        mounted() {
+            this.state = GSTC.api.stateFromConfig(this.config);
+            this.$emit("state", this.state);
+            this.$refs.gstc.addEventListener('gstc-loaded', () => {
+                this.gstc.api.scrollToTime(new Date().getTime());
+            });
+            this.gstc = GSTC({
+                element: this.$refs.gstc,
+                state: this.state
+            });
+            //
+            this.$watch(
+                "config",
+                (config) => {
+                    this.state.update("config", current => {
+                        if (current !== config) {
+                            return GSTC.api.mergeDeep({}, current, config);
+                        } else {
+                            return current;
+                        }
+                    });
+                },
+                {deep: true}
+            );
+        },
+        destroyed() {
+            this.gstc.app.destroy();
+        },
+        methods: {
+            getGstc() {
+                return this.gstc;
+            },
+            getState() {
+                return this.state;
+            },
+            setPeriod(val) {
+                GSTC.api.setPeriod(val);
+            },
+            updateTime(id, time) {
+                this.state.update('config.chart.items', items => {
+                    for (let itemId in items) {
+                        if (items.hasOwnProperty(itemId) && itemId == id) {
+                            items[itemId].time = $A.cloneData(time);
+                        }
+                    }
+                    return items;
+                });
+            }
+        }
+    };
+</script>

+ 357 - 0
resources/assets/js/main/components/project/gantt/index.vue

@@ -0,0 +1,357 @@
+<template>
+    <div class="project-gstc-gantt">
+        <GSTC v-if="loadFinish" ref="gstc" :config="config" @state="emitState"/>
+        <Dropdown class="project-gstc-dropdown" @on-click="tapView">
+            <Icon class="project-gstc-dropdown-icon" type="md-funnel" />
+            <DropdownMenu slot="list">
+                <DropdownItem name="now">{{$L('现在')}}</DropdownItem>
+                <DropdownItem name="day" :class="{'project-gstc-dropdown-period':period=='day'}">{{$L('天视图')}}</DropdownItem>
+                <DropdownItem name="week" :class="{'project-gstc-dropdown-period':period=='week'}">{{$L('周视图')}}</DropdownItem>
+                <DropdownItem name="month" :class="{'project-gstc-dropdown-period':period=='month'}">{{$L('月视图')}}</DropdownItem>
+            </DropdownMenu>
+        </Dropdown>
+        <div class="project-gstc-close" @click="$emit('on-close')"><Icon type="md-close" /></div>
+    </div>
+</template>
+
+<style lang="scss">
+    .project-gstc-gantt {
+        position: absolute;
+        top: 15px;
+        left: 15px;
+        right: 15px;
+        bottom: 15px;
+        z-index: 1;
+        transform: translateZ(0);
+        background-color: #fdfdfd;
+        border-radius: 3px;
+        .gantt-schedule-timeline-calendar {
+            background-color: transparent;
+            padding: 12px;
+        }
+        .gantt-schedule-timeline-calendar__list-column-header-resizer-container {
+            line-height: 92px;
+        }
+        .project-gstc-dropdown {
+            position: absolute;
+            top: 44px;
+            left: 276px;
+            .project-gstc-dropdown-icon {
+                cursor: pointer;
+                color: #999;
+                font-size: 20px;
+            }
+            .project-gstc-dropdown-period {
+                color: #058ce4;
+            }
+        }
+        .project-gstc-close {
+            position: absolute;
+            top: 8px;
+            left: 12px;
+            cursor: pointer;
+            &:hover {
+                i {
+                    transform: scale(1) rotate(45deg);
+                }
+            }
+            i {
+                color: #666666;
+                font-size: 28px;
+                transform: scale(0.92);
+                transition: all .2s;
+            }
+        }
+    }
+</style>
+<script>
+    import GSTC from "./GSTC";
+    import ItemMovement from "gantt-schedule-timeline-calendar/dist/ItemMovement.plugin.js"
+    import CalendarScroll from "gantt-schedule-timeline-calendar/dist/CalendarScroll.plugin.js"
+    import WeekendHighlight from "gantt-schedule-timeline-calendar/dist/WeekendHighlight.plugin.js"
+
+    /**
+     * 甘特图
+     */
+    export default {
+        name: 'ProjectGantt',
+        components: { GSTC },
+        props: {
+            projectLabel: {
+                default: []
+            },
+        },
+
+        data () {
+            return {
+                loadFinish: false,
+
+                period: 'day',
+
+                rows: {},
+                items: {},
+
+                config: {},
+            }
+        },
+
+        mounted() {
+            this.initData();
+            this.loadFinish = true;
+            //
+            this.$watch(
+                "projectLabel",
+                (projectLabel) => {
+                    this.initData(this.loadFinish == true);
+                },
+                {deep: true}
+            );
+        },
+
+        methods: {
+            initData(isUpdate) {
+                this.rows = {};
+                this.items = {};
+                this.projectLabel.forEach((item) => {
+                    item.taskLists.forEach((taskData) => {
+                        let start = taskData.startdate || taskData.indate;
+                        let end = taskData.enddate || taskData.indate;
+                        if (end == start) {
+                            end = Math.round(new Date($A.formatDate('Y-m-d 23:59:59', end)).getTime()/1000);
+                        }
+                        start*= 1000;
+                        end*= 1000;
+                        if (end == start) end++;
+                        //
+                        let color = '#058ce4';
+                        if (taskData.complete) {
+                            color = '#c1c1c1';
+                        } else {
+                            if (taskData.level === 1) {
+                                color = '#ff0000';
+                            }else if (taskData.level === 2) {
+                                color = '#BB9F35';
+                            }else if (taskData.level === 3) {
+                                color = '#449EDD';
+                            }else if (taskData.level === 4) {
+                                color = '#84A83B';
+                            }
+                        }
+                        //
+                        let label = `<div class="gantt-schedule-timeline-calendar-title">${taskData['title']}</div>`;
+                        if (taskData.overdue) {
+                            label = `<div class="gantt-schedule-timeline-calendar-overdue"><em>${this.$L('已超期')}</em></div>${label}`;
+                        }
+                        label = `${label}<div class="gantt-schedule-timeline-calendar-goto" data-id="${taskData['id']}"></div>`;
+                        this.rows[taskData['id']] = {
+                            id: taskData['id'],
+                            label: label,
+                            complete: taskData.complete,
+                            overdue: taskData.overdue,
+                        };
+                        this.items[taskData['id']] = {
+                            id: taskData['id'],
+                            rowId: taskData['id'],
+                            label: taskData['title'],
+                            time: { start, end },
+                            style: { background: color },
+                        };
+                    });
+                });
+                //
+                this.config = Object.assign({
+                    plugins: [ItemMovement({
+                        moveable: 'x',
+                        resizeable: true,
+                        collisionDetection: true
+                    }), CalendarScroll(), WeekendHighlight()],
+                    height: this.$el.clientHeight - 24,
+                    list: {
+                        toggle: {
+                            display: false
+                        },
+                        rows: this.rows,
+                        columns: {
+                            percent: 100,
+                            resizer: {
+                                inRealTime: true,
+                                dots: 0
+                            },
+                            data: {
+                                label: {
+                                    id: "label",
+                                    data: "label",
+                                    width: 300,
+                                    isHTML: true,
+                                    header: {
+                                        content: this.$L("任务名称")
+                                    }
+                                }
+                            }
+                        }
+                    },
+                    chart: {
+                        time: {
+                            period: 'day',
+                            additionalSpaces: {
+                                hour: { before: 24, after: 24, period: 'hour' },
+                                day: { before: 1, after: 1, period: 'month' },
+                                week: { before: 1, after: 1, period: 'year' },
+                                month: { before: 6, after: 6, period: 'year' },
+                                year: { before: 12, after: 12, period: 'year' }
+                            }
+                        },
+                        items: this.items
+                    },
+
+                }, isUpdate ? {} : {
+                    actions: {
+                        "list-column-row": [this.actionRow],
+                        'chart-timeline-items-row-item': [this.actionResizing]
+                    }
+                }, this.getLanguage() == 'en' ? {} : {
+                    locale: {
+                        name: "zh-cn",
+                        weekdays: ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"],
+                        weekdaysShort: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
+                        weekdaysMin: ["日", "一", "二", "三", "四", "五", "六"],
+                        months: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
+                        monthsShort: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
+                        ordinal: function(t, e) {
+                            switch (e) {
+                                case "W":
+                                    return "".concat(t, "周");
+                                default:
+                                    return "".concat(t, "日")
+                            }
+                        },
+                        weekStart: 1,
+                        yearStart: 4,
+                        formats: {
+                            LT: "HH:mm",
+                            LTS: "HH:mm:ss",
+                            L: "YYYY/MM/DD",
+                            LL: "YYYY年M月D日",
+                            LLL: "YYYY年M月D日Ah点mm分",
+                            LLLL: "YYYY年M月D日ddddAh点mm分",
+                            l: "YYYY/M/D",
+                            ll: "YYYY年M月D日",
+                            lll: "YYYY年M月D日 HH:mm",
+                            llll: "YYYY年M月D日dddd HH:mm"
+                        },
+                        relativeTime: {
+                            future: "%s内",
+                            past: "%s前",
+                            s: "几秒",
+                            m: "1 分钟",
+                            mm: "%d 分钟",
+                            h: "1 小时",
+                            hh: "%d 小时",
+                            d: "1 天",
+                            dd: "%d 天",
+                            M: "1 个月",
+                            MM: "%d 个月",
+                            y: "1 年",
+                            yy: "%d 年"
+                        },
+                    },
+                });
+            },
+
+            actionRow(element, data) {
+                let onClick = (event) => {
+                    //打开任务
+                    if (event.target.className == 'gantt-schedule-timeline-calendar-goto') {
+                        let rowId = event.target.getAttribute("data-id");
+                        rowId && this.$refs.gstc.getGstc().api.scrollToTime(this.items[rowId].time.start)
+                    } else {
+                        this.taskDetail(data.rowId);
+                    }
+                }
+                element.addEventListener('click', onClick);
+                return {
+                    update(element, newData) {
+                        data = newData;
+                    },
+                    destroy(element, data) {
+                        element.removeEventListener('click', onClick);
+                    }
+                };
+            },
+
+            actionResizing(element, data) {
+                let thas = this;
+                return {
+                    update(element, newData) {
+                        data = newData;
+                        if (data.item.isResizing) {
+                            if (Math.abs(thas.items[data.item['id']].time.end - data.item.time.end) > 60000
+                                || Math.abs(thas.items[data.item['id']].time.start - data.item.time.start) > 60000) {
+                                //修改时间(变化超过1分钟)
+                                let backTime = $A.cloneData(thas.items[data.item['id']].time);
+                                let newTime = $A.cloneData(data.item.time);
+                                let timeStart = $A.formatDate('Y-m-d H:i', Math.round(newTime.start / 1000));
+                                let timeEnd = $A.formatDate('Y-m-d H:i', Math.round(newTime.end / 1000));
+                                thas.items[data.item['id']].time = newTime;
+                                thas.$refs.gstc.updateTime(data.item['id'], newTime);
+                                thas.$Modal.confirm({
+                                    title: thas.$L("计划时间"),
+                                    content: thas.$L('确定要修改任务【%】的计划时间吗?<br/>开始时间:%<br/>结束时间:%', data.item['label'], timeStart, timeEnd),
+                                    onCancel: () => {
+                                        thas.items[data.item['id']].time = backTime;
+                                        thas.$refs.gstc.updateTime(data.item['id'], backTime);
+                                    },
+                                    onOk: () => {
+                                        let ajaxData = {
+                                            act: 'plannedtime',
+                                            taskid: data.item['id'],
+                                            content: timeStart + "," + timeEnd,
+                                        };
+                                        $A.apiAjax({
+                                            url: 'project/task/edit',
+                                            data: ajaxData,
+                                            error: () => {
+                                                alert(thas.$L('网络繁忙,请稍后再试!'));
+                                                thas.items[data.item['id']].time = backTime;
+                                                thas.$refs.gstc.updateTime(data.item['id'], backTime);
+                                            },
+                                            success: (res) => {
+                                                if (res.ret === 1) {
+                                                    thas.$Message.success(res.msg);
+                                                    $A.triggerTaskInfoListener(ajaxData.act, res.data);
+                                                    $A.triggerTaskInfoChange(ajaxData.taskid);
+                                                } else {
+                                                    setTimeout(() => {
+                                                        thas.$Modal.error({title: thas.$L('温馨提示'), content: res.msg});
+                                                    }, 350)
+                                                    thas.items[data.item['id']].time = backTime;
+                                                    thas.$refs.gstc.updateTime(data.item['id'], backTime);
+                                                }
+                                            }
+                                        });
+                                    }
+                                });
+                            }
+                        }
+                    }
+                };
+            },
+
+            emitState(GSTCState) {
+                GSTCState.subscribe('config.plugin.ItemMovement.item', data => {
+                    if (!data) return;
+                    GSTCState.update(`config.chart.items.${data.id}.isResizing`, !(data.waiting || data.moving || data.resizing));
+                });
+            },
+
+            tapView(e) {
+                if ("now" === e) {
+                    var i = this.$refs.gstc.getGstc()
+                    return i.api.scrollToTime(i.api.time.date().valueOf())
+                }
+                this.period = e;
+                this.$refs.gstc.setPeriod(e);
+            }
+        }
+    }
+</script>

+ 171 - 0
resources/assets/js/main/components/project/setting.vue

@@ -0,0 +1,171 @@
+<template>
+    <drawer-tabs-container>
+        <div class="project-setting">
+            <Form ref="formSystem" :model="formSystem" :label-width="110">
+                <div class="project-setting-title">{{$L('权限设置')}}:</div>
+                <FormItem :label="$L('添加任务')">
+                    <Checkbox :value="true" disabled>{{$L('项目负责人')}}</Checkbox>
+                    <CheckboxGroup v-model="formSystem.add_role" class="project-setting-group">
+                        <Checkbox label="member">{{$L('项目成员')}}</Checkbox>
+                    </CheckboxGroup>
+                </FormItem>
+                <FormItem :label="$L('修改任务')">
+                    <Checkbox :value="true" disabled>{{$L('项目负责人')}}</Checkbox>
+                    <CheckboxGroup v-model="formSystem.edit_role" class="project-setting-group">
+                        <Checkbox label="owner">{{$L('任务负责人')}}</Checkbox>
+                        <Checkbox label="member">{{$L('项目成员')}}</Checkbox>
+                    </CheckboxGroup>
+                </FormItem>
+                <FormItem :label="$L('标记完成')">
+                    <Checkbox :value="true" disabled>{{$L('项目负责人')}}</Checkbox>
+                    <CheckboxGroup v-model="formSystem.complete_role" class="project-setting-group">
+                        <Checkbox label="owner">{{$L('任务负责人')}}</Checkbox>
+                        <Checkbox label="member">{{$L('项目成员')}}</Checkbox>
+                    </CheckboxGroup>
+                </FormItem>
+                <FormItem :label="$L('归档任务')">
+                    <Checkbox :value="true" disabled>{{$L('项目负责人')}}</Checkbox>
+                    <CheckboxGroup v-model="formSystem.archived_role" class="project-setting-group">
+                        <Checkbox label="owner">{{$L('任务负责人')}}</Checkbox>
+                        <Checkbox label="member">{{$L('项目成员')}}</Checkbox>
+                    </CheckboxGroup>
+                </FormItem>
+                <FormItem :label="$L('删除任务')">
+                    <Checkbox :value="true" disabled>{{$L('项目负责人')}}</Checkbox>
+                    <CheckboxGroup v-model="formSystem.del_role" class="project-setting-group">
+                        <Checkbox label="owner">{{$L('任务负责人')}}</Checkbox>
+                        <Checkbox label="member">{{$L('项目成员')}}</Checkbox>
+                    </CheckboxGroup>
+                </FormItem>
+                <div class="project-setting-title">{{$L('面板显示')}}:</div>
+                <FormItem :label="$L('显示已完成')">
+                    <div>
+                        <RadioGroup v-model="formSystem.complete_show">
+                            <Radio label="show">{{$L('显示')}}</Radio>
+                            <Radio label="hide">{{$L('隐藏')}}</Radio>
+                        </RadioGroup>
+                    </div>
+                    <div v-if="formSystem.complete_show=='show'" class="form-placeholder">
+                        {{$L('项目面板显示已完成的任务。')}}
+                    </div>
+                    <div v-else class="form-placeholder">
+                        {{$L('项目面板隐藏已完成的任务。')}}
+                    </div>
+                </FormItem>
+                <FormItem>
+                    <Button :loading="loadIng > 0" type="primary" @click="handleSubmit('formSystem')">{{$L('提交')}}</Button>
+                    <Button :loading="loadIng > 0" @click="handleReset('formSystem')" style="margin-left: 8px">{{$L('重置')}}</Button>
+                </FormItem>
+            </Form>
+        </div>
+    </drawer-tabs-container>
+</template>
+
+<style lang="scss" scoped>
+    .project-setting {
+        padding: 0 12px;
+        .project-setting-title {
+            padding: 12px;
+            font-size: 14px;
+            font-weight: 600;
+        }
+        .project-setting-group {
+            display: inline-block;
+        }
+        .form-placeholder {
+            font-size: 12px;
+            color: #999999;
+        }
+        .form-placeholder:hover {
+            color: #000000;
+        }
+    }
+</style>
+<script>
+    import DrawerTabsContainer from "../DrawerTabsContainer";
+    export default {
+        name: 'ProjectSetting',
+        components: {DrawerTabsContainer},
+        props: {
+            projectid: {
+                default: 0
+            },
+            canload: {
+                type: Boolean,
+                default: true
+            },
+        },
+        data () {
+            return {
+                loadYet: false,
+
+                loadIng: 0,
+
+                formSystem: {},
+            }
+        },
+        mounted() {
+            if (this.canload) {
+                this.loadYet = true;
+                this.getSetting();
+            }
+        },
+
+        watch: {
+            projectid() {
+                if (this.loadYet) {
+                    this.getSetting();
+                }
+            },
+            canload(val) {
+                if (val && !this.loadYet) {
+                    this.loadYet = true;
+                    this.getSetting();
+                }
+            }
+        },
+
+        methods: {
+            getSetting(save) {
+                this.loadIng++;
+                $A.apiAjax({
+                    url: 'project/setting?act=' + (save ? 'save' : 'get'),
+                    data: Object.assign(this.formSystem, {
+                        projectid: this.projectid
+                    }),
+                    complete: () => {
+                        this.loadIng--;
+                    },
+                    success: (res) => {
+                        if (res.ret === 1) {
+                            this.formSystem = res.data;
+                            if (save) {
+                                this.$Message.success(this.$L('修改成功'));
+                                this.$emit('on-change', res.data);
+                            }
+                        } else {
+                            if (save) {
+                                this.$Modal.error({title: this.$L('温馨提示'), content: res.msg });
+                            }
+                        }
+                    }
+                });
+            },
+            handleSubmit(name) {
+                this.$refs[name].validate((valid) => {
+                    if (valid) {
+                        switch (name) {
+                            case "formSystem": {
+                                this.getSetting(true);
+                                break;
+                            }
+                        }
+                    }
+                })
+            },
+            handleReset(name) {
+                this.$refs[name].resetFields();
+            },
+        }
+    }
+</script>

+ 4 - 2
resources/assets/js/main/components/project/task/detail/detail.vue

@@ -450,7 +450,7 @@
                         return;
 
                     case 'plannedtime':
-                        this.timeValue = $A.date2string(this.timeValue);
+                        this.timeValue = $A.date2string(this.timeValue, "Y-m-d H:i");
                         ajaxData.content = this.timeValue[0] + "," + this.timeValue[1];
                         this.$refs.timeRef.handleClose();
                         break;
@@ -560,7 +560,9 @@
                             $A.triggerTaskInfoChange(ajaxData.taskid);
                         } else {
                             ajaxCallback(0);
-                            this.$Modal.error({title: this.$L('温馨提示'), content: res.msg});
+                            setTimeout(() =>  {
+                                this.$Modal.error({title: this.$L('温馨提示'), content: res.msg});
+                            }, 350);
                         }
                     }
                 });

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

@@ -132,7 +132,7 @@ import '../../sass/main.scss';
         },
 
         /**
-         * 获取会员昵称
+         * 获取会员账号
          * @returns string
          */
         getUserName() {
@@ -144,6 +144,18 @@ import '../../sass/main.scss';
         },
 
         /**
+         * 获取会员昵称
+         * @returns string
+         */
+        getNickName() {
+            if ($A.getToken() === false) {
+                return "";
+            }
+            let userInfo = $A.getUserInfo();
+            return $A.ishave(userInfo.nickname) ? userInfo.nickname : $A.getUserName();
+        },
+
+        /**
          * 获取用户信息(并保存)
          * @param callback                  网络请求获取到用户信息回调(监听用户信息发生变化)
          * @param continueListenerName      持续监听标识(字符串或boolean,true:自动生成监听标识,false:自动生成监听标识但首次不请求网络)
@@ -364,7 +376,7 @@ import '../../sass/main.scss';
          * 监听任务发生变化
          * @param listenerName      监听标识
          * @param callback          监听回调
-         * @param callSpecial       是否监听几种特殊情况
+         * @param callSpecial       是否监听几种特殊事件(非操作任务的)
          */
         setOnTaskInfoListener(listenerName, callback, callSpecial) {
             if (typeof listenerName != "string") {
@@ -383,7 +395,7 @@ import '../../sass/main.scss';
                 if (!$A.__taskInfoListenerObject.hasOwnProperty(key)) continue;
                 item = $A.__taskInfoListenerObject[key];
                 if (typeof item.callback === "function") {
-                    if (['deleteproject', 'deletelabel', 'leveltask'].indexOf(act) === -1 || item.special === true) {
+                    if (['addlabel', 'deleteproject', 'deletelabel', 'labelsort', 'tasksort'].indexOf(act) === -1 || item.special === true) {
                         if (typeof taskDetail.__modifyUsername === "undefined") {
                             taskDetail.__modifyUsername = $A.getUserName();
                         }

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

@@ -743,6 +743,7 @@
                             okText: this.$L('保存并返回'),
                             onOk: () => {
                                 this.handleClick('save');
+                                this.goBackDirect();
                             }
                         });
                         break;

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

@@ -68,7 +68,7 @@
                 </li>
             </ul>
             <!-- 分页 -->
-            <Page v-if="listTotal > 0" class="pageBox" :total="listTotal" :current="listPage" :disabled="loadIng > 0" @on-change="setPage" @on-page-size-change="setPageSize" :page-size-opts="[10,20,30,50,100]" placement="top" transfer show-elevator show-sizer show-total></Page>
+            <Page v-if="listTotal > 0" class="pageBox" :total="listTotal" :current="listPage" :disabled="loadIng > 0" :pageSize="listPageSize" @on-change="setPage" @on-page-size-change="setPageSize" :page-size-opts="[20,40,60,100]" placement="top" transfer show-elevator show-sizer show-total></Page>
         </w-content>
 
         <Modal
@@ -311,6 +311,7 @@
                 lists: [],
                 listPage: 1,
                 listTotal: 0,
+                listPageSize: 20,
 
                 projectDrawerShow: false,
                 projectDrawerTab: 'archived',
@@ -392,7 +393,7 @@
             },
 
             setPageSize(size) {
-                if (Math.max($A.runNum(this.listPageSize), 10) != size) {
+                if (Math.max($A.runNum(this.listPageSize), 20) != size) {
                     this.listPageSize = size;
                     this.getLists();
                 }
@@ -407,7 +408,7 @@
                     url: 'project/lists',
                     data: {
                         page: Math.max(this.listPage, 1),
-                        pagesize: Math.max($A.runNum(this.listPageSize), 10),
+                        pagesize: Math.max($A.runNum(this.listPageSize), 20),
                     },
                     complete: () => {
                         this.loadIng--;

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

@@ -15,9 +15,10 @@
                 <div class="w-nav-flex"></div>
                 <div class="w-nav-right">
                     <span class="ft hover" @click="openProjectDrawer('lists')"><i class="ft icon">&#xE89E;</i> {{$L('任务列表')}}</span>
+                    <span class="ft hover" :class="{active:projectGanttShow}" @click="projectGanttShow=!projectGanttShow"><i class="ft icon">&#59141;</i> {{$L('甘特图')}}</span>
                     <span class="ft hover" @click="openProjectDrawer('files')"><i class="ft icon">&#xE701;</i> {{$L('文件列表')}}</span>
                     <span class="ft hover" @click="openProjectDrawer('logs')"><i class="ft icon">&#xE753;</i> {{$L('项目动态')}}</span>
-                    <span class="ft hover" @click="openProjectSettingDrawer('archived')"><i class="ft icon">&#xE7A7;</i> {{$L('设置')}}</span>
+                    <span class="ft hover" @click="openProjectSettingDrawer('setting')"><i class="ft icon">&#xE7A7;</i> {{$L('设置')}}</span>
                 </div>
             </div>
         </div>
@@ -27,6 +28,7 @@
                 v-model="projectLabel"
                 class="label-box"
                 draggable=".label-draggable"
+                :style="{visibility: projectGanttShow ? 'hidden' : 'visible'}"
                 :animation="150"
                 :disabled="projectSortDisabled"
                 @sort="projectSortUpdate(true)">
@@ -83,6 +85,7 @@
                     </div>
                 </div>
             </draggable>
+            <project-gantt v-if="projectGanttShow" @on-close="projectGanttShow=false" :projectLabel="projectLabel"></project-gantt>
         </w-content>
 
         <WDrawer v-model="projectDrawerShow" maxWidth="1080">
@@ -101,6 +104,9 @@
 
         <WDrawer v-model="projectSettingDrawerShow" maxWidth="1000">
             <Tabs v-if="projectSettingDrawerShow" v-model="projectSettingDrawerTab">
+                <TabPane :label="$L('项目设置')" name="setting">
+                    <project-setting :canload="projectSettingDrawerShow && projectSettingDrawerTab == 'setting'" :projectid="projectid" @on-change="getDetail"></project-setting>
+                </TabPane>
                 <TabPane :label="$L('已归档任务')" name="archived">
                     <project-archived :canload="projectSettingDrawerShow && projectSettingDrawerTab == 'archived'" :projectid="projectid"></project-archived>
                 </TabPane>
@@ -241,6 +247,10 @@
                                     border-left-color: #84A83B;
                                 }
                                 &.complete {
+                                    .task-title {
+                                        color: #666666;
+                                        text-decoration: line-through;
+                                    }
                                     .task-more {
                                         .task-status {
                                             color: #666666;
@@ -315,9 +325,13 @@
     import ProjectUsers from "../../components/project/users";
     import ProjectStatistics from "../../components/project/statistics";
     import WDrawer from "../../components/iview/WDrawer";
+    import ProjectGantt from "../../components/project/gantt/index";
+    import ProjectSetting from "../../components/project/setting";
 
     export default {
         components: {
+            ProjectSetting,
+            ProjectGantt,
             WDrawer,
             ProjectStatistics,
             ProjectUsers,
@@ -340,21 +354,57 @@
                 projectDrawerTab: 'lists',
 
                 projectSettingDrawerShow: false,
-                projectSettingDrawerTab: 'archived',
+                projectSettingDrawerTab: 'setting',
+
+                projectGanttShow: false,
+
+                routeName: '',
             }
         },
         mounted() {
+            this.routeName = this.$route.name;
             $A.setOnTaskInfoListener('pages/project-panel',(act, detail) => {
                 if (detail.projectid != this.projectid) {
                     return;
                 }
                 //
                 switch (act) {
-                    case 'deleteproject':   // 删除项目
+                    case 'addlabel':        // 添加分类
+                        let tempLists = this.projectLabel.filter((res) => { return res.id == detail.labelid });
+                        if (tempLists.length == 0) {
+                            this.projectLabel.push(Object.assign(detail, {id: detail.labelid}));
+                            this.projectSortData = this.getProjectSort();
+                        }
+                        return;
+
                     case 'deletelabel':     // 删除分类
+                        this.projectLabel.some((label, index) => {
+                            if (label.id == detail.labelid) {
+                                this.projectLabel.splice(index, 1);
+                                this.projectSortData = this.getProjectSort();
+                                return true;
+                            }
+                        });
                         return;
-                    case 'tasklevel':       // 调整级别
-                        this.getDetail(true);
+
+                    case 'deleteproject':   // 删除项目
+                        return;
+
+                    case "labelsort":       // 调整分类排序
+                    case "tasksort":        // 调整任务排序
+                        if (detail.__modifyUsername != $A.getUserName()) {
+                            if (this.routeName == this.$route.name) {
+                                this.$Modal.confirm({
+                                    title: this.$L("更新提示"),
+                                    content: this.$L('团队成员(%)调整了%,<br/>更新时间:%。<br/><br/>点击【确定】加载最新数据。', detail.nickname, this.$L(act == 'labelsort' ? '分类排序' : '任务排序'), $A.formatDate("Y-m-d H:i:s", detail.time)),
+                                    onOk: () => {
+                                        this.getDetail(true);
+                                    }
+                                });
+                            } else {
+                                this.getDetail(true);
+                            }
+                        }
                         return;
                 }
                 //
@@ -381,6 +431,22 @@
                         this.projectSortData = this.getProjectSort();
                         break;
 
+                    case "create":          // 创建任务
+                        this.projectLabel.some((label) => {
+                            if (label.id == detail.labelid) {
+                                let tempLists = label.taskLists.filter((res) => { return res.id == detail.id });
+                                if (tempLists.length == 0) {
+                                    detail.isNewtask = true;
+                                    label.taskLists.unshift(detail);
+                                    this.$nextTick(() => {
+                                        this.$set(detail, 'isNewtask', false);
+                                    });
+                                }
+                                return true;
+                            }
+                        });
+                        break;
+
                     case "unarchived":      // 取消归档
                         this.projectLabel.forEach((label) => {
                             if (label.id == detail.labelid) {
@@ -416,6 +482,7 @@
             if ($A.getToken() === false) {
                 this.projectid = 0;
             }
+            this.projectGanttShow = false;
             this.projectDrawerShow = false;
             this.projectSettingDrawerShow = false;
         },
@@ -649,12 +716,13 @@
                     loading: true,
                     onOk: () => {
                         if (this.labelValue) {
+                            let data = {
+                                projectid: this.projectid,
+                                title: this.labelValue
+                            };
                             $A.apiAjax({
                                 url: 'project/label/add',
-                                data: {
-                                    projectid: this.projectid,
-                                    title: this.labelValue
-                                },
+                                data: data,
                                 error: () => {
                                     this.$Modal.remove();
                                     alert(this.$L('网络繁忙,请稍后再试!'));
@@ -663,6 +731,7 @@
                                     this.$Modal.remove();
                                     this.projectLabel.push(res.data);
                                     this.projectSortData = this.getProjectSort();
+                                    $A.triggerTaskInfoListener('addlabel', Object.assign(data, {labelid: res.data.id}));
                                     setTimeout(() => {
                                         if (res.ret === 1) {
                                             this.$Message.success(res.msg);
@@ -729,6 +798,7 @@
                     success: (res) => {
                         if (res.ret === 1) {
                             this.$Message.success(res.msg);
+                            $A.triggerTaskInfoListener(isLabel ? 'labelsort' : 'tasksort', { projectid: this.projectid, nickname: $A.getNickName(), time: Math.round(new Date().getTime()/1000) });
                         } else {
                             this.getDetail();
                             this.$Modal.error({title: this.$L('温馨提示'), content: res.msg});

+ 4 - 3
resources/assets/js/main/pages/todo.vue

@@ -383,13 +383,15 @@
                     }
                     return;
                 }
-                //
+                //特殊事件(非操作任务的)
                 switch (act) {
                     case 'deleteproject':   // 删除项目
                     case 'deletelabel':     // 删除分类
                         this.refreshTask();
                         return;
-                    case 'tasklevel':       // 调整级别
+                    case 'addlabel':        // 添加分类
+                    case "labelsort":       // 调整分类排序
+                    case "tasksort":        // 调整任务排序
                         return;
                 }
                 //
@@ -689,7 +691,6 @@
                     success: (res) => {
                         if (res.ret === 1) {
                             this.$Message.success(res.msg);
-                            $A.triggerTaskInfoListener('tasklevel', res.data.taskLevel);
                         } else {
                             this.refreshTask();
                             this.$Modal.error({title: this.$L('温馨提示'), content: res.msg});

+ 4 - 0
resources/assets/sass/main.scss

@@ -113,6 +113,10 @@
             span {
                 margin: 0 12px 0 0;
                 cursor: pointer;
+                &.active {
+                    color: #0285d7;
+                    font-weight: 500;
+                }
             }
             span + span {
                 padding-left: 12px;

+ 19 - 2
resources/lang/en/general.js

@@ -85,7 +85,7 @@ export default {
     "移动": "Mobile",
     "无标题": "Untitled",
     "默认节点": "The default node",
-    "任务名称": "Mission name",
+    "任务名称": "Task name",
     "创建人": "Founder",
     "负责人": "Principal",
     "归档时间": "Archive Time",
@@ -194,7 +194,7 @@ export default {
     "完成时间": "Complete time",
     "添加成员": "Add Members",
     "成员角色": "Member role",
-    "项目负责人": "Project manager",
+    "项目负责人": "Project leader",
     "成员": "Member",
     "移出成员": "Members removed",
     "你确定要将此成员移出项目吗?": "Are you sure you want to do this project out of the members?",
@@ -456,4 +456,21 @@ export default {
     "所有会员都可以阅读分享地址。": "All members can read the Shared address.",
     "所有人(含游客)都可以阅读分享地址。": "Everyone (including visitors) can read the Shared address.",
     "是否放弃保存修改的内容?": "Do you want to abandon saving the modified content?",
+    "甘特图": "Gantt chart",
+    "现在": "Now",
+    "天视图": "Day",
+    "周视图": "Week",
+    "月视图": "Month",
+    "修改任务": "Modify task",
+    "删除任务": "Delete task",
+    "归档任务": "Archiving task",
+    "显示已完成": "Display completed",
+    "标记完成": "Mark completed",
+    "任务负责人": "Task leader",
+    "项目成员": "Project Members",
+    "项目设置": "Project Settings",
+    "面板显示": "Panel displays",
+    "项目面板显示已完成的任务。": "The project panel displays the completed tasks.",
+    "项目面板隐藏已完成的任务。": "The project panel hides completed tasks.",
+    "确定要修改任务【%】的计划时间吗?<br/>开始时间:%<br/>结束时间:%": "Are you sure you want to change the scheduled time for task [%]? <br/> start time: %<br/> end time: %",
 }

+ 2 - 0
resources/lang/en/general.php

@@ -129,4 +129,6 @@ return [
     "知识库仅对会员开放,请登录后再试!" => "Knowledge base is open to members only, please login and try again!",
     "知识库仅对成员开放!" => "Knowledge base is open to members only!",
     "知识库仅对作者开放!" => "Knowledge base for authors only!",
+    "操作权限不足!" => "Not permissions!",
+    "此操作仅限项目负责人!" => "This operation is only for the project leader!",
 ];