diff --git a/code/link56/link56-backend/src/main/java/cn/link56/auth/code/model/AuthCodePurpose.java b/code/link56/link56-backend/src/main/java/cn/link56/auth/code/model/AuthCodePurpose.java index d28262e1..f66c33a3 100644 --- a/code/link56/link56-backend/src/main/java/cn/link56/auth/code/model/AuthCodePurpose.java +++ b/code/link56/link56-backend/src/main/java/cn/link56/auth/code/model/AuthCodePurpose.java @@ -6,5 +6,6 @@ package cn.link56.auth.code.model; public enum AuthCodePurpose { REGISTER, // 注册 LOGIN, // 登录 - RESET_PASSWORD // 找回密码 + RESET_PASSWORD, // 找回密码 + UPDATE_EMAIL // 更新邮箱 } diff --git a/code/link56/link56-backend/src/main/java/cn/link56/auth/code/sender/EmailAuthCodeSender.java b/code/link56/link56-backend/src/main/java/cn/link56/auth/code/sender/EmailAuthCodeSender.java index 0209d6ed..fca13229 100644 --- a/code/link56/link56-backend/src/main/java/cn/link56/auth/code/sender/EmailAuthCodeSender.java +++ b/code/link56/link56-backend/src/main/java/cn/link56/auth/code/sender/EmailAuthCodeSender.java @@ -23,12 +23,15 @@ public class EmailAuthCodeSender { if (purpose.equals(AuthCodePurpose.LOGIN)){ subject = "登录"; templateHtml = "Login.html"; - }else if(purpose.equals(AuthCodePurpose.REGISTER)){ + } else if (purpose.equals(AuthCodePurpose.REGISTER)){ subject = "注册"; templateHtml = "Register.html"; - }else if(purpose.equals(AuthCodePurpose.RESET_PASSWORD)){ + } else if (purpose.equals(AuthCodePurpose.RESET_PASSWORD)){ subject = "找回密码"; templateHtml = "ForgetPassword.html"; + } else if (purpose.equals(AuthCodePurpose.UPDATE_EMAIL)){ + subject = "更新认证邮箱"; + templateHtml = "UpdateEmail.html"; } payload.put("Code",code); diff --git a/code/link56/link56-backend/src/main/resources/templates/Login.html b/code/link56/link56-backend/src/main/resources/templates/Login.html index 769bc125..80719e6c 100644 --- a/code/link56/link56-backend/src/main/resources/templates/Login.html +++ b/code/link56/link56-backend/src/main/resources/templates/Login.html @@ -480,7 +480,11 @@

您好

-

您正在登录我们的 link56 平台,系统已为您生成了登录验证码。该验证码仅在 10 分钟内有效。

+

+ 您正在 link56 平台进行登录操作, + 系统已为您生成邮箱验证码。 + 该验证码仅在 10 分钟内有效。 +

[[${Code}]] diff --git a/code/link56/link56-backend/src/main/resources/templates/Register.html b/code/link56/link56-backend/src/main/resources/templates/Register.html index 32c0dd6c..20494120 100644 --- a/code/link56/link56-backend/src/main/resources/templates/Register.html +++ b/code/link56/link56-backend/src/main/resources/templates/Register.html @@ -480,7 +480,12 @@

您好

-

您正在注册我们的 link56 平台,系统已为您生成了注册验证码。该验证码仅在 10 分钟内有效。

+

+ 您正在 link56 平台进行注册操作, + 系统已为您生成邮箱验证码。 + 该验证码仅在 10 分钟内有效。 +

+

[[${Code}]] diff --git a/code/link56/link56-backend/src/main/resources/templates/UpdateEmail.html b/code/link56/link56-backend/src/main/resources/templates/UpdateEmail.html new file mode 100644 index 00000000..ca5725c3 --- /dev/null +++ b/code/link56/link56-backend/src/main/resources/templates/UpdateEmail.html @@ -0,0 +1,526 @@ + + + + + + + + + + + + + + + +Use this link to reset your password. The link is only valid for 24 hours. + + + + + + + + \ No newline at end of file diff --git a/code/link56/link56-frontend-new/src/components/EmailCodeInput.vue b/code/link56/link56-frontend-new/src/components/EmailCodeInput.vue new file mode 100644 index 00000000..9fa81b9c --- /dev/null +++ b/code/link56/link56-frontend-new/src/components/EmailCodeInput.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/code/link56/link56-frontend-new/src/request/api/company/user.ts b/code/link56/link56-frontend-new/src/request/api/company/user.ts index 5fd205d5..7993b2b1 100644 --- a/code/link56/link56-frontend-new/src/request/api/company/user.ts +++ b/code/link56/link56-frontend-new/src/request/api/company/user.ts @@ -1,6 +1,9 @@ -import type { CompanyUser, QueryCompanyUserParams } from '@/types/company/user' +import type { AddCompanyUserParams, CompanyUser, QueryCompanyUserParams } from '@/types/company/user' import { httpGet, httpPostJson } from '@/request/utils' export function queryCompanyUserApi(params: QueryCompanyUserParams) { return httpPostJson>('/company/user/query', params) } +export function addCompanyUserApi(params: AddCompanyUserParams) { + return httpPostJson>('/company/user/add', params) +} diff --git a/code/link56/link56-frontend-new/src/router/index.ts b/code/link56/link56-frontend-new/src/router/index.ts index 0c187dba..190f681d 100644 --- a/code/link56/link56-frontend-new/src/router/index.ts +++ b/code/link56/link56-frontend-new/src/router/index.ts @@ -32,7 +32,7 @@ const router = createRouter({ }, { path: '/settings/info/user', - component: () => import('@/views/settings/info/user.vue'), + component: () => import('@/views/settings/info/user/index.vue'), }, ], }, diff --git a/code/link56/link56-frontend-new/src/types/auth/code.ts b/code/link56/link56-frontend-new/src/types/auth/code.ts index b0e3c709..94406834 100644 --- a/code/link56/link56-frontend-new/src/types/auth/code.ts +++ b/code/link56/link56-frontend-new/src/types/auth/code.ts @@ -7,6 +7,7 @@ export enum AuthCodePurpose { REGISTER = 'REGISTER', RESET_PASSWORD = 'RESET_PASSWORD', LOGIN = 'LOGIN', + UPDATE_EMAIL = 'UPDATE_EMAIL', } export interface AuthCodeSendParams { diff --git a/code/link56/link56-frontend-new/src/types/auto-import.d.ts b/code/link56/link56-frontend-new/src/types/auto-import.d.ts index 67fe5ab3..5fc8fcde 100644 --- a/code/link56/link56-frontend-new/src/types/auto-import.d.ts +++ b/code/link56/link56-frontend-new/src/types/auto-import.d.ts @@ -14,6 +14,7 @@ declare module 'vue' { AnchorMenu: typeof import('./../components/anchor-menu/index.vue')['default'] AreaSelect: typeof import('./../components/AreaSelect.vue')['default'] Directory: typeof import('./../components/wang-editor/modules/directory.vue')['default'] + EmailCodeInput: typeof import('./../components/EmailCodeInput.vue')['default'] HelloWorld: typeof import('./../components/HelloWorld.vue')['default'] ICarbonAccessibility: typeof import('~icons/carbon/accessibility')['default'] 'IFa7Solid:userShield': typeof import('~icons/fa7-solid/user-shield')['default'] diff --git a/code/link56/link56-frontend-new/src/types/company/user.ts b/code/link56/link56-frontend-new/src/types/company/user.ts index 7dbe50b6..63a38cd0 100644 --- a/code/link56/link56-frontend-new/src/types/company/user.ts +++ b/code/link56/link56-frontend-new/src/types/company/user.ts @@ -19,3 +19,19 @@ export interface CompanyUser { phone: string canuse: string } + +export interface CompanyUserForm extends CompanyUser { + password: string + emailCode: string +} + +export interface AddCompanyUserParams { + companyId: number + email: string + password: string + + name: string + position: string + phone: string + canuse: string +} diff --git a/code/link56/link56-frontend-new/src/views/auth/login.vue b/code/link56/link56-frontend-new/src/views/auth/login.vue index e4ae3ee8..ecdfa427 100644 --- a/code/link56/link56-frontend-new/src/views/auth/login.vue +++ b/code/link56/link56-frontend-new/src/views/auth/login.vue @@ -3,6 +3,7 @@ import type { FormInstance } from 'element-plus' import { ElMessage } from 'element-plus' import { computed, reactive, ref } from 'vue' import { useRouter } from 'vue-router' +import EmailCodeInput from '@/components/EmailCodeInput.vue' import { loginByEmailCodeApi, loginByPasswordApi } from '@/request/api/auth' import { authCodeSendApi } from '@/request/api/auth/code' import { useUserStore } from '@/store/user' @@ -52,22 +53,6 @@ const carouselList = [ }, ] -// --- 发送验证码状态 & 倒计时 --- -const sending = ref(false) -const countdown = ref(0) -let timer: number | undefined - -function startCountdown(seconds = 60) { - countdown.value = seconds - timer = window.setInterval(() => { - countdown.value -= 1 - if (countdown.value <= 0) { - clearInterval(timer) - timer = undefined - } - }, 1000) -} - const form = reactive<{ email: string password: string @@ -116,39 +101,8 @@ const formRules = { ], } -const isEmailValid = computed(() => validateEmail(form.email)) - const submitLoading = ref(false) -function handleSendEmailCode() { - if (sending.value || countdown.value > 0 || !isEmailValid.value) - return - - sending.value = true - - authCodeSendApi({ - channel: AuthCodeChannel.EMAIL, - purpose: AuthCodePurpose.LOGIN, - target: form.email, - }).then((res) => { - if (res.isSuccess) { - ElMessage.success('发送成功') - startCountdown() - } - else { - sending.value = false - ElMessage.error(res.message || '发送失败,请检查邮箱是否正确或稍后再试!') - } - }).catch((err) => { - console.log(err) - - ElMessage.error('发送失败,请稍后再试') - sending.value = false - }).finally(() => { - sending.value = false - }) -} - async function submitForm(formEl: FormInstance | undefined) { if (!formEl) return @@ -301,30 +255,14 @@ function navigateTo(path: string) { 忘记密码? - + :email="form.email" + :channel="AuthCodeChannel.EMAIL" + :purpose="AuthCodePurpose.LOGIN" + placeholder="请输入邮箱验证码" + /> diff --git a/code/link56/link56-frontend-new/src/views/auth/register/components/AdminForm.vue b/code/link56/link56-frontend-new/src/views/auth/register/components/AdminForm.vue index 51a9a964..3a02afa0 100644 --- a/code/link56/link56-frontend-new/src/views/auth/register/components/AdminForm.vue +++ b/code/link56/link56-frontend-new/src/views/auth/register/components/AdminForm.vue @@ -2,10 +2,12 @@ import type { FormInstance } from 'element-plus' import type { RegisterAdminForm } from '@/types/auth' import { ElMessage } from 'element-plus' -import { computed, onUnmounted, reactive, ref } from 'vue' -import { authCodeSendApi, authCodeVerifyApi } from '@/request/api/auth/code' +import { reactive, ref } from 'vue' +import EmailCodeInput from '@/components/EmailCodeInput.vue' + +import { authCodeVerifyApi } from '@/request/api/auth/code' import { AuthCodeChannel, AuthCodePurpose } from '@/types/auth/code' -import { validateEmail, validatePhone } from '@/utils/validate' +import { validatePhone } from '@/utils/validate' const emit = defineEmits<{ (e: 'prev'): void @@ -73,61 +75,6 @@ function phoneValidator(rule: any, value: any, callback: any) { } } -const isEmailValid = computed(() => validateEmail(form.email)) - -// --- 发送验证码状态 & 倒计时 --- -const sending = ref(false) -const countdown = ref(0) -let timer: number | undefined - -function startCountdown(seconds = 60) { - countdown.value = seconds - timer = window.setInterval(() => { - countdown.value -= 1 - if (countdown.value <= 0) { - clearInterval(timer) - timer = undefined - } - }, 1000) -} - -function handleSendEmailCode() { - if (sending.value || countdown.value > 0 || !isEmailValid.value) - return - - sending.value = true - - authCodeSendApi({ - channel: AuthCodeChannel.EMAIL, - purpose: AuthCodePurpose.REGISTER, - target: form.email, - }).then((res) => { - if (res.isSuccess) { - ElMessage.success('发送成功') - startCountdown() - } - else { - sending.value = false - ElMessage.error(res.message || '发送失败,请检查邮箱是否正确或稍后再试!') - } - }).catch((err) => { - console.log(err) - - ElMessage.error('发送失败,请稍后再试') - sending.value = false - }).finally(() => { - sending.value = false - }) -} - -// 清理 -onUnmounted(() => { - if (timer) { - clearInterval(timer) - timer = undefined - } -}) - async function handleSubmit(formEl: FormInstance | undefined) { if (!formEl) return @@ -206,28 +153,13 @@ function handlePrev() { - + @@ -274,21 +206,6 @@ function handlePrev() { overflow-x: hidden; } - .email-code { - :deep(.el-input-group__append) { - background-color: #409eff; - color: #fff; - cursor: pointer; - } - - &.disabled { - :deep(.el-input-group__append) { - background-color: #f5f7fa; - color: #c0c4cc; - } - } - } - .section-title { font-size: 17px; font-weight: 600; diff --git a/code/link56/link56-frontend-new/src/views/auth/register/components/CompanyForm.vue b/code/link56/link56-frontend-new/src/views/auth/register/components/CompanyForm.vue index ce503b52..ab156cd9 100644 --- a/code/link56/link56-frontend-new/src/views/auth/register/components/CompanyForm.vue +++ b/code/link56/link56-frontend-new/src/views/auth/register/components/CompanyForm.vue @@ -95,12 +95,12 @@ async function handleSubmit(formEl: FormInstance | undefined) { - + - + +import type { FormInstance } from 'element-plus' +import type { PropType } from 'vue' +import type { CompanyUser, CompanyUserForm } from '@/types/company/user' +import { ElMessage } from 'element-plus' +import { computed, ref, watch } from 'vue' +import EmailCodeInput from '@/components/EmailCodeInput.vue' +import { authCodeVerifyApi } from '@/request/api/auth/code' +import { addCompanyUserApi } from '@/request/api/company/user' +import { useUserStore } from '@/store/user' +import { AuthCodeChannel, AuthCodePurpose } from '@/types/auth/code' +import { validatePhone } from '@/utils/validate' + +const props = defineProps({ + user: { + type: Object as PropType, + default: null, + }, +}) + +const userStore = useUserStore() +const roleOptions = [ + { + value: 0, + label: '系统管理员', + }, + { + value: 1, + label: '普通成员', + }, +] + +const originEmail = ref('') + +const form = ref({ + id: 0, + role: 1, + name: '', + position: '', + email: '', + phone: '', + canuse: '1', + password: '', + emailCode: '', +}) +watch( + () => props.user, + (user) => { + if (!user) + return + + form.value = { ...user, password: '', emailCode: '' } + originEmail.value = user.email + }, + { immediate: true }, +) + +const isUpdate = computed(() => !!props.user && !!props.user.id) + +const isEmailChanged = computed(() => { + return form.value.email !== originEmail.value +}) + +const formRef = ref() + +const formRules = { + name: [ + { required: true, message: '名称不能为空', trigger: 'blur' }, + ], + email: [ + { required: true, message: '邮箱不能为空', trigger: 'blur' }, + { type: 'email', message: '请输入正确的邮箱格式', trigger: ['blur', 'change'] }, + ], + emailCode: [ + { + validator: (rule: any, value: any, callback: any) => { + if (!isEmailChanged.value) { + callback() + return + } + + if (!value) { + callback(new Error('邮箱验证码不能为空')) + return + } + callback() + }, + trigger: ['blur', 'change'], + }, + ], + password: [ + { + validator: (rule: any, value: any, callback: any) => { + // 更新时允许不填 + if (isUpdate.value && !value) { + callback() + return + } + + if (!value) { + callback(new Error('密码不能为空')) + } + else { + callback() + } + }, + trigger: 'blur', + }, + ], + role: [ + { required: true, message: '角色不能为空', trigger: 'blur' }, + ], + canuse: [ + { required: true, message: '启用不能为空', trigger: 'blur' }, + ], + phone: [{ validator: phoneValidator, trigger: 'blur' }], +} +const submitLoading = ref(false) + +function phoneValidator(rule: any, value: any, callback: any) { + // 未填写:直接通过 + if (!value) { + callback() + return + } + + if (!validatePhone(value)) { + callback(new Error('手机号格式不正确')) + } + else { + callback() + } +} + +async function handleSubmit(formEl: FormInstance | undefined) { + if (!formEl) + return + + await formEl.validate((valid) => { + if (valid) { + if (isEmailChanged.value) { + submitLoading.value = true + + authCodeVerifyApi({ + channel: AuthCodeChannel.EMAIL, + purpose: isUpdate.value ? AuthCodePurpose.UPDATE_EMAIL : AuthCodePurpose.REGISTER, + target: form.value.email, + code: form.value.emailCode, + }).then((res) => { + if (res.isSuccess) { + addCompanyUserApi({ + companyId: userStore.userInfo.company.id, + email: form.value.email, + password: form.value.password, + name: form.value.name, + position: form.value.position, + phone: form.value.phone, + canuse: form.value.canuse, + }).then((res) => { + if (res.isSuccess) { + ElMessage.success('保存成功') + } + else { + ElMessage.error(res.message || '新增失败,请稍后再试') + } + }).catch((err) => { + console.log(err) + ElMessage.error('新增失败,请稍后再试') + }).finally(() => { + submitLoading.value = false + }) + } + else { + submitLoading.value = false + + ElMessage.error(res.message || '验证码无效或已过期,请重新获取') + } + }).catch((err) => { + submitLoading.value = false + + console.log(err) + ElMessage.error('邮箱验证码校验失败,请稍后再试') + }) + } + } + else { + console.log('===验证失败===') + } + }) +} + + + + + diff --git a/code/link56/link56-frontend-new/src/views/settings/info/user.vue b/code/link56/link56-frontend-new/src/views/settings/info/user/index.vue similarity index 89% rename from code/link56/link56-frontend-new/src/views/settings/info/user.vue rename to code/link56/link56-frontend-new/src/views/settings/info/user/index.vue index 5f1b98b1..58676faa 100644 --- a/code/link56/link56-frontend-new/src/views/settings/info/user.vue +++ b/code/link56/link56-frontend-new/src/views/settings/info/user/index.vue @@ -5,6 +5,7 @@ import { ElMessage } from 'element-plus' import { onMounted, reactive, ref } from 'vue' import { queryCompanyUserApi } from '@/request/api/company/user' import { useUserStore } from '@/store/user' +import EditDrawer from './components/EditDrawer.vue' const userStore = useUserStore() const activeCollapseNames = ref(['1']) @@ -43,6 +44,9 @@ const canuseOptions = [ ] const tableData = ref([]) +const showEdit = ref(false) +const editUser = ref(null) + onMounted(() => { getCompanyUser() }) @@ -65,6 +69,11 @@ function getCompanyUser() { ElMessage.error('查询失败,请稍后再试') }) } + +function handleShowEditDrawer(user: CompanyUser | null) { + editUser.value = user + showEdit.value = true +} @@ -207,7 +226,6 @@ function getCompanyUser() { .table-container { flex: 1; - margin-top: 16px; display: flex; flex-direction: column; min-height: 0; @@ -227,10 +245,6 @@ function getCompanyUser() { align-items: center; justify-content: space-between; height: 56px; - .table-title { - font-size: 18px; - font-weight: 500; - } } :deep(.el-table__header) { .el-table__cell {