批量推送
This commit is contained in:
@@ -30,10 +30,15 @@
|
||||
<!-- 右侧登录卡片 -->
|
||||
<section class="login-section">
|
||||
<div class="login-card">
|
||||
<h1 class="login-title">账号登录</h1>
|
||||
<p class="login-subtitle">请输入您的账号信息</p>
|
||||
<h1 class="login-title">{{ loginMode === 'password' ? '账号登录' : '手机号登录' }}</h1>
|
||||
<p class="login-subtitle">{{ loginMode === 'password' ? '请输入您的账号信息' : '请输入手机号接收验证码' }}</p>
|
||||
|
||||
<form id="loginForm" @submit.prevent="onSubmit">
|
||||
<div class="login-tabs">
|
||||
<span class="tab" :class="{ active: loginMode === 'password' }" @click="switchMode('password')">账号密码登录</span>
|
||||
<span class="tab" :class="{ active: loginMode === 'sms' }" @click="switchMode('sms')">手机验证码登录</span>
|
||||
</div>
|
||||
|
||||
<form v-if="loginMode === 'password'" id="loginForm" @submit.prevent="onSubmit">
|
||||
<div class="form-group">
|
||||
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
@@ -65,6 +70,29 @@
|
||||
<a class="bottom-link" @click="onForgot">忘记密码</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form v-else id="smsLoginForm" @submit.prevent="onSmsLoginSubmit">
|
||||
<div class="form-group">
|
||||
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
||||
</svg>
|
||||
<input v-model="smsForm.phone" type="text" class="form-input" placeholder="请输入手机号" maxlength="11" autocomplete="tel">
|
||||
</div>
|
||||
|
||||
<div class="form-group sms-group">
|
||||
<input v-model="smsForm.smsCode" type="text" class="form-input sms-input" placeholder="短信验证码" maxlength="6">
|
||||
<button type="button" class="sms-btn" :disabled="smsSmsLocked || smsSmsCountdown > 0 || !smsForm.phone" @click="sendLoginSmsCode">
|
||||
{{ smsSmsLocked ? '发送中...' : smsSmsCountdown > 0 ? `${smsSmsCountdown}s 后重试` : '获取验证码' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="login-btn" :disabled="smsLoginLocked">{{ smsLoginLocked ? '登录中...' : '登 录' }}</button>
|
||||
|
||||
<div class="form-bottom-links">
|
||||
<a class="bottom-link" @click="onRegister">新用户注册</a>
|
||||
<a class="bottom-link" @click="onForgot">忘记密码</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
@@ -94,15 +122,68 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 忘记密码弹窗 -->
|
||||
<div class="modal-overlay" :class="{ active: showForgotModal }" @click.self="closeForgot">
|
||||
<div class="modal">
|
||||
<h2 class="modal-title">忘记密码</h2>
|
||||
<p class="modal-subtitle">通过注册时填写的手机号验证后重置密码</p>
|
||||
|
||||
<form @submit.prevent="onResetSubmit">
|
||||
<div class="form-group">
|
||||
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
||||
</svg>
|
||||
<input v-model="forgotForm.phone" type="text" class="form-input" placeholder="请输入注册时的手机号" maxlength="11" autocomplete="tel">
|
||||
</div>
|
||||
|
||||
<div class="form-group sms-group">
|
||||
<input v-model="forgotForm.smsCode" type="text" class="form-input sms-input" placeholder="短信验证码" maxlength="6">
|
||||
<button type="button" class="sms-btn" :disabled="forgotSmsLocked || forgotSmsCountdown > 0 || !forgotForm.phone" @click="sendForgotSms">
|
||||
{{ forgotSmsLocked ? '发送中...' : forgotSmsCountdown > 0 ? `${forgotSmsCountdown}s 后重试` : '获取验证码' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||
</svg>
|
||||
<input v-model="forgotForm.password" type="password" class="form-input" placeholder="新密码 (6-20 位)" autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||
</svg>
|
||||
<input v-model="forgotForm.confirmPassword" type="password" class="form-input" placeholder="确认新密码" autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button class="modal-btn secondary" type="button" @click="closeForgot">取消</button>
|
||||
<button class="modal-btn primary" type="submit" :disabled="forgotLoading">{{ forgotLoading ? '提交中...' : '重置密码' }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { reactive, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { login, getInfo, getCaptcha } from '@/api/auth'
|
||||
import request from '@/utils/request'
|
||||
import { login, getInfo, getCaptcha, sendLoginSms, smsLogin } from '@/api/auth'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { useAsyncLock } from '@/utils/useAsyncLock'
|
||||
|
||||
// 图形验证码刷新 + 忘记密码短信 + 短信登录 各自独立锁 (避免重复请求)
|
||||
const { locked: captchaLocked, run: runCaptchaOnce } = useAsyncLock()
|
||||
const { locked: forgotSmsLocked, run: runForgotSmsOnce } = useAsyncLock()
|
||||
const { locked: smsSmsLocked, run: runSmsOnce } = useAsyncLock()
|
||||
const { locked: smsLoginLocked, run: runSmsLoginOnce } = useAsyncLock()
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
@@ -114,6 +195,12 @@ const loading = ref(false)
|
||||
const showRoleModal = ref(false)
|
||||
const registerUserType = ref('')
|
||||
|
||||
// 登录模式: 'password' | 'sms'
|
||||
const loginMode = ref('password')
|
||||
const smsForm = reactive({ phone: '', smsCode: '', uuid: '' })
|
||||
const smsSmsCountdown = ref(0)
|
||||
let smsSmsTimer = null
|
||||
|
||||
const registerTypes = [
|
||||
{ value: 'expert', name: '项目参与专家', desc: '负责项目方案设计与评审' },
|
||||
{ value: 'executor', name: '项目执行单位(供应商)', desc: '承担项目执行,需走入库流程' },
|
||||
@@ -130,16 +217,18 @@ const userTypes = [
|
||||
]
|
||||
|
||||
async function loadCaptcha() {
|
||||
try {
|
||||
const res = await getCaptcha()
|
||||
captchaEnabled.value = res?.captchaEnabled !== false
|
||||
if (res?.uuid) {
|
||||
form.uuid = res.uuid
|
||||
captchaImg.value = 'data:image/png;base64,' + (res.img || '')
|
||||
await runCaptchaOnce(async () => {
|
||||
try {
|
||||
const res = await getCaptcha()
|
||||
captchaEnabled.value = res?.captchaEnabled !== false
|
||||
if (res?.uuid) {
|
||||
form.uuid = res.uuid
|
||||
captchaImg.value = 'data:image/png;base64,' + (res.img || '')
|
||||
}
|
||||
} catch (e) {
|
||||
captchaImg.value = ''
|
||||
}
|
||||
} catch (e) {
|
||||
captchaImg.value = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
@@ -154,36 +243,7 @@ async function onSubmit() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await login({ username: form.username, password: form.password, code: form.code || '', uuid: form.uuid || '' })
|
||||
// username 推断仅作兜底, 真实 role 以 /getInfo 返回的 sys_user.role_type 为准
|
||||
const fallbackRole = autoRole(form.username)
|
||||
userStore.setToken(res.token)
|
||||
// /login 接口只返回 token; 调 /getInfo 拿完整 user (含 accountType/parentUserId/roleType, 让 isMain/isSub/role 都从后端真实值取)
|
||||
let role = fallbackRole
|
||||
try {
|
||||
const info = await getInfo()
|
||||
const u = info.user || {}
|
||||
role = u.roleType || fallbackRole
|
||||
userStore.setUser({
|
||||
userId: u.userId,
|
||||
userName: u.userName || form.username,
|
||||
nickName: u.nickName || form.username,
|
||||
accountType: u.accountType || 'MAIN',
|
||||
parentUserId: u.parentUserId || null,
|
||||
role
|
||||
})
|
||||
} catch {
|
||||
// /getInfo 失败时回退: 只存基本信息
|
||||
userStore.setUser({
|
||||
userId: res.userId,
|
||||
userName: form.username,
|
||||
nickName: form.username,
|
||||
accountType: 'MAIN',
|
||||
parentUserId: null,
|
||||
role: fallbackRole
|
||||
})
|
||||
}
|
||||
ElMessage.success(`欢迎,${form.username}(${userTypes.find(u => u.value === role)?.name || role})`)
|
||||
router.replace(roleHome[role] || '/leader/home')
|
||||
await afterLogin(res.token, form.username, autoRole(form.username))
|
||||
} catch (e) {
|
||||
// 错误提示已由 utils/request.js 拦截器统一弹 (ElMessage.error), 这里只刷新验证码
|
||||
loadCaptcha()
|
||||
@@ -192,6 +252,97 @@ async function onSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录后置: 存 token → 调 /getInfo 拿真实 user → 跳角色首页
|
||||
* 密码登录和短信登录共用
|
||||
*/
|
||||
async function afterLogin(token, displayName, fallbackRole) {
|
||||
userStore.setToken(token)
|
||||
let role = fallbackRole
|
||||
try {
|
||||
const info = await getInfo()
|
||||
const u = info.user || {}
|
||||
role = u.roleType || fallbackRole
|
||||
userStore.setUser({
|
||||
userId: u.userId,
|
||||
userName: u.userName || displayName,
|
||||
nickName: u.nickName || displayName,
|
||||
accountType: u.accountType || 'MAIN',
|
||||
parentUserId: u.parentUserId || null,
|
||||
role
|
||||
})
|
||||
} catch {
|
||||
// /getInfo 失败时回退: 只存基本信息
|
||||
userStore.setUser({
|
||||
userId: null,
|
||||
userName: displayName,
|
||||
nickName: displayName,
|
||||
accountType: 'MAIN',
|
||||
parentUserId: null,
|
||||
role: fallbackRole
|
||||
})
|
||||
}
|
||||
ElMessage.success(`欢迎,${displayName}(${userTypes.find(u => u.value === role)?.name || role})`)
|
||||
router.replace(roleHome[role] || '/leader/home')
|
||||
}
|
||||
|
||||
function switchMode(mode) {
|
||||
if (loginMode.value === mode) return
|
||||
loginMode.value = mode
|
||||
// 切换时清空两条表单, 避免脏数据
|
||||
if (mode === 'password') {
|
||||
smsForm.phone = ''
|
||||
smsForm.smsCode = ''
|
||||
smsForm.uuid = ''
|
||||
if (smsSmsTimer) { clearInterval(smsSmsTimer); smsSmsTimer = null }
|
||||
smsSmsCountdown.value = 0
|
||||
} else {
|
||||
form.username = ''
|
||||
form.password = ''
|
||||
form.code = ''
|
||||
// 切到短信登录时不需要图形验证码, 不再 loadCaptcha
|
||||
}
|
||||
}
|
||||
|
||||
async function sendLoginSmsCode() {
|
||||
if (!/^1\d{10}$/.test(smsForm.phone)) {
|
||||
return ElMessage.warning('请输入正确的手机号')
|
||||
}
|
||||
await runSmsOnce(async () => {
|
||||
try {
|
||||
const r = await sendLoginSms(smsForm.phone)
|
||||
smsForm.uuid = r.data || ''
|
||||
ElMessage.success('验证码已发送')
|
||||
smsSmsCountdown.value = 60
|
||||
smsSmsTimer = setInterval(() => {
|
||||
smsSmsCountdown.value--
|
||||
if (smsSmsCountdown.value <= 0) {
|
||||
clearInterval(smsSmsTimer)
|
||||
smsSmsTimer = null
|
||||
}
|
||||
}, 1000)
|
||||
} catch (e) {
|
||||
// request.js 已弹错误, 这里只重置 uuid
|
||||
smsForm.uuid = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function onSmsLoginSubmit() {
|
||||
if (!/^1\d{10}$/.test(smsForm.phone)) return ElMessage.warning('请输入正确的手机号')
|
||||
if (!smsForm.smsCode) return ElMessage.warning('请输入短信验证码')
|
||||
if (!smsForm.uuid) return ElMessage.warning('请先获取验证码')
|
||||
await runSmsLoginOnce(async () => {
|
||||
try {
|
||||
const res = await smsLogin({ phone: smsForm.phone, smsCode: smsForm.smsCode, uuid: smsForm.uuid })
|
||||
// 短信登录后无 username, 用 phone 作为显示名; role 兜底默认 leader
|
||||
await afterLogin(res.token, smsForm.phone, 'leader')
|
||||
} catch (e) {
|
||||
// request.js 已弹错误
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function autoRole(username) {
|
||||
const u = (username || '').toLowerCase()
|
||||
if (u === 'admin' || u === 'ry') return 'admin'
|
||||
@@ -241,10 +392,88 @@ function confirmRegister() {
|
||||
}
|
||||
|
||||
function onForgot() {
|
||||
ElMessage.info('请联系系统管理员重置密码')
|
||||
showForgotModal.value = true
|
||||
}
|
||||
|
||||
// ===== 忘记密码弹窗 =====
|
||||
const showForgotModal = ref(false)
|
||||
const forgotLoading = ref(false)
|
||||
const forgotSmsCountdown = ref(0)
|
||||
let forgotSmsTimer = null
|
||||
const forgotForm = reactive({ phone: '', smsCode: '', password: '', confirmPassword: '' })
|
||||
let forgotSmsUuid = ''
|
||||
|
||||
function closeForgot() {
|
||||
showForgotModal.value = false
|
||||
forgotForm.phone = ''
|
||||
forgotForm.smsCode = ''
|
||||
forgotForm.password = ''
|
||||
forgotForm.confirmPassword = ''
|
||||
forgotSmsUuid = ''
|
||||
if (forgotSmsTimer) { clearInterval(forgotSmsTimer); forgotSmsTimer = null }
|
||||
forgotSmsCountdown.value = 0
|
||||
}
|
||||
|
||||
async function sendForgotSms() {
|
||||
if (!/^1\d{10}$/.test(forgotForm.phone)) {
|
||||
return ElMessage.warning('请输入正确的手机号')
|
||||
}
|
||||
await runForgotSmsOnce(async () => {
|
||||
try {
|
||||
const r = await request({ url: '/business/auth/forgotSendSms', method: 'post', data: { phone: forgotForm.phone } })
|
||||
forgotSmsUuid = r.data || ''
|
||||
ElMessage.success('验证码已发送')
|
||||
forgotSmsCountdown.value = 60
|
||||
forgotSmsTimer = setInterval(() => {
|
||||
forgotSmsCountdown.value--
|
||||
if (forgotSmsCountdown.value <= 0) {
|
||||
clearInterval(forgotSmsTimer)
|
||||
forgotSmsTimer = null
|
||||
}
|
||||
}, 1000)
|
||||
} catch (e) {
|
||||
// request.js 拦截器已弹 ElMessage.error, 这里只重置
|
||||
forgotSmsUuid = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function onResetSubmit() {
|
||||
if (!/^1\d{10}$/.test(forgotForm.phone)) return ElMessage.warning('请输入正确的手机号')
|
||||
if (!forgotForm.smsCode) return ElMessage.warning('请输入短信验证码')
|
||||
if (!forgotForm.password || forgotForm.password.length < 6 || forgotForm.password.length > 20) {
|
||||
return ElMessage.warning('密码长度 6-20 位')
|
||||
}
|
||||
if (forgotForm.password !== forgotForm.confirmPassword) {
|
||||
return ElMessage.warning('两次密码输入不一致')
|
||||
}
|
||||
forgotLoading.value = true
|
||||
try {
|
||||
await request({
|
||||
url: '/business/auth/resetPassword',
|
||||
method: 'post',
|
||||
data: {
|
||||
phone: forgotForm.phone,
|
||||
smsCode: forgotForm.smsCode,
|
||||
uuid: forgotSmsUuid,
|
||||
password: forgotForm.password,
|
||||
confirmPassword: forgotForm.confirmPassword
|
||||
}
|
||||
})
|
||||
ElMessage.success('密码重置成功, 请用新密码登录')
|
||||
closeForgot()
|
||||
} catch (e) {
|
||||
// request.js 已弹错误
|
||||
} finally {
|
||||
forgotLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => { loadCaptcha() })
|
||||
onUnmounted(() => {
|
||||
if (forgotSmsTimer) clearInterval(forgotSmsTimer)
|
||||
if (smsSmsTimer) clearInterval(smsSmsTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -396,6 +625,30 @@ onMounted(() => { loadCaptcha() })
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
/* 登录模式切换 tabs */
|
||||
.login-page .login-tabs{
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.login-page .login-tabs .tab{
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
transition: color 0.2s, border-color 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
.login-page .login-tabs .tab:hover{ color: #1e3a8a; }
|
||||
.login-page .login-tabs .tab.active{
|
||||
color: #1e3a8a;
|
||||
border-bottom-color: #1e3a8a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.login-page .form-group{
|
||||
margin-bottom: 18px;
|
||||
position: relative;
|
||||
@@ -689,4 +942,41 @@ onMounted(() => { loadCaptcha() })
|
||||
.modal-btn.secondary:hover { background: #ebeef5; }
|
||||
.modal-btn.primary { background: var(--brand-primary, #1890ff); color: #fff; }
|
||||
.modal-btn.primary:hover { background: var(--brand-primary-deep, #096dd9); }
|
||||
|
||||
/* ===== 忘记密码弹窗 ===== */
|
||||
.modal-subtitle {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
margin: -12px 0 20px;
|
||||
}
|
||||
.sms-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.sms-group .sms-input {
|
||||
flex: 1;
|
||||
padding-left: 14px;
|
||||
}
|
||||
.sms-btn {
|
||||
flex-shrink: 0;
|
||||
width: 110px;
|
||||
height: 44px;
|
||||
border: 1px solid #d1d5db;
|
||||
background: #f9fafb;
|
||||
color: #1e3a8a;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.sms-btn:hover:not(:disabled) {
|
||||
border-color: #1e3a8a;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
.sms-btn:disabled {
|
||||
color: #9ca3af;
|
||||
background: #f3f4f6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user