kuaifan пре 5 година
родитељ
комит
97f603f442

+ 31 - 1
app/Http/Controllers/Api/ProjectController.php

@@ -1437,6 +1437,7 @@ class ProjectController extends Controller
      * - delete: 删除任务
      * - comment: 评论
      * - attention: 添加关注
+     * - subtask: 修改子任务
      * @apiParam {String} [content]         内容数据
      *
      * @throws \Throwable
@@ -1500,7 +1501,7 @@ class ProjectController extends Controller
             }
         }
         //
-        $content = trim(Request::input('content'));
+        $content = Base::newTrim(Request::input('content'));
         $message = "";
         $upArray = [];
         $logArray = [];
@@ -1913,6 +1914,35 @@ class ProjectController extends Controller
                 break;
             }
 
+            /**
+             * 修改子任务
+             */
+            case 'subtask': {
+                if (!is_array($content)) {
+                    return Base::retError('参数错误!');
+                }
+                $content = Base::array2string($content);
+                if ($content == $task['subtask']) {
+                    return Base::retError('子任务未做改变!');
+                }
+                $upArray['subtask'] = $content;
+                $logArray[] = [
+                    'type' => '日志',
+                    'projectid' => $task['projectid'],
+                    'taskid' => $task['id'],
+                    'username' => $user['username'],
+                    'detail' => '修改子任务',
+                    'indate' => Base::time(),
+                    'other' => Base::array2string([
+                        'type' => 'task',
+                        'id' => $task['id'],
+                        'subtask' => $content,
+                        'old_subtask' => $task['subtask'],
+                    ])
+                ];
+                break;
+            }
+
             default: {
                 return Base::retError('参数错误!');
                 break;

+ 175 - 8
resources/assets/js/main/components/project/task/detail/detail.vue

@@ -44,9 +44,48 @@
                         <em v-else class="unfinished">{{$L('未完成')}}</em>
                     </li>
                 </ul>
+                <div class="detail-h2 detail-subtask-icon detail-icon">
+                    <strong class="active">{{$L('子任务')}}</strong>
+                    <Button class="detail-button" size="small" @click="handleTask('subtaskAdd')">{{$L('添加子任务')}}</Button>
+                </div>
+                <div class="detail-subtask-box">
+                    <div v-if="detail.subtask.length == 0" class="detail-subtask-none" @click="handleTask('subtaskAdd')">{{$L('暂无子任务')}}</div>
+                    <div v-else>
+                        <Progress class="detail-subtask-progress" :percent="subtaskProgress" :stroke-width="5" status="active" />
+                        <div v-for="(subitem, subindex) in detail.subtask" :key="subindex" class="detail-subtask-item">
+                            <Checkbox v-model="subitem.status"
+                                      true-value="complete"
+                                      false-value="unfinished"
+                                      @on-change="handleTask('subtaskBlur')"></Checkbox>
+                            <input v-model="subitem.detail"
+                                   :ref="`subtaskInput_${subindex}`"
+                                   :placeholder="$L('子任务描述...')"
+                                   class="detail-subtask-input"
+                                   :class="{'subtask-complete':subitem.status=='complete'}"
+                                   @keydown.enter="handleTask('subtaskEnter', subindex)"
+                                   @blur="handleTask('subtaskBlur')"/>
+                            <div v-if="subitem.detail==''" class="detail-subtask-delete">
+                                <Icon type="md-trash" @click="handleTask('subtaskDelete', subindex)"/>
+                            </div>
+                            <Poptip
+                                v-else
+                                class="detail-subtask-delete"
+                                transfer
+                                confirm
+                                :title="$L('你确定你要删除这个子任务吗?')"
+                                :style="subitem.showPoptip===true?{opacity:1}:{}"
+                                @on-ok="handleTask('subtaskDelete', subindex)"
+                                @on-popper-show="$set(subitem, 'showPoptip', true)"
+                                @on-popper-hide="$set(subitem, 'showPoptip', false)"><Icon type="md-trash" /></Poptip>
+                        </div>
+                    </div>
+                </div>
                 <div :style="`${detail.filenum>0?'':'display:none'}`">
-                    <div class="detail-h2 detail-file-box detail-icon"><strong class="active">{{$L('附件')}}</strong></div>
-                    <project-task-files ref="upload" :taskid="taskid" :simple="true" @change="handleTask('filechange', $event)"></project-task-files>
+                    <div class="detail-h2 detail-file-box detail-icon">
+                        <strong class="active">{{$L('附件')}}</strong>
+                        <Button class="detail-button" size="small" @click="handleTask('fileupload')">{{$L('添加附件')}}</Button>
+                    </div>
+                    <project-task-files ref="upload" :taskid="taskid" :projectid="detail.projectid" :simple="true" @change="handleTask('filechange', $event)"></project-task-files>
                 </div>
                 <div class="detail-h2 detail-comment-box detail-icon"><strong class="link" :class="{active:logType=='评论'}" @click="logType='评论'">{{$L('评论')}}</strong><em></em><strong class="link" :class="{active:logType=='日志'}" @click="logType='日志'">{{$L('操作记录')}}</strong></div>
                 <div class="detail-log-box">
@@ -124,6 +163,8 @@
 <script>
     import ProjectTaskLogs from "../logs";
     import ProjectTaskFiles from "../files";
+    import cloneDeep from "lodash/cloneDeep";
+
     export default {
         components: {ProjectTaskFiles, ProjectTaskLogs},
         data() {
@@ -225,7 +266,7 @@
                     this.visible = true;
                 }, 0)
             });
-            this.bakData = $A.cloneData(this.detail);
+            this.bakData = cloneDeep(this.detail);
             this.myUsername = $A.getUserName();
             this.getTaskDetail();
             //
@@ -244,10 +285,20 @@
         },
         watch: {
             taskid() {
-                this.bakData = $A.cloneData(this.detail);
+                this.bakData = cloneDeep(this.detail);
                 this.getTaskDetail();
             }
         },
+        computed: {
+            subtaskProgress() {
+                const countLists = this.detail.subtask;
+                if (countLists.length === 0) {
+                    return 0;
+                }
+                const completeLists = countLists.filter((item) => { return item.status == 'complete'});
+                return parseFloat(((completeLists.length / countLists.length) * 100).toFixed(2));
+            }
+        },
         methods: {
             levelFormt(p) {
                 switch (parseInt(p)) {
@@ -322,7 +373,7 @@
                     success: (res) => {
                         if (res.ret === 1) {
                             this.detail = res.data;
-                            this.bakData = $A.cloneData(this.detail);
+                            this.bakData = cloneDeep(this.detail);
                         } else {
                             this.$Modal.error({
                                 title: this.$L('温馨提示'),
@@ -354,7 +405,7 @@
                         if (this.detail[act] == this.bakData[act]) {
                             return;
                         }
-                        if (!this.detail[act]) {
+                        if (act == 'title' && !this.detail[act]) {
                             this.$set(this.detail, act, this.bakData[act]);
                             return;
                         }
@@ -366,6 +417,55 @@
                         };
                         break;
 
+                    case 'subtaskAdd':
+                        if (!$A.isArray(this.detail.subtask)) {
+                            this.detail.subtask = [];
+                        }
+                        this.detail.subtask.push({
+                            status: 'unfinished',
+                            detail: '',
+                        });
+                        this.$nextTick(() => {
+                            this.$refs['subtaskInput_' + (this.detail.subtask.length  - 1)][0].focus();
+                        });
+                        return;
+
+                    case 'subtaskDelete':
+                        this.detail.subtask.splice(eve, 1);
+                        this.handleTask('subtaskBlur');
+                        return;
+
+                    case 'subtaskEnter':
+                        if (!$A.isArray(this.detail.subtask)) {
+                            this.detail.subtask = [];
+                        }
+                        if (eve + 1 >= this.detail.subtask.length) {
+                            this.handleTask('subtaskAdd');
+                            return;
+                        }
+                        this.handleTask('subtaskBlur')
+                        return;
+
+                    case 'subtaskBlur':
+                        this.handleTask('subtask');
+                        return;
+
+                    case 'subtask':
+                        let newArray = cloneDeep(this.detail[act]);
+                        while (newArray.length > 0 && newArray[newArray.length - 1].detail == '') {
+                            newArray.splice(newArray.length - 1, 1);
+                        }
+                        if ($A.jsonStringify(newArray) === $A.jsonStringify(this.bakData[act])) {
+                            return;
+                        }
+                        ajaxData.content = newArray;
+                        ajaxCallback = (res) => {
+                            if (res !== 1) {
+                                this.$set(this.detail, act, this.bakData[act]);
+                            }
+                        };
+                        break;
+
                     case 'fileupload':
                         this.$refs.upload.uploadHandleClick();
                         return;
@@ -486,7 +586,7 @@
                     case 'unattention':
                         ajaxData.content = eve.username;
                         if (eve.uisynch === true) {
-                            let bakFollower = $A.cloneData(this.detail.follower);
+                            let bakFollower = cloneDeep(this.detail.follower);
                             this.$set(this.detail, 'follower', this.detail.follower.filter((uname) => { return uname != eve }));
                             ajaxCallback = (res) => {
                                 if (res !== 1) {
@@ -555,7 +655,7 @@
                         runTime = Math.round(new Date().getTime()) - runTime;
                         if (res.ret === 1) {
                             this.detail = res.data;
-                            this.bakData = $A.cloneData(this.detail);
+                            this.bakData = cloneDeep(this.detail);
                             $A.triggerTaskInfoListener(ajaxData.act, res.data);
                             $A.triggerTaskInfoChange(ajaxData.taskid);
                             setTimeout(() =>  {
@@ -649,6 +749,14 @@
                         height: 10px;
                         background: #cccccc;
                     }
+                    .detail-button {
+                        position: absolute;
+                        right: 12px;
+                        top: 50%;
+                        font-size: 12px;
+                        transform: translate(0, -50%);
+                        opacity: 0.8;
+                    }
                 }
                 .detail-icon {
                     position: relative;
@@ -806,6 +914,65 @@
                         padding-left: 2px;
                     }
                 }
+                .detail-subtask-icon {
+                    &:before {
+                        content: "\E819";
+                        font-size: 16px;
+                        padding-left: 2px;
+                    }
+                }
+                .detail-subtask-box {
+                    padding: 12px;
+                    .detail-subtask-progress {
+                        margin: 2px 0 6px;
+                    }
+                    .detail-subtask-item {
+                        display: flex;
+                        flex-direction: row;
+                        align-items: center;
+                        margin: 4px 2px;
+                        position: relative;
+                        &:hover {
+                            .detail-subtask-delete {
+                                opacity: 1;
+                            }
+                        }
+                        .detail-subtask-input {
+                            flex: 1;
+                            border: 0;
+                            background: #ffffff;
+                            margin-left: 2px;
+                            height: 30px;
+                            line-height: 30px;
+                            cursor: pointer;
+                            color: #172b4d;
+                            outline: none;
+                            border-bottom: 1px solid #f6f6f6;
+                            &.subtask-complete {
+                                text-decoration: line-through;
+                                color: #999;
+                            }
+                        }
+                        .detail-subtask-delete {
+                            opacity: 0;
+                            position: absolute;
+                            top: 50%;
+                            right: 0;
+                            transform: translate(0, -50%);
+                            width: 26px;
+                            height: 24px;
+                            font-size: 16px;
+                            line-height: 24px;
+                            text-align: center;
+                            cursor: pointer;
+                            background: #ffffff;
+                        }
+                    }
+                    .detail-subtask-none {
+                        color: #666666;
+                        cursor: pointer;
+                    }
+                }
                 .detail-comment-box {
                     &:before {
                         content: "\E753";

+ 27 - 2
resources/assets/js/main/pages/project/panel.vue

@@ -64,6 +64,7 @@
                                         task.overdue ? 'overdue' : '',
                                         task.isNewtask === true ? 'newtask' : ''
                                     ]" @click="openTaskModal(task)">
+                                    <div v-if="task.subtask.length > 0" class="subtask-progress"><em :style="{width: subtaskProgress(task.subtask) + '%'}"></em></div>
                                     <div class="task-title">{{task.title}}</div>
                                     <div class="task-more">
                                         <div v-if="task.overdue" class="task-status">{{$L('已超期')}}</div>
@@ -221,10 +222,10 @@
                             width: 100%;
                             .task-shadow {
                                 margin: 5px 0 4px;
-                                padding: 8px;
+                                padding: 8px 10px 8px 8px;
                                 background-color: #ffffff;
                                 border-left: 2px solid #BF9F03;
-                                border-right: 2px solid #ffffff;
+                                border-right: 0;
                                 color: #091e42;
                                 border-radius: 3px;
                                 cursor: pointer;
@@ -296,6 +297,22 @@
                                         }
                                     }
                                 }
+                                .subtask-progress {
+                                    position: absolute;
+                                    top: 0;
+                                    left: 0;
+                                    width: 100%;
+                                    height: 100%;
+                                    z-index: -1;
+                                    border-radius: 0 3px 3px 0;
+                                    overflow: hidden;
+                                    pointer-events: none;
+                                    em {
+                                        display: block;
+                                        height: 100%;
+                                        background-color: rgba(3, 150, 242, 0.07);
+                                    }
+                                }
                             }
                         }
                     }
@@ -808,6 +825,14 @@
                 });
             },
 
+            subtaskProgress(subtask) {
+                if (subtask.length === 0) {
+                    return 0;
+                }
+                const completeLists = subtask.filter((item) => { return item.status == 'complete'});
+                return parseFloat(((completeLists.length / subtask.length) * 100).toFixed(2));
+            },
+
             openTaskModal(taskDetail) {
                 this.taskDetail(taskDetail);
             },

+ 24 - 0
resources/assets/js/main/pages/todo.vue

@@ -52,6 +52,7 @@
                                     @sort="taskSortUpdate"
                                     @remove="taskSortUpdate">
                                     <div v-for="task in taskDatas[index].lists" class="content-li task-draggable" :key="task.id" :class="{complete:task.complete}" @click="openTaskModal(task)">
+                                        <div v-if="task.subtask.length > 0" class="subtask-progress"><em :style="{width: subtaskProgress(task.subtask) + '%'}"></em></div>
                                         <Icon v-if="task.complete" class="task-check" type="md-checkbox-outline" @click.stop="taskComplete(task, false)"/>
                                         <Icon v-else class="task-check" type="md-square-outline" @click.stop="taskComplete(task, true)"/>
                                         <div v-if="!!task.loadIng" class="task-loading"><w-loading></w-loading></div>
@@ -269,6 +270,21 @@
                                             color: #000000;
                                         }
                                     }
+                                    .subtask-progress {
+                                        position: absolute;
+                                        top: 0;
+                                        left: 0;
+                                        width: 100%;
+                                        height: 100%;
+                                        z-index: -1;
+                                        overflow: hidden;
+                                        pointer-events: none;
+                                        em {
+                                            display: block;
+                                            height: 100%;
+                                            background-color: rgba(3, 150, 242, 0.07);
+                                        }
+                                    }
                                 }
                             }
                             .content-loading {
@@ -701,6 +717,14 @@
                 });
             },
 
+            subtaskProgress(subtask) {
+                if (subtask.length === 0) {
+                    return 0;
+                }
+                const completeLists = subtask.filter((item) => { return item.status == 'complete'});
+                return parseFloat(((completeLists.length / subtask.length) * 100).toFixed(2));
+            },
+
             openTaskModal(taskDetail) {
                 this.taskDetail(taskDetail);
             },