mirror of
https://github.com/kuaifan/dootask.git
synced 2026-06-11 18:02:22 +00:00
feat(team): 团队管理支持标记成员邮箱认证状态
- 成员行操作菜单新增「标记邮箱为已认证/未认证」,复用 users.email_verity 字段与 api/users/operation 接口,新增 setverity/clearverity 操作类型 - 创建用户:邮箱下方新增「标记邮箱为已认证」复选框(默认勾选), 「首次登录需改密」复选框移到初始密码下方 - 批量导入:预览列表邮箱右侧显示主题色已认证图标(错误行不显示), 支持勾选行后批量标记已/未认证;部门与认证批量行加标签对齐、 三个批量按钮样式随选中状态统一 - createByAdmin 新增 emailVerity 选项,createuser/import 透传逐行认证状态 - 新增导入预览默认认证状态单测 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
08ed396444
commit
8c809bbff1
@ -1094,6 +1094,8 @@ class UsersController extends AbstractController
|
||||
* - clearadmin 取消管理员
|
||||
* - settemp 设为临时帐号
|
||||
* - cleartemp 取消临时身份(取消临时帐号)
|
||||
* - setverity 标记邮箱为已认证
|
||||
* - clearverity 标记邮箱为未认证
|
||||
* - checkin_macs 修改自动签到mac地址(需要参数 checkin_macs)
|
||||
* - checkin_face 修改签到人脸图片(需要参数 checkin_face)
|
||||
* - department 修改部门(需要参数 department)
|
||||
@ -1157,6 +1159,16 @@ class UsersController extends AbstractController
|
||||
$upArray['identity'] = array_diff($userInfo->identity, ['temp']);
|
||||
break;
|
||||
|
||||
case 'setverity':
|
||||
$msg = '设置成功';
|
||||
$upArray['email_verity'] = 1;
|
||||
break;
|
||||
|
||||
case 'clearverity':
|
||||
$msg = '取消成功';
|
||||
$upArray['email_verity'] = 0;
|
||||
break;
|
||||
|
||||
case 'checkin_macs':
|
||||
$list = is_array($data['checkin_macs']) ? $data['checkin_macs'] : [];
|
||||
$array = [];
|
||||
@ -1354,6 +1366,7 @@ class UsersController extends AbstractController
|
||||
* @apiParam {String} email 邮箱
|
||||
* @apiParam {String} password 初始密码
|
||||
* @apiParam {String} nickname 昵称
|
||||
* @apiParam {Number} [email_verity] 是否标记邮箱为已认证(1是、0否,默认1)
|
||||
* @apiParam {String} [profession] 职位/职称(可选,2-20字)
|
||||
* @apiParam {Array} [department] 部门ID列表(可选,最多10个)
|
||||
*/
|
||||
@ -1364,10 +1377,12 @@ class UsersController extends AbstractController
|
||||
$password = trim(Request::input('password'));
|
||||
$nickname = trim(Request::input('nickname'));
|
||||
$changePass = intval(Request::input('changepass', 1)) === 1;
|
||||
$emailVerity = intval(Request::input('email_verity', 1)) === 1;
|
||||
$profession = trim((string)Request::input('profession', ''));
|
||||
$department = Request::input('department', []);
|
||||
$user = User::createByAdmin($email, $password, $nickname, [
|
||||
'changePass' => $changePass,
|
||||
'emailVerity' => $emailVerity,
|
||||
'profession' => $profession,
|
||||
'department' => is_array($department) ? $department : [],
|
||||
]);
|
||||
@ -1405,7 +1420,7 @@ class UsersController extends AbstractController
|
||||
/**
|
||||
* @api {post} api/users/import 批量导入用户(管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份(管理员)。提交预览确认后的行数据 rows(每行 {email,nickname,password,profession},可选 department[])进行创建
|
||||
* @apiDescription 需要token身份(管理员)。提交预览确认后的行数据 rows(每行 {email,nickname,password,profession},可选 department[]、email_verity(1已认证/0未认证,默认0))进行创建
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName import
|
||||
|
||||
@ -432,7 +432,7 @@ class User extends AbstractModel
|
||||
* @param string $email
|
||||
* @param string $password
|
||||
* @param string $nickname
|
||||
* @param array $options changePass(bool,默认true) / department(int[]) / profession(string)
|
||||
* @param array $options changePass(bool,默认true) / emailVerity(bool,默认false,标记邮箱已认证) / department(int[]) / profession(string)
|
||||
* @return self
|
||||
* @throws ApiException
|
||||
*/
|
||||
@ -443,6 +443,7 @@ class User extends AbstractModel
|
||||
throw new ApiException('昵称需为2-20个字');
|
||||
}
|
||||
$changePass = ($options['changePass'] ?? true) ? 1 : 0;
|
||||
$emailVerity = ($options['emailVerity'] ?? false) ? 1 : 0;
|
||||
$profession = trim((string)($options['profession'] ?? ''));
|
||||
// 校验前置(reg 之前快速失败,且可在无 Swoole 环境单测)
|
||||
self::assertValidProfession($profession);
|
||||
@ -454,6 +455,7 @@ class User extends AbstractModel
|
||||
$user->identity = Base::arrayImplode(array_diff($user->identity, ['temp']));
|
||||
}
|
||||
$user->changepass = $changePass; // 复用现有首登强制改密机制
|
||||
$user->email_verity = $emailVerity; // 管理员可在创建时直接标记邮箱认证状态
|
||||
if ($profession !== '') {
|
||||
$user->profession = $profession;
|
||||
}
|
||||
@ -619,6 +621,7 @@ class User extends AbstractModel
|
||||
try {
|
||||
self::createByAdmin($row['email'], $row['password'], $row['nickname'], [
|
||||
'changePass' => $changePass,
|
||||
'emailVerity' => !empty($row['email_verity']),
|
||||
'department' => $row['department'] ?? [],
|
||||
'profession' => $row['profession'] ?? '',
|
||||
]);
|
||||
@ -692,6 +695,7 @@ class User extends AbstractModel
|
||||
'nickname' => $row['nickname'] ?? '',
|
||||
'password' => $row['password'] ?? '',
|
||||
'profession' => $row['profession'] ?? '',
|
||||
'email_verity' => 1, // 默认标记为已认证,前端可在预览中按行调整
|
||||
'status' => $ok ? 'ok' : 'error',
|
||||
'reason' => $reason ?? '',
|
||||
];
|
||||
|
||||
@ -1668,6 +1668,12 @@ WiFi签到延迟时长为±1分钟。
|
||||
|
||||
你确定将【(*)】设为管理员吗?
|
||||
你确定取消【(*)】管理员身份吗?
|
||||
你确定将【(*)】的邮箱标记为已认证吗?
|
||||
你确定将【(*)】的邮箱标记为未认证吗?
|
||||
标记邮箱为已认证
|
||||
标记邮箱为未认证
|
||||
标记选中(*)项为已认证
|
||||
标记选中(*)项为未认证
|
||||
|
||||
你确定要取消任务时间吗?
|
||||
更新子任务
|
||||
|
||||
@ -7,12 +7,14 @@
|
||||
<Form ref="form" :model="formData" :label-width="80" @submit.native.prevent>
|
||||
<FormItem :label="$L('邮箱')" required>
|
||||
<Input v-model="formData.email" :placeholder="$L('请输入邮箱')" clearable/>
|
||||
<Checkbox v-model="formData.email_verity" style="margin-top:8px">{{$L('标记邮箱为已认证')}}</Checkbox>
|
||||
</FormItem>
|
||||
<FormItem :label="$L('昵称')" required>
|
||||
<Input v-model="formData.nickname" :placeholder="$L('请输入昵称')" clearable/>
|
||||
</FormItem>
|
||||
<FormItem :label="$L('初始密码')" required>
|
||||
<Input v-model="formData.password" type="password" password :placeholder="$L('请输入初始密码')" clearable/>
|
||||
<Checkbox v-model="formData.changepass" style="margin-top:8px">{{$L('员工首次登录需修改密码')}}</Checkbox>
|
||||
</FormItem>
|
||||
<FormItem :label="$L('职位')">
|
||||
<Input v-model="formData.profession" :maxlength="20" :placeholder="$L('请输入职位/职称')" clearable/>
|
||||
@ -33,9 +35,6 @@
|
||||
</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Checkbox v-model="formData.changepass">{{$L('员工首次登录需修改密码')}}</Checkbox>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<div slot="footer">
|
||||
<Button type="default" @click="show=false">{{$L('取消')}}</Button>
|
||||
@ -61,14 +60,14 @@ export default {
|
||||
return {
|
||||
show: false,
|
||||
loading: false,
|
||||
formData: {email: '', nickname: '', password: '', changepass: true, profession: '', department: []},
|
||||
formData: {email: '', nickname: '', password: '', changepass: true, email_verity: true, profession: '', department: []},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
this.show = val;
|
||||
if (val) {
|
||||
this.formData = {email: '', nickname: '', password: '', changepass: true, profession: '', department: []};
|
||||
this.formData = {email: '', nickname: '', password: '', changepass: true, email_verity: true, profession: '', department: []};
|
||||
}
|
||||
},
|
||||
show(val) {
|
||||
@ -115,7 +114,7 @@ export default {
|
||||
this.show = false;
|
||||
},
|
||||
onSubmit() {
|
||||
const {email, nickname, password, changepass, profession, department} = this.formData;
|
||||
const {email, nickname, password, changepass, email_verity, profession, department} = this.formData;
|
||||
if (!email || !nickname || !password) {
|
||||
$A.messageWarning('邮箱、昵称、初始密码均为必填');
|
||||
return;
|
||||
@ -123,7 +122,7 @@ export default {
|
||||
this.loading = true;
|
||||
this.$store.dispatch("call", {
|
||||
url: 'users/createuser',
|
||||
data: {email, nickname, password, changepass: changepass ? 1 : 0, profession, department},
|
||||
data: {email, nickname, password, changepass: changepass ? 1 : 0, email_verity: email_verity ? 1 : 0, profession, department},
|
||||
}).then(() => {
|
||||
this.loading = false;
|
||||
$A.messageSuccess('创建成功');
|
||||
|
||||
@ -38,6 +38,7 @@
|
||||
@on-selection-change="onSelectionChange"/>
|
||||
<!-- 勾选行后批量设置部门(错误行不可勾选) -->
|
||||
<div class="import-setdept">
|
||||
<span class="import-batch-label">{{$L('所属部门')}}</span>
|
||||
<Select
|
||||
v-model="setDepartmentIds"
|
||||
multiple
|
||||
@ -54,10 +55,20 @@
|
||||
<div :class="`department-level-name level-${item.level - 1}`">{{ item.name }}</div>
|
||||
</Option>
|
||||
</Select>
|
||||
<Button type="primary" :disabled="selectedRows.length === 0" @click="onApplyDepartment">
|
||||
<Button :type="selectedRows.length === 0 ? 'default' : 'primary'" :disabled="selectedRows.length === 0" @click="onApplyDepartment">
|
||||
{{$L('设置部门到选中(*)项', selectedRows.length)}}
|
||||
</Button>
|
||||
</div>
|
||||
<!-- 勾选行后批量设置邮箱认证状态(错误行不可勾选) -->
|
||||
<div class="import-setverity">
|
||||
<span class="import-batch-label">{{$L('邮箱认证')}}</span>
|
||||
<Button :type="selectedRows.length === 0 ? 'default' : 'primary'" :disabled="selectedRows.length === 0" @click="onApplyVerity(1)">
|
||||
{{$L('标记选中(*)项为已认证', selectedRows.length)}}
|
||||
</Button>
|
||||
<Button :type="selectedRows.length === 0 ? 'default' : 'primary'" :disabled="selectedRows.length === 0" @click="onApplyVerity(0)">
|
||||
{{$L('标记选中(*)项为未认证', selectedRows.length)}}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="import-option">
|
||||
<Checkbox v-model="changepass">{{$L('员工首次登录需修改密码')}}</Checkbox>
|
||||
</div>
|
||||
@ -117,7 +128,22 @@ export default {
|
||||
previewColumns: [
|
||||
{type: 'selection', width: 50, align: 'center'},
|
||||
{title: this.$L('行号'), key: 'line', width: 64, align: 'center'},
|
||||
{title: this.$L('邮箱'), minWidth: 150, render: (h, {row}) => h('AutoTip', row.email || '-')},
|
||||
{
|
||||
title: this.$L('邮箱'),
|
||||
minWidth: 150,
|
||||
render: (h, {row}) => {
|
||||
// 列渲染发生在 Table 的上下文,scoped 样式不生效,故图标颜色/布局内联(与会员列表 $primary-color 一致)
|
||||
const arr = [h('AutoTip', {style: {minWidth: '50px'}}, row.email || '-')];
|
||||
if (row.email_verity && row.status === 'ok') {
|
||||
arr.push(h('Icon', {
|
||||
props: {type: 'md-mail'},
|
||||
attrs: {title: this.$L('已邮箱认证')},
|
||||
style: {color: '#84C56A', marginLeft: '6px', fontSize: '16px', flexShrink: 0},
|
||||
}));
|
||||
}
|
||||
return h('div', {style: {display: 'flex', alignItems: 'center'}}, arr);
|
||||
}
|
||||
},
|
||||
{title: this.$L('昵称'), width: 90, render: (h, {row}) => h('AutoTip', row.nickname || '-')},
|
||||
{
|
||||
title: this.$L('初始密码'),
|
||||
@ -254,6 +280,7 @@ export default {
|
||||
const data = res.data;
|
||||
(data.rows || []).forEach(row => {
|
||||
this.$set(row, 'department', []); // 逐行部门,默认空
|
||||
this.$set(row, 'email_verity', row.email_verity ? 1 : 0); // 逐行邮箱认证,默认已认证
|
||||
if (row.status !== 'ok') {
|
||||
this.$set(row, '_disabled', true); // 错误行不可勾选
|
||||
}
|
||||
@ -280,19 +307,32 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
onApplyVerity(verity) {
|
||||
if (this.selectedRows.length === 0) {
|
||||
return;
|
||||
}
|
||||
// 与 onApplyDepartment 一致:按唯一 line 匹配回 preview.rows 的原始对象再写入
|
||||
const selectedLines = new Set(this.selectedRows.map(row => row.line));
|
||||
(this.preview && this.preview.rows ? this.preview.rows : []).forEach(row => {
|
||||
if (selectedLines.has(row.line)) {
|
||||
this.$set(row, 'email_verity', verity ? 1 : 0);
|
||||
}
|
||||
});
|
||||
},
|
||||
onConfirmImport() {
|
||||
if (!this.preview || this.preview.valid === 0) {
|
||||
return;
|
||||
}
|
||||
const rows = this.preview.rows
|
||||
.filter(row => row.status === 'ok')
|
||||
.map(({line, email, nickname, password, profession, department}) => ({
|
||||
.map(({line, email, nickname, password, profession, department, email_verity}) => ({
|
||||
line,
|
||||
email,
|
||||
nickname,
|
||||
password,
|
||||
profession: profession || '',
|
||||
department: Array.isArray(department) ? department : [],
|
||||
email_verity: email_verity ? 1 : 0,
|
||||
}));
|
||||
this.importing = true;
|
||||
this.$store.dispatch("call", {
|
||||
@ -336,15 +376,26 @@ export default {
|
||||
.import-tip { color: #808695; margin-bottom: 12px; }
|
||||
.import-actions { display: flex; gap: 12px; align-items: center; }
|
||||
.import-option { margin-top: 12px; }
|
||||
.import-batch-label {
|
||||
flex-shrink: 0;
|
||||
min-width: 64px;
|
||||
color: #515a6e;
|
||||
}
|
||||
.import-setdept {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
.import-setdept-select {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
.import-setverity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.import-preview { margin-top: 16px; }
|
||||
.import-result { margin-top: 16px; }
|
||||
|
||||
|
||||
@ -538,7 +538,7 @@ export default {
|
||||
align: 'center',
|
||||
width: 100,
|
||||
render: (h, params) => {
|
||||
const identity = params.row.identity;
|
||||
const {identity, email_verity} = params.row;
|
||||
const dropdownItems = [];
|
||||
dropdownItems.push(h('EDropdownItem', {
|
||||
props: {
|
||||
@ -573,6 +573,20 @@ export default {
|
||||
},
|
||||
}, [h('div', this.$L('设为临时帐号'))]));
|
||||
}
|
||||
// 邮箱认证状态
|
||||
if (email_verity) {
|
||||
dropdownItems.push(h('EDropdownItem', {
|
||||
props: {
|
||||
command: 'clearverity',
|
||||
},
|
||||
}, [h('div', this.$L('标记邮箱为未认证'))]));
|
||||
} else {
|
||||
dropdownItems.push(h('EDropdownItem', {
|
||||
props: {
|
||||
command: 'setverity',
|
||||
},
|
||||
}, [h('div', this.$L('标记邮箱为已认证'))]));
|
||||
}
|
||||
// 编辑用户信息
|
||||
dropdownItems.push(h('EDropdownItem', {
|
||||
props: {
|
||||
@ -961,6 +975,32 @@ export default {
|
||||
});
|
||||
break;
|
||||
|
||||
case 'setverity':
|
||||
$A.modalConfirm({
|
||||
content: `你确定将【ID:${row.userid}, ${row.nickname}】的邮箱标记为已认证吗?`,
|
||||
loading: true,
|
||||
onOk: () => {
|
||||
return this.operationUser({
|
||||
userid: row.userid,
|
||||
type: name
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'clearverity':
|
||||
$A.modalConfirm({
|
||||
content: `你确定将【ID:${row.userid}, ${row.nickname}】的邮箱标记为未认证吗?`,
|
||||
loading: true,
|
||||
onOk: () => {
|
||||
return this.operationUser({
|
||||
userid: row.userid,
|
||||
type: name
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'setdisable':
|
||||
this.disableData = {
|
||||
type: 'setdisable',
|
||||
|
||||
@ -141,6 +141,20 @@ class AdminCreateUserTest extends TestCase
|
||||
$this->assertSame(0, User::whereEmail('newp1@test.local')->count());
|
||||
}
|
||||
|
||||
public function test_import_preview_defaults_email_verity_to_verified()
|
||||
{
|
||||
// 预览默认逐行标记为已认证(email_verity=1),前端可再按行调整
|
||||
$rows = [
|
||||
['line' => 2, 'email' => 'verity1@test.local', 'nickname' => '张三', 'password' => 'Abc123456'],
|
||||
['line' => 3, 'email' => 'bad', 'nickname' => '李四', 'password' => 'Abc123456'], // 错误行同样带默认值
|
||||
];
|
||||
|
||||
$preview = User::importPreview($rows);
|
||||
|
||||
$this->assertSame(1, $preview['rows'][0]['email_verity']);
|
||||
$this->assertSame(1, $preview['rows'][1]['email_verity']);
|
||||
}
|
||||
|
||||
public function test_import_collects_all_invalid_rows_without_creating()
|
||||
{
|
||||
// 全部非法 → 不触发 createByAdmin/SO,可在无 Swoole 环境稳定运行
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user