Bladeren bron

no message

kuaifan 5 jaren geleden
bovenliggende
commit
d079d76f36

+ 448 - 0
app/Http/Controllers/Api/ProjectController.php

@@ -0,0 +1,448 @@
+<?php
+
+namespace App\Http\Controllers\Api;
+
+use App\Http\Controllers\Controller;
+use App\Module\Base;
+use App\Module\Users;
+use DB;
+use Request;
+use Session;
+
+/**
+ * @apiDefine project
+ *
+ * 项目
+ */
+class ProjectController extends Controller
+{
+    public function __invoke($method, $action = '')
+    {
+        $app = $method ? $method : 'main';
+        if ($action) {
+            $app .= "__" . $action;
+        }
+        return (method_exists($this, $app)) ? $this->$app() : Base::ajaxError("404 not found (" . str_replace("__", "/", $app) . ").");
+    }
+
+    /**
+     * 项目列表
+     */
+    public function lists()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $lists = DB::table('project_lists')
+            ->join('project_users', 'project_lists.id', '=', 'project_users.projectid')
+            ->select(['project_lists.*', 'project_users.isowner'])
+            ->where([
+                ['project_lists.delete', 0],
+                ['project_users.type', '成员'],
+                ['project_users.username', $user['username']]
+            ])
+            ->orderByDesc('project_lists.id')->paginate(Min(Max(Base::nullShow(Request::input('pagesize'), 10), 1), 200));
+        $lists = Base::getPageList($lists);
+        if ($lists['total'] == 0) {
+            return Base::retError('未找到任何相关的项目');
+        }
+        return Base::retSuccess('success', $lists);
+    }
+
+    /**
+     * 添加项目
+     */
+    public function add()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //项目名称
+        $title = trim(Request::input('title'));
+        if (mb_strlen($title) < 2) {
+            return Base::retError('项目名称不可以少于2个字!');
+        } elseif (mb_strlen($title) > 32) {
+            return Base::retError('项目名称最多只能设置32个字!');
+        }
+        //流程
+        $labels = Request::input('labels');
+        if (!is_array($labels)) $labels = [];
+        $insertLabels = [];
+        $inorder = 0;
+        foreach ($labels AS $label) {
+            $label = trim($label);
+            if ($label) {
+                $insertLabels[] = [
+                    'title' => $label,
+                    'inorder' => $inorder++,
+                ];
+            }
+        }
+        if (empty($insertLabels)) {
+            $insertLabels[] = [
+                'title' => '默认',
+                'inorder' => 0,
+            ];
+        }
+        //开始创建
+        $projectid = DB::table('project_lists')->insertGetId([
+            'title' => $title,
+            'username' => $user['username'],
+            'createuser' => $user['username'],
+            'indate' => Base::time()
+        ]);
+        if ($projectid) {
+            DB::table('project_label')->insert($insertLabels);
+            DB::table('project_log')->insert([
+                'type' => '日志',
+                'projectid' => $projectid,
+                'username' => $user['username'],
+                'detail' => '创建项目',
+                'indate' => Base::time()
+            ]);
+            DB::table('project_users')->insert([
+                'type' => '成员',
+                'projectid' => $projectid,
+                'isowner' => 1,
+                'username' => $user['username'],
+                'indate' => Base::time()
+            ]);
+            return Base::retSuccess('添加成功!');
+        } else {
+            return Base::retError('添加失败!');
+        }
+    }
+
+    /**
+     * 收藏项目
+     * @throws \Throwable
+     */
+    public function favor()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $projectid = trim(Request::input('projectid'));
+        $item = Base::DBC2A(DB::table('project_lists')->where('id', $projectid)->first());
+        if (empty($item)) {
+            return Base::retError('项目不存在或已被删除!');
+        }
+        return DB::transaction(function () use ($item, $user) {
+            $row = Base::DBC2A(DB::table('project_users')->where([
+                'type' => '收藏',
+                'projectid' => $item['id'],
+                'username' => $user['username'],
+            ])->lockForUpdate()->first());
+            if (empty($row)) {
+                DB::table('project_users')->insert([
+                    'type' => '收藏',
+                    'projectid' => $item['id'],
+                    'isowner' => $item['username'] == $user['username'] ? 1 : 0,
+                    'username' => $user['username'],
+                    'indate' => Base::time()
+                ]);
+                DB::table('project_log')->insert([
+                    'type' => '日志',
+                    'projectid' => $item['id'],
+                    'username' => $user['username'],
+                    'detail' => '收藏项目',
+                    'indate' => Base::time()
+                ]);
+                return Base::retSuccess('收藏成功');
+            }
+            return Base::retSuccess('已收藏');
+        });
+    }
+
+    /**
+     * 重命名项目
+     */
+    public function rename()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $projectid = trim(Request::input('projectid'));
+        $item = Base::DBC2A(DB::table('project_lists')->where('id', $projectid)->first());
+        if (empty($item)) {
+            return Base::retError('项目不存在或已被删除!');
+        }
+        if ($item['username'] != $user['username']) {
+            return Base::retError('你不是项目负责人!');
+        }
+        //
+        $title = trim(Request::input('title'));
+        if (mb_strlen($title) < 2) {
+            return Base::retError('项目名称不可以少于2个字!');
+        } elseif (mb_strlen($title) > 32) {
+            return Base::retError('项目名称最多只能设置32个字!');
+        }
+        //
+        DB::table('project_lists')->where('id', $item['id'])->update([
+            'title' => $title
+        ]);
+        DB::table('project_log')->insert([
+            'type' => '日志',
+            'projectid' => $item['id'],
+            'username' => $user['username'],
+            'detail' => '【' . $item['title'] . '】重命名【' . $title . '】',
+            'indate' => Base::time()
+        ]);
+        //
+        return Base::retSuccess('修改成功');
+    }
+
+    /**
+     * 移交项目
+     * @throws \Throwable
+     */
+    public function transfer()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $projectid = trim(Request::input('projectid'));
+        $item = Base::DBC2A(DB::table('project_lists')->where('id', $projectid)->first());
+        if (empty($item)) {
+            return Base::retError('项目不存在或已被删除!');
+        }
+        if ($item['username'] != $user['username']) {
+            return Base::retError('你不是项目负责人!');
+        }
+        //
+        $username = trim(Request::input('username'));
+        if ($username == $item['username']) {
+            return Base::retError('你已是项目负责人!');
+        }
+        $count = DB::table('users')->where('username', $username)->count();
+        if ($count <= 0) {
+            return Base::retError('成员用户名(' . $username . ')不存在!');
+        }
+        //判断是否已在项目
+        $count = DB::table('project_users')->where([
+            'type' => '成员',
+            'projectid' => $item['id'],
+            'username' => $username,
+        ])->count();
+        if ($count <= 0) {
+            DB::table('project_log')->insert([
+                'type' => '日志',
+                'projectid' => $item['id'],
+                'username' => $username,
+                'detail' => '加入项目',
+                'indate' => Base::time()
+            ]);
+            DB::table('project_users')->insert([
+                'type' => '成员',
+                'projectid' => $item['id'],
+                'isowner' => 0,
+                'username' => $username,
+                'indate' => Base::time()
+            ]);
+        }
+        //开始移交
+        return DB::transaction(function () use ($user, $username, $item) {
+            DB::table('project_lists')->where('id', $item['id'])->update([
+                'username' => $username
+            ]);
+            DB::table('project_log')->insert([
+                'type' => '日志',
+                'projectid' => $item['id'],
+                'username' => $user['username'],
+                'detail' => '【' . $item['username'] . '】移交给【' . $username . '】',
+                'indate' => Base::time()
+            ]);
+            DB::table('project_users')->where([
+                'projectid' => $item['id'],
+                'username' => $item['username'],
+            ])->update([
+                'isowner' => 0
+            ]);
+            DB::table('project_users')->where([
+                'projectid' => $item['id'],
+                'username' => $username,
+            ])->update([
+                'isowner' => 1
+            ]);
+            return Base::retSuccess('移交成功');
+        });
+    }
+
+    /**
+     * 删除项目
+     */
+    public function delete()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $projectid = trim(Request::input('projectid'));
+        $item = Base::DBC2A(DB::table('project_lists')->where('id', $projectid)->first());
+        if (empty($item)) {
+            return Base::retError('项目不存在或已被删除!');
+        }
+        if ($item['username'] != $user['username']) {
+            return Base::retError('你不是项目负责人!');
+        }
+        //
+        DB::table('project_lists')->where('id', $item['id'])->update([
+            'delete' => 1,
+            'deletedate' => Base::time()
+        ]);
+        DB::table('project_log')->insert([
+            'type' => '日志',
+            'projectid' => $item['id'],
+            'username' => $user['username'],
+            'detail' => '删除项目',
+            'indate' => Base::time()
+        ]);
+        //
+        return Base::retSuccess('删除成功');
+    }
+
+    /**
+     * 退出项目
+     */
+    public function out()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $projectid = trim(Request::input('projectid'));
+        $item = Base::DBC2A(DB::table('project_lists')->where('id', $projectid)->first());
+        if (empty($item)) {
+            return Base::retError('项目不存在或已被删除!');
+        }
+        if ($item['username'] == $user['username']) {
+            return Base::retError('你是项目负责人,不可退出项目!');
+        }
+        $count = DB::table('project_users')->where([
+            'type' => '成员',
+            'projectid' => $item['id'],
+            'username' => $user['username'],
+        ])->count();
+        if ($count <= 0) {
+            return Base::retError('你不在项目成员内!');
+        }
+        //
+        DB::table('project_users')->where([
+            'type' => '成员',
+            'projectid' => $item['id'],
+            'username' => $user['username'],
+        ])->delete();
+        DB::table('project_log')->insert([
+            'type' => '日志',
+            'projectid' => $item['id'],
+            'username' => $user['username'],
+            'detail' => '退出项目',
+            'indate' => Base::time()
+        ]);
+        //
+        return Base::retSuccess('退出项目成功');
+    }
+
+    /**
+     * 项目成员
+     */
+    public function users()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $projectid = intval(Request::input('projectid'));
+        $count = DB::table('project_users')->where([
+            'type' => '成员',
+            'projectid' => $projectid,
+            'username' => $user['username'],
+        ])->count();
+        if ($count <= 0) {
+            return Base::retError('你不在项目成员内!');
+        }
+        //
+        $lists = DB::table('project_lists')
+            ->join('project_users', 'project_lists.id', '=', 'project_users.projectid')
+            ->select(['project_lists.title', 'project_users.*'])
+            ->where([
+                ['project_lists.id', $projectid],
+                ['project_lists.delete', 0],
+                ['project_users.type', '成员'],
+            ])
+            ->orderByDesc('project_lists.id')->paginate(Min(Max(Base::nullShow(Request::input('pagesize'), 10), 1), 200));
+        $lists = Base::getPageList($lists);
+        if ($lists['total'] == 0) {
+            return Base::retError('未找到任何相关的成员');
+        }
+        foreach ($lists['lists'] AS $key => $item) {
+            $userInfo = Users::username2basic($item['username']);
+            $lists['lists'][$key]['userimg'] = $userInfo['userimg'];
+            $lists['lists'][$key]['nickname'] = $userInfo['nickname'];
+            $lists['lists'][$key]['profession'] = $userInfo['profession'];
+        }
+        return Base::retSuccess('success', $lists);
+    }
+
+    /**
+     * 任务-列表
+     */
+    public function task__lists()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $whereArray = [];
+        $whereArray[] = ['project_lists.delete', '=', 0];
+        if (Request::input('projectid') > 0) $whereArray[] = ['project_lists.id', '=', intval(Request::input('projectid'))];
+        if (in_array(intval(Request::input('archived')), [0, 1])) $whereArray[] = ['project_task.archived', '=', Request::input('archived')];
+        //
+        $orderBy = 'project_task.id';
+        if (intval(Request::input('archived')) === 1) {
+            $orderBy = 'project_task.archiveddate';
+        }
+        //
+        $lists = DB::table('project_lists')
+            ->join('project_task', 'project_lists.id', '=', 'project_task.projectid')
+            ->select(['project_task.*'])
+            ->where($whereArray)
+            ->orderByDesc($orderBy)->paginate(Min(Max(Base::nullShow(Request::input('pagesize'), 10), 1), 200));
+        $lists = Base::getPageList($lists);
+        if ($lists['total'] == 0) {
+            return Base::retError('未找到任何相关的任务');
+        }
+        return Base::retSuccess('success', $lists);
+    }
+}

+ 30 - 0
app/Http/Controllers/Api/UsersController.php

@@ -81,6 +81,36 @@ class UsersController extends Controller
     }
 
     /**
+     * 搜索会员列表
+     */
+    public function searchinfo()
+    {
+        $keys = Request::input('where');
+        $whereArr = [];
+        $whereFunc = null;
+        if ($keys['usernameequal'])     $whereArr[] = ['username', '=', $keys['usernameequal']];
+        if ($keys['identity'])          $whereArr[] = ['identity', 'like', '%,' . $keys['identity'] . ',%'];
+        if ($keys['noidentity'])        $whereArr[] = ['identity', 'not like', '%,' . $keys['noidentity'] . ',%'];
+        if ($keys['username']) {
+            $whereFunc = function($query) use ($keys) {
+                $query->where('username', 'like', '%' . $keys['username'] . '%')->orWhere('nickname', 'like', '%' . $keys['username'] . '%');
+            };
+        }
+        //
+        $lists = DB::table('users')->select(['id', 'username', 'nickname', 'userimg', 'profession'])->where($whereArr)->where($whereFunc)->orderBy('id')->paginate(Min(Max(Base::nullShow(Request::input('pagesize'), 10), 1), 200));
+        $lists = Base::getPageList($lists);
+        if ($lists['total'] == 0) {
+            return Base::retError('未搜索到任何相关的会员');
+        }
+        foreach ($lists['lists'] AS $key => $item) {
+            $lists['lists'][$key]['userimg'] = Base::fillUrl($item['userimg']);
+            $lists['lists'][$key]['identitys'] = explode(",", trim($item['identity'], ","));
+            $lists['lists'][$key]['setting'] = Base::string2array($item['setting']);
+        }
+        return Base::retSuccess('success', $lists);
+    }
+
+    /**
      * 修改资料
      * @return array|mixed
      */

+ 18 - 0
app/Module/Users.php

@@ -196,6 +196,24 @@ class Users
     }
 
     /**
+     * username 获取 基本信息
+     * @param string $username           用户名
+     * @return array
+     */
+    public static function username2basic($username)
+    {
+        if (empty($username)) {
+            return [];
+        }
+        $fields = ['username', 'nickname', 'userimg', 'profession'];
+        $userInfo = DBCache::table('users')->where('username', $username)->select($fields)->cacheMinutes(1)->first();
+        if ($userInfo) {
+            $userInfo['userimg'] = Users::userimg($userInfo['userimg']);
+        }
+        return $userInfo ?: [];
+    }
+
+    /**
      * 用户头像,不存在时返回默认
      * @param string|int $var 头像地址 或 会员ID
      * @return \Illuminate\Contracts\Routing\UrlGenerator|string

+ 2 - 0
resources/assets/js/main/app.js

@@ -13,7 +13,9 @@ Vue.use(ViewUI);
 Vue.use(Language);
 
 import Title from '../_components/Title.vue'
+import UseridInput from './components/UseridInput'
 Vue.component('VTitle', Title);
+Vue.component('UseridInput', UseridInput);
 
 const router = new VueRouter({routes});
 

+ 360 - 0
resources/assets/js/main/components/UseridInput.vue

@@ -0,0 +1,360 @@
+<template>
+    <div v-clickoutside="handleClose">
+        <div class="user-id-input" ref="reference">
+            <Input v-model="nickName" :placeholder="placeholder" :disabled="disabled" icon="md-search" @on-click="searchEnter" @on-enter="searchEnter" @on-blur="searchEnter(true)">
+                <div v-if="$slots.prepend !== undefined" slot="prepend"><slot name="prepend"></slot></div>
+                <div v-if="$slots.append !== undefined" slot="append"><slot name="append"></slot></div>
+            </Input>
+            <div v-if="userName" class="user-id-subtitle">用户名: {{userName}}</div>
+            <div v-if="spinShow" class="user-id-spin"><div><w-loading></w-loading></div></div>
+        </div>
+
+        <transition name="fade">
+            <div
+                    v-show="!disabled && visible"
+                    ref="popper"
+                    class="user-id-input-body"
+                    :data-transfer="transfer"
+                    v-transfer-dom>
+                <Table highlight-row
+                       v-if="searchShow"
+                       size="small"
+                       class="user-id-input-table"
+                       :style="tableStyle"
+                       :columns="columns"
+                       :data="userLists"
+                       @on-current-change="userChange"
+                       :no-data-text="nodatatext"></Table>
+            </div>
+        </transition>
+    </div>
+</template>
+<style lang="scss" scoped>
+    .user-id-input {
+        display: inline-block;
+        width: 100%;
+        position: relative;
+        vertical-align: middle;
+        z-index: 5;
+
+        .user-id-subtitle {
+            position: absolute;
+            top: 2px;
+            right: 32px;
+            height: 30px;
+            line-height: 30px;
+            color: #cccccc;
+            z-index: 2;
+        }
+
+        .user-id-spin {
+            position: absolute;
+            top: 0;
+            bottom: 0;
+            left: 0;
+            right: 0;
+            border-radius: 4px;
+            background-color: rgba(255, 255, 255, 0.26);
+            > div {
+                width: 18px;
+                height: 18px;
+                position: absolute;
+                top: 50%;
+                left: 6px;
+                transform: translate(0, -50%);
+            }
+        }
+    }
+</style>
+<style lang="scss">
+    .user-id-input-body {
+        z-index: 99999
+    }
+    .user-id-input-table {
+        border-radius: 4px;
+        overflow: hidden;
+        box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.1);
+
+        .ivu-table table {
+            width: 100% !important;
+        }
+
+        .ivu-table:before, .ivu-table:after {
+            display: none !important;
+        }
+
+        .ivu-table-body {
+            max-height: 180px;
+            overflow: auto;
+        }
+
+        .ivu-table-small td {
+            cursor: pointer;
+        }
+    }
+</style>
+<script>
+
+    import clickoutside from './directives/clickoutside';
+    import TransferDom from './directives/transfer-dom';
+    import Popper from './directives/popper-novalue';
+    import WLoading from "./WLoading";
+
+    export default {
+        name: 'UseridInput',
+        components: {WLoading},
+        directives: {clickoutside, TransferDom},
+        mixins: [Popper],
+        props: {
+            placement: {
+                default: 'bottom'
+            },
+            value: {
+                default: ''
+            },
+            identity: {
+                default: ''
+            },
+            noidentity: {
+                default: ''
+            },
+            placeholder: {
+                default: ''
+            },
+            disabled: {
+                type: Boolean,
+                default: false
+            },
+            transfer: {
+                type: Boolean,
+                default () {
+                    return true;
+                }
+            },
+            loadstatus: {
+                default: false
+            }
+        },
+        data () {
+            return {
+                userName: '',
+                nickName: '',
+                nickName__: '',
+                seleName: '',
+                searchShow: false,
+                spinShow: false,
+                skipSearch: false,
+
+                winStyle: {},
+
+                columns: [
+                    {
+                        "title": "用户名",
+                        "key": "username",
+                        "minWidth": 80,
+                        "ellipsis": true,
+                        "tooltip": true,
+                    }, {
+                        "title": "昵称",
+                        "key": "nickname",
+                        "minWidth": 80,
+                        "ellipsis": true,
+                        "tooltip": true,
+                        render: (h, params) => {
+                            return h('span', params.row.nickname || '-');
+                        }
+                    },
+                ],
+                userLists: [],
+                nodatatext: "数据加载中.....",
+            }
+        },
+        watch: {
+            value (val) {
+                this.userName = $A.clone(val)
+            },
+
+            userName (val) {
+                if (this.skipSearch === true) {
+                    this.skipSearch = false;
+                }else{
+                    this.nickName = '';
+                    if (val) {
+                        let where = { usernameequal: val };
+                        if (typeof this.identity === "string") {
+                            where['identity'] = this.identity;
+                        }
+                        if (typeof this.noidentity === "string") {
+                            where['noidentity'] = this.noidentity;
+                        }
+                        $A.aAjax({
+                            url: window.location.origin + '/api/users/searchinfo',
+                            data: {
+                                where: where,
+                                pagesize: 1
+                            },
+                            beforeSend: () => {
+                                this.spinShow = true;
+                            },
+                            complete: () => {
+                                this.spinShow = false;
+                            },
+                            success: (res) => {
+                                if (res.ret === 1 && res.data.total > 0) {
+                                    let tmpData = res.data.lists[0];
+                                    this.userName = tmpData.username;
+                                    this.seleName = tmpData.nickname || tmpData.username;
+                                    this.nickName = tmpData.nickname || tmpData.username;
+                                    this.nickName__ = tmpData.nickname || tmpData.username;
+                                    this.$emit('input', this.userName);
+                                    this.$emit('change', tmpData);
+                                }
+                            }
+                        });
+                    }
+                }
+            },
+
+            nickName(val) {
+                if (val != this.seleName || val == '') {
+                    this.userName = '';
+                    this.$emit('input', this.userName);
+                    this.$emit('change', {});
+                }
+            },
+
+            spinShow(val) {
+                if (typeof this.loadstatus === 'number') {
+                    this.$emit('update:loadstatus', val ? this.loadstatus + 1 : this.loadstatus - 1);
+                }else if (typeof this.loadstatus === 'boolean') {
+                    this.$emit('update:loadstatus', val);
+                }
+            },
+
+            searchShow(val) {
+                if (val) {
+                    this.handleShowPopper();
+                } else {
+                    this.handleClosePopper();
+                }
+            }
+        },
+        computed: {
+            tableStyle() {
+                return this.winStyle;
+            }
+        },
+        methods: {
+            handleShowPopper() {
+                if (this.timeout) clearTimeout(this.timeout);
+                this.timeout = setTimeout(() => {
+                    this.visible = true;
+                }, this.delay);
+            },
+
+            handleClosePopper() {
+                if (this.timeout) {
+                    clearTimeout(this.timeout);
+                    if (!this.controlled) {
+                        this.timeout = setTimeout(() => {
+                            this.visible = false;
+                        }, 100);
+                    }
+                }
+            },
+
+            updateStyle() {
+                this.winStyle = {
+                    width: `${Math.max(this.$el.offsetWidth, 230)}px`,
+                };
+            },
+
+            emptyAll() {
+                this.userName = '';
+                this.nickName = '';
+                this.nickName__ = '';
+                this.seleName = '';
+                this.searchShow = false;
+                this.spinShow = false;
+            },
+
+            searchEnter(verify) {
+                if (this.disabled === true) {
+                    return;
+                }
+                if (this.spinShow === true) {
+                    return;
+                }
+                if (verify === true) {
+                    if (this.nickName === '') {
+                        this.nickName__ = this.nickName;
+                    }
+                    if (this.nickName__ === this.nickName) {
+                        return;
+                    }
+                }
+                this.updateStyle();
+                this.nickName__ = this.nickName;
+                //
+                let where = {username: this.nickName};
+                if (typeof this.identity === "string") {
+                    where['identity'] = this.identity;
+                }
+                if (typeof this.noidentity === "string") {
+                    where['noidentity'] = this.noidentity;
+                }
+                $A.aAjax({
+                    url: window.location.origin + '/api/users/searchinfo',
+                    data: {
+                        where: where,
+                        pagesize: 30
+                    },
+                    beforeSend: () => {
+                        this.spinShow = true;
+                    },
+                    complete: () => {
+                        this.spinShow = false;
+                    },
+                    success: (res) => {
+                        if (res.ret === 1) {
+                            this.userLists = res.data.lists;
+                            for (let i = 0; i < this.userLists.length; i++) {
+                                if (this.userLists[i].id == this.userName) {
+                                    this.userLists[i]['_highlight'] = true;
+                                }
+                            }
+                            this.searchShow = true;
+                        } else {
+                            this.$Message.warning(res.msg);
+                            this.emptyAll();
+                        }
+                    }
+                });
+            },
+
+            userChange(item) {
+                this.userName = item.username;
+                this.seleName = item.nickname || item.username;
+                this.nickName = item.nickname || item.username;
+                this.nickName__ = item.nickname || item.username;
+                this.skipSearch = true;
+                this.searchShow = false;
+                this.$emit('input', this.userName);
+                this.$emit('change', item);
+            },
+
+            handleClose() {
+                if (this.searchShow === true) {
+                    this.searchShow = false;
+                }
+            }
+        },
+        mounted() {
+            this.updatePopper();
+            //
+            if ($A.runNum(this.value) > 0) {
+                this.userName = this.value;
+            }
+        }
+    };
+</script>

+ 9 - 9
resources/assets/js/main/components/WHeader.vue

@@ -187,20 +187,20 @@
         created() {
             this.ruleDatum = {
                 nickname: [
-                    { required: true, message: this.$L('请输入昵称'), trigger: 'blur' },
-                    { type: 'string', min: 2, message: this.$L('昵称长度至少2位'), trigger: 'blur' }
+                    { required: true, message: this.$L('请输入昵称!'), trigger: 'change' },
+                    { type: 'string', min: 2, message: this.$L('昵称长度至少2位!'), trigger: 'change' }
                 ]
             };
             this.rulePass = {
                 oldpass: [
-                    { required: true, message: this.$L('请输入旧密码'), trigger: 'blur' },
-                    { type: 'string', min: 6, message: this.$L('密码长度至少6位'), trigger: 'blur' }
+                    { required: true, message: this.$L('请输入旧密码!'), trigger: 'change' },
+                    { type: 'string', min: 6, message: this.$L('密码长度至少6位!'), trigger: 'change' }
                 ],
                 newpass: [
                     {
                         validator: (rule, value, callback) => {
                             if (value === '') {
-                                callback(new Error(this.$L('请输入新密码')));
+                                callback(new Error(this.$L('请输入新密码')));
                             } else {
                                 if (this.formPass.checkpass !== '') {
                                     this.$refs.formPass.validateField('checkpass');
@@ -209,15 +209,15 @@
                             }
                         },
                         required: true,
-                        trigger: 'blur'
+                        trigger: 'change'
                     },
-                    { type: 'string', min: 6, message: this.$L('密码长度至少6位'), trigger: 'blur' }
+                    { type: 'string', min: 6, message: this.$L('密码长度至少6位!'), trigger: 'change' }
                 ],
                 checkpass: [
                     {
                         validator: (rule, value, callback) => {
                             if (value === '') {
-                                callback(new Error(this.$L('请输入确认新密码')));
+                                callback(new Error(this.$L('请输入确认新密码')));
                             } else if (value !== this.formPass.newpass) {
                                 callback(new Error(this.$L('两次密码输入不一致!')));
                             } else {
@@ -225,7 +225,7 @@
                             }
                         },
                         required: true,
-                        trigger: 'blur'
+                        trigger: 'change'
                     }
                 ],
             };

+ 108 - 0
resources/assets/js/main/components/WLoading.vue

@@ -0,0 +1,108 @@
+<template>
+    <svg viewBox="25 25 50 50" class="w-loading"><circle cx="50" cy="50" r="20" fill="none" stroke-width="5" stroke-miterlimit="10" class="w-path"></circle></svg>
+</template>
+
+<style lang="scss" scoped>
+    .w-loading {
+        -webkit-animation: rotate 2s linear infinite;
+        animation: rotate 2s linear infinite;
+        -webkit-transform-origin: center center;
+        transform-origin: center center;
+        width: 30px;
+        height: 30px;
+        max-width: 100%;
+        max-height: 100%;
+        margin: auto;
+        overflow: hidden;
+        .w-path {
+            stroke-dasharray: 1,200;
+            stroke-dashoffset: 0;
+            -webkit-animation: dash 1.5s ease-in-out infinite,color 6s ease-in-out infinite;
+            animation: dash 1.5s ease-in-out infinite,color 6s ease-in-out infinite;
+            stroke-linecap: round;
+        }
+    }
+    @-webkit-keyframes rotate {
+        to {
+            -webkit-transform: rotate(1turn);
+            transform: rotate(1turn)
+        }
+    }
+    @keyframes rotate {
+        to {
+            -webkit-transform: rotate(1turn);
+            transform: rotate(1turn)
+        }
+    }
+    @-webkit-keyframes dash {
+        0% {
+            stroke-dasharray: 1,200;
+            stroke-dashoffset: 0
+        }
+
+        50% {
+            stroke-dasharray: 89,200;
+            stroke-dashoffset: -35
+        }
+
+        to {
+            stroke-dasharray: 89,200;
+            stroke-dashoffset: -124
+        }
+    }
+    @keyframes dash {
+        0% {
+            stroke-dasharray: 1,200;
+            stroke-dashoffset: 0
+        }
+
+        50% {
+            stroke-dasharray: 89,200;
+            stroke-dashoffset: -35
+        }
+
+        to {
+            stroke-dasharray: 89,200;
+            stroke-dashoffset: -124
+        }
+    }
+    @-webkit-keyframes color {
+        0%,to {
+            stroke: #d62d20
+        }
+
+        40% {
+            stroke: #0057e7
+        }
+
+        66% {
+            stroke: #008744
+        }
+
+        80%,90% {
+            stroke: #ffa700
+        }
+    }
+    @keyframes color {
+        0%,to {
+            stroke: #d62d20
+        }
+
+        40% {
+            stroke: #0057e7
+        }
+
+        66% {
+            stroke: #008744
+        }
+
+        80%,90% {
+            stroke: #ffa700
+        }
+    }
+</style>
+<script>
+    export default {
+        name: 'WLoading',
+    }
+</script>

+ 5 - 94
resources/assets/js/main/components/WSpinner.vue

@@ -1,6 +1,6 @@
 <template>
     <div class="w-spinner">
-        <svg viewBox="25 25 50 50" class="w-circular"><circle cx="50" cy="50" r="20" fill="none" stroke-width="5" stroke-miterlimit="10" class="w-path"></circle></svg>
+        <w-loading class="w-circular"></w-loading>
     </div>
 </template>
 
@@ -15,109 +15,20 @@
         width: 30px;
         height: 30px;
         .w-circular {
-            -webkit-animation: rotate 2s linear infinite;
-            animation: rotate 2s linear infinite;
-            height: 100%;
-            -webkit-transform-origin: center center;
-            transform-origin: center center;
-            width: 100%;
             position: absolute;
             top: 0;
             bottom: 0;
             left: 0;
             right: 0;
-            margin: auto;
-            overflow: hidden;
-            .w-path {
-                stroke-dasharray: 1,200;
-                stroke-dashoffset: 0;
-                -webkit-animation: dash 1.5s ease-in-out infinite,color 6s ease-in-out infinite;
-                animation: dash 1.5s ease-in-out infinite,color 6s ease-in-out infinite;
-                stroke-linecap: round;
-            }
-        }
-        @-webkit-keyframes rotate {
-            to {
-                -webkit-transform: rotate(1turn);
-                transform: rotate(1turn)
-            }
-        }
-        @keyframes rotate {
-            to {
-                -webkit-transform: rotate(1turn);
-                transform: rotate(1turn)
-            }
-        }
-        @-webkit-keyframes dash {
-            0% {
-                stroke-dasharray: 1,200;
-                stroke-dashoffset: 0
-            }
-
-            50% {
-                stroke-dasharray: 89,200;
-                stroke-dashoffset: -35
-            }
-
-            to {
-                stroke-dasharray: 89,200;
-                stroke-dashoffset: -124
-            }
-        }
-        @keyframes dash {
-            0% {
-                stroke-dasharray: 1,200;
-                stroke-dashoffset: 0
-            }
-
-            50% {
-                stroke-dasharray: 89,200;
-                stroke-dashoffset: -35
-            }
-
-            to {
-                stroke-dasharray: 89,200;
-                stroke-dashoffset: -124
-            }
-        }
-        @-webkit-keyframes color {
-            0%,to {
-                stroke: #d62d20
-            }
-
-            40% {
-                stroke: #0057e7
-            }
-
-            66% {
-                stroke: #008744
-            }
-
-            80%,90% {
-                stroke: #ffa700
-            }
-        }
-        @keyframes color {
-            0%,to {
-                stroke: #d62d20
-            }
-
-            40% {
-                stroke: #0057e7
-            }
-
-            66% {
-                stroke: #008744
-            }
-
-            80%,90% {
-                stroke: #ffa700
-            }
+            width: 100%;
+            height: 100%;
         }
     }
 </style>
 <script>
+    import WLoading from "./WLoading";
     export default {
         name: 'WSpinner',
+        components: {WLoading},
     }
 </script>

+ 21 - 0
resources/assets/js/main/components/directives/clickoutside.js

@@ -0,0 +1,21 @@
+export default {
+    bind (el, binding, vnode) {
+        function documentHandler (e) {
+            if (el.contains(e.target)) {
+                return false;
+            }
+            if (binding.expression) {
+                binding.value(e);
+            }
+        }
+        el.__vueClickOutside__ = documentHandler;
+        document.addEventListener('click', documentHandler);
+    },
+    update () {
+
+    },
+    unbind (el, binding) {
+        document.removeEventListener('click', el.__vueClickOutside__);
+        delete el.__vueClickOutside__;
+    }
+};

+ 108 - 0
resources/assets/js/main/components/directives/popper-novalue.js

@@ -0,0 +1,108 @@
+/**
+ * https://github.com/freeze-component/vue-popper
+ * */
+import Vue from 'vue';
+const isServer = Vue.prototype.$isServer;
+const Popper = isServer ? function() {} : require('popper.js/dist/umd/popper.js');  // eslint-disable-line
+
+export default {
+    props: {
+        placement: {
+            type: String,
+            default: 'bottom'
+        },
+        boundariesPadding: {
+            type: Number,
+            default: 5
+        },
+        reference: Object,
+        popper: Object,
+        offset: {
+            default: 0
+        },
+        transition: String,
+        options: {
+            type: Object,
+            default () {
+                return {
+                    modifiers: {
+                        computeStyle:{
+                            gpuAcceleration: false,
+                        },
+                        preventOverflow :{
+                            boundariesElement: 'window'
+                        }
+                    }
+                };
+            }
+        }
+    },
+    data () {
+        return {
+            visible: false
+        };
+    },
+    watch: {
+        visible(val) {
+            if (val) {
+                if (this.handleIndexIncrease) this.handleIndexIncrease();  // just use for Poptip
+                this.updatePopper();
+                this.$emit('on-popper-show');
+            } else {
+                this.$emit('on-popper-hide');
+            }
+        }
+    },
+    methods: {
+        createPopper() {
+            if (isServer) return;
+            if (!/^(top|bottom|left|right)(-start|-end)?$/g.test(this.placement)) {
+                return;
+            }
+
+            const options = this.options;
+            const popper = this.popper || this.$refs.popper;
+            const reference = this.reference || this.$refs.reference;
+
+            if (!popper || !reference) return;
+
+            if (this.popperJS && this.popperJS.hasOwnProperty('destroy')) {
+                this.popperJS.destroy();
+            }
+
+            options.placement = this.placement;
+
+            if (!options.modifiers.offset) {
+                options.modifiers.offset = {};
+            }
+            options.modifiers.offset.offset = this.offset;
+            options.onCreate =()=>{
+                this.$nextTick(this.updatePopper);
+                this.$emit('created', this);
+            };
+
+            this.popperJS = new Popper(reference, popper, options);
+
+        },
+        updatePopper() {
+            if (isServer) return;
+            this.popperJS ? this.popperJS.update() : this.createPopper();
+        },
+        doDestroy() {
+            if (isServer) return;
+            if (this.visible) return;
+            this.popperJS.destroy();
+            this.popperJS = null;
+        }
+    },
+    updated (){
+        this.$nextTick(()=>this.updatePopper());
+
+    },
+    beforeDestroy() {
+        if (isServer) return;
+        if (this.popperJS) {
+            this.popperJS.destroy();
+        }
+    }
+};

+ 77 - 0
resources/assets/js/main/components/directives/transfer-dom.js

@@ -0,0 +1,77 @@
+// Thanks to: https://github.com/airyland/vux/blob/v2/src/directives/transfer-dom/index.js
+// Thanks to: https://github.com/calebroseland/vue-dom-portal
+
+/**
+ * Get target DOM Node
+ * @param {(Node|string|Boolean)} [node=document.body] DOM Node, CSS selector, or Boolean
+ * @return {Node} The target that the el will be appended to
+ */
+function getTarget (node) {
+    if (node === void 0) {
+        node = document.body
+    }
+    if (node === true) { return document.body }
+    return node instanceof window.Node ? node : document.querySelector(node)
+}
+
+const directive = {
+    inserted (el, { value }, vnode) {
+        if ( el.dataset && el.dataset.transfer !== 'true') return false;
+        el.className = el.className ? el.className + ' v-transfer-dom' : 'v-transfer-dom';
+        const parentNode = el.parentNode;
+        if (!parentNode) return;
+        const home = document.createComment('');
+        let hasMovedOut = false;
+
+        if (value !== false) {
+            parentNode.replaceChild(home, el); // moving out, el is no longer in the document
+            getTarget(value).appendChild(el); // moving into new place
+            hasMovedOut = true
+        }
+        if (!el.__transferDomData) {
+            el.__transferDomData = {
+                parentNode: parentNode,
+                home: home,
+                target: getTarget(value),
+                hasMovedOut: hasMovedOut
+            }
+        }
+    },
+    componentUpdated (el, { value }) {
+        if ( el.dataset && el.dataset.transfer !== 'true') return false;
+        // need to make sure children are done updating (vs. `update`)
+        const ref$1 = el.__transferDomData;
+        if (!ref$1) return;
+        // homes.get(el)
+        const parentNode = ref$1.parentNode;
+        const home = ref$1.home;
+        const hasMovedOut = ref$1.hasMovedOut; // recall where home is
+
+        if (!hasMovedOut && value) {
+            // remove from document and leave placeholder
+            parentNode.replaceChild(home, el);
+            // append to target
+            getTarget(value).appendChild(el);
+            el.__transferDomData = Object.assign({}, el.__transferDomData, { hasMovedOut: true, target: getTarget(value) });
+        } else if (hasMovedOut && value === false) {
+            // previously moved, coming back home
+            parentNode.replaceChild(el, home);
+            el.__transferDomData = Object.assign({}, el.__transferDomData, { hasMovedOut: false, target: getTarget(value) });
+        } else if (value) {
+            // already moved, going somewhere else
+            getTarget(value).appendChild(el);
+        }
+    },
+    unbind (el) {
+        if (el.dataset && el.dataset.transfer !== 'true') return false;
+        el.className = el.className.replace('v-transfer-dom', '');
+        const ref$1 = el.__transferDomData;
+        if (!ref$1) return;
+        if (el.__transferDomData.hasMovedOut === true) {
+            el.__transferDomData.parentNode && el.__transferDomData.parentNode.appendChild(el)
+        }
+        el.__transferDomData = null
+    }
+};
+
+export default directive;

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

@@ -0,0 +1,134 @@
+<template>
+    <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" @on-change="setPage" @on-page-size-change="setPageSize" :page-size-opts="[10,20,30,50,100]" placement="top" show-elevator show-sizer show-total></Page>
+    </div>
+</template>
+
+<style lang="scss" scoped>
+    .project-complete {
+        .tableFill {
+            margin: 20px;
+        }
+    }
+</style>
+<script>
+    export default {
+        name: 'ProjectComplete',
+        props: {
+            projectid: {
+                default: 0
+            },
+        },
+        data () {
+            return {
+                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": "归档时间",
+                "minWidth": 160,
+                render: (h, params) => {
+                    return h('span', $A.formatDate("Y-m-d H:i:s", params.row.archiveddate));
+                }
+            }, {
+                "title": "操作",
+                "key": 'action',
+                "width": 80,
+                "align": 'center',
+                render: (h, params) => {
+                    return h('Button', {
+                        props: {
+                            type: 'primary',
+                            size: 'small'
+                        },
+                        on: {
+                            click: () => {
+
+                            }
+                        }
+                    }, '取消归档');
+                }
+            }];
+        },
+        mounted() {
+            this.listPage = 1;
+            this.getLists();
+        },
+
+        watch: {
+            projectid() {
+                this.listPage = 1;
+                this.getLists();
+            }
+        },
+
+        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() {
+                if (this.projectid == 0) {
+                    this.lists = [];
+                    this.listTotal = 0;
+                    this.noDataText = "没有相关的数据";
+                    return;
+                }
+                this.loadIng++;
+                $A.aAjax({
+                    url: 'project/task/lists',
+                    data: {
+                        page: Math.max(this.listPage, 1),
+                        pagesize: Math.max($A.runNum(this.listPageSize), 10),
+                        projectid: this.projectid,
+                        archived: 1,
+                    },
+                    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>

+ 164 - 0
resources/assets/js/main/components/project/users.vue

@@ -0,0 +1,164 @@
+<template>
+    <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" @on-change="setPage" @on-page-size-change="setPageSize" :page-size-opts="[10,20,30,50,100]" placement="top" show-elevator show-sizer show-total></Page>
+    </div>
+</template>
+
+<style lang="scss" scoped>
+    .project-complete {
+        .tableFill {
+            margin: 20px;
+        }
+    }
+</style>
+<script>
+    export default {
+        name: 'ProjectUsers',
+        props: {
+            projectid: {
+                default: 0
+            },
+        },
+        data () {
+            return {
+                loadIng: 0,
+
+                columns: [],
+
+                lists: [],
+                listPage: 1,
+                listTotal: 0,
+                noDataText: "数据加载中.....",
+            }
+        },
+        created() {
+            this.columns = [{
+                "title": "头像",
+                "minWidth": 50,
+                "maxWidth": 100,
+                render: (h, params) => {
+                    return h('img', {
+                        style: {
+                            width: "36px",
+                            height: "36px",
+                            verticalAlign: "middle",
+                            objectFit: "cover",
+                            borderRadius: "50%"
+                        },
+                        attrs: {
+                            src: params.row.userimg
+                        },
+                    });
+                }
+            }, {
+                "title": "昵称",
+                "minWidth": 80,
+                "ellipsis": true,
+                render: (h, params) => {
+                    return h('span', params.row.nickname || '-');
+                }
+            }, {
+                "title": "用户名",
+                "key": 'username',
+                "minWidth": 80,
+                "ellipsis": true,
+            }, {
+                "title": "职位/职称",
+                "minWidth": 100,
+                "ellipsis": true,
+                render: (h, params) => {
+                    return h('span', params.row.profession || '-');
+                }
+            }, {
+                "title": "成员角色",
+                "minWidth": 100,
+                render: (h, params) => {
+                    return h('span', params.row.isowner ? '项目负责人' : '成员');
+                }
+            }, {
+                "title": "加入时间",
+                "minWidth": 160,
+                render: (h, params) => {
+                    return h('span', $A.formatDate("Y-m-d H:i:s", params.row.indate));
+                }
+            }, {
+                "title": "操作",
+                "key": 'action',
+                "width": 80,
+                "align": 'center',
+                render: (h, params) => {
+                    return h('Button', {
+                        props: {
+                            type: 'primary',
+                            size: 'small'
+                        },
+                        on: {
+                            click: () => {
+
+                            }
+                        }
+                    }, '删除');
+                }
+            }];
+        },
+        mounted() {
+            this.listPage = 1;
+            this.getLists();
+        },
+
+        watch: {
+            projectid() {
+                this.listPage = 1;
+                this.getLists();
+            }
+        },
+
+        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() {
+                if (this.projectid == 0) {
+                    this.lists = [];
+                    this.listTotal = 0;
+                    this.noDataText = "没有相关的数据";
+                    return;
+                }
+                this.loadIng++;
+                $A.aAjax({
+                    url: 'project/users',
+                    data: {
+                        page: Math.max(this.listPage, 1),
+                        pagesize: Math.max($A.runNum(this.listPageSize), 10),
+                        projectid: this.projectid,
+                    },
+                    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>

+ 6 - 5
resources/assets/js/main/pages/index.vue

@@ -95,12 +95,12 @@
             :mask-closable="false">
             <Form ref="login" :model="formLogin" :rules="ruleLogin">
                 <FormItem prop="username">
-                    <Input type="text" v-model="formLogin.username" :placeholder="$L('用户名')">
+                    <Input type="text" v-model="formLogin.username" :placeholder="$L('用户名')" @on-enter="onLogin">
                         <Icon type="ios-person-outline" slot="prepend"></Icon>
                     </Input>
                 </FormItem>
                 <FormItem prop="userpass">
-                    <Input type="password" v-model="formLogin.userpass" :placeholder="$L('密码')">
+                    <Input type="password" v-model="formLogin.userpass" :placeholder="$L('密码')" @on-enter="onLogin">
                         <Icon type="ios-lock-outline" slot="prepend"></Icon>
                     </Input>
                 </FormItem>
@@ -333,11 +333,12 @@
         created() {
             this.ruleLogin = {
                 username: [
-                    { required: true, message: this.$L('请填写用户名!'), trigger: 'blur' }
+                    { required: true, message: this.$L('请填写用户名!'), trigger: 'change' },
+                    { type: 'string', min: 2, message: this.$L('用户名长度至少2位!'), trigger: 'change' }
                 ],
                 userpass: [
-                    { required: true, message: this.$L('请填写登录密码!'), trigger: 'blur' },
-                    { type: 'string', min: 6, message: this.$L('密码长度不能少于6位!'), trigger: 'blur' }
+                    { required: true, message: this.$L('请填写登录密码!'), trigger: 'change' },
+                    { type: 'string', min: 6, message: this.$L('用户名长度至少6位!'), trigger: 'change' }
                 ]
             };
         },

+ 608 - 7
resources/assets/js/main/pages/project.vue

@@ -8,38 +8,295 @@
         <div class="w-nav">
             <div class="nav-row">
                 <div class="w-nav-left">
-                    <span class="ft hover"><i class="ft icon"></i> {{$L('新建项目')}}</span>
+                    <span class="ft hover" @click="addShow=true"><i class="ft icon">&#xE740;</i> {{$L('新建项目')}}</span>
                 </div>
                 <div class="w-nav-flex"></div>
                 <div class="w-nav-right">
-                    <span class="ft hover"><i class="ft icon"></i> {{$L('收藏的项目')}}</span>
-                    <span class="ft hover"><i class="ft icon"></i> {{$L('参与的项目')}}</span>
-                    <span class="ft hover"><i class="ft icon"></i> {{$L('我创建的项目')}}</span>
+                    <span class="ft hover" @click="handleProject('myfavor')"><i class="ft icon">&#xE720;</i> {{$L('收藏的项目')}}</span>
+                    <span class="ft hover" @click="handleProject('myjoin')"><i class="ft icon">&#xE75E;</i> {{$L('参与的项目')}}</span>
+                    <span class="ft hover" @click="handleProject('mycreate')"><i class="ft icon">&#xE764;</i> {{$L('我创建的项目')}}</span>
                 </div>
             </div>
         </div>
 
-        <w-content></w-content>
+        <w-content>
+            <!-- 列表 -->
+            <ul class="project-list">
+                <li v-for="(item, index) in lists">
+                    <div class="project-item" @click="handleProject('open', item)">
+                        <div class="project-head">
+                            <div v-if="item.loadIng === true" class="project-loading">
+                                <w-loading></w-loading>
+                            </div>
+                            <div class="project-title">{{item.title}}</div>
+                            <div class="project-setting">
+                                <Dropdown class="right-info" trigger="click" @on-click="handleProject($event, item)" transfer>
+                                    <Icon class="project-setting-icon" type="md-settings" size="16"/>
+                                    <Dropdown-menu slot="list">
+                                        <Dropdown-item name="favor">{{$L('收藏')}}</Dropdown-item>
+                                        <Dropdown-item v-if="item.isowner" name="rename">{{$L('重命名')}}</Dropdown-item>
+                                        <Dropdown-item v-if="item.isowner" name="transfer">{{$L('移交项目')}}</Dropdown-item>
+                                        <Dropdown-item v-if="item.isowner" name="delete">{{$L('删除')}}</Dropdown-item>
+                                        <Dropdown-item name="out">{{$L('退出')}}</Dropdown-item>
+                                    </Dropdown-menu>
+                                </Dropdown>
+                            </div>
+                        </div>
+                        <div class="project-num">
+                            <div class="project-complete"><em>{{item.complete}}</em>已完成数</div>
+                            <div class="project-num-line"></div>
+                            <div class="project-unfinished"><em>{{item.unfinished}}</em>未完成数</div>
+                        </div>
+                        <div class="project-bottom">
+                            <div class="project-iconbtn" @click.stop="handleProject('complete', item)">
+                                <Icon class="project-iconbtn-icon1" type="md-checkmark-circle-outline" size="24" />
+                                <div class="project-iconbtn-text">已完成任务</div>
+                            </div>
+                            <div class="project-iconbtn" @click.stop="handleProject('member', item)">
+                                <Icon class="project-iconbtn-icon2" type="md-people" size="24" />
+                                <div class="project-iconbtn-text">成员管理</div>
+                            </div>
+                            <div class="project-iconbtn" @click.stop="handleProject('statistics', item)">
+                                <Icon class="project-iconbtn-icon3" type="md-stats" size="24" />
+                                <div class="project-iconbtn-text">项目统计</div>
+                            </div>
+                        </div>
+                    </div>
+                </li>
+            </ul>
+            <!-- 分页 -->
+            <Page v-if="listTotal > 0" class="pageBox" :total="listTotal" :current="listPage" @on-change="setPage" @on-page-size-change="setPageSize" :page-size-opts="[10,20,30,50,100]" placement="top" show-elevator show-sizer show-total></Page>
+        </w-content>
 
+        <Modal
+            v-model="addShow"
+            :title="$L('添加团队成员')"
+            :closable="false"
+            :mask-closable="false">
+            <Form ref="add" :model="formAdd" :rules="ruleAdd" :label-width="80">
+                <FormItem prop="title" :label="$L('项目名称')">
+                    <Input type="text" v-model="formAdd.title"></Input>
+                </FormItem>
+                <FormItem prop="labels" :label="$L('项目模板')">
+                    <Select v-model="formAdd.template" @on-change="(res) => {$set(formAdd, 'labels', labelLists[res].value)}">
+                        <Option v-for="(item, index) in labelLists" :value="index" :key="index">{{ item.label }}</Option>
+                    </Select>
+                </FormItem>
+                <FormItem :label="$L('项目流程')">
+                    <div style="line-height:38px">
+                        <span v-for="(item, index) in formAdd.labels">
+                            <span v-if="index > 0">&gt;</span>
+                            <Tag @on-close="() => { formAdd.labels.splice(index, 1)}" closable size="large" color="primary">{{item}}</Tag>
+                        </span>
+                    </div>
+                    <div v-if="formAdd.labels.length > 0" style="margin-top:4px;"></div>
+                    <div style="margin-bottom:-16px">
+                        <Button icon="ios-add" type="dashed" @click="addLabels">添加流程</Button>
+                    </div>
+                </FormItem>
+            </Form>
+            <div slot="footer">
+                <Button type="default" @click="addShow=false">{{$L('取消')}}</Button>
+                <Button type="primary" :loading="loadIng > 0" @click="onAdd">{{$L('添加')}}</Button>
+            </div>
+        </Modal>
+
+        <Drawer v-model="projectDrawerShow" width="75%">
+            <Tabs v-model="projectDrawerTab">
+                <TabPane :label="$L('已完成任务')" name="complete">
+                    <project-complete :projectid="handleProjectId"></project-complete>
+                </TabPane>
+                <TabPane :label="$L('成员管理')" name="member">
+                    <project-users :projectid="handleProjectId"></project-users>
+                </TabPane>
+                <TabPane :label="$L('项目统计')" name="statistics"></TabPane>
+                <TabPane :label="$L('收藏的项目')" name="myfavor"></TabPane>
+                <TabPane :label="$L('参与的项目')" name="myjoin"></TabPane>
+                <TabPane :label="$L('创建的项目')" name="mycreate"></TabPane>
+            </Tabs>
+        </Drawer>
     </div>
 </template>
 
 <style lang="scss" scoped>
     .project {
+        ul.project-list {
+            padding: 5px;
+            max-width: 2000px;
+            li {
+                float: left;
+                width: 25%;
+                display: flex;
+                @media (max-width: 1400px) {
+                    width: 33.33%;
+                }
+                @media (max-width: 1080px) {
+                    width: 50%;
+                }
+                @media (max-width: 640px) {
+                    width: 100%;
+                }
+                .project-item {
+                    flex: 1;
+                    margin: 10px;
+                    width: 100%;
+                    height: 280px;
+                    padding: 20px;
+                    background-color: #ffffff;
+                    border-radius: 4px;
+                    display: flex;
+                    flex-direction: column;
+                    .project-head{
+                        display: flex;
+                        flex-direction: row;
+                        .project-loading {
+                            width: 18px;
+                            height: 18px;
+                            margin-right: 6px;
+                            margin-top: 3px;
+                        }
+                        .project-title{
+                            flex: 1;
+                            font-size: 16px;
+                            padding-right: 6px;
+                            overflow:hidden;
+                            text-overflow:ellipsis;
+                            white-space:nowrap;
+                            color: #333333;
+                        }
+                        .project-setting{
+                            .project-setting-icon {
+                                cursor: pointer;
+                                color: #333333;
+                            }
+                        }
+                    }
+                    .project-num {
+                        flex: 1;
+                        padding: 24px 0;
+                        display: flex;
+                        flex-direction: row;
+                        align-items: center;
+                        .project-complete,
+                        .project-unfinished {
+                            flex: 1;
+                            text-align: center;
+                            font-size: 14px;
+                            color: #999999;
+                            em {
+                                display: block;
+                                font-size: 32px;
+                                color: #0396f2;
+                                overflow: hidden;
+                                text-overflow: ellipsis;
+                                white-space: nowrap;
+                                max-width: 120px;
+                                margin: 0 auto;
+                            }
+                        }
+                        .project-num-line {
+                            width: 1px;
+                            height: 90%;
+                            background-color: #e8e8e8;
+                        }
+                    }
+                    .project-bottom {
+                        display: flex;
+                        flex-direction: row;
+                        align-items: center;
+                        border-top: 1px solid #efefef;
+                        padding: 6px 0;
+                        .project-iconbtn {
+                            flex: 1;
+                            text-align: center;
+                            cursor: pointer;
+                            &:hover {
+                                .project-iconbtn-text {
+                                    color: #0396f2;
+                                }
+                            }
+                            .project-iconbtn-icon1 {
+                                margin: 12px 0;
+                                color: #ff7a7a;
+                            }
+                            .project-iconbtn-icon2 {
+                                margin: 12px 0;
+                                color: #764df8;
+                            }
+                            .project-iconbtn-icon3 {
+                                margin: 12px 0;
+                                color: #ffca65;
+                            }
+                            .project-iconbtn-text {
+                                color: #999999;
+                            }
+                        }
+                    }
+                }
+            }
+            &:before,
+            &:after {
+                display: table;
+                content: "";
+            }
+            &:after {
+                clear: both;
+            }
+        }
     }
 </style>
 <script>
     import WHeader from "../components/WHeader";
     import WContent from "../components/WContent";
+    import WLoading from "../components/WLoading";
+    import ProjectComplete from "../components/project/complete";
+    import ProjectUsers from "../components/project/users";
     export default {
-        components: {WContent, WHeader},
+        components: {ProjectUsers, ProjectComplete, WLoading, WContent, WHeader},
         data () {
             return {
+                loadIng: 0,
+
+                addShow: false,
+                formAdd: {
+                    title: '',
+                    labels: [],
+                    template: 0,
+                },
+                ruleAdd: {},
 
+                labelLists: [{
+                    label: '空白模板',
+                    value: [],
+                }, {
+                    label: '软件开发',
+                    value: ['产品规划','前端开发','后端开发','测试','发布','其它'],
+                }, {
+                    label: '产品开发',
+                    value: ['产品计划', '正在设计', '正在研发', '测试', '准备发布', '发布成功'],
+                }],
+
+                lists: [],
+                listPage: 1,
+                listTotal: 0,
+
+                projectDrawerShow: false,
+                projectDrawerTab: 'complete',
+
+                handleProjectId: 0,
             }
         },
+        created() {
+            this.ruleAdd = {
+                title: [
+                    { required: true, message: this.$L('请填写项目名称!'), trigger: 'change' },
+                    { type: 'string', min: 2, message: this.$L('项目名称至少2个字!'), trigger: 'change' }
+                ]
+            };
+        },
         mounted() {
-
+            this.listPage = 1;
+            this.getLists();
         },
         computed: {
 
@@ -48,7 +305,351 @@
 
         },
         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() {
+                this.loadIng++;
+                $A.aAjax({
+                    url: 'project/lists',
+                    data: {
+                        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;
+                        }
+                    }
+                });
+            },
+
+            addLabels() {
+                this.labelsValue = "";
+                this.$Modal.confirm({
+                    render: (h) => {
+                        return h('div', [
+                            h('div', {
+                                style: {
+                                    fontSize: '16px',
+                                    fontWeight: '500',
+                                    marginBottom: '20px',
+                                }
+                            }, '添加流程'),
+                            h('Input', {
+                                props: {
+                                    value: this.labelsValue,
+                                    autofocus: true,
+                                    placeholder: '请输入流程名称,多个请用空格分隔。'
+                                },
+                                on: {
+                                    input: (val) => {
+                                        this.labelsValue = val;
+                                    }
+                                }
+                            })
+                        ])
+                    },
+                    onOk: () => {
+                        if (this.labelsValue) {
+                            let array = $A.trim(this.labelsValue).split(" ");
+                            array.forEach((name) => {
+                                if ($A.trim(name)) {
+                                    this.formAdd.labels.push($A.trim(name));
+                                }
+                            });
+                        }
+                    },
+                })
+            },
+
+            onAdd() {
+                this.$refs.add.validate((valid) => {
+                    if (valid) {
+                        this.loadIng++;
+                        $A.aAjax({
+                            url: 'project/add',
+                            data: this.formAdd,
+                            complete: () => {
+                                this.loadIng--;
+                            },
+                            success: (res) => {
+                                if (res.ret === 1) {
+                                    this.addShow = false;
+                                    this.$Message.success(res.msg);
+                                    this.$refs.add.resetFields();
+                                    this.$set(this.formAdd, 'template', 0);
+                                    //
+                                    this.listPage = 1;
+                                    this.getLists();
+                                }else{
+                                    this.$Modal.error({title: this.$L('温馨提示'), content: res.msg });
+                                }
+                            }
+                        });
+                    }
+                });
+            },
+
+            handleProject(event, item) {
+                this.handleProjectId = item.id;
+                switch (event) {
+                    case 'favor': {
+                        this.favorProject(item);
+                        break;
+                    }
+                    case 'rename': {
+                        this.renameProject(item);
+                        break;
+                    }
+                    case 'transfer': {
+                        this.transferProject(item);
+                        break;
+                    }
+                    case 'delete': {
+                        this.deleteProject(item);
+                        break;
+                    }
+                    case 'out': {
+                        this.outProject(item);
+                        break;
+                    }
+
+                    case 'open': {
+                        break;
+                    }
+                    case 'complete':
+                    case 'member':
+                    case 'statistics':
+                    case 'myfavor':
+                    case 'myjoin':
+                    case 'mycreate': {
+                        this.projectDrawerShow = true;
+                        this.projectDrawerTab = event;
+                        break;
+                    }
+                }
+            },
+
+            favorProject(item) {
+                this.$set(item, 'loadIng', true);
+                $A.aAjax({
+                    url: 'project/favor',
+                    data: {
+                        id: item.id,
+                    },
+                    complete: () => {
+                        this.$set(item, 'loadIng', false);
+                    },
+                    success: (res) => {
+                        if (res.ret === 1) {
+                            this.$Message.success(res.msg);
+                        }else{
+                            this.$Modal.error({title: this.$L('温馨提示'), content: res.msg });
+                        }
+                    }
+                });
+            },
+
+            renameProject(item) {
+                this.renameValue = "";
+                this.$Modal.confirm({
+                    render: (h) => {
+                        return h('div', [
+                            h('div', {
+                                style: {
+                                    fontSize: '16px',
+                                    fontWeight: '500',
+                                    marginBottom: '20px',
+                                }
+                            }, '重命名项目'),
+                            h('Input', {
+                                props: {
+                                    value: this.renameValue,
+                                    autofocus: true,
+                                    placeholder: '请输入新的项目名称'
+                                },
+                                on: {
+                                    input: (val) => {
+                                        this.renameValue = val;
+                                    }
+                                }
+                            })
+                        ])
+                    },
+                    loading: true,
+                    onOk: () => {
+                        if (this.renameValue) {
+                            this.$set(item, 'loadIng', true);
+                            let title = this.renameValue;
+                            $A.aAjax({
+                                url: 'project/rename',
+                                data: {
+                                    projectid: item.id,
+                                    title: title,
+                                },
+                                complete: () => {
+                                    this.$set(item, 'loadIng', false);
+                                },
+                                error: () => {
+                                    this.$Modal.remove();
+                                    this.$Message.error(this.$L('网络繁忙,请稍后再试!'));
+                                },
+                                success: (res) => {
+                                    this.$Modal.remove();
+                                    setTimeout(() => {
+                                        if (res.ret === 1) {
+                                            this.$Message.success(res.msg);
+                                            this.$set(item, 'title', title);
+                                        } else {
+                                            this.$Modal.error({title: this.$L('温馨提示'), content: res.msg});
+                                        }
+                                    }, 350);
+                                }
+                            });
+                        } else {
+                            this.$Modal.remove();
+                        }
+                    },
+                });
+            },
+
+            transferProject(item) {
+                this.transferValue = "";
+                this.$Modal.confirm({
+                    render: (h) => {
+                        return h('div', [
+                            h('div', {
+                                style: {
+                                    fontSize: '16px',
+                                    fontWeight: '500',
+                                    marginBottom: '20px',
+                                }
+                            }, '移交项目'),
+                            h('UseridInput', {
+                                props: {
+                                    value: this.transferValue,
+                                    autofocus: true,
+                                    placeholder: '请输入昵称/用户名搜索'
+                                },
+                                on: {
+                                    input: (val) => {
+                                        this.transferValue = val;
+                                    }
+                                }
+                            })
+                        ])
+                    },
+                    loading: true,
+                    onOk: () => {
+                        if (this.transferValue) {
+                            this.$set(item, 'loadIng', true);
+                            let username = this.transferValue;
+                            $A.aAjax({
+                                url: 'project/transfer',
+                                data: {
+                                    projectid: item.id,
+                                    username: username,
+                                },
+                                complete: () => {
+                                    this.$set(item, 'loadIng', false);
+                                },
+                                error: () => {
+                                    this.$Modal.remove();
+                                    this.$Message.error(this.$L('网络繁忙,请稍后再试!'));
+                                },
+                                success: (res) => {
+                                    this.$Modal.remove();
+                                    setTimeout(() => {
+                                        if (res.ret === 1) {
+                                            this.$Message.success(res.msg);
+                                            this.getLists();
+                                        } else {
+                                            this.$Modal.error({title: this.$L('温馨提示'), content: res.msg});
+                                        }
+                                    }, 350);
+                                }
+                            });
+                        } else {
+                            this.$Modal.remove();
+                        }
+                    },
+                });
+            },
+
+            deleteProject(item) {
+                this.$Modal.confirm({
+                    title: '删除项目',
+                    content: '你确定要删除此项目吗?',
+                    loading: true,
+                    onOk: () => {
+                        $A.aAjax({
+                            url: 'project/delete?projectid=' + item.id,
+                            error: () => {
+                                this.$Modal.remove();
+                                this.$Message.error(this.$L('网络繁忙,请稍后再试!'));
+                            },
+                            success: (res) => {
+                                this.$Modal.remove();
+                                setTimeout(() => {
+                                    if (res.ret === 1) {
+                                        this.$Message.success(res.msg);
+                                        //
+                                        this.getLists();
+                                    }else{
+                                        this.$Modal.error({title: this.$L('温馨提示'), content: res.msg });
+                                    }
+                                }, 350);
+                            }
+                        });
+                    }
+                });
+            },
+
+            outProject(item) {
+                this.$Modal.confirm({
+                    title: '退出项目',
+                    content: '你确定要退出此项目吗?',
+                    loading: true,
+                    onOk: () => {
+                        $A.aAjax({
+                            url: 'project/out?projectid=' + item.id,
+                            error: () => {
+                                this.$Modal.remove();
+                                this.$Message.error(this.$L('网络繁忙,请稍后再试!'));
+                            },
+                            success: (res) => {
+                                this.$Modal.remove();
+                                setTimeout(() => {
+                                    if (res.ret === 1) {
+                                        this.$Message.success(res.msg);
+                                        //
+                                        this.getLists();
+                                    }else{
+                                        this.$Modal.error({title: this.$L('温馨提示'), content: res.msg });
+                                    }
+                                }, 350);
+                            }
+                        });
+                    }
+                });
+            }
         },
     }
 </script>

+ 13 - 5
resources/assets/js/main/pages/team.vue

@@ -134,16 +134,19 @@
             }, {
                 "title": "昵称",
                 "minWidth": 80,
+                "ellipsis": true,
                 render: (h, params) => {
                     return h('span', params.row.nickname || '-');
                 }
             }, {
-                "title": "账号",
+                "title": "用户名",
                 "key": 'username',
                 "minWidth": 80,
+                "ellipsis": true,
             }, {
                 "title": "职位/职称",
-                "minWidth": 80,
+                "minWidth": 100,
+                "ellipsis": true,
                 render: (h, params) => {
                     return h('span', params.row.profession || '-');
                 }
@@ -192,6 +195,10 @@
                                         onOk: () => {
                                             $A.aAjax({
                                                 url: 'users/team/delete?id=' + params.row.id,
+                                                error: () => {
+                                                    this.$Modal.remove();
+                                                    this.$Message.error(this.$L('网络繁忙,请稍后再试!'));
+                                                },
                                                 success: (res) => {
                                                     this.$Modal.remove();
                                                     setTimeout(() => {
@@ -217,11 +224,12 @@
             //
             this.ruleAdd = {
                 username: [
-                    { required: true, message: this.$L('请填写用户名!'), trigger: 'blur' }
+                    { required: true, message: this.$L('请填写用户名!'), trigger: 'change' },
+                    { type: 'string', min: 2, message: this.$L('用户名长度至少2位!'), trigger: 'change' }
                 ],
                 userpass: [
-                    { required: true, message: this.$L('请填写登录密码!'), trigger: 'blur' },
-                    { type: 'string', min: 6, message: this.$L('密码长度不能少于6位!'), trigger: 'blur' }
+                    { required: true, message: this.$L('请填写登录密码!'), trigger: 'change' },
+                    { type: 'string', min: 6, message: this.$L('密码长度至少6位!'), trigger: 'change' }
                 ]
             };
         },

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

@@ -135,7 +135,6 @@
                 overflow-x: auto;
                 -webkit-backface-visibility: hidden;
                 -webkit-overflow-scrolling: touch;
-                -webkit-perspective: 1000;
             }
         }
     }

+ 3 - 0
routes/web.php

@@ -10,6 +10,9 @@ Route::prefix('api')->middleware(ApiMiddleware::class)->group(function () {
     //会员
     Route::any('users/{method}',                    'Api\UsersController');
     Route::any('users/{method}/{action}',           'Api\UsersController');
+    //项目
+    Route::any('project/{method}',                  'Api\ProjectController');
+    Route::any('project/{method}/{action}',         'Api\ProjectController');
 });