This commit is contained in:
oneao committed 2025-12-18 22:39:59 +08:00
1 parent 9a35a56a97
commit 1fea531918
16 files changed
+1026 -184

No files matched your search

@@ -6,5 +6,6 @@ package cn.link56.auth.code.model;
public enum AuthCodePurpose {
REGISTER, // 注册
LOGIN, // 登录
RESET_PASSWORD // 找回密码
RESET_PASSWORD, // 找回密码
UPDATE_EMAIL // 更新邮箱
}
@@ -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);
@@ -480,7 +480,11 @@
<td class="content-cell">
<div class="f-fallback">
<h1>您好</h1>
<p>您正在登录我们的 link56 平台,系统已为您生成了登录验证码。<strong>该验证码仅在 10 分钟内有效。</strong></p>
<p>
您正在 <strong>link56</strong> 平台进行<strong>登录</strong>操作,
系统已为您生成邮箱验证码。
<strong>该验证码仅在 10 分钟内有效。</strong>
</p>
<p class="body-action">
[[${Code}]]
@@ -480,7 +480,12 @@
<td class="content-cell">
<div class="f-fallback">
<h1>您好</h1>
<p>您正在注册我们的 link56 平台,系统已为您生成了注册验证码。<strong>该验证码仅在 10 分钟内有效。</strong></p>
<p>
您正在 <strong>link56</strong> 平台进行<strong>注册</strong>操作,
系统已为您生成邮箱验证码。
<strong>该验证码仅在 10 分钟内有效。</strong>
</p>
<p class="body-action">
[[${Code}]]
@@ -0,0 +1,526 @@
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
<title></title>
<style type="text/css" rel="stylesheet" media="all">
/* Base ------------------------------ */
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
body {
width: 100% !important;
height: 100%;
margin: 0;
-webkit-text-size-adjust: none;
}
a {
color: #3869D4;
}
a img {
border: none;
}
td {
word-break: break-word;
}
.preheader {
display: none !important;
visibility: hidden;
mso-hide: all;
font-size: 1px;
line-height: 1px;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
}
/* Type ------------------------------ */
body,
td,
th {
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
}
h1 {
margin-top: 0;
color: #333333;
font-size: 22px;
font-weight: bold;
text-align: left;
}
h2 {
margin-top: 0;
color: #333333;
font-size: 16px;
font-weight: bold;
text-align: left;
}
h3 {
margin-top: 0;
color: #333333;
font-size: 14px;
font-weight: bold;
text-align: left;
}
td,
th {
font-size: 16px;
}
p,
ul,
ol,
blockquote {
margin: .4em 0 1.1875em;
font-size: 16px;
line-height: 1.625;
}
p.sub {
font-size: 13px;
}
/* Utilities ------------------------------ */
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
.u-margin-bottom-none {
margin-bottom: 0;
}
/* Buttons ------------------------------ */
.button {
background-color: #3869D4;
border-top: 10px solid #3869D4;
border-right: 18px solid #3869D4;
border-bottom: 10px solid #3869D4;
border-left: 18px solid #3869D4;
display: inline-block;
color: #FFF;
text-decoration: none;
border-radius: 3px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
-webkit-text-size-adjust: none;
box-sizing: border-box;
}
.button--green {
background-color: #22BC66;
border-top: 10px solid #22BC66;
border-right: 18px solid #22BC66;
border-bottom: 10px solid #22BC66;
border-left: 18px solid #22BC66;
}
.button--red {
background-color: #FF6136;
border-top: 10px solid #FF6136;
border-right: 18px solid #FF6136;
border-bottom: 10px solid #FF6136;
border-left: 18px solid #FF6136;
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
text-align: center !important;
}
}
/* Attribute list ------------------------------ */
.attributes {
margin: 0 0 21px;
}
.attributes_content {
background-color: #F4F4F7;
padding: 16px;
}
.attributes_item {
padding: 0;
}
/* Related Items ------------------------------ */
.related {
width: 100%;
margin: 0;
padding: 25px 0 0 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.related_item {
padding: 10px 0;
color: #CBCCCF;
font-size: 15px;
line-height: 18px;
}
.related_item-title {
display: block;
margin: .5em 0 0;
}
.related_item-thumb {
display: block;
padding-bottom: 10px;
}
.related_heading {
border-top: 1px solid #CBCCCF;
text-align: center;
padding: 25px 0 10px;
}
/* Discount Code ------------------------------ */
.discount {
width: 100%;
margin: 0;
padding: 24px;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #F4F4F7;
border: 2px dashed #CBCCCF;
}
.discount_heading {
text-align: center;
}
.discount_body {
text-align: center;
font-size: 15px;
}
/* Social Icons ------------------------------ */
.social {
width: auto;
}
.social td {
padding: 0;
width: auto;
}
.social_icon {
height: 20px;
margin: 0 8px 10px 8px;
padding: 0;
}
/* Data table ------------------------------ */
.purchase {
width: 100%;
margin: 0;
padding: 35px 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_content {
width: 100%;
margin: 0;
padding: 25px 0 0 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_item {
padding: 10px 0;
color: #51545E;
font-size: 15px;
line-height: 18px;
}
.purchase_heading {
padding-bottom: 8px;
border-bottom: 1px solid #EAEAEC;
}
.purchase_heading p {
margin: 0;
color: #85878E;
font-size: 12px;
}
.purchase_footer {
padding-top: 15px;
border-top: 1px solid #EAEAEC;
}
.purchase_total {
margin: 0;
text-align: right;
font-weight: bold;
color: #333333;
}
.purchase_total--label {
padding: 0 15px 0 0;
}
body {
background-color: #F2F4F6;
color: #51545E;
}
p {
color: #51545E;
}
.email-wrapper {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #F2F4F6;
}
.email-content {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
/* Masthead ----------------------- */
.email-masthead {
padding: 25px 0;
text-align: center;
}
.email-masthead_logo {
width: 94px;
}
.email-masthead_name {
font-size: 16px;
font-weight: bold;
color: #A8AAAF;
text-decoration: none;
text-shadow: 0 1px 0 white;
}
/* Body ------------------------------ */
.email-body {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.email-body_inner {
width: 570px;
margin: 0 auto;
padding: 0;
-premailer-width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #FFFFFF;
}
.email-footer {
width: 570px;
margin: 0 auto;
padding: 0;
-premailer-width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
text-align: center;
}
.email-footer p {
color: #A8AAAF;
}
.body-action {
width: 100%;
margin: 30px auto;
padding: 0;
font-size: 42px;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
text-align: center;
}
.body-sub {
margin-top: 25px;
padding-top: 25px;
border-top: 1px solid #EAEAEC;
}
.content-cell {
padding: 45px;
}
/*Media Queries ------------------------------ */
@media only screen and (max-width: 600px) {
.email-body_inner,
.email-footer {
width: 100% !important;
}
}
@media (prefers-color-scheme: dark) {
body,
.email-body,
.email-body_inner,
.email-content,
.email-wrapper,
.email-masthead,
.email-footer {
background-color: #333333 !important;
color: #FFF !important;
}
p,
ul,
ol,
blockquote,
h1,
h2,
h3,
span,
.purchase_item {
color: #FFF !important;
}
.attributes_content,
.discount {
background-color: #222 !important;
}
.email-masthead_name {
text-shadow: none !important;
}
}
:root {
color-scheme: light dark;
supported-color-schemes: light dark;
}
</style>
<!--[if mso]>
<style type="text/css">
.f-fallback {
font-family: Arial, sans-serif;
}
</style>
<![endif]-->
</head>
<body>
<span class="preheader">Use this link to reset your password. The link is only valid for 24 hours.</span>
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="email-masthead">
<a href="https://example.com" class="f-fallback email-masthead_name">
link56
</a>
</td>
</tr>
<!-- Email Body -->
<tr>
<td class="email-body" width="570" cellpadding="0" cellspacing="0">
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0"
role="presentation">
<!-- Body content -->
<tr>
<td class="content-cell">
<div class="f-fallback">
<h1>您好</h1>
<p>
您正在 <strong>link56</strong> 平台进行<strong>认证邮箱修改</strong>操作,
系统已为您生成邮箱验证码。
<strong>该验证码仅在 10 分钟内有效。</strong>
</p>
<p class="body-action">
[[${Code}]]
</p>
<p>如果不是您本人操作,请忽略此邮件,或如有疑问,请 <a
href="{{SupportUrl}}">联系客服</a>。</p>
<p>
感谢,<br>
link56 团队
</p>
</div>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0"
role="presentation">
<tr>
<td class="content-cell" align="center">
<p class="f-fallback sub align-center">
宁波江东远通计算机有限公司
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
@@ -0,0 +1,139 @@
<script lang="ts" setup>
import type { PropType } from 'vue'
import type { AuthCodeChannel, AuthCodePurpose } from '@/types/auth/code'
import { ElMessage } from 'element-plus'
import { computed, ref, watch } from 'vue'
import { authCodeSendApi } from '@/request/api/auth/code'
import { validateEmail } from '@/utils/validate'
const props = defineProps({
modelValue: {
type: String,
required: true,
},
email: {
type: String,
required: true,
},
placeholder: {
type: String,
},
buttonText: {
type: String,
default: '发送验证码',
},
countdownSeconds: {
type: Number,
default: 60,
},
channel: {
type: String as PropType<AuthCodeChannel>,
required: true,
},
purpose: {
type: String as PropType<AuthCodePurpose>,
required: true,
},
})
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'sendSuccess'): void
}>()
const code = ref(props.modelValue)
watch(() => props.modelValue, val => code.value = val)
watch(code, val => emit('update:modelValue', val))
const isEmailValid = computed(() => validateEmail(props.email))
const sending = ref(false)
const countdown = ref(0)
let timer: number | undefined
const inputClass = computed(() => (sending.value || countdown.value > 0 || !isEmailValid.value) ? 'disabled' : '')
function startCountdown(seconds = props.countdownSeconds || 60) {
countdown.value = seconds
timer = window.setInterval(() => {
countdown.value -= 1
if (countdown.value <= 0) {
clearInterval(timer)
timer = undefined
}
}, 1000)
}
async function handleSendCode() {
if (sending.value || countdown.value > 0 || !isEmailValid.value)
return
sending.value = true
try {
const res = await authCodeSendApi({
channel: props.channel,
purpose: props.purpose,
target: props.email,
})
if (res.isSuccess) {
ElMessage.success('发送成功')
emit('sendSuccess')
startCountdown()
}
else {
ElMessage.error(res.message || '发送失败,请检查邮箱是否正确或稍后再试!')
}
}
catch (err) {
console.error(err)
ElMessage.error('发送失败,请稍后再试')
}
finally {
sending.value = false
}
}
</script>
<template>
<el-input
v-model="code"
:placeholder="placeholder"
:class="inputClass"
class="code"
>
<template #append>
<el-button
:disabled="sending || countdown > 0 || !isEmailValid"
@click="handleSendCode"
>
<template v-if="sending">
发送中...
</template>
<template v-else-if="countdown > 0">
{{ countdown }}s
</template>
<template v-else>
{{ buttonText }}
</template>
</el-button>
</template>
</el-input>
</template>
<style lang="scss" scoped>
.code {
:deep(.el-input-group__append) {
background-color: #409eff;
color: #fff;
cursor: pointer;
}
&.disabled {
:deep(.el-input-group__append) {
background-color: #f5f7fa;
color: #c0c4cc;
}
}
}
</style>
@@ -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<BasePageResult<CompanyUser>>('/company/user/query', params)
}
export function addCompanyUserApi(params: AddCompanyUserParams) {
return httpPostJson<BasePageResult<void>>('/company/user/add', params)
}
@@ -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'),
},
],
},
@@ -7,6 +7,7 @@ export enum AuthCodePurpose {
REGISTER = 'REGISTER',
RESET_PASSWORD = 'RESET_PASSWORD',
LOGIN = 'LOGIN',
UPDATE_EMAIL = 'UPDATE_EMAIL',
}
export interface AuthCodeSendParams {
@@ -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']
@@ -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
}
@@ -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) {
<span class="forgot" @click="navigateTo('/auth/forget')">忘记密码?</span>
</span>
</template>
<el-input
<EmailCodeInput
v-model="form.code"
placeholder="请输入邮箱验证码"
class="email-code"
size="large"
:class="(sending || countdown > 0 || !isEmailValid) ? 'disabled' : ''"
>
<template #append>
<el-button
:disabled="sending || countdown > 0 || !isEmailValid"
@click="handleSendEmailCode"
>
<template v-if="sending">
发送中...
</template>
<template v-else-if="countdown > 0">
{{ countdown }}s
</template>
<template v-else>
发送验证码
</template>
</el-button>
</template>
</el-input>
:email="form.email"
:channel="AuthCodeChannel.EMAIL"
:purpose="AuthCodePurpose.LOGIN"
placeholder="请输入邮箱验证码"
/>
</el-form-item>
<el-form-item>
<el-checkbox v-model="form.rememberMe" label="记住我" class="remeber" />
@@ -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() {
</el-col>
<el-col :span="12">
<el-form-item label="邮箱验证码" prop="emailCode">
<el-input
v-model="form.emailCode" placeholder="请输入邮箱验证码"
class="email-code"
:class="(sending || countdown > 0 || !isEmailValid) ? 'disabled' : ''"
>
<template #append>
<el-button
:disabled="sending || countdown > 0 || !isEmailValid"
@click="handleSendEmailCode"
>
<template v-if="sending">
发送中...
</template>
<template v-else-if="countdown > 0">
{{ countdown }}s
</template>
<template v-else>
发送验证码
</template>
</el-button>
</template>
</el-input>
<EmailCodeInput
v-model="form.emailCode"
:email="form.email"
:channel="AuthCodeChannel.EMAIL"
:purpose="AuthCodePurpose.REGISTER"
placeholder="请输入邮箱验证码"
/>
</el-form-item>
</el-col>
<el-col :span="12">
@@ -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;
@@ -95,12 +95,12 @@ async function handleSubmit(formEl: FormInstance | undefined) {
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属行业" prop="industry">
<el-form-item label="所属行业" prop="industries">
<IndustrySelect v-model="form.industries" placeholder="请输入所属行业" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="公司地址" prop="address">
<el-form-item label="公司地址" prop="areas">
<AreaSelect
v-model="form.areas"
placeholder="请输入公司地址"
@@ -0,0 +1,274 @@
<script lang="ts" setup>
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<CompanyUser | null>,
default: null,
},
})
const userStore = useUserStore()
const roleOptions = [
{
value: 0,
label: '系统管理员',
},
{
value: 1,
label: '普通成员',
},
]
const originEmail = ref('')
const form = ref<CompanyUserForm>({
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<FormInstance>()
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('===验证失败===')
}
})
}
</script>
<template>
<el-drawer v-bind="$attrs" title="成员编辑" resizable>
<el-alert
title="提示"
style="margin-bottom: 24px;"
:closable="false"
>
<div>
<p>• 修改邮箱需要进行邮箱验证码验证</p>
<p>• 出于安全考虑,密码仅支持重置修改,无法查看原密码</p>
<p>• 角色无法更改</p>
</div>
</el-alert>
<el-form
ref="formRef"
:model="form"
:rules="formRules"
label-width="auto"
label-position="right"
>
<el-form-item label="名称" prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="职位" prop="position">
<el-input v-model="form.position" />
</el-form-item>
<el-form-item label="角色" prop="role">
<el-select v-model="form.role" disabled>
<el-option
v-for="item in roleOptions "
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" />
</el-form-item>
<el-form-item label="邮箱号" prop="email">
<el-input v-model="form.email" />
</el-form-item>
<el-form-item
v-if="isEmailChanged"
label="邮箱验证码"
prop="emailCode"
>
<EmailCodeInput
v-model="form.emailCode"
:email="form.email"
:channel=" AuthCodeChannel.EMAIL"
:purpose="isUpdate ? AuthCodePurpose.UPDATE_EMAIL : AuthCodePurpose.REGISTER"
/>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="form.password" show-password />
</el-form-item>
<el-form-item label="启用" prop="canuse">
<el-switch v-model="form.canuse" active-value="1" inactive-value="0" />
</el-form-item>
</el-form>
<template #footer>
<div style="flex: auto">
<el-button>
取消
</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit(formRef)">
确认
</el-button>
</div>
</template>
</el-drawer>
</template>
<style lang="scss" scoped>
//
</style>
@@ -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<CompanyUser[]>([])
const showEdit = ref(false)
const editUser = ref<CompanyUser | null>(null)
onMounted(() => {
getCompanyUser()
})
@@ -65,6 +69,11 @@ function getCompanyUser() {
ElMessage.error('查询失败,请稍后再试')
})
}
function handleShowEditDrawer(user: CompanyUser | null) {
editUser.value = user
showEdit.value = true
}
</script>
<template>
@@ -121,12 +130,8 @@ function getCompanyUser() {
</el-collapse>
<div class="table-container">
<div class="table-header">
<div class="table-title">
成员列表
</div>
<div class="table-actions">
<el-button type="primary" plain :icon="Plus">
<el-button type="primary" plain :icon="Plus" @click="handleShowEditDrawer(null)">
新增
</el-button>
<el-button type="danger" plain :icon="Delete">
@@ -140,7 +145,19 @@ function getCompanyUser() {
<div class="table-body-wrapper">
<el-table :data="tableData" border stripe>
<el-table-column type="selection" width="55" align="center" />
<el-table-column prop="name" label="名称" align="center" />
<el-table-column prop="name" label="名称" align="center">
<template #default="scope">
<template v-if="scope.row.role === 1">
{{ scope.row.name }}
</template>
<div v-else>
<div>
{{ scope.row.name }}
</div>
<el-tag>超级管理员</el-tag>
</div>
</template>
</el-table-column>
<el-table-column prop="position" label="职位" align="center" />
<el-table-column prop="email" label="邮箱" align="center" />
<el-table-column prop="phone" label="电话" align="center" />
@@ -180,6 +197,8 @@ function getCompanyUser() {
/>
</div>
</div>
<EditDrawer v-model="showEdit" :user="editUser" />
</div>
</template>
@@ -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 {