Files
DJKB/my-vue-app/src/views/login/login.vue
chenpanliang 2f380b1fe5 fix(登录): 修复登录流程问题并优化样式
- 在登录前清除本地存储的用户数据
- 允许直接访问登录页面无需重定向
- 调整销售时间线组件的字体大小和间距
2025-08-28 21:37:50 +08:00

1126 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="login-container">
<div class="login-card">
<div class="login-header">
<h1>欢迎登录</h1>
<p>请输入您的账号和密码</p>
</div>
<form @submit.prevent="handleLogin" class="login-form">
<div class="form-group">
<label for="username">账号</label>
<input
id="username"
v-model="loginForm.username"
type="text"
placeholder="请输入账号"
required
:disabled="loading"
/>
</div>
<div class="form-group">
<label for="password">密码</label>
<input
id="password"
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
required
:disabled="loading"
/>
</div>
<button type="submit" class="login-btn" :disabled="loading" @click="handleLogin">
<span v-if="loading" class="loading-spinner"></span>
{{ loading ? '登录中...' : '登录' }}
</button>
</form>
<div class="forgot-password-link">
<a href="#" @click.prevent="showForgotPasswordModalHandler" class="forgot-link">忘记密码</a>
</div>
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
</div>
<!-- 密保设置弹窗 -->
<div v-if="showSecurityModal" class="security-modal-overlay">
<div class="security-modal">
<div class="security-modal-header">
<h2>设置密保问题</h2>
<p>首次登录需要设置密保问题用于账户安全验证</p>
</div>
<div class="security-modal-body">
<div class="form-group">
<label for="security-question">密保问题</label>
<input
id="security-question"
v-model="securityForm.question"
type="text"
placeholder="请输入密保问题,例如:你的城市是?"
required
:disabled="securityLoading"
/>
</div>
<div class="form-group">
<label for="security-answer">密保答案</label>
<input
id="security-answer"
v-model="securityForm.answer"
type="text"
placeholder="请输入密保答案"
required
:disabled="securityLoading"
/>
</div>
</div>
<div class="security-modal-footer">
<button
type="button"
class="btn-cancel"
@click="cancelSecuritySetup"
:disabled="securityLoading"
>
取消
</button>
<button
type="button"
class="btn-confirm"
@click="handleSetSecurity"
:disabled="securityLoading"
>
<span v-if="securityLoading" class="loading-spinner"></span>
{{ securityLoading ? '设置中...' : '确认设置' }}
</button>
</div>
</div>
</div>
<!-- 忘记密码弹窗 -->
<div v-if="showForgotPasswordModal" class="forgot-password-modal-overlay">
<div class="forgot-password-modal">
<!-- 密保验证步骤 -->
<div v-if="forgotPasswordStep === 1" class="forgot-password-step">
<div class="forgot-password-modal-header">
<h2>密保验证</h2>
<p>请输入您的用户名和密保答案进行验证</p>
</div>
<div class="forgot-password-modal-body">
<div class="form-group">
<label for="forgot-username">用户名</label>
<input
id="forgot-username"
v-model="forgotPasswordForm.username"
type="text"
placeholder="请输入用户名"
required
:disabled="forgotPasswordLoading"
/>
</div>
<div class="form-group">
<label for="security-answer">密保答案</label>
<input
id="security-answer"
v-model="forgotPasswordForm.securityAnswer"
type="text"
placeholder="请输入密保答案"
required
:disabled="forgotPasswordLoading"
/>
</div>
</div>
<div class="forgot-password-modal-footer">
<button
type="button"
class="btn-cancel"
@click="closeForgotPasswordModal"
:disabled="forgotPasswordLoading"
>
取消
</button>
<button
type="button"
class="btn-confirm"
@click="verifySecurityQuestion"
:disabled="forgotPasswordLoading"
>
<span v-if="forgotPasswordLoading" class="loading-spinner"></span>
{{ forgotPasswordLoading ? '验证中...' : '验证' }}
</button>
</div>
</div>
<!-- 密码修改步骤 -->
<div v-if="forgotPasswordStep === 2" class="forgot-password-step">
<div class="forgot-password-modal-header">
<h2>修改密码</h2>
<p>密保验证通过请输入新密码</p>
</div>
<div class="forgot-password-modal-body">
<div class="form-group">
<label for="new-password">新密码</label>
<input
id="new-password"
v-model="forgotPasswordForm.newPassword"
type="password"
placeholder="请输入新密码"
required
:disabled="forgotPasswordLoading"
/>
</div>
<div class="form-group">
<label for="confirm-password">确认密码</label>
<input
id="confirm-password"
v-model="forgotPasswordForm.confirmPassword"
type="password"
placeholder="请再次输入新密码"
required
:disabled="forgotPasswordLoading"
/>
</div>
</div>
<div class="forgot-password-modal-footer">
<button
type="button"
class="btn-cancel"
@click="closeForgotPasswordModal"
:disabled="forgotPasswordLoading"
>
取消
</button>
<button
type="button"
class="btn-confirm"
@click="changePasswordBySecurity"
:disabled="forgotPasswordLoading"
>
<span v-if="forgotPasswordLoading" class="loading-spinner"></span>
{{ forgotPasswordLoading ? '修改中...' : '确认修改' }}
</button>
</div>
</div>
</div>
</div>
<!-- 部门选择弹窗 -->
<div v-if="showDepartmentModal" class="department-modal-overlay">
<div class="department-modal">
<div class="department-modal-header">
<h2>选择部门</h2>
<p>检测到用户名重复请选择您所属的部门</p>
</div>
<div class="department-modal-body">
<div class="department-list">
<label
v-for="dept in departments"
:key="dept.id"
class="department-item"
:class="{ active: selectedDepartment === dept.id }"
>
<input
type="radio"
:value="dept.id"
v-model="selectedDepartment"
:disabled="departmentLoading"
/>
<span>{{ dept.name }}</span>
</label>
</div>
</div>
<div class="department-modal-footer">
<button
type="button"
class="btn-confirm"
@click="confirmDepartment"
:disabled="departmentLoading || !selectedDepartment"
>
<span v-if="departmentLoading" class="loading-spinner"></span>
{{ departmentLoading ? '确认中...' : '确认' }}
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import http from '@/utils/https'
import { useUserStore } from '@/stores/user'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
// 响应式数据
const loginForm = ref({
username: '',
password: ''
})
const loading = ref(false)
const errorMessage = ref('')
const showSecurityModal = ref(false)
const securityForm = ref({
question: '',
answer: ''
})
const securityLoading = ref(false)
const showDepartmentModal = ref(false)
const selectedDepartment = ref('')
const departmentLoading = ref(false)
const departments = ref([])
// 忘记密码相关状态
const showForgotPasswordModal = ref(false)
const forgotPasswordStep = ref(1) // 1: 密保验证, 2: 密码修改
const forgotPasswordLoading = ref(false)
const forgotPasswordForm = ref({
username: '',
securityAnswer: '',
newPassword: '',
confirmPassword: ''
})
// 忘记密码处理函数
const showForgotPasswordModalHandler = () => {
showForgotPasswordModal.value = true
forgotPasswordStep.value = 1
forgotPasswordForm.value = {
username: '',
securityAnswer: '',
newPassword: '',
confirmPassword: ''
}
}
const closeForgotPasswordModal = () => {
showForgotPasswordModal.value = false
forgotPasswordStep.value = 1
forgotPasswordForm.value = {
username: '',
securityAnswer: '',
newPassword: '',
confirmPassword: ''
}
}
const verifySecurityQuestion = async () => {
if (!forgotPasswordForm.value.username || !forgotPasswordForm.value.securityAnswer) {
alert('请输入用户名和密保答案')
return
}
forgotPasswordLoading.value = true
const params= {
user_name: forgotPasswordForm.value.username,
security_question_answer: forgotPasswordForm.value.securityAnswer
}
try {
const response = await http.post('/api/v1/verify_security_question', params)
if (response.code === 200 || response.success) {
// 验证成功,进入密码修改步骤
forgotPasswordStep.value = 2
} else {
alert(response.message || '密保验证失败')
}
} catch (error) {
console.error('密保验证失败:', error)
alert('密保验证失败,请重试')
} finally {
forgotPasswordLoading.value = false
}
}
const changePasswordBySecurity = async () => {
if (!forgotPasswordForm.value.newPassword || !forgotPasswordForm.value.confirmPassword) {
alert('请输入新密码和确认密码')
return
}
if (forgotPasswordForm.value.newPassword !== forgotPasswordForm.value.confirmPassword) {
alert('两次输入的密码不一致')
return
}
forgotPasswordLoading.value = true
try {
const response = await http.post('/api/v1/change_password_by_security', {
user_name: forgotPasswordForm.value.username,
new_password: forgotPasswordForm.value.newPassword
})
if (response.code === 200 || response.success) {
alert('密码修改成功,请使用新密码登录')
closeForgotPasswordModal()
} else {
alert(response.message || '密码修改失败')
}
} catch (error) {
console.error('密码修改失败:', error)
alert('密码修改失败,请重试')
} finally {
forgotPasswordLoading.value = false
}
}
// 登录处理函数
const handleLogin = async () => {
if (!loginForm.value.username || !loginForm.value.password) {
errorMessage.value = '请输入账号和密码'
return
}
loading.value = true
errorMessage.value = ''
// 清除本地存储的用户数据,确保使用最新的登录信息
userStore.logout()
try {
// 调用登录API
// token检测
const response = await http.post('/api/v1/login', {
username: loginForm.value.username,
password: loginForm.value.password
})
// 登录成功处理
if (response.code === 200 || response.success) {
// 保存登录响应数据
loginResponseData = response
// 使用Pinia存储用户信息和token
if (response && response.token) {
userStore.login(response.token, response.name, response.user_level, response.department, response.department_id)
}
// 检查用户是否重名
await checkUserDuplicate()
} else {
errorMessage.value = response.message || '登录失败'
}
} catch (error) {
// 错误已在axios拦截器中处理这里只需要设置本地错误信息
errorMessage.value = error.message || '登录失败,请重试'
} finally {
loading.value = false
}
}
// 根据用户级别跳转页面
const navigateToUserPage = (userLevel) => {
if (userLevel === 1) {
router.push('/sale')
} else if (userLevel === 2) {
router.push('/manager')
} else if (userLevel === 3) {
router.push('/senior-manager')
} else if (userLevel === 4) {
router.push('/second-top')
} else if (userLevel === 5) {
router.push('/top')
}
}
// 存储登录响应数据,用于密保设置后跳转
let loginResponseData = null
// 检查用户是否重名
const checkUserDuplicate = async () => {
try {
const response = await http.post('/api/v1/check_user', {
username: loginForm.value.username
})
if (response.code === 200 || response.success) {
if (response.is_duplicate === true) {
// 用户重名,设置部门列表并显示部门选择弹窗
if (response.departments && Array.isArray(response.departments)) {
departments.value = response.departments.map(dept => ({
id: dept.department_id,
name: dept.department
}))
}
showDepartmentModal.value = true
} else {
// 用户不重名,检查是否需要设置密保
handleAfterDuplicateCheck()
}
} else {
alert(response.message || '检查用户信息失败')
}
} catch (error) {
console.error('检查用户重名失败:', error)
alert('检查用户信息失败,请重试')
}
}
// 处理重名检查后的逻辑
const handleAfterDuplicateCheck = () => {
if (loginResponseData.is_security_set === false) {
// 首次登录,需要设置密保
showSecurityModal.value = true
} else {
// 不是首次登录,跳转到首页或对应页面
navigateToUserPage(loginResponseData.user_level)
}
}
// 确认部门选择
const confirmDepartment = async () => {
if (!selectedDepartment.value) {
alert('请选择部门')
return
}
departmentLoading.value = true
try {
// 这里可以添加绑定部门的API调用
// const response = await http.post('/api/v1/bind_department', {
// department_id: selectedDepartment.value
// })
// 关闭部门选择弹窗
showDepartmentModal.value = false
// 继续后续流程
handleAfterDuplicateCheck()
} catch (error) {
console.error('绑定部门失败:', error)
alert('绑定部门失败,请重试')
} finally {
departmentLoading.value = false
}
}
// 设置密保
const handleSetSecurity = async () => {
if (!securityForm.value.question || !securityForm.value.answer) {
alert('请填写完整的密保问题和答案')
return
}
securityLoading.value = true
try {
const response = await http.post('/api/v1/set_security_question', {
question: securityForm.value.question,
answer: securityForm.value.answer
})
console.log(11111,response)
if (response.code === 200 || response.success) {
// 密保设置成功,关闭弹窗并跳转页面
showSecurityModal.value = false
// 使用保存的登录响应数据进行跳转
if (loginResponseData) {
navigateToUserPage(loginResponseData.user_level)
}
} else {
alert(response.message || '密保设置失败')
}
} catch (error) {
console.error('密保设置失败:', error)
alert('密保设置失败,请重试')
} finally {
securityLoading.value = false
}
}
// 取消密保设置(强制设置,不允许取消)
const cancelSecuritySetup = () => {
alert('首次登录必须设置密保问题')
}
// Token验证登录函数
const handleTokenLogin = async (token, username = null, userLevel = null) => {
loading.value = true
errorMessage.value = ''
try {
// 如果URL中包含用户信息直接使用跳过API验证
if (username && userLevel) {
// 解码用户名
const decodedUsername = decodeURIComponent(username)
// 直接设置用户信息到store
userStore.login(token, decodedUsername, parseInt(userLevel), '', '')
// 根据用户等级跳转到对应页面
navigateToUserPage(parseInt(userLevel))
return
}
// 使用token进行API验证登录
const response = await http.post('/api/v1/token_login', {
token: token
})
if (response.code === 200 || response.success) {
// 保存登录响应数据
loginResponseData = response
// 使用Pinia存储用户信息和token
if (response && response.token) {
userStore.login(response.token, response.name, response.user_level, response.department, response.department_id)
}
// 检查用户是否重名
await checkUserDuplicate()
} else {
errorMessage.value = response.message || 'Token验证失败'
}
} catch (error) {
errorMessage.value = error.message || 'Token验证失败请重试'
} finally {
loading.value = false
}
}
// 组件挂载时检查路由参数中的token
onMounted(() => {
const token = route.query.token
const username = route.query.username
const userLevel = route.query.level
if (token) {
// 如果路由参数中有token进行token验证登录
// 如果同时有用户信息直接使用否则通过API验证
handleTokenLogin(token, username, userLevel)
}
})
</script>
<style scoped>
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
/* background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); */
padding: 20px;
}
.login-card {
background: white;
border-radius: 16px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
padding: 40px;
width: 100%;
max-width: 400px;
animation: slideUp 0.6s ease-out;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.login-header {
text-align: center;
margin-bottom: 32px;
}
.login-header h1 {
font-size: 28px;
font-weight: 700;
color: #1a202c;
margin: 0 0 8px 0;
}
.login-header p {
color: #718096;
font-size: 14px;
margin: 0;
}
.login-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
font-size: 14px;
font-weight: 600;
color: #374151;
}
.form-group input {
padding: 12px 16px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 16px;
transition: all 0.3s ease;
background: #f9fafb;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
background: white;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-group input:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.login-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
padding: 14px 24px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 8px;
}
.login-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3);
}
.login-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
}
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.error-message {
background: #fee2e2;
color: #dc2626;
padding: 12px 16px;
border-radius: 8px;
font-size: 14px;
text-align: center;
margin-top: 16px;
border: 1px solid #fecaca;
}
/* 响应式设计 */
@media (max-width: 480px) {
.login-card {
padding: 24px;
margin: 16px;
}
.login-header h1 {
font-size: 24px;
}
}
/* 密保设置弹窗样式 */
.security-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(4px);
}
.security-modal {
background: white;
border-radius: 16px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
animation: modalSlideIn 0.3s ease-out;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: scale(0.9) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.security-modal-header {
padding: 24px 24px 16px 24px;
border-bottom: 1px solid #e2e8f0;
text-align: center;
}
.security-modal-header h2 {
font-size: 20px;
font-weight: 600;
color: #1a202c;
margin: 0 0 8px 0;
}
.security-modal-header p {
font-size: 14px;
color: #64748b;
margin: 0;
}
.security-modal-body {
padding: 24px;
}
.security-modal-footer {
padding: 16px 24px 24px 24px;
display: flex;
gap: 12px;
justify-content: flex-end;
}
.btn-cancel {
padding: 10px 20px;
border: 1px solid #d1d5db;
background: white;
color: #374151;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-cancel:hover:not(:disabled) {
background: #f9fafb;
border-color: #9ca3af;
}
.btn-confirm {
padding: 10px 20px;
border: none;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.btn-confirm:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-confirm:disabled,
.btn-cancel:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
/* 忘记密码链接样式 */
.forgot-password-link {
text-align: center;
margin-top: 15px;
}
.forgot-link {
color: #007bff;
text-decoration: none;
font-size: 14px;
transition: color 0.3s ease;
}
.forgot-link:hover {
color: #0056b3;
text-decoration: underline;
}
/* 忘记密码弹窗样式 */
.forgot-password-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.forgot-password-modal {
background: white;
border-radius: 8px;
padding: 0;
width: 90%;
max-width: 450px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
animation: modalSlideIn 0.3s ease-out;
}
.forgot-password-modal-header {
padding: 24px 24px 16px;
border-bottom: 1px solid #e9ecef;
}
.forgot-password-modal-header h2 {
margin: 0 0 8px 0;
color: #333;
font-size: 20px;
font-weight: 600;
}
.forgot-password-modal-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.forgot-password-modal-body {
padding: 24px;
}
.forgot-password-modal-body .form-group {
margin-bottom: 20px;
}
.forgot-password-modal-body .form-group:last-child {
margin-bottom: 0;
}
.forgot-password-modal-body label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
font-size: 14px;
}
.forgot-password-modal-body input {
width: 100%;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
box-sizing: border-box;
}
.forgot-password-modal-body input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}
.forgot-password-modal-body input:disabled {
background-color: #f8f9fa;
cursor: not-allowed;
}
.forgot-password-modal-footer {
padding: 16px 24px 24px;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.forgot-password-modal-footer .btn-cancel {
padding: 10px 20px;
border: 1px solid #ddd;
background: white;
color: #666;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.forgot-password-modal-footer .btn-cancel:hover:not(:disabled) {
background: #f8f9fa;
border-color: #bbb;
}
.forgot-password-modal-footer .btn-confirm {
padding: 10px 20px;
border: none;
background: #007bff;
color: white;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.forgot-password-modal-footer .btn-confirm:hover:not(:disabled) {
background: #0056b3;
}
.forgot-password-modal-footer .btn-confirm:disabled,
.forgot-password-modal-footer .btn-cancel:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 部门选择弹窗样式 */
.department-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(4px);
}
.department-modal {
background: white;
border-radius: 16px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
width: 90%;
max-width: 400px;
max-height: 90vh;
overflow-y: auto;
animation: modalSlideIn 0.3s ease-out;
}
.department-modal-header {
padding: 24px 24px 16px 24px;
border-bottom: 1px solid #e2e8f0;
text-align: center;
}
.department-modal-header h2 {
font-size: 20px;
font-weight: 600;
color: #1a202c;
margin: 0 0 8px 0;
}
.department-modal-header p {
font-size: 14px;
color: #64748b;
margin: 0;
}
.department-modal-body {
padding: 24px;
}
.department-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.department-item {
display: flex;
align-items: center;
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
background: white;
}
.department-item:hover {
border-color: #667eea;
background: #f8faff;
}
.department-item.active {
border-color: #667eea;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.department-item input[type="radio"] {
margin-right: 12px;
width: 16px;
height: 16px;
}
.department-item span {
font-size: 14px;
font-weight: 500;
}
.department-modal-footer {
padding: 16px 24px 24px 24px;
display: flex;
justify-content: center;
}
.department-modal-footer .btn-confirm {
min-width: 120px;
}
</style>