From 8c809bbff14ed80c2519800a2b620d9e570f27c0 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Tue, 2 Jun 2026 05:49:33 +0000 Subject: [PATCH] =?UTF-8?q?feat(team):=20=E5=9B=A2=E9=98=9F=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=94=AF=E6=8C=81=E6=A0=87=E8=AE=B0=E6=88=90=E5=91=98?= =?UTF-8?q?=E9=82=AE=E7=AE=B1=E8=AE=A4=E8=AF=81=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 成员行操作菜单新增「标记邮箱为已认证/未认证」,复用 users.email_verity 字段与 api/users/operation 接口,新增 setverity/clearverity 操作类型 - 创建用户:邮箱下方新增「标记邮箱为已认证」复选框(默认勾选), 「首次登录需改密」复选框移到初始密码下方 - 批量导入:预览列表邮箱右侧显示主题色已认证图标(错误行不显示), 支持勾选行后批量标记已/未认证;部门与认证批量行加标签对齐、 三个批量按钮样式随选中状态统一 - createByAdmin 新增 emailVerity 选项,createuser/import 透传逐行认证状态 - 新增导入预览默认认证状态单测 Co-Authored-By: Claude Opus 4.8 (1M context) --- app/Http/Controllers/Api/UsersController.php | 17 +++++- app/Models/User.php | 6 +- language/original-web.txt | 6 ++ .../manage/components/CreateUserModal.vue | 13 ++-- .../manage/components/ImportUserModal.vue | 59 +++++++++++++++++-- .../manage/components/TeamManagement.vue | 42 ++++++++++++- tests/Feature/AdminCreateUserTest.php | 14 +++++ 7 files changed, 143 insertions(+), 14 deletions(-) diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 8479b4a27..9228d27e0 100755 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -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 diff --git a/app/Models/User.php b/app/Models/User.php index e1c79840a..6452c41d1 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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 ?? '', ]; diff --git a/language/original-web.txt b/language/original-web.txt index 9d15d1d59..e9629988b 100644 --- a/language/original-web.txt +++ b/language/original-web.txt @@ -1668,6 +1668,12 @@ WiFi签到延迟时长为±1分钟。 你确定将【(*)】设为管理员吗? 你确定取消【(*)】管理员身份吗? +你确定将【(*)】的邮箱标记为已认证吗? +你确定将【(*)】的邮箱标记为未认证吗? +标记邮箱为已认证 +标记邮箱为未认证 +标记选中(*)项为已认证 +标记选中(*)项为未认证 你确定要取消任务时间吗? 更新子任务 diff --git a/resources/assets/js/pages/manage/components/CreateUserModal.vue b/resources/assets/js/pages/manage/components/CreateUserModal.vue index ac5c4a16b..a883f33cd 100644 --- a/resources/assets/js/pages/manage/components/CreateUserModal.vue +++ b/resources/assets/js/pages/manage/components/CreateUserModal.vue @@ -7,12 +7,14 @@
+ {{$L('标记邮箱为已认证')}} + {{$L('员工首次登录需修改密码')}} @@ -33,9 +35,6 @@ - - {{$L('员工首次登录需修改密码')}} -
@@ -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('创建成功'); diff --git a/resources/assets/js/pages/manage/components/ImportUserModal.vue b/resources/assets/js/pages/manage/components/ImportUserModal.vue index be8288114..09e44b539 100644 --- a/resources/assets/js/pages/manage/components/ImportUserModal.vue +++ b/resources/assets/js/pages/manage/components/ImportUserModal.vue @@ -38,6 +38,7 @@ @on-selection-change="onSelectionChange"/>
+ {{$L('所属部门')}} -
+ +
+ {{$L('邮箱认证')}} + + +
{{$L('员工首次登录需修改密码')}}
@@ -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; } diff --git a/resources/assets/js/pages/manage/components/TeamManagement.vue b/resources/assets/js/pages/manage/components/TeamManagement.vue index 856864377..767e46e7a 100644 --- a/resources/assets/js/pages/manage/components/TeamManagement.vue +++ b/resources/assets/js/pages/manage/components/TeamManagement.vue @@ -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', diff --git a/tests/Feature/AdminCreateUserTest.php b/tests/Feature/AdminCreateUserTest.php index 943c39cc9..155db7827 100644 --- a/tests/Feature/AdminCreateUserTest.php +++ b/tests/Feature/AdminCreateUserTest.php @@ -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 环境稳定运行