kuaifan 5 anos atrás
pai
commit
355b45e57c

+ 25 - 2
app/Http/Controllers/Api/ProjectController.php

@@ -887,10 +887,14 @@ class ProjectController extends Controller
      * - 未完成
      * - 已超期
      * - 已完成
+     * @apiParam {Number} [attention]           是否仅获取关注数据(1:是)
      * @apiParam {Number} [statistics]          是否获取统计数据(1:获取)
-     * @apiParam {Object} [sorts]               排序方式,格式:{key:'', order:''}("archived=已归档"时无效)
+     * @apiParam {Object} [sorts]               排序方式,格式:{key:'', order:''}
      * - key: title|labelid|enddate|username|level|indate|type
      * - order: asc|desc
+     * - 【archived=已归档】或【startdate和enddate赋值】时无效
+     * @apiParam {String} [startdate]           任务开始时间,格式:YYYY-MM-DD
+     * @apiParam {String} [enddate]             任务结束时间,格式:YYYY-MM-DD
      * @apiParam {Number} [idlater]             获取数据ID之后的数据
      * @apiParam {Number} [page]                当前页,默认:1
      * @apiParam {Number} [pagesize]            每页显示数量,默认:20,最大:100
@@ -933,6 +937,8 @@ class ProjectController extends Controller
             }
         }
         //
+        $whereRaw = null;
+        $whereFunc = null;
         $whereArray = [];
         $whereArray[] = ['project_task.delete', '=', 0];
         if ($projectid > 0) {
@@ -953,6 +959,10 @@ class ProjectController extends Controller
         if (intval(Request::input('idlater')) > 0) {
             $whereArray[] = ['project_task.id', '<', intval(Request::input('idlater'))];
         }
+        if (intval(Request::input('attention')) === 1) {
+            $whereRaw.= $whereRaw ? ' AND ' : '';
+            $whereRaw.= "`username` in (select username from `" . env('DB_PREFIX') . "project_users` where `type`='关注' AND `username`='" . $user['username'] . "')";
+        }
         $archived = trim(Request::input('archived'));
         if (empty($archived)) $archived = "未归档";
         switch ($archived) {
@@ -978,10 +988,22 @@ class ProjectController extends Controller
                 $whereArray[] = ['project_task.complete', '=', 1];
                 break;
         }
+        $startdate = trim(Request::input('startdate'));
+        $enddate = trim(Request::input('enddate'));
+        if (Base::isDate($startdate) || Base::isDate($enddate)) {
+            $startdate = strtotime($startdate . ' 00:00:00');
+            $enddate = strtotime($enddate . ' 23:59:59');
+            $whereRaw.= $whereRaw ? ' AND ' : '';
+            $whereRaw.= "((`startdate` >= " . $startdate . " OR `startdate` = 0) AND (`enddate` <= " . $enddate . " OR `enddate` = 0))";
+            $orderBy = '`startdate` DESC';
+        }
         //
         $builder = DB::table('project_task');
         if ($projectid > 0) {
-            $builder = $builder->join('project_lists', 'project_lists.id', '=', 'project_task.projectid');
+            $builder->join('project_lists', 'project_lists.id', '=', 'project_task.projectid');
+        }
+        if ($whereRaw) {
+            $builder->whereRaw($whereRaw);
         }
         $lists = $builder->select(['project_task.*'])
             ->where($whereArray)
@@ -1071,6 +1093,7 @@ class ProjectController extends Controller
             'level' => max(1, min(4, intval(Request::input('level')))),
             'inorder' => intval(DB::table('project_task')->where('projectid', $projectid)->orderByDesc('inorder')->value('inorder')) + 1,
             'indate' => Base::time(),
+            'startdate' => Base::time(),
             'subtask' => Base::array2string([]),
             'files' => Base::array2string([]),
             'follower' => Base::array2string([]),

+ 177 - 0
resources/assets/js/main/components/FullCalendar/FullCalendar.vue

@@ -0,0 +1,177 @@
+<template>
+    <div class="comp-full-calendar">
+        <!-- header pick month -->
+        <fc-header :current-date="currentDate"
+                   :title-format="titleFormat"
+                   :first-day="firstDay"
+                   :month-names="monthNames"
+                   :tableType="tableType"
+                   @change="changeDateRange">
+
+            <div slot="header-left">
+                <slot name="fc-header-left">
+                </slot>
+            </div>
+
+            <div slot="header-right" class="btn-group">
+                <div>
+                    <button :class="{cancel:tableType=='month',primary:tableType=='week'}" @click="changeType('week')">
+                        周
+                    </button>
+                    <button :class="{cancel:tableType=='week',primary:tableType=='month'}" @click="changeType('month')">
+                        月
+                    </button>
+                </Div>
+            </div>
+        </fc-header>
+        <!-- body display date day and events -->
+        <fc-body :current-date="currentDate" :events="events" :month-names="monthNames" ref="fcbody"
+                 :tableType="tableType"
+                 :loading="loading"
+                 :week-names="weekNames" :first-day="firstDay"
+                 :weekDays="weekDays"
+                 @eventclick="emitEventClick" @dayclick="emitDayClick"
+                 @moreclick="emitMoreClick">
+            <template slot="body-card">
+                <slot name="fc-body-card">
+                </slot>
+            </template>
+        </fc-body>
+    </div>
+</template>
+<script type="text/babel">
+    import langSets from './dataMap/langSets'
+    import fcBody from './components/body'
+    import fcHeader from './components/header'
+
+    export default {
+        name: "FullCalendar",
+        components: {
+            'fc-body': fcBody,
+            'fc-header': fcHeader
+        },
+        props: {
+            events: {
+                type: Array,
+                default: []
+            },
+            lang: {
+                type: String,
+                default: 'en'
+            },
+            firstDay: {
+                type: Number | String,
+                validator(val) {
+                    let res = parseInt(val)
+                    return res >= 0 && res <= 6
+                },
+                default: 0
+            },
+            titleFormat: {
+                type: String,
+                default() {
+                    return langSets[this.lang].titleFormat
+                }
+            },
+            monthNames: {
+                type: Array,
+                default() {
+                    return langSets[this.lang].monthNames
+                }
+            },
+            weekNames: {
+                type: Array,
+                default() {
+                    let arr = langSets[this.lang].weekNames
+                    return arr.slice(this.firstDay).concat(arr.slice(0, this.firstDay))
+                }
+            },
+            loading: {
+                type: Boolean,
+                default: false
+            },
+        },
+        data() {
+            return {
+                tableType: 'week',
+                weekDays: [],
+                currentDate: new Date()
+            }
+        },
+        created() {
+
+        },
+        methods: {
+            changeDateRange(start, end, currentStart, current, weekDays) {
+                this.currentDate = current
+                this.weekDays = weekDays;
+                this.$emit('change', start, end, currentStart, weekDays)
+            },
+            emitEventClick(event, jsEvent) {
+                this.$emit('eventClick', event, jsEvent)
+            },
+            emitDayClick(day, jsEvent) {
+                this.$emit('dayClick', day, jsEvent)
+            },
+            emitMoreClick(day, events, jsEvent) {
+                this.$emit('moreClick', day, event, jsEvent)
+            },
+            changeType(type) {
+                this.tableType = type;
+                this.$emit('changeType', type)
+            },
+        },
+    }
+
+</script>
+<style lang="scss">
+    .comp-full-calendar {
+        // font-family: "elvetica neue", tahoma, "hiragino sans gb";
+        // background: #fff;
+        min-width: 960px;
+        margin: 0 auto;
+
+        ul, p {
+            margin: 0;
+            padding: 0;
+            font-size: 14px;
+        }
+
+        .cancel {
+            border: 0;
+            outline: none;
+            box-shadow: unset;
+            background-color: #ECECED;
+            color: #8B8F94;
+
+            &:hover {
+                color: #3E444C;
+                z-index: 0;
+            }
+        }
+
+        .primary {
+            border: 0;
+            outline: none;
+            box-shadow: unset;
+            background-color: #5272FF;
+            color: #fff;
+
+            &:hover {
+                z-index: 0;
+            }
+        }
+
+        .btn-group {
+            width: 100%;
+            display: flex;
+            justify-content: flex-end;
+
+            button {
+                width: 80px;
+                cursor: pointer;
+                height: 26px;
+            }
+        }
+    }
+</style>

+ 789 - 0
resources/assets/js/main/components/FullCalendar/components/body.vue

@@ -0,0 +1,789 @@
+<template>
+    <div class="full-calendar-body">
+
+        <div class="right-body">
+            <div class="weeks">
+                <div class="blank" v-if="tableType=='week'" style="width: 60px"></div>
+                <strong class="week" v-for="(week,index) in weekNames">
+                    {{week}}
+                    <span v-if="tableType=='week' && weekDate.length">({{weekDate[index].showDate}})</span>
+                </strong>
+            </div>
+            <div class="dates" ref="dates" v-if="tableType=='month'">
+                <Spin v-if="loading" fix></Spin>
+                <!-- absolute so we can make dynamic td -->
+                <div class="dates-events">
+                    <div class="events-week" v-for="week in currentDates"
+                         v-if="week[0].isCurMonth || week[week.length-1].isCurMonth">
+                        <div class="events-day" v-for="day in week" track-by="$index"
+                             :class="{'today': day.isToday, 'not-cur-month': !day.isCurMonth}">
+                            <p class="day-number">{{day.monthDay}}</p>
+                            <div class="event-box" v-if="day.events.length">
+                                <div class="event-item"
+                                     :class="{selected: showCard == event.id}"
+                                     v-for="event in day.events"
+                                     :style="`background-color: ${showCard == event.id ? (event.selectedColor||'#C7E6FD') : (event.color||'#C7E6FD')}`"
+                                     @click="eventClick(event,$event)">
+                                    <img class="avatar" v-if="event.avatar" :src="event.avatar" @error="($set(event, 'avatar', null))"/>
+                                    <span v-else :class="`icon icon${event.id%4}`">{{event.name}}</span>
+                                    <p class="info">{{isBegin(event, day.date, day.weekDay)}}</p>
+                                    <div id="card" :class="cardClass" v-if="event &&showCard == event.id" @click.stop>
+                                        <slot name="body-card"></slot>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="time" ref="time" v-else-if="tableType=='week'">
+                <Spin v-if="loading" fix></Spin>
+                <!-- <div class="column" v-for="day in weekDate" v-if="weekDate.length">
+                  <div class="single" v-for="time in timeDivide"></div>
+                </div> -->
+                <div class="row" v-for="(time,index) in timeDivide">
+                    <div class="left-info" v-if="tableType=='week'">
+                        <div class="time-info first" v-if="index==0">
+                            <span class="center">上午</span>
+                        </div>
+                        <div class="time-info" v-if="index==1">
+                            <span class="top">12:00</span>
+                            <span class="center">下午</span>
+                        </div>
+                        <div class="time-info" v-if="index==2">
+                            <span class="top">18:00</span>
+                            <span class="center">晚上</span>
+                        </div>
+                    </div>
+                    <div class="events-day" v-for="item in weekDate" v-if="weekDate.length"
+                         :class="{today: item.isToday}">
+                        <div class="event-box" v-if="item.events.length">
+                            <div class="event-item" v-for="event in item.events"
+                                 :class="{selected: showCard == event.id}"
+                                 :style="`background-color: ${showCard == event.id ? (event.selectedColor||'#C7E6FD') : (event.color||'#C7E6FD')}`"
+                                 v-if="isTheday(item.date, event.start) && isInTime(time, event.start)"
+                                 @click="eventClick(event,$event)">
+                                <img class="avatar" v-if="event.avatar" :src="event.avatar" @error="($set(event, 'avatar', null))"/>
+                                <span v-else :class="`icon icon${event.id%4}`">{{event.name}}</span>
+                                <p class="info">{{event.title}}</p>
+                                <div id="card" :class="cardClass" v-if="event && showCard == event.id" @click.stop>
+                                    <slot name="body-card"></slot>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+<script type="text/babel">
+    import dateFunc from './dateFunc'
+    import bus from './bus'
+
+    export default {
+        props: {
+            currentDate: {},
+            events: {
+                type: Array
+            },
+            weekNames: {
+                type: Array,
+                default: []
+            },
+            monthNames: {},
+            firstDay: {},
+            tableType: '',
+            weekDays: {
+                type: Array,
+                default() {
+                    return dateFunc.getDates(new Date())
+                }
+            },
+            isLimit: {
+                type: Boolean,
+                default: false
+            },
+            loading: {
+                type: Boolean,
+                default: true
+            }
+        },
+        created() {
+            let _this = this
+            document.addEventListener('click', function (e) {
+                _this.showCard = -1
+            })
+            // 监听header组件事件
+            bus.$on('changeWeekDays', res => {
+            })
+        },
+        data() {
+            return {
+                // weekNames : DAY_NAMES,
+                weekMask: [1, 2, 3, 4, 5, 6, 7],
+                // events : [],
+                eventLimit: 18,
+                showMore: false,
+                morePos: {
+                    top: 0,
+                    left: 0
+                },
+                selectDay: {},
+                timeDivide: [{
+                    start: 0,
+                    end: 12,
+                    label: '上午'
+                }, {
+                    start: 12,
+                    end: 18,
+                    label: '下午'
+                }, {
+                    start: 18,
+                    end: 23,
+                    label: '晚上'
+                }],
+                showCard: -1,
+                cardLeft: 0,
+                cardRight: 0,
+            }
+        },
+
+        watch: {
+            // events(val){
+            //   this.getCalendar()
+            // },
+            currentDate() {
+                this.getCalendar()
+            }
+        },
+        computed: {
+            currentDates() {
+                return this.getCalendar()
+            },
+            weekDate() {
+                return this.getWeekDate()
+            }
+        },
+        methods: {
+            isBegin(event, date, index) {
+                let st = new Date(event.start)
+
+                if (index == 0 || st.toDateString() == date.toDateString()) {
+                    return event.title
+                }
+                return ' '
+            },
+            moreTitle(date) {
+                let dt = new Date(date)
+                return this.weekNames[dt.getDay()] + ', ' + this.monthNames[dt.getMonth()] + dt.getDate()
+            },
+            classNames(cssClass) {
+                if (!cssClass) return ''
+                // string
+                if (typeof cssClass == 'string') return cssClass
+
+                // Array
+                if (Array.isArray(cssClass)) return cssClass.join(' ')
+
+                // else
+                return ''
+            },
+            getCalendar() {
+                // calculate 2d-array of each month
+                // first day of this month
+                let now = new Date() // today
+                let current = new Date(this.currentDate)
+
+                let startDate = dateFunc.getStartDate(current) // 1st day of this month
+
+                let curWeekDay = startDate.getDay()
+
+                // begin date of this table may be some day of last month
+                let diff = parseInt(this.firstDay) - curWeekDay + 1
+                diff = diff > 0 ? (diff - 7) : diff
+
+                startDate.setDate(startDate.getDate() + diff)
+                let calendar = []
+                for (let perWeek = 0; perWeek < 6; perWeek++) {
+
+                    let week = []
+
+                    for (let perDay = 0; perDay < 7; perDay++) {
+                        // console.log(startDate)
+                        week.push({
+                            monthDay: startDate.getDate(),
+                            isToday: now.toDateString() == startDate.toDateString(),
+                            isCurMonth: startDate.getMonth() == current.getMonth(),
+                            weekDay: perDay,
+                            date: new Date(startDate),
+                            events: this.slotEvents(new Date(startDate))
+                        })
+                        startDate.setDate(startDate.getDate() + 1)
+                    }
+                    calendar.push(week)
+                }
+                return calendar
+            },
+            slotEvents(date) {
+                // console.log(date)
+                let thisDayEvents = []
+                this.events.filter(event => {
+                    let day = new Date(event.start)
+                    if (date.toLocaleDateString() === day.toLocaleDateString()) {
+                        thisDayEvents.push(event)
+                    }
+                })
+                this.judgeTime(thisDayEvents)
+                return thisDayEvents
+            },
+            // 获取周视图的天元素格式化
+            getWeekDate() {
+                let newWeekDays = this.weekDays
+                newWeekDays.forEach((e, index) => {
+                    e.showDate = dateFunc.format(e, 'MM-dd');
+                    e.date = dateFunc.format(e, 'yyyy-MM-dd');
+                    e.isToday = (new Date().toDateString() == e.toDateString())
+                    e.events = this.slotTimeEvents(e) // 整理事件集合 (拿事件去比较时间,分发事件到时间插槽内)
+                })
+                return newWeekDays
+            },
+            // 发现该时间段所有的事件
+            slotTimeEvents(date) {
+                let thisDayEvents = this.events
+                thisDayEvents.filter(event => {
+                    let day = new Date(event.start)
+                    return date.toLocaleDateString() == day.toLocaleDateString()
+                })
+                this.judgeTime(thisDayEvents)
+                return thisDayEvents
+            },
+            judgeTime(arr) {
+                arr.forEach(event => {
+                    let day = new Date(event.start)
+                    // 加上时间戳后判断时间段
+                    let hour = day.getHours()
+                    let week = day.getDay()
+                    week == 0 ? week = 7 : ''
+                    if (this.timeDivide[0].start < hour < this.timeDivide[0].end) {
+                        event.time = 0
+                    } else if (this.timeDivide[1].start < hour < this.timeDivide[1].end) {
+                        event.time = 1
+                    } else if (this.timeDivide[2].start < hour < this.timeDivide[2].end) {
+                        event.time = 2
+                    }
+                    event.weekDay = this.weekNames[Number(week) - 1]
+                    event.weekDayIndex = week
+                })
+            },
+            isTheday(day1, day2) {
+                return new Date(day1).toDateString() === new Date(day2).toDateString()
+            },
+            isStart(eventDate, date) {
+                let st = new Date(eventDate)
+                return st.toDateString() == date.toDateString()
+            },
+            isEnd(eventDate, date) {
+                let ed = new Date(eventDate)
+                return ed.toDateString() == date.toDateString()
+            },
+            isInTime(time, date) {
+                let hour = new Date(date).getHours()
+                return (time.start <= hour) && (hour < time.end)
+            },
+            selectThisDay(day, jsEvent) {
+                this.selectDay = day
+                this.showMore = true
+                this.morePos = this.computePos(event.target)
+                this.morePos.top -= 100
+                let events = day.events.filter(item => {
+                    return item.isShow == true
+                })
+                this.$emit('moreclick', day.date, events, jsEvent)
+            },
+            computePos(target) {
+                let eventRect = target.getBoundingClientRect()
+                let pageRect = this.$refs.dates.getBoundingClientRect()
+                return {
+                    left: eventRect.left - pageRect.left,
+                    top: eventRect.top + eventRect.height - pageRect.top
+                }
+            },
+            dayClick(day, jsEvent) {
+                // console.log('dayClick')
+                // this.$emit('dayclick', day, jsEvent)
+            },
+            eventClick(event, jsEvent) {
+                // console.log(event,jsEvent, 'evenvet')
+                this.showCard = event.id
+                jsEvent.stopPropagation()
+                // let pos = this.computePos(jsEvent.target)
+                this.$emit('eventclick', event, jsEvent)
+                let x = jsEvent.x
+                let y = jsEvent.y
+                // console.log(jsEvent)
+                // 判断出左右中三边界的取值范围进而分配class
+                if (x > 400 && x < window.innerWidth - 200) {
+                    this.cardClass = "center-card"
+                } else if (x <= 400) {
+                    this.cardClass = "left-card"
+                } else {
+                    this.cardClass = "right-card"
+                }
+                if (y > window.innerHeight - 300) {
+                    this.cardClass += ' ' + 'bottom-card'
+                }
+            },
+        }
+    }
+</script>
+<style lang="scss">
+    .full-calendar-body {
+        background-color: #fff;
+        display: flex;
+        margin-top: 12px;
+        border: 1px solid #D0D9FF;
+
+        .left-info {
+            width: 60px;
+
+            .time-info {
+                height: 100%;
+                position: relative;
+
+                &.first {
+                    border-top: 1px solid #EFF2FF;
+                }
+
+                &:nth-child(2) {
+                    border-top: 1px solid #EFF2FF;
+                    border-bottom: 1px solid #EFF2FF;
+                }
+
+                .center {
+                    position: absolute;
+                    top: 50%;
+                    left: 50%;
+                    transform: translate(-50%, -50%);
+                    width: 14px;
+                    font-size: 14px;
+                    word-wrap: break-word;
+                    letter-spacing: 10px;
+                }
+
+                .top {
+                    position: absolute;
+                    top: -8px;
+                    width: 100%;
+                    text-align: center;
+                }
+            }
+        }
+
+        .right-body {
+            flex: 1;
+            width: 100%;
+            position: relative;
+
+            .weeks {
+                display: flex;
+                border-bottom: 1px solid #FFCC36;
+
+                .week {
+                    flex: 1;
+                    text-align: center;
+                    height: 40px;
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                }
+            }
+
+            .dates {
+                position: relative;
+                .dates-events {
+                    z-index: 1;
+                    width: 100%;
+
+                    .events-week {
+                        display: flex;
+                        min-height: 180px;
+
+                        .events-day {
+                            text-overflow: ellipsis;
+                            flex: 1;
+                            width: 0;
+                            height: auto;
+                            padding: 4px;
+                            border-right: 1px solid #EFF2FF;
+                            border-bottom: 1px solid #EFF2FF;
+                            background-color: #fff;
+
+                            .day-number {
+                                text-align: left;
+                                padding: 4px 5px 4px 4px;
+                            }
+
+                            &.not-cur-month {
+                                .day-number {
+                                    color: #ECECED;
+                                }
+                            }
+
+                            &.today {
+                                background-color: #FFFCF3;
+                            }
+
+                            &:last-child {
+                                border-right: 0;
+                            }
+
+                            .event-box {
+                                max-height: 280px;
+                                overflow: auto;
+                                .event-item {
+                                    cursor: pointer;
+                                    font-size: 12px;
+                                    background-color: #C7E6FD;
+                                    margin-bottom: 2px;
+                                    color: rgba(0, 0, 0, .87);
+                                    padding: 8px 0 8px 4px;
+                                    height: auto;
+                                    line-height: 30px;
+                                    display: flex;
+                                    // transform:translate(0,0);
+                                    align-items: flex-start;
+                                    position: relative;
+                                    border-radius: 4px;
+
+                                    &.is-end {
+                                        display: none;
+                                    }
+
+                                    &.is-start {
+                                        display: block;
+                                    }
+
+                                    &.is-opacity {
+                                        display: none;
+                                    }
+
+                                    .avatar {
+                                        width: 18px;
+                                        height: 18px;
+                                        border: 0;
+                                        border-radius: 50%;
+                                        overflow: hidden;
+                                        display: inline-block;
+                                    }
+
+                                    .icon {
+                                        width: 18px;
+                                        height: 18px;
+                                        line-height: 18px;
+                                        border-radius: 10px;
+                                        text-align: center;
+                                        color: #fff;
+                                        display: inline-block;
+
+                                        &.icon0 {
+                                            background: #27BA9C;
+                                        }
+
+                                        &.icon1 {
+                                            background: #5272FF;
+                                        }
+
+                                        &.icon2 {
+                                            background: #FFCC36;
+                                        }
+
+                                        &.icon3 {
+                                            background: #FF7062;
+                                        }
+                                    }
+
+                                    .info {
+                                        width: calc(100% - 30px);
+                                        display: inline-block;
+                                        margin-left: 5px;
+                                        line-height: 18px;
+                                        word-break: break-all;
+                                        word-wrap: break-word;
+                                        font-size: 12px;
+                                    }
+
+                                    #card {
+                                        cursor: initial;
+                                        position: absolute;
+                                        z-index: 999;
+                                        min-width: 250px;
+                                        height: auto;
+                                        left: 50%;
+                                        top: calc(100% + 10px);
+                                        transform: translate(-50%, 0);
+                                        min-height: 100px;
+                                        background: #fff;
+                                        // border: 1px solid #eee;
+                                        box-shadow: 0px 2px 10px 0px rgba(0, 0, 0, 0.1);
+                                        border-radius: 4px;
+                                        overflow: hidden;
+
+                                        &.left-card {
+                                            left: 0;
+                                            transform: translate(0, 0);
+                                        }
+
+                                        &.right-card {
+                                            right: 0;
+                                            left: auto;
+                                            transform: translate(0, 0);
+                                        }
+
+                                        &.bottom-card {
+                                            top: auto;
+                                            bottom: calc(100% + 10px);
+                                        }
+                                    }
+
+                                    &:hover {
+                                        .info {
+                                            // font-weight: bold;
+                                        }
+                                    }
+
+                                    &.selected {
+                                        .info {
+                                            color: #fff;
+                                            font-weight: normal;
+                                        }
+
+                                        .icon {
+                                            background-color: transparent !important;
+                                        }
+                                    }
+                                }
+
+                                .more-link {
+                                    cursor: pointer;
+                                    // text-align: right;
+                                    padding-left: 8px;
+                                    padding-right: 2px;
+                                    color: rgba(0, 0, 0, .38);
+                                    font-size: 12px;
+                                }
+                            }
+                        }
+
+                        &:last-child {
+                            .events-day {
+                                border-bottom: 0;
+                            }
+                        }
+                    }
+                }
+
+                .more-events {
+                    position: absolute;
+                    width: 150px;
+                    z-index: 2;
+                    border: 1px solid #eee;
+                    box-shadow: 0 2px 6px rgba(0, 0, 0, .15);
+
+                    .more-header {
+                        background-color: #eee;
+                        padding: 5px;
+                        display: flex;
+                        align-items: center;
+                        font-size: 14px;
+
+                        .title {
+                            flex: 1;
+                        }
+
+                        .close {
+                            margin-right: 2px;
+                            cursor: pointer;
+                            font-size: 16px;
+                        }
+                    }
+
+                    .more-body {
+                        height: 125px;
+                        overflow: hidden;
+                        background: #fff;
+
+                        .body-list {
+                            height: 120px;
+                            padding: 5px;
+                            overflow: auto;
+                            background-color: #fff;
+
+                            .body-item {
+                                cursor: pointer;
+                                font-size: 12px;
+                                background-color: #C7E6FD;
+                                margin-bottom: 2px;
+                                color: rgba(0, 0, 0, .87);
+                                padding: 0 0 0 4px;
+                                height: 18px;
+                                line-height: 18px;
+                                white-space: nowrap;
+                                overflow: hidden;
+                                text-overflow: ellipsis;
+                            }
+                        }
+                    }
+                }
+            }
+
+            .time {
+                position: relative;
+                .row {
+                    width: 100%;
+                    display: flex;
+                    min-height: 180px;
+
+                    .events-day {
+                        border-bottom: 1px solid #EFF2FF;
+                        border-left: 1px solid #EFF2FF;
+                        background-color: #fff;
+                        height: auto;
+                        text-overflow: ellipsis;
+                        flex: 1;
+                        width: 0;
+                        padding: 4px;
+
+                        .event-box {
+                            max-height: 280px;
+                            overflow: auto;
+                        }
+
+                        &.today {
+                            background-color: #FFFCF3;
+                        }
+                    }
+
+                    .event-item {
+                        cursor: pointer;
+                        font-size: 12px;
+                        background-color: #C7E6FD;
+                        margin-bottom: 2px;
+                        color: rgba(0, 0, 0, .87);
+                        padding: 8px 0 8px 4px;
+                        height: auto;
+                        line-height: 30px;
+                        display: flex;
+                        align-items: flex-start;
+                        position: relative;
+                        border-radius: 4px;
+
+                        .avatar {
+                            width: 18px;
+                            height: 18px;
+                            border: 0;
+                            border-radius: 50%;
+                            overflow: hidden;
+                            display: inline-block;
+                        }
+
+                        .icon {
+                            width: 18px;
+                            height: 18px;
+                            line-height: 18px;
+                            border-radius: 10px;
+                            text-align: center;
+                            color: #fff;
+                            display: inline-block;
+                            padding: 0 2px;
+                            overflow: hidden;
+
+                            &.icon0 {
+                                background: #27BA9C;
+                            }
+
+                            &.icon1 {
+                                background: #5272FF;
+                            }
+
+                            &.icon2 {
+                                background: #FFCC36;
+                            }
+
+                            &.icon3 {
+                                background: #FF7062;
+                            }
+                        }
+
+                        .info {
+
+                            width: calc(100% - 30px);
+                            display: inline-block;
+                            margin-left: 5px;
+                            line-height: 18px;
+                            word-break: break-all;
+                            word-wrap: break-word;
+                            font-size: 12px;
+                        }
+
+                        #card {
+                            cursor: initial;
+                            position: absolute;
+                            z-index: 999;
+                            min-width: 250px;
+                            height: auto;
+                            left: 50%;
+                            top: calc(100% + 10px);
+                            transform: translate(-50%, 0);
+                            min-height: 100px;
+                            background: #fff;
+                            box-shadow: 0px 2px 10px 0px rgba(0, 0, 0, 0.1);
+                            border-radius: 4px;
+                            overflow: hidden;
+
+                            &.left-card {
+                                left: 0;
+                                transform: translate(0, 0);
+                            }
+
+                            &.right-card {
+                                right: 0;
+                                left: auto;
+                                transform: translate(0, 0);
+                            }
+
+                            &.bottom-card {
+                                top: auto;
+                                bottom: calc(100% + 10px);
+                            }
+                        }
+
+                        &:hover {
+                            .info {
+                                // font-weight: bold;
+                            }
+                        }
+
+                        &.selected {
+                            .info {
+                                color: #fff;
+                                font-weight: normal;
+                            }
+
+                            // background-color: #5272FF !important;
+                            .icon {
+                                background-color: transparent !important;
+                            }
+                        }
+                    }
+
+                    &:last-child {
+                        .single {
+                            border-bottom: 0;
+                        }
+                    }
+                }
+            }
+        }
+    }
+</style>

+ 3 - 0
resources/assets/js/main/components/FullCalendar/components/bus.js

@@ -0,0 +1,3 @@
+import Vue from 'vue'
+
+export default new Vue()

+ 106 - 0
resources/assets/js/main/components/FullCalendar/components/dateFunc.js

@@ -0,0 +1,106 @@
+let shortMonth = [
+    'Jan',
+    'Feb',
+    'Mar',
+    'Apr',
+    'May',
+    'Jun',
+    'Jul',
+    'Aug',
+    'Sep',
+    'Oct',
+    'Nov',
+    'Dec'
+]
+let defMonthNames = [
+    'January',
+    'February',
+    'March',
+    'April',
+    'May',
+    'June',
+    'July',
+    'August',
+    'September',
+    'October',
+    'November',
+    'December'
+]
+
+let dateFunc = {
+    getDuration(date) {
+        // how many days of this month
+        let dt = new Date(date)
+        let month = dt.getMonth();
+        dt.setMonth(dt.getMonth() + 1)
+        dt.setDate(0);
+        return dt.getDate()
+    },
+    changeDay(date, num) {
+        let dt = new Date(date)
+        return new Date(dt.setDate(dt.getDate() + num))
+    },
+    getStartDate(date) {
+        // return first day of this month
+        // console.log(new Date(date.getFullYear(), date.getMonth(), 1,0,0))
+        return new Date(date.getFullYear(), date.getMonth(), 1)
+    },
+    getEndDate(date) {
+        // get last day of this month
+        let dt = new Date(date.getFullYear(), date.getMonth() + 1, 1, 0, 0) // 1st day of next month
+        return new Date(dt.setDate(dt.getDate() - 1)) // last day of this month
+    },
+// 获取当前周日期数组
+    getDates(date) {
+        let new_Date = date
+        let timesStamp = new Date(new_Date.getFullYear(), new_Date.getMonth(), new_Date.getDate(), 0, 0, 0).getTime()
+        // let timesStamp = new_Date.getTime();
+        let currenDay = new_Date.getDay();
+        let dates = [];
+        for (let i = 0; i < 8; i++) {
+            dates.push(new Date(timesStamp + 24 * 60 * 60 * 1000 * (i - (currenDay + 6) % 7)));
+        }
+        return dates
+    },
+    format(date, format, monthNames) {
+        monthNames = monthNames || defMonthNames
+        if (typeof date === 'string') {
+            date = new Date(date.replace(/-/g, '/'))
+        } else {
+            date = new Date(date)
+        }
+
+        let map = {
+            'M': date.getMonth() + 1,
+            'd': date.getDate(),
+            'h': date.getHours(),
+            'm': date.getMinutes(),
+            's': date.getSeconds(),
+            'q': Math.floor((date.getMonth() + 3) / 3),
+            'S': date.getMilliseconds()
+        }
+
+        format = format.replace(/([yMdhmsqS])+/g, (all, t) => {
+            let v = map[t]
+            if (v !== undefined) {
+                if (all === 'MMMM') {
+                    return monthNames[v - 1]
+                }
+                if (all === 'MMM') {
+                    return shortMonth[v - 1]
+                }
+                if (all.length > 1) {
+                    v = '0' + v
+                    v = v.substr(v.length - 2)
+                }
+                return v
+            } else if (t === 'y') {
+                return String(date.getFullYear()).substr(4 - all.length)
+            }
+            return all
+        })
+        return format
+    }
+}
+
+module.exports = dateFunc

+ 130 - 0
resources/assets/js/main/components/FullCalendar/components/header.vue

@@ -0,0 +1,130 @@
+<template>
+    <div class="full-calendar-header">
+        <div class="header-left">
+            <slot name="header-left"></slot>
+        </div>
+        <div class="header-center">
+            <span class="prev-month" @click.stop="goPrev">{{leftArrow}}</span>
+            <span class="title">{{title}}</span>
+            <span class="next-month" @click.stop="goNext">{{rightArrow}}</span>
+        </div>
+        <div class="header-right">
+            <slot name="header-right"></slot>
+        </div>
+    </div>
+</template>
+<script type="text/babel">
+    import dateFunc from './dateFunc'
+    import bus from './bus'
+
+    export default {
+        created() {
+            this.dispatchEvent(this.tableType)
+        },
+        props: {
+            currentDate: {},
+            titleFormat: {},
+            firstDay: {},
+            monthNames: {},
+            tableType: ''
+        },
+        data() {
+            return {
+                title: '',
+                leftArrow: '<',
+                rightArrow: '>',
+                headDate: new Date()
+            }
+        },
+        watch: {
+            currentDate(val) {
+                if (!val) return
+                this.headDate = val
+            },
+            tableType(val) {
+                this.dispatchEvent(this.tableType)
+            }
+        },
+        methods: {
+            goPrev() {
+                this.headDate = this.changeDateRange(this.headDate, -1)
+                this.dispatchEvent(this.tableType)
+            },
+            goNext() {
+                this.headDate = this.changeDateRange(this.headDate, 1)
+                this.dispatchEvent(this.tableType)
+            },
+            changeDateRange(date, num) {
+                let dt = new Date(date)
+                if (this.tableType == 'month') {
+                    return new Date(dt.setMonth(dt.getMonth() + num))
+                } else {
+                    return new Date(dt.valueOf() + num * 7 * 24 * 60 * 60 * 1000)
+                }
+            },
+            dispatchEvent(type) {
+                if (type == 'month') {
+                    this.title = dateFunc.format(this.headDate, this.titleFormat, this.monthNames)
+                    let startDate = dateFunc.getStartDate(this.headDate)
+                    let curWeekDay = startDate.getDay()
+                    // 1st day of this monthView
+                    let diff = parseInt(this.firstDay) - curWeekDay
+                    if (diff) diff -= 7
+                    startDate.setDate(startDate.getDate() + diff)
+
+                    // the month view is 6*7
+                    let endDate = dateFunc.changeDay(startDate, 41)
+
+                    // 1st day of current month
+                    let currentDate = dateFunc.getStartDate(this.headDate)
+                    this.$emit('change',
+                        dateFunc.format(startDate, 'yyyy-MM-dd'),
+                        dateFunc.format(endDate, 'yyyy-MM-dd'),
+                        dateFunc.format(currentDate, 'yyyy-MM-dd'),
+                        this.headDate
+                    )
+                } else if (type == 'week') {
+                    let weekDays = dateFunc.getDates(this.headDate)
+
+                    this.title = dateFunc.format(weekDays[0], 'MM-dd') + `  至  ` + dateFunc.format(weekDays[6], 'MM-dd')
+                    this.$emit('change',
+                        dateFunc.format(weekDays[0], 'yyyy-MM-dd'),
+                        dateFunc.format(weekDays[6], 'yyyy-MM-dd'),
+                        dateFunc.format(weekDays[0], 'yyyy-MM-dd'),
+                        this.headDate,
+                        weekDays
+                    )
+                    bus.$emit('changeWeekDays', weekDays)
+                }
+            },
+
+        }
+    }
+</script>
+<style lang="scss">
+    .full-calendar-header {
+        display: flex;
+        align-items: center;
+
+        .header-left, .header-right {
+            flex: 1;
+        }
+
+        .header-center {
+            flex: 3;
+            text-align: center;
+            user-select: none;
+            font-weight: bold;
+
+            .title {
+                margin: 0 5px;
+                width: 150px;
+            }
+
+            .prev-month, .next-month {
+                cursor: pointer;
+                padding: 10px 15px;
+            }
+        }
+    }
+</style>

+ 17 - 0
resources/assets/js/main/components/FullCalendar/dataMap/langSets.js

@@ -0,0 +1,17 @@
+export default {
+    en: {
+        weekNames: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+        monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+        titleFormat: 'MMMM yyyy'
+    },
+    zh: {
+        weekNames: ['周一', '周二', '周三', '周四', '周五', '周六', '周日',],
+        monthNames: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
+        titleFormat: 'yyyy年MM月'
+    },
+    fr: {
+        weekNames: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
+        monthNames: ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'],
+        titleFormat: 'MMMM yyyy'
+    }
+}

+ 5 - 0
resources/assets/js/main/components/FullCalendar/index.js

@@ -0,0 +1,5 @@
+import vueFullcalendar from './fullCalendar'
+
+const fc = vueFullcalendar
+
+module.exports = fc

+ 124 - 0
resources/assets/js/main/components/project/todo/attention.vue

@@ -0,0 +1,124 @@
+<template>
+    <drawer-tabs-container>
+        <div class="project-complete">
+            <!-- 列表 -->
+            <Table class="tableFill" ref="tableRef" :columns="columns" :data="lists" :loading="loadIng > 0" :no-data-text="noDataText" stripe></Table>
+            <!-- 分页 -->
+            <Page 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" show-elevator show-sizer show-total transfer></Page>
+        </div>
+    </drawer-tabs-container>
+</template>
+
+<style lang="scss" scoped>
+    .project-complete {
+        .tableFill {
+            margin: 12px 12px 20px;
+        }
+    }
+</style>
+<script>
+    import DrawerTabsContainer from "../../DrawerTabsContainer";
+    export default {
+        name: 'TodoAttention',
+        components: {DrawerTabsContainer},
+        props: {
+            canload: {
+                type: Boolean,
+                default: true
+            },
+        },
+        data () {
+            return {
+                loadYet: false,
+
+                loadIng: 0,
+
+                columns: [],
+
+                lists: [],
+                listPage: 1,
+                listTotal: 0,
+                noDataText: "数据加载中.....",
+            }
+        },
+        created() {
+            this.columns = [{
+                "title": "任务名称",
+                "key": 'title',
+                "minWidth": 100,
+            }, {
+                "title": "创建人",
+                "key": 'createuser',
+                "minWidth": 80,
+            }, {
+                "title": "负责人",
+                "key": 'username',
+                "minWidth": 80,
+            }, {
+                "title": "完成时间",
+                "width": 160,
+                render: (h, params) => {
+                    return h('span', $A.formatDate("Y-m-d H:i:s", params.row.complete));
+                }
+            }];
+        },
+        mounted() {
+            if (this.canload) {
+                this.loadYet = true;
+                this.getLists(true);
+            }
+        },
+
+        watch: {
+            canload(val) {
+                if (val && !this.loadYet) {
+                    this.loadYet = true;
+                    this.getLists(true);
+                }
+            }
+        },
+
+        methods: {
+            setPage(page) {
+                this.listPage = page;
+                this.getLists();
+            },
+
+            setPageSize(size) {
+                if (Math.max($A.runNum(this.listPageSize), 10) != size) {
+                    this.listPageSize = size;
+                    this.getLists();
+                }
+            },
+
+            getLists(resetLoad) {
+                if (resetLoad === true) {
+                    this.listPage = 1;
+                }
+                this.loadIng++;
+                $A.aAjax({
+                    url: 'project/task/lists',
+                    data: {
+                        attention: 1,
+                        archived: '全部',
+                        page: Math.max(this.listPage, 1),
+                        pagesize: Math.max($A.runNum(this.listPageSize), 10),
+                    },
+                    complete: () => {
+                        this.loadIng--;
+                    },
+                    success: (res) => {
+                        if (res.ret === 1) {
+                            this.lists = res.data.lists;
+                            this.listTotal = res.data.total;
+                        } else {
+                            this.lists = [];
+                            this.listTotal = 0;
+                            this.noDataText = res.msg;
+                        }
+                    }
+                });
+            },
+        }
+    }
+</script>

+ 106 - 0
resources/assets/js/main/components/project/todo/calendar.vue

@@ -0,0 +1,106 @@
+<template>
+    <drawer-tabs-container>
+        <div class="todo-calendar">
+            <FullCalendar :events="lists" :loading="loadIng>0" @eventClick="clickEvent" lang="zh" @change="changeDateRange"></FullCalendar>
+        </div>
+    </drawer-tabs-container>
+</template>
+
+<style lang="scss" scoped>
+    .todo-calendar {
+    }
+</style>
+<script>
+    import DrawerTabsContainer from "../../DrawerTabsContainer";
+    import FullCalendar from "../../FullCalendar/FullCalendar";
+    export default {
+        name: 'TodoCalendar',
+        components: {FullCalendar, DrawerTabsContainer},
+        props: {
+
+        },
+        data () {
+            return {
+                loadIng: 0,
+
+                lists: [],
+
+                startdate: '',
+                enddate: '',
+            }
+        },
+        created() {
+
+        },
+        mounted() {
+
+        },
+
+        methods: {
+            clickEvent(event, jsEvent){
+                console.log(event, jsEvent)
+            },
+
+            changeDateRange(startdate, enddate){
+                this.startdate = startdate;
+                this.enddate = enddate;
+                this.getLists(1);
+            },
+
+            getLists(page) {
+                this.lists = [];
+                this.loadIng++;
+                $A.aAjax({
+                    url: 'project/task/lists',
+                    data: {
+                        startdate: this.startdate,
+                        enddate: this.enddate,
+                        page: page,
+                        pagesize: 100
+                    },
+                    complete: () => {
+                        this.loadIng--;
+                    },
+                    success: (res) => {
+                        if (res.ret === 1) {
+                            let inLists,
+                                data;
+                            res.data.lists.forEach((temp) => {
+                                let title = temp.title;
+                                let startdate = temp.startdate || temp.indate;
+                                let enddate = temp.enddate || temp.indate;
+                                if (startdate != enddate) {
+                                    title = $A.formatDate('H:i', startdate) + "~" + $A.formatDate('H:i', enddate) + " " + title;
+                                } else {
+                                    title = $A.formatDate('H:i', startdate) + " " + title;
+                                }
+                                data = {
+                                    "id": temp.id,
+                                    "start": $A.formatDate('Y-m-d H:i:s', startdate),
+                                    "end": $A.formatDate('Y-m-d H:i:s', enddate),
+                                    "title": title,
+                                    "color": "#D8F8F2",
+                                    "avatar": temp.userimg,
+                                    "name": temp.nickname || temp.username
+                                };
+                                inLists = false;
+                                this.lists.some((item, i) => {
+                                    if (item.id == data.id) {
+                                        this.lists.splice(i, 1, data);
+                                        return inLists = true;
+                                    }
+                                });
+                                if (!inLists) {
+                                    this.lists.push(data);
+                                }
+                            });
+                            if (res.data.hasMorePages === true) {
+                                this.getLists(res.data.currentPage + 1)
+                            }
+                        }
+                    }
+                });
+            },
+        }
+    }
+</script>

+ 123 - 0
resources/assets/js/main/components/project/todo/complete.vue

@@ -0,0 +1,123 @@
+<template>
+    <drawer-tabs-container>
+        <div class="project-complete">
+            <!-- 列表 -->
+            <Table class="tableFill" ref="tableRef" :columns="columns" :data="lists" :loading="loadIng > 0" :no-data-text="noDataText" stripe></Table>
+            <!-- 分页 -->
+            <Page 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" show-elevator show-sizer show-total transfer></Page>
+        </div>
+    </drawer-tabs-container>
+</template>
+
+<style lang="scss" scoped>
+    .project-complete {
+        .tableFill {
+            margin: 12px 12px 20px;
+        }
+    }
+</style>
+<script>
+    import DrawerTabsContainer from "../../DrawerTabsContainer";
+    export default {
+        name: 'TodoComplete',
+        components: {DrawerTabsContainer},
+        props: {
+            canload: {
+                type: Boolean,
+                default: true
+            },
+        },
+        data () {
+            return {
+                loadYet: false,
+
+                loadIng: 0,
+
+                columns: [],
+
+                lists: [],
+                listPage: 1,
+                listTotal: 0,
+                noDataText: "数据加载中.....",
+            }
+        },
+        created() {
+            this.columns = [{
+                "title": "任务名称",
+                "key": 'title',
+                "minWidth": 100,
+            }, {
+                "title": "创建人",
+                "key": 'createuser',
+                "minWidth": 80,
+            }, {
+                "title": "负责人",
+                "key": 'username',
+                "minWidth": 80,
+            }, {
+                "title": "完成时间",
+                "width": 160,
+                render: (h, params) => {
+                    return h('span', $A.formatDate("Y-m-d H:i:s", params.row.complete));
+                }
+            }];
+        },
+        mounted() {
+            if (this.canload) {
+                this.loadYet = true;
+                this.getLists(true);
+            }
+        },
+
+        watch: {
+            canload(val) {
+                if (val && !this.loadYet) {
+                    this.loadYet = true;
+                    this.getLists(true);
+                }
+            }
+        },
+
+        methods: {
+            setPage(page) {
+                this.listPage = page;
+                this.getLists();
+            },
+
+            setPageSize(size) {
+                if (Math.max($A.runNum(this.listPageSize), 10) != size) {
+                    this.listPageSize = size;
+                    this.getLists();
+                }
+            },
+
+            getLists(resetLoad) {
+                if (resetLoad === true) {
+                    this.listPage = 1;
+                }
+                this.loadIng++;
+                $A.aAjax({
+                    url: 'project/task/lists',
+                    data: {
+                        type: '已完成',
+                        page: Math.max(this.listPage, 1),
+                        pagesize: Math.max($A.runNum(this.listPageSize), 10),
+                    },
+                    complete: () => {
+                        this.loadIng--;
+                    },
+                    success: (res) => {
+                        if (res.ret === 1) {
+                            this.lists = res.data.lists;
+                            this.listTotal = res.data.total;
+                        } else {
+                            this.lists = [];
+                            this.listTotal = 0;
+                            this.noDataText = res.msg;
+                        }
+                    }
+                });
+            },
+        }
+    }
+</script>

+ 36 - 5
resources/assets/js/main/pages/todo.vue

@@ -12,10 +12,10 @@
                 </div>
                 <div class="w-nav-flex"></div>
                 <div class="w-nav-right">
-                    <span class="ft hover"><i class="ft icon">&#xE706;</i> {{$L('待办日程')}}</span>
-                    <span class="ft hover"><i class="ft icon">&#xE73D;</i> {{$L('已完成的任务')}}</span>
-                    <span class="ft hover"><i class="ft icon">&#xE748;</i> {{$L('我关注的任务')}}</span>
-                    <span class="ft hover"><i class="ft icon">&#xE743;</i> {{$L('周报/日报')}}</span>
+                    <span class="ft hover" @click="handleTodo('calendar')"><i class="ft icon">&#xE706;</i> {{$L('待办日程')}}</span>
+                    <span class="ft hover" @click="handleTodo('complete')"><i class="ft icon">&#xE73D;</i> {{$L('已完成的任务')}}</span>
+                    <span class="ft hover" @click="handleTodo('attention')"><i class="ft icon">&#xE748;</i> {{$L('我关注的任务')}}</span>
+                    <span class="ft hover" @click="handleTodo('report')"><i class="ft icon">&#xE743;</i> {{$L('周报/日报')}}</span>
                 </div>
             </div>
         </div>
@@ -58,6 +58,19 @@
             </div>
         </w-content>
 
+        <Drawer v-model="todoDrawerShow" width="75%">
+            <Tabs v-if="todoDrawerShow" v-model="todoDrawerTab">
+                <TabPane :label="$L('待办日程')" name="calendar">
+                    <todo-calendar :canload="todoDrawerShow && todoDrawerTab == 'calendar'"></todo-calendar>
+                </TabPane>
+                <TabPane :label="$L('已完成的任务')" name="complete">
+                    <todo-complete :canload="todoDrawerShow && todoDrawerTab == 'complete'"></todo-complete>
+                </TabPane>
+                <TabPane :label="$L('我关注的任务')" name="attention">
+                    <todo-attention :canload="todoDrawerShow && todoDrawerTab == 'attention'"></todo-attention>
+                </TabPane>
+            </Tabs>
+        </Drawer>
     </div>
 </template>
 
@@ -275,8 +288,11 @@
     import WHeader from "../components/WHeader";
     import WContent from "../components/WContent";
     import WLoading from "../components/WLoading";
+    import TodoCalendar from "../components/project/todo/calendar";
+    import TodoComplete from "../components/project/todo/complete";
+    import TodoAttention from "../components/project/todo/attention";
     export default {
-        components: {WContent, WHeader, WLoading},
+        components: {TodoAttention, TodoComplete, TodoCalendar, WContent, WHeader, WLoading},
         data () {
             return {
                 userInfo: {},
@@ -287,6 +303,9 @@
                     "3": {lists: [], hasMorePages: false},
                     "4": {lists: [], hasMorePages: false},
                 },
+
+                todoDrawerShow: false,
+                todoDrawerTab: 'calendar',
             }
         },
         mounted() {
@@ -411,6 +430,18 @@
                         });
                     }
                 });
+            },
+
+            handleTodo(event) {
+                switch (event) {
+                    case 'calendar':
+                    case 'complete':
+                    case 'attention': {
+                        this.todoDrawerShow = true;
+                        this.todoDrawerTab = event;
+                        break;
+                    }
+                }
             }
         },
     }