feat: 新增部门功能

This commit is contained in:
kuaifan 2022-11-16 23:34:18 +08:00
parent 70ec88e57d
commit cbadf25623
6 changed files with 538 additions and 82 deletions

View File

@ -8,6 +8,7 @@ use App\Models\Project;
use App\Models\UmengAlias; use App\Models\UmengAlias;
use App\Models\User; use App\Models\User;
use App\Models\UserDelete; use App\Models\UserDelete;
use App\Models\UserDepartment;
use App\Models\UserEmailVerification; use App\Models\UserEmailVerification;
use App\Models\UserTransfer; use App\Models\UserTransfer;
use App\Models\WebSocket; use App\Models\WebSocket;
@ -1111,4 +1112,123 @@ class UsersController extends AbstractController
} }
return Base::retSuccess('success', $user); return Base::retSuccess('success', $user);
} }
/**
* @api {get} api/users/department/list 20. 部门列表(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName department__list
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function department__list()
{
User::auth('admin');
//
return Base::retSuccess('success', UserDepartment::orderBy('id')->get());
}
/**
* @api {get} api/users/department/add 21. 新建、修改部门(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName department__add
*
* @apiParam {Number} [id] 部门id留空为创建部门
* @apiParam {String} name 部门名称
* @apiParam {Number} [parent_id] 上级部门ID
* @apiParam {Number} owner_userid 部门负责人ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function department__add()
{
User::auth('admin');
//
$id = intval(Request::input('id'));
$name = trim(Request::input('name'));
$parent_id = intval(Request::input('parent_id'));
$owner_userid = intval(Request::input('owner_userid'));
//
if (mb_strlen($name) < 2 || mb_strlen($name) > 20) {
return Base::retError('部门名称长度限制2-20个字');
}
//
if ($id > 0) {
$userDepartment = UserDepartment::find($id);
if (empty($userDepartment)) {
return Base::retError('部门不存在或已被删除');
}
} else {
if (UserDepartment::count() > 200) {
return Base::retError('最多只能创建200个部门');
}
$userDepartment = UserDepartment::createInstance();
}
//
if ($parent_id > 0) {
$parentDepartment = UserDepartment::find($parent_id);
if (empty($parentDepartment)) {
return Base::retError('上级部门不存在或已被删除');
}
if ($parentDepartment->parent_id > 0) {
return Base::retError('上级部门层级错误');
}
if (UserDepartment::whereParentId($parent_id)->count() > 20) {
return Base::retError('每个部门最多只能创建20个子部门');
}
if ($id > 0 && UserDepartment::whereParentId($id)->exists()) {
return Base::retError('含有子部门无法修改上级部门');
}
}
if (empty($owner_userid) || !User::whereUserid($owner_userid)->exists()) {
return Base::retError('请选择正确的部门负责人');
}
//
$userDepartment->updateInstance([
'name' => $name,
'parent_id' => $parent_id,
'owner_userid' => $owner_userid,
]);
$userDepartment->saveDepartment();
//
return Base::retSuccess($parent_id > 0 ? '保存成功' : '新建成功');
}
/**
* @api {get} api/users/department/del 22. 删除部门(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName department__del
*
* @apiParam {Number} id 部门id
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function department__del()
{
User::auth('admin');
//
$id = intval(Request::input('id'));
//
$userDepartment = UserDepartment::find($id);
if (empty($userDepartment)) {
return Base::retError('部门不存在或已被删除');
}
$userDepartment->deleteDepartment();
//
return Base::retSuccess('删除成功');
}
} }

View File

@ -0,0 +1,51 @@
<?php
namespace App\Models;
/**
* App\Models\UserDepartment
*
* @property int $id
* @property string|null $name 部门名称
* @property int|null $dialog_id 聊天会话ID
* @property int|null $parent_id 上级部门
* @property int|null $owner_userid 部门负责人
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment query()
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereOwnerUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereParentId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereUpdatedAt($value)
* @mixin \Eloquent
*/
class UserDepartment extends AbstractModel
{
/**
* 保存部门
* @return bool
*/
public function saveDepartment() {
// todo 聊天室相关
return $this->save();
}
/**
* 删除部门
* @return void
*/
public function deleteDepartment() {
$list = self::whereParentId($this->id)->get();
foreach ($list as $item) {
$item->deleteDepartment();
}
// todo 移动成员
$this->delete();
}
}

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUserDepartmentsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('user_departments', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name', 100)->nullable()->default('')->comment('部门名称');
$table->bigInteger('dialog_id')->nullable()->default(0)->comment('聊天会话ID');
$table->bigInteger('parent_id')->nullable()->default(0)->comment('上级部门');
$table->bigInteger('owner_userid')->nullable()->default(0)->comment('部门负责人');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('user_departments');
}
}

View File

@ -325,7 +325,7 @@
<DrawerOverlay <DrawerOverlay
v-model="allUserShow" v-model="allUserShow"
placement="right" placement="right"
:size="1200"> :size="1380">
<TeamManagement v-if="allUserShow"/> <TeamManagement v-if="allUserShow"/>
</DrawerOverlay> </DrawerOverlay>

View File

@ -6,88 +6,163 @@
<Loading v-if="loadIng > 0"/> <Loading v-if="loadIng > 0"/>
</div> </div>
</div> </div>
<div class="search-container lr"> <div class="management-box">
<ul> <div class="management-department">
<li> <ul>
<div class="search-label"> <li class="level-1">
{{$L("关键词")}} <i class="taskfont department-icon">&#xe766;</i>
</div> <div class="department-title">{{$L('默认部门')}}</div>
<div class="search-content"> <EDropdown
<Input v-model="keys.key" :placeholder="$L('邮箱、昵称、职位')" clearable/> size="medium"
</div> trigger="click"
</li> @command="onOpDepartment">
<li> <i class="taskfont department-menu">&#xe6e9;</i>
<div class="search-label"> <EDropdownMenu slot="dropdown">
{{$L("身份")}} <EDropdownItem command="add_0">
</div> <div>{{$L('添加子部门')}}</div>
<div class="search-content"> </EDropdownItem>
<Select v-model="keys.identity" :placeholder="$L('全部')"> </EDropdownMenu>
<Option value="">{{$L('全部')}}</Option> </EDropdown>
<Option value="admin">{{$L('管理员')}}</Option> </li>
<Option value="noadmin">{{$L('非管理员')}}</Option> <li v-for="item in departmentList" :key="item.id" :class="`level-${item.level}`">
</Select> <UserAvatar :userid="item.owner_userid" :size="20" class="department-icon">
</div> <p><strong>{{$L('部门负责人')}}</strong></p>
</li> </UserAvatar>
<li> <div class="department-title">{{item.name}}</div>
<div class="search-label"> <EDropdown
{{$L("在职状态")}} size="medium"
</div> trigger="click"
<div class="search-content"> @command="onOpDepartment">
<Select v-model="keys.disable" :placeholder="$L('在职')"> <i class="taskfont department-menu">&#xe6e9;</i>
<Option value="">{{$L('在职')}}</Option> <EDropdownMenu slot="dropdown">
<Option value="yes">{{$L('离职')}}</Option> <EDropdownItem v-if="item.level <= 2" :command="`add_${item.id}`">
<Option value="all">{{$L('全部')}}</Option> <div>{{$L('添加子部门')}}</div>
</Select> </EDropdownItem>
</div> <EDropdownItem :command="`edit_${item.id}`">
</li> <div>{{$L('编辑')}}</div>
<li> </EDropdownItem>
<div class="search-label"> <EDropdownItem :command="`del_${item.id}`">
{{$L("邮箱认证")}} <div style="color:#f00">{{$L('删除')}}</div>
</div> </EDropdownItem>
<div class="search-content"> </EDropdownMenu>
<Select v-model="keys.email_verity" :placeholder="$L('全部')"> </EDropdown>
<Option value="">{{$L('全部')}}</Option> </li>
<Option value="yes">{{$L('已邮箱认证')}}</Option> </ul>
<Option value="no">{{$L('未邮箱认证')}}</Option> <div class="department-buttons">
</Select> <Button type="primary" icon="md-add" @click="onShowDepartment(null)">{{$L('新建部门')}}</Button>
</div> </div>
</li> </div>
<li class="search-button"> <div class="management-user">
<Tooltip <div class="search-container lr">
theme="light" <ul>
placement="bottom" <li>
transfer-class-name="search-button-clear" <div class="search-label">
transfer> {{$L("关键词")}}
<Button :loading="loadIng > 0" type="primary" icon="ios-search" @click="onSearch">{{$L('搜索')}}</Button> </div>
<div slot="content"> <div class="search-content">
<Button v-if="keyIs" type="text" @click="keyIs=false">{{$L('取消筛选')}}</Button> <Input v-model="keys.key" :placeholder="$L('邮箱、昵称、职位')" clearable/>
<Button v-else :loading="loadIng > 0" type="text" @click="getLists">{{$L('刷新')}}</Button> </div>
</div> </li>
</Tooltip> <li>
</li> <div class="search-label">
</ul> {{$L("身份")}}
</div> </div>
<div class="table-page-box"> <div class="search-content">
<Table <Select v-model="keys.identity" :placeholder="$L('全部')">
:columns="columns" <Option value="">{{$L('全部')}}</Option>
:data="list" <Option value="admin">{{$L('管理员')}}</Option>
:loading="loadIng > 0" <Option value="noadmin">{{$L('非管理员')}}</Option>
:no-data-text="$L(noText)" </Select>
stripe/> </div>
<Page </li>
:total="total" <li>
:current="page" <div class="search-label">
:page-size="pageSize" {{$L("在职状态")}}
:disabled="loadIng > 0" </div>
:simple="windowSmall" <div class="search-content">
:page-size-opts="[10,20,30,50,100]" <Select v-model="keys.disable" :placeholder="$L('在职')">
show-elevator <Option value="">{{$L('在职')}}</Option>
show-sizer <Option value="yes">{{$L('离职')}}</Option>
show-total <Option value="all">{{$L('全部')}}</Option>
@on-change="setPage" </Select>
@on-page-size-change="setPageSize"/> </div>
</li>
<li>
<div class="search-label">
{{$L("邮箱认证")}}
</div>
<div class="search-content">
<Select v-model="keys.email_verity" :placeholder="$L('全部')">
<Option value="">{{$L('全部')}}</Option>
<Option value="yes">{{$L('已邮箱认证')}}</Option>
<Option value="no">{{$L('未邮箱认证')}}</Option>
</Select>
</div>
</li>
<li class="search-button">
<Tooltip
theme="light"
placement="bottom"
transfer-class-name="search-button-clear"
transfer>
<Button :loading="loadIng > 0" type="primary" icon="ios-search" @click="onSearch">{{$L('搜索')}}</Button>
<div slot="content">
<Button v-if="keyIs" type="text" @click="keyIs=false">{{$L('取消筛选')}}</Button>
<Button v-else :loading="loadIng > 0" type="text" @click="getLists">{{$L('刷新')}}</Button>
</div>
</Tooltip>
</li>
</ul>
</div>
<div class="table-page-box">
<Table
:columns="columns"
:data="list"
:loading="loadIng > 0"
:no-data-text="$L(noText)"
stripe/>
<Page
:total="total"
:current="page"
:page-size="pageSize"
:disabled="loadIng > 0"
:simple="windowSmall"
:page-size-opts="[10,20,30,50,100]"
show-elevator
show-sizer
show-total
@on-change="setPage"
@on-page-size-change="setPageSize"/>
</div>
</div>
</div> </div>
<!--新建部门修改部门-->
<Modal
v-model="departmentShow"
:title="$L(departmentData.id > 0 ? '修改部门' : '新建部门')"
:mask-closable="false">
<Form ref="addProject" :model="departmentData" label-width="auto" @submit.native.prevent>
<FormItem prop="name" :label="$L('部门名称')">
<Input type="text" v-model="departmentData.name" :placeholder="$L('请输入部门名称')"></Input>
</FormItem>
<FormItem prop="parent_id" :label="$L('上级部门')">
<Select v-model="departmentData.parent_id" :disabled="departmentParentDisabled" :placeholder="$L('请选择上级部门')">
<Option :value="0">{{ $L('默认部门') }}</Option>
<Option v-for="(item, index) in departmentList" v-if="item.parent_id == 0 && item.id != departmentData.id" :value="item.id" :key="index" :label="item.name">&nbsp;&nbsp;&nbsp;&nbsp;{{ item.name }}</Option>
</Select>
<div v-if="departmentParentDisabled" class="form-tip" style="margin-bottom:-16px">{{$L('含有子部门无法修改上级部门')}}</div>
</FormItem>
<FormItem prop="owner_userid" :label="$L('部门负责人')">
<UserInput v-model="departmentData.owner_userid" :multiple-max="1" :placeholder="$L('请选择部门负责人')"/>
</FormItem>
</Form>
<div slot="footer" class="adaption">
<Button type="default" @click="departmentShow=false">{{$L('取消')}}</Button>
<Button type="primary" :loading="departmentLoading > 0" @click="onSaveDepartment">{{$L(departmentData.id > 0 ? '保存' : '新建')}}</Button>
</div>
</Modal>
<!--操作离职--> <!--操作离职-->
<Modal <Modal
v-model="disableShow" v-model="disableShow"
@ -128,7 +203,6 @@
</template> </template>
<script> <script>
import {mapState} from "vuex";
import UserInput from "../../../components/UserInput"; import UserInput from "../../../components/UserInput";
export default { export default {
@ -152,10 +226,21 @@ export default {
disableShow: false, disableShow: false,
disableLoading: 0, disableLoading: 0,
disableData: {}, disableData: {},
departmentShow: false,
departmentLoading: 0,
departmentData: {
id: 0,
name: '',
parent_id: 0,
owner_userid: []
},
departmentList: [],
} }
}, },
mounted() { mounted() {
this.getLists(); this.getLists();
this.getDepartmentLists();
}, },
watch: { watch: {
keyIs(v) { keyIs(v) {
@ -165,6 +250,11 @@ export default {
} }
} }
}, },
computed: {
departmentParentDisabled() {
return !!(this.departmentData.id > 0 && this.departmentList.find(({parent_id}) => parent_id == this.departmentData.id));
},
},
methods: { methods: {
initLanguage() { initLanguage() {
this.columns = [ this.columns = [
@ -541,6 +631,95 @@ export default {
} }
}) })
}) })
},
getDepartmentLists() {
this.departmentLoading++;
this.$store.dispatch("call", {
url: 'users/department/list',
}).then(({data}) => {
this.departmentList = []
this.generateDepartmentList(data, 0, 1)
}).finally(_ => {
this.departmentLoading--;
})
},
generateDepartmentList(data, parent_id, level) {
data.some(item => {
if (item.parent_id == parent_id) {
this.departmentList.push(Object.assign(item, {
level: level + 1
}))
this.generateDepartmentList(data, item.id, level + 1)
}
})
},
onShowDepartment(data) {
this.departmentData = Object.assign({
id: 0,
name: '',
parent_id: 0,
owner_userid: []
}, data || {})
this.departmentShow = true
},
onSaveDepartment() {
this.departmentLoading++;
this.$store.dispatch("call", {
url: 'users/department/add',
data: Object.assign(this.departmentData, {
owner_userid: this.departmentData.owner_userid[0],
}),
}).then(({msg}) => {
$A.messageSuccess(msg)
this.getDepartmentLists()
this.departmentShow = false
}).catch(({msg}) => {
$A.modalError(msg);
}).finally(_ => {
this.departmentLoading--;
})
},
onOpDepartment(val) {
if ($A.leftExists(val, 'add_')) {
this.onShowDepartment({
parent_id: parseInt(val.substr(4))
})
} else if ($A.leftExists(val, 'edit_')) {
const editItem = this.departmentList.find(({id}) => id === parseInt(val.substr(5)))
if (editItem) {
this.onShowDepartment(editItem)
}
} else if ($A.leftExists(val, 'del_')) {
const delItem = this.departmentList.find(({id}) => id === parseInt(val.substr(4)))
if (delItem) {
$A.modalConfirm({
title: this.$L('删除部门'),
content: `<div>${this.$L(`你确定要删除【${delItem.name}】部门吗?`)}</div><div style="color:#f00;font-weight:600">${this.$L(`注意:此操作不可恢复,部门下的成员将向上移动。`)}</div>`,
language: false,
loading: true,
onOk: () => {
return new Promise((resolve, reject) => {
this.$store.dispatch("call", {
url: 'users/department/del',
data: {
id: delItem.id
},
}).then(({msg}) => {
resolve(msg);
this.getDepartmentLists();
}).catch(({msg}) => {
reject(msg);
})
})
}
});
}
}
} }
} }
} }

View File

@ -31,6 +31,77 @@
} }
} }
.management-box {
flex: 1;
display: flex;
height: 0;
.management-department {
width: 239px;
border-right: 1px solid #efefef;
flex-shrink: 0;
display: flex;
flex-direction: column;
> ul {
flex: 1;
overflow: auto;
> li {
list-style: none;
padding: 0;
margin: 0;
height: 40px;
display: flex;
align-items: center;
cursor: pointer;
&:hover,
&.active {
background-color: #ecf5ff;
}
&.level-1 {
font-weight: 500;
}
&.level-2 {
margin-left: 24px;
}
&.level-3 {
margin-left: 48px;
}
&.level-4 {
margin-left: 72px;
}
.department-icon {
padding: 8px;
font-size: 16px;
}
.department-title {
flex: 1;
flex-shrink: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.department-menu {
display: inline-block;
padding: 12px;
font-size: 16px;
transform: rotate(-90deg);
}
}
}
.department-buttons {
margin-top: 20px;
display: flex;
align-items: center;
justify-content: center;
}
}
.management-user {
flex: 1;
display: flex;
flex-direction: column;
padding-left: 20px;
}
}
.team-email { .team-email {
display: flex; display: flex;
align-items: center; align-items: center;