feat: 通过链接邀请加入项目
This commit is contained in:
parent
0665f2de5f
commit
dd86bb88c6
@ -6,6 +6,7 @@ use App\Exceptions\ApiException;
|
||||
use App\Models\AbstractModel;
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectColumn;
|
||||
use App\Models\ProjectInvite;
|
||||
use App\Models\ProjectLog;
|
||||
use App\Models\ProjectTask;
|
||||
use App\Models\ProjectTaskFile;
|
||||
@ -258,6 +259,110 @@ class ProjectController extends AbstractController
|
||||
return Base::retSuccess('修改成功', ['id' => $project->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邀请链接(限:项目负责人)
|
||||
*
|
||||
* @apiParam {Number} project_id 项目ID
|
||||
* @apiParam {String} refresh 刷新链接
|
||||
* - no: 只获取(默认)
|
||||
* - yes: 刷新链接,之前的将失效
|
||||
*/
|
||||
public function invite()
|
||||
{
|
||||
User::auth();
|
||||
//
|
||||
$project_id = intval(Request::input('project_id'));
|
||||
$refresh = Request::input('refresh', 'no');
|
||||
//
|
||||
$project = Project::userProject($project_id);
|
||||
if (!$project->owner) {
|
||||
return Base::retError('仅限项目负责人查看');
|
||||
}
|
||||
//
|
||||
$invite = Base::settingFind('system', 'project_invite');
|
||||
if ($invite == 'close') {
|
||||
return Base::retError('未开放此功能');
|
||||
}
|
||||
//
|
||||
$projectInvite = ProjectInvite::whereProjectId($project->id)->first();
|
||||
if (empty($projectInvite)) {
|
||||
$projectInvite = ProjectInvite::createInstance([
|
||||
'project_id' => $project->id,
|
||||
'code' => Base::generatePassword(64),
|
||||
]);
|
||||
$projectInvite->save();
|
||||
} else {
|
||||
if ($refresh == 'yes') {
|
||||
$projectInvite->code = Base::generatePassword(64);
|
||||
$projectInvite->save();
|
||||
}
|
||||
}
|
||||
return Base::retSuccess('success', [
|
||||
'url' => Base::fillUrl('manage/project/invite?code=' . $projectInvite->code),
|
||||
'num' => $projectInvite->num
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过邀请链接code获取项目信息
|
||||
*
|
||||
* @apiParam {String} code
|
||||
*/
|
||||
public function invite__info()
|
||||
{
|
||||
User::auth();
|
||||
//
|
||||
$code = Request::input('code');
|
||||
//
|
||||
$invite = Base::settingFind('system', 'project_invite');
|
||||
if ($invite == 'close') {
|
||||
return Base::retError('未开放此功能');
|
||||
}
|
||||
//
|
||||
$projectInvite = ProjectInvite::with(['project'])->whereCode($code)->first();
|
||||
if (empty($projectInvite)) {
|
||||
return Base::retError('邀请code不存在');
|
||||
}
|
||||
return Base::retSuccess('success', $projectInvite);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过邀请链接code加入项目
|
||||
*
|
||||
* @apiParam {String} code
|
||||
*/
|
||||
public function invite__join()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$code = Request::input('code');
|
||||
//
|
||||
$invite = Base::settingFind('system', 'project_invite');
|
||||
if ($invite == 'close') {
|
||||
return Base::retError('未开放此功能');
|
||||
}
|
||||
//
|
||||
$projectInvite = ProjectInvite::with(['project'])->whereCode($code)->first();
|
||||
if (empty($projectInvite)) {
|
||||
return Base::retError('邀请code不存在');
|
||||
}
|
||||
if ($projectInvite->already) {
|
||||
return Base::retSuccess('已加入', $projectInvite);
|
||||
}
|
||||
if (!$projectInvite->project?->joinProject($user->userid)) {
|
||||
return Base::retError('加入失败,请稍后再试');
|
||||
}
|
||||
$projectInvite->num++;
|
||||
$projectInvite->save();
|
||||
//
|
||||
$projectInvite->project->syncDialogUser();
|
||||
$projectInvite->project->addLog("会员ID:" . $user->userid . " 通过邀请链接加入项目");
|
||||
//
|
||||
$data = $projectInvite->toArray();
|
||||
$data['already'] = true;
|
||||
return Base::retSuccess('加入成功', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移交项目(限:项目负责人)
|
||||
*
|
||||
|
@ -24,7 +24,8 @@ class SystemController extends AbstractController
|
||||
*
|
||||
* @apiParam {String} type
|
||||
* - get: 获取(默认)
|
||||
* - save: 保存设置(参数:reg、login_code、password_policy、chat_nickname)
|
||||
* - all: 获取所有(需要管理员权限)
|
||||
* - save: 保存设置(参数:reg、reg_invite、login_code、password_policy、project_invite、chat_nickname)
|
||||
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@ -40,7 +41,7 @@ class SystemController extends AbstractController
|
||||
User::auth('admin');
|
||||
$all = Request::input();
|
||||
foreach ($all AS $key => $value) {
|
||||
if (!in_array($key, ['reg', 'login_code', 'password_policy', 'chat_nickname'])) {
|
||||
if (!in_array($key, ['reg', 'reg_invite', 'login_code', 'password_policy', 'project_invite', 'chat_nickname'])) {
|
||||
unset($all[$key]);
|
||||
}
|
||||
}
|
||||
@ -49,9 +50,17 @@ class SystemController extends AbstractController
|
||||
$setting = Base::setting('system');
|
||||
}
|
||||
//
|
||||
if ($type == 'all') {
|
||||
User::auth('admin');
|
||||
$setting['reg_invite'] = $setting['reg_invite'] ?: Base::generatePassword(8);
|
||||
} else {
|
||||
if (isset($setting['reg_invite'])) unset($setting['reg_invite']);
|
||||
}
|
||||
//
|
||||
$setting['reg'] = $setting['reg'] ?: 'open';
|
||||
$setting['login_code'] = $setting['login_code'] ?: 'auto';
|
||||
$setting['password_policy'] = $setting['password_policy'] ?: 'simple';
|
||||
$setting['project_invite'] = $setting['project_invite'] ?: 'open';
|
||||
$setting['chat_nickname'] = $setting['chat_nickname'] ?: 'optional';
|
||||
//
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
|
55
app/Models/ProjectInvite.php
Normal file
55
app/Models/ProjectInvite.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* App\Models\ProjectInvite
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $project_id 项目ID
|
||||
* @property int|null $num 累计邀请
|
||||
* @property string|null $code 链接码
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read bool $already
|
||||
* @property-read \App\Models\Project|null $project
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereCode($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereNum($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereProjectId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ProjectInvite extends AbstractModel
|
||||
{
|
||||
protected $appends = [
|
||||
'already',
|
||||
];
|
||||
|
||||
/**
|
||||
* 是否已加入
|
||||
* @return bool
|
||||
*/
|
||||
public function getAlreadyAttribute()
|
||||
{
|
||||
if (!isset($this->appendattrs['already'])) {
|
||||
$this->appendattrs['already'] = false;
|
||||
if (User::userid()) {
|
||||
$this->appendattrs['already'] = (bool)$this->project?->projectUser?->where('userid', User::userid())->count();
|
||||
}
|
||||
}
|
||||
return $this->appendattrs['already'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function project(): \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
{
|
||||
return $this->hasOne(Project::class, 'id', 'project_id');
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ class SettingsTableSeeder extends Seeder
|
||||
'id' => 1,
|
||||
'name' => 'system',
|
||||
'desc' => '',
|
||||
'setting' => '{"reg":"open","login_code":"auto"}',
|
||||
'setting' => '{"reg":"open","project_invite":"open","login_code":"auto"}',
|
||||
'created_at' => seeders_at('2021-07-01 11:05:06'),
|
||||
'updated_at' => seeders_at('2021-07-01 12:27:12'),
|
||||
),
|
||||
|
@ -34,7 +34,8 @@
|
||||
<EDropdownMenu v-if="projectData.owner_userid === userId" slot="dropdown">
|
||||
<EDropdownItem command="setting">{{$L('项目设置')}}</EDropdownItem>
|
||||
<EDropdownItem command="user">{{$L('成员管理')}}</EDropdownItem>
|
||||
<EDropdownItem command="log">{{$L('项目动态')}}</EDropdownItem>
|
||||
<EDropdownItem command="invite">{{$L('邀请链接')}}</EDropdownItem>
|
||||
<EDropdownItem command="log" divided>{{$L('项目动态')}}</EDropdownItem>
|
||||
<EDropdownItem command="archived_task">{{$L('已归档任务')}}</EDropdownItem>
|
||||
<EDropdownItem command="transfer" divided>{{$L('移交项目')}}</EDropdownItem>
|
||||
<EDropdownItem command="archived">{{$L('归档项目')}}</EDropdownItem>
|
||||
@ -320,7 +321,7 @@
|
||||
v-model="settingShow"
|
||||
:title="$L('项目设置')"
|
||||
:mask-closable="false">
|
||||
<Form ref="addProject" :model="settingData" label-width="auto" @submit.native.prevent>
|
||||
<Form :model="settingData" label-width="auto" @submit.native.prevent>
|
||||
<FormItem prop="name" :label="$L('项目名称')">
|
||||
<Input ref="projectName" type="text" v-model="settingData.name" :maxlength="32" :placeholder="$L('必填')"></Input>
|
||||
</FormItem>
|
||||
@ -339,7 +340,7 @@
|
||||
v-model="userShow"
|
||||
:title="$L('成员管理')"
|
||||
:mask-closable="false">
|
||||
<Form ref="addProject" :model="userData" label-width="auto" @submit.native.prevent>
|
||||
<Form :model="userData" label-width="auto" @submit.native.prevent>
|
||||
<FormItem prop="userids" :label="$L('项目成员')">
|
||||
<UserInput v-if="userShow" v-model="userData.userids" :uncancelable="userData.uncancelable" :multiple-max="100" :placeholder="$L('选择项目成员')"/>
|
||||
</FormItem>
|
||||
@ -369,12 +370,39 @@
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<!--邀请链接-->
|
||||
<Modal
|
||||
v-model="inviteShow"
|
||||
:title="$L('邀请链接')"
|
||||
:mask-closable="false">
|
||||
<Form :model="inviteData" label-width="auto" @submit.native.prevent>
|
||||
<FormItem :label="$L('链接地址')">
|
||||
<Input ref="inviteInput" v-model="inviteData.url" type="textarea" :rows="3" @on-focus="inviteFocus" readonly/>
|
||||
<div class="form-tip">{{$L('可通过此链接注册、加入项目。')}}</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<div slot="footer" class="adaption">
|
||||
<Button type="default" @click="inviteShow=false">{{$L('取消')}}</Button>
|
||||
<Poptip
|
||||
confirm
|
||||
placement="bottom"
|
||||
style="margin-left:8px"
|
||||
@on-ok="inviteGet(true)"
|
||||
transfer>
|
||||
<div slot="title">
|
||||
<p><strong>{{$L('注意:刷新将导致原来的邀请链接失效!')}}</strong></p>
|
||||
</div>
|
||||
<Button type="primary" :loading="inviteLoad > 0">{{$L('刷新')}}</Button>
|
||||
</Poptip>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<!--移交项目-->
|
||||
<Modal
|
||||
v-model="transferShow"
|
||||
:title="$L('移交项目')"
|
||||
:mask-closable="false">
|
||||
<Form ref="addProject" :model="transferData" label-width="auto" @submit.native.prevent>
|
||||
<Form :model="transferData" label-width="auto" @submit.native.prevent>
|
||||
<FormItem prop="owner_userid" :label="$L('项目负责人')">
|
||||
<UserInput v-if="transferShow" v-model="transferData.owner_userid" :multiple-max="1" :placeholder="$L('选择项目负责人')"/>
|
||||
</FormItem>
|
||||
@ -404,6 +432,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import VueClipboard from 'vue-clipboard2'
|
||||
Vue.use(VueClipboard)
|
||||
|
||||
import Draggable from 'vuedraggable'
|
||||
import TaskPriority from "./TaskPriority";
|
||||
import TaskAdd from "./TaskAdd";
|
||||
@ -451,6 +483,10 @@ export default {
|
||||
userData: {},
|
||||
userLoad: 0,
|
||||
|
||||
inviteShow: false,
|
||||
inviteData: {},
|
||||
inviteLoad: 0,
|
||||
|
||||
transferShow: false,
|
||||
transferData: {},
|
||||
transferLoad: 0,
|
||||
@ -1090,6 +1126,12 @@ export default {
|
||||
this.userShow = true;
|
||||
break;
|
||||
|
||||
case "invite":
|
||||
this.inviteData = {};
|
||||
this.inviteShow = true;
|
||||
this.inviteGet()
|
||||
break;
|
||||
|
||||
case "log":
|
||||
this.logShow = true;
|
||||
break;
|
||||
@ -1153,6 +1195,39 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
inviteGet(refresh) {
|
||||
this.inviteLoad++;
|
||||
this.$store.dispatch("call", {
|
||||
url: 'project/invite',
|
||||
data: {
|
||||
project_id: this.projectId,
|
||||
refresh: refresh === true ? 'yes' : 'no'
|
||||
},
|
||||
}).then(({data}) => {
|
||||
this.inviteLoad--;
|
||||
this.inviteData = data;
|
||||
this.inviteCopy();
|
||||
}).catch(({msg}) => {
|
||||
$A.modalError(msg);
|
||||
this.inviteLoad--;
|
||||
});
|
||||
},
|
||||
|
||||
inviteCopy() {
|
||||
if (!this.inviteData.url) {
|
||||
return;
|
||||
}
|
||||
this.$copyText(this.inviteData.url).then(() => {
|
||||
$A.messageSuccess(this.$L('复制成功!'));
|
||||
}, () => {
|
||||
$A.messageError(this.$L('复制失败!'));
|
||||
});
|
||||
},
|
||||
|
||||
inviteFocus() {
|
||||
this.$refs.inviteInput.focus({cursor:'all'});
|
||||
},
|
||||
|
||||
toggleCompleted() {
|
||||
this.$store.dispatch('toggleTablePanel', 'completedTask');
|
||||
this.completeTask = [];
|
||||
|
111
resources/assets/js/pages/manage/projectInvite.vue
Normal file
111
resources/assets/js/pages/manage/projectInvite.vue
Normal file
@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div class="page-invite">
|
||||
<PageTitle :title="$L('加入项目')"/>
|
||||
<div v-if="loadIng > 0" class="invite-load">
|
||||
<Loading/>
|
||||
</div>
|
||||
<div v-else class="invite-warp">
|
||||
<Card v-if="project.id > 0">
|
||||
<p slot="title">{{project.name}}</p>
|
||||
<div v-if="project.desc" class="invite-desc">{{project.desc}}</div>
|
||||
<div v-else>{{$L('暂无介绍')}}</div>
|
||||
<div class="invite-footer">
|
||||
<Button v-if="already" type="success" icon="ios-checkmark-circle-outline" @click="goProject">{{$L('已加入')}}</Button>
|
||||
<Button v-else type="primary" :loading="joinLoad > 0" @click="joinProject">{{$L('加入项目')}}</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<Card v-else>
|
||||
<p>{{$L('邀请地址不存在或已被删除!')}}</p>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-invite {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.invite-warp {
|
||||
.invite-desc {
|
||||
max-width: 460px;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
.invite-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 24px;
|
||||
> button {
|
||||
height: 36px;
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loadIng: 0,
|
||||
joinLoad: 0,
|
||||
|
||||
already: false,
|
||||
project: {},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route': {
|
||||
handler(route) {
|
||||
this.code = route.query ? route.query.code : '';
|
||||
this.getData();
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getData() {
|
||||
this.loadIng++;
|
||||
this.$store.dispatch("call", {
|
||||
url: 'project/invite/info',
|
||||
data: {
|
||||
code: this.code,
|
||||
},
|
||||
}).then(({data}) => {
|
||||
this.loadIng--;
|
||||
this.already = data.already;
|
||||
this.project = data.project;
|
||||
}).catch(() => {
|
||||
this.loadIng--;
|
||||
this.project = {}
|
||||
});
|
||||
},
|
||||
|
||||
joinProject() {
|
||||
this.joinLoad++;
|
||||
this.$store.dispatch("call", {
|
||||
url: 'project/invite/join',
|
||||
data: {
|
||||
code: this.code,
|
||||
},
|
||||
}).then(({data}) => {
|
||||
this.joinLoad--;
|
||||
this.already = data.already;
|
||||
this.project = data.project;
|
||||
this.goProject();
|
||||
}).catch(({msg}) => {
|
||||
this.joinLoad--;
|
||||
$A.modalError(msg);
|
||||
});
|
||||
},
|
||||
|
||||
goProject() {
|
||||
this.$nextTick(() => {
|
||||
this.goForward({path: '/manage/project/' + this.project.id});
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -4,8 +4,16 @@
|
||||
<FormItem :label="$L('允许注册')" prop="reg">
|
||||
<RadioGroup v-model="formDatum.reg">
|
||||
<Radio label="open">{{$L('允许')}}</Radio>
|
||||
<Radio label="invite">{{$L('邀请码')}}</Radio>
|
||||
<Radio label="close">{{$L('禁止')}}</Radio>
|
||||
</RadioGroup>
|
||||
<div v-if="formDatum.reg == 'open'" class="form-tip">{{$L('允许:开放注册功能。')}}</div>
|
||||
<template v-else-if="formDatum.reg == 'invite'">
|
||||
<div class="form-tip">{{$L('邀请码:注册时需填写下方邀请码。')}}</div>
|
||||
<Input v-model="formDatum.reg_invite" style="width:200px;margin-top:6px">
|
||||
<span slot="prepend">{{$L('邀请码')}}</span>
|
||||
</Input>
|
||||
</template>
|
||||
</FormItem>
|
||||
<FormItem :label="$L('登录验证码')" prop="loginCode">
|
||||
<RadioGroup v-model="formDatum.login_code">
|
||||
@ -23,6 +31,13 @@
|
||||
<div v-if="formDatum.password_policy == 'simple'" class="form-tip">{{$L('简单:大于或等于6个字符。')}}</div>
|
||||
<div v-else-if="formDatum.password_policy == 'complex'" class="form-tip">{{$L('复杂:大于或等于6个字符,包含数字、字母大小写或者特殊字符。')}}</div>
|
||||
</FormItem>
|
||||
<FormItem :label="$L('邀请项目')" prop="projectInvite">
|
||||
<RadioGroup v-model="formDatum.project_invite">
|
||||
<Radio label="open">{{$L('开启')}}</Radio>
|
||||
<Radio label="close">{{$L('关闭')}}</Radio>
|
||||
</RadioGroup>
|
||||
<div v-if="formDatum.project_invite == 'open'" class="form-tip">{{$L('开启:项目管理员可生成链接邀请成员加入项目。')}}</div>
|
||||
</FormItem>
|
||||
<FormItem :label="$L('聊天昵称')" prop="chatNickname">
|
||||
<RadioGroup v-model="formDatum.chat_nickname">
|
||||
<Radio label="optional">{{$L('可选')}}</Radio>
|
||||
@ -68,7 +83,7 @@ export default {
|
||||
systemSetting(save) {
|
||||
this.loadIng++;
|
||||
this.$store.dispatch("call", {
|
||||
url: 'system/setting?type=' + (save ? 'save' : 'get'),
|
||||
url: 'system/setting?type=' + (save ? 'save' : 'all'),
|
||||
data: this.formDatum,
|
||||
}).then(({data}) => {
|
||||
if (save) {
|
||||
|
@ -58,6 +58,11 @@ export default [
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'manage-project-invite',
|
||||
path: 'project/invite',
|
||||
component: () => import('./pages/manage/projectInvite.vue'),
|
||||
},
|
||||
{
|
||||
name: 'manage-project',
|
||||
path: 'project/:id',
|
||||
|
@ -1,5 +1,8 @@
|
||||
body {
|
||||
overflow: hidden;
|
||||
.form-tip {
|
||||
color: #999999;
|
||||
}
|
||||
.ivu-input,
|
||||
.ivu-select-selection {
|
||||
border-color: #e8e8e8;
|
||||
|
@ -106,9 +106,6 @@
|
||||
.ivu-form {
|
||||
overflow: auto;
|
||||
}
|
||||
.form-tip {
|
||||
color: #999999;
|
||||
}
|
||||
.setting-color {
|
||||
min-width: 400px;
|
||||
max-width: 600px;
|
||||
|
Loading…
x
Reference in New Issue
Block a user