feat: 本次提交更新内容如下
登录页面新样式
This commit is contained in:
54
nkebao/src/api/auth.ts
Normal file
54
nkebao/src/api/auth.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import request from './request';
|
||||||
|
|
||||||
|
export interface LoginParams {
|
||||||
|
phone: string;
|
||||||
|
password?: string;
|
||||||
|
verificationCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
code: number;
|
||||||
|
msg: string;
|
||||||
|
data: {
|
||||||
|
token: string;
|
||||||
|
token_expired: string;
|
||||||
|
member: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
phone: string;
|
||||||
|
s2_accountId: string;
|
||||||
|
avatar?: string;
|
||||||
|
email?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendCodeResponse {
|
||||||
|
code: number;
|
||||||
|
msg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 密码登录
|
||||||
|
export function loginWithPassword(phone: string, password: string) {
|
||||||
|
return request<LoginResponse>('/v1/auth/login', { phone, password }, 'POST');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证码登录
|
||||||
|
export function loginWithCode(phone: string, verificationCode: string) {
|
||||||
|
return request<LoginResponse>('/v1/auth/login-code', { phone, verificationCode }, 'POST');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送验证码
|
||||||
|
export function sendVerificationCode(phone: string) {
|
||||||
|
return request<SendCodeResponse>('/v1/auth/send-code', { phone }, 'POST');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
export function logout() {
|
||||||
|
return request('/v1/auth/logout', {}, 'POST');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
export function getUserInfo() {
|
||||||
|
return request('/v1/auth/user-info', {}, 'GET');
|
||||||
|
}
|
||||||
@@ -27,33 +27,24 @@ instance.interceptors.response.use(
|
|||||||
if (code === 200 || success) {
|
if (code === 200 || success) {
|
||||||
return res.data.data ?? res.data;
|
return res.data.data ?? res.data;
|
||||||
}
|
}
|
||||||
// 业务错误统一提示
|
|
||||||
Toast.show({ content: msg || '接口错误', position: 'top' });
|
Toast.show({ content: msg || '接口错误', position: 'top' });
|
||||||
// 分类处理
|
|
||||||
if (code === 401) {
|
if (code === 401) {
|
||||||
// 未登录或登录失效
|
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
} else if (code === 403) {
|
const currentPath = window.location.pathname + window.location.search;
|
||||||
// 无权限
|
if (currentPath === '/login') {
|
||||||
} else if (code === 500) {
|
window.location.href = '/login';
|
||||||
// 服务端异常
|
} else {
|
||||||
|
window.location.href = `/login?redirect=${encodeURIComponent(currentPath)}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Promise.reject(msg || '接口错误');
|
return Promise.reject(msg || '接口错误');
|
||||||
},
|
},
|
||||||
err => {
|
err => {
|
||||||
// 网络错误、超时等
|
|
||||||
Toast.show({ content: err.message || '网络异常', position: 'top' });
|
Toast.show({ content: err.message || '网络异常', position: 'top' });
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* @param url 接口地址
|
|
||||||
* @param data 请求参数
|
|
||||||
* @param method 请求方法
|
|
||||||
* @param config axios 配置
|
|
||||||
* @param debounceGap 防抖时间(毫秒),不传则用默认值
|
|
||||||
*/
|
|
||||||
export function request(
|
export function request(
|
||||||
url: string,
|
url: string,
|
||||||
data?: any,
|
data?: any,
|
||||||
@@ -84,4 +75,4 @@ export function request(
|
|||||||
return instance(axiosConfig);
|
return instance(axiosConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default request;
|
export default request;
|
||||||
|
|||||||
53
nkebao/src/pages/login/api.ts
Normal file
53
nkebao/src/pages/login/api.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import request from '@/api/request';
|
||||||
|
export interface LoginParams {
|
||||||
|
phone: string;
|
||||||
|
password?: string;
|
||||||
|
verificationCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
code: number;
|
||||||
|
msg: string;
|
||||||
|
data: {
|
||||||
|
token: string;
|
||||||
|
token_expired: string;
|
||||||
|
member: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
phone: string;
|
||||||
|
s2_accountId: string;
|
||||||
|
avatar?: string;
|
||||||
|
email?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendCodeResponse {
|
||||||
|
code: number;
|
||||||
|
msg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 密码登录
|
||||||
|
export function loginWithPassword(params:any) {
|
||||||
|
return request('/v1/auth/login', params, 'POST');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证码登录
|
||||||
|
export function loginWithCode(params:any) {
|
||||||
|
return request('/v1/auth/login-code', params, 'POST');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送验证码
|
||||||
|
export function sendVerificationCode(params:any) {
|
||||||
|
return request('/v1/auth/send-code',params, 'POST');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
export function logout() {
|
||||||
|
return request('/v1/auth/logout', {}, 'POST');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
export function getUserInfo() {
|
||||||
|
return request('/v1/auth/user-info', {}, 'GET');
|
||||||
|
}
|
||||||
@@ -0,0 +1,462 @@
|
|||||||
|
.login-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 背景装饰
|
||||||
|
.bg-decoration {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-circle {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
animation: float 6s ease-in-out infinite;
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
top: -100px;
|
||||||
|
right: -100px;
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
bottom: -75px;
|
||||||
|
left: -75px;
|
||||||
|
animation-delay: 2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
top: 50%;
|
||||||
|
right: 10%;
|
||||||
|
animation-delay: 4s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0px) rotate(0deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-20px) rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 32px 24px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 20px;
|
||||||
|
box-shadow: 0 6px 12px rgba(24, 144, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 800;
|
||||||
|
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-text {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标签页样式
|
||||||
|
.tab-container {
|
||||||
|
display: flex;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 3px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 7px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
width: calc(50% - 3px);
|
||||||
|
height: calc(100% - 6px);
|
||||||
|
background: white;
|
||||||
|
border-radius: 7px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&.slide {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单样式
|
||||||
|
.login-form {
|
||||||
|
:global(.adm-form) {
|
||||||
|
--adm-font-size-main: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: #1890ff;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-prefix {
|
||||||
|
padding: 0 12px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-right: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-input,
|
||||||
|
.password-input,
|
||||||
|
.code-input {
|
||||||
|
flex: 1;
|
||||||
|
border: none !important;
|
||||||
|
background: transparent !important;
|
||||||
|
padding: 12px 14px !important;
|
||||||
|
font-size: 15px !important;
|
||||||
|
color: #333 !important;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.eye-icon {
|
||||||
|
padding: 0 12px;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-code-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
margin-right: 6px;
|
||||||
|
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover:not(.disabled) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 3px 8px rgba(24, 144, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
background: #e5e5e5;
|
||||||
|
color: #999;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
:global(.adm-checkbox) {
|
||||||
|
margin-top: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-text {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-link {
|
||||||
|
color: #1890ff;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
height: 46px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 6px 12px rgba(24, 144, 255, 0.3);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 8px 16px rgba(24, 144, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: #e5e5e5;
|
||||||
|
color: #999;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
margin: 24px 0;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
padding: 0 12px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.third-party-login {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.third-party-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-icon,
|
||||||
|
.apple-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 18px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-icon {
|
||||||
|
background: #07c160;
|
||||||
|
box-shadow: 0 3px 8px rgba(7, 193, 96, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(7, 193, 96, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.apple-icon {
|
||||||
|
background: #000;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.login-container {
|
||||||
|
padding: 24px 20px;
|
||||||
|
margin: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-text {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.third-party-login {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.third-party-item {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-icon,
|
||||||
|
.apple-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,72 +1,360 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Form, Input, Button, Toast } from "antd-mobile";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { EyeInvisibleOutline, EyeOutline } from "antd-mobile-icons";
|
import { Form, Input, Button, Toast, Tabs, Checkbox } from "antd-mobile";
|
||||||
import request from "@/api/request";
|
import {
|
||||||
|
EyeInvisibleOutline,
|
||||||
|
EyeOutline,
|
||||||
|
UserOutline,
|
||||||
|
} from "antd-mobile-icons";
|
||||||
|
import { useUserStore } from "@/store/module/user";
|
||||||
|
import { loginWithPassword, loginWithCode, sendVerificationCode } from "./api";
|
||||||
import style from "./login.module.scss";
|
import style from "./login.module.scss";
|
||||||
|
|
||||||
const Login: React.FC = () => {
|
const Login: React.FC = () => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [activeTab, setActiveTab] = useState("password");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [visible, setVisible] = useState(false);
|
const [countdown, setCountdown] = useState(0);
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [agreeToTerms, setAgreeToTerms] = useState(false);
|
||||||
|
|
||||||
const onFinish = async (values: any) => {
|
const navigate = useNavigate();
|
||||||
setLoading(true);
|
const [searchParams] = useSearchParams();
|
||||||
|
const { login } = useUserStore();
|
||||||
|
|
||||||
|
// 倒计时效果
|
||||||
|
useEffect(() => {
|
||||||
|
if (countdown > 0) {
|
||||||
|
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [countdown]);
|
||||||
|
|
||||||
|
// 检查URL是否为登录页面
|
||||||
|
const isLoginPage = (url: string) => {
|
||||||
try {
|
try {
|
||||||
// 假设接口为 /api/login,实际请根据后端接口调整
|
const urlObj = new URL(url, window.location.origin);
|
||||||
const res = await request("/api/login", values, "POST");
|
return urlObj.pathname === "/login" || urlObj.pathname.endsWith("/login");
|
||||||
// 登录成功后保存 token
|
} catch {
|
||||||
localStorage.setItem("token", res.token);
|
return false;
|
||||||
Toast.show({ content: "登录成功", position: "top" });
|
}
|
||||||
// 跳转首页或其他页面
|
};
|
||||||
window.location.href = "/";
|
|
||||||
} catch (err: any) {
|
// 手机号格式验证
|
||||||
Toast.show({ content: err?.message || "登录失败", position: "top" });
|
const validatePhone = (phone: string) => {
|
||||||
|
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||||
|
return phoneRegex.test(phone);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送验证码
|
||||||
|
const handleSendVerificationCode = async () => {
|
||||||
|
const phone = form.getFieldValue("phone");
|
||||||
|
|
||||||
|
if (!phone) {
|
||||||
|
Toast.show({ content: "请输入手机号", position: "top" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validatePhone(phone)) {
|
||||||
|
Toast.show({ content: "请输入正确的11位手机号", position: "top" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await sendVerificationCode(phone);
|
||||||
|
|
||||||
|
if (response.code === 200) {
|
||||||
|
Toast.show({ content: "验证码已发送", position: "top" });
|
||||||
|
setCountdown(60);
|
||||||
|
} else {
|
||||||
|
Toast.show({ content: response.msg || "发送失败", position: "top" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Toast.show({ content: "发送失败,请稍后重试", position: "top" });
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 表单验证
|
||||||
|
const validateForm = async () => {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
|
||||||
|
if (!agreeToTerms) {
|
||||||
|
Toast.show({ content: "请同意用户协议和隐私政策", position: "top" });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validatePhone(values.phone)) {
|
||||||
|
Toast.show({ content: "请输入正确的11位手机号", position: "top" });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTab === "password" && !values.password) {
|
||||||
|
Toast.show({ content: "请输入密码", position: "top" });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTab === "verification" && !values.verificationCode) {
|
||||||
|
Toast.show({ content: "请输入验证码", position: "top" });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 登录处理
|
||||||
|
const handleLogin = async () => {
|
||||||
|
const values = await validateForm();
|
||||||
|
if (!values) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
let response;
|
||||||
|
|
||||||
|
if (activeTab === "password") {
|
||||||
|
response = await loginWithPassword(values.phone, values.password);
|
||||||
|
} else {
|
||||||
|
response = await loginWithCode(values.phone, values.verificationCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.code === 200 && response.data) {
|
||||||
|
// 保存登录信息到localStorage
|
||||||
|
localStorage.setItem("token", response.data.token);
|
||||||
|
localStorage.setItem("token_expired", response.data.token_expired);
|
||||||
|
localStorage.setItem("s2_accountId", response.data.member.s2_accountId);
|
||||||
|
localStorage.setItem("userInfo", JSON.stringify(response.data.member));
|
||||||
|
|
||||||
|
// 更新状态管理
|
||||||
|
login(response.data.token, response.data.member);
|
||||||
|
|
||||||
|
Toast.show({ content: "登录成功", position: "top" });
|
||||||
|
|
||||||
|
// 跳转到首页或重定向URL
|
||||||
|
const returnUrl = searchParams.get("returnUrl");
|
||||||
|
if (returnUrl) {
|
||||||
|
const decodedUrl = decodeURIComponent(returnUrl);
|
||||||
|
if (isLoginPage(decodedUrl)) {
|
||||||
|
navigate("/");
|
||||||
|
} else {
|
||||||
|
window.location.href = decodedUrl;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.show({ content: response.msg || "登录失败", position: "top" });
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
Toast.show({
|
||||||
|
content: error?.message || "登录失败,请稍后重试",
|
||||||
|
position: "top",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 第三方登录处理
|
||||||
|
const handleWechatLogin = () => {
|
||||||
|
Toast.show({ content: "微信登录功能开发中", position: "top" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAppleLogin = () => {
|
||||||
|
Toast.show({ content: "Apple登录功能开发中", position: "top" });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style["login-page"]}>
|
<div className={style["login-page"]}>
|
||||||
<div className={style["login-title"]}>账号登录</div>
|
{/* 背景装饰 */}
|
||||||
<Form
|
<div className={style["bg-decoration"]}>
|
||||||
layout="horizontal"
|
<div className={style["bg-circle"]}></div>
|
||||||
onFinish={onFinish}
|
<div className={style["bg-circle"]}></div>
|
||||||
footer={
|
<div className={style["bg-circle"]}></div>
|
||||||
<Button
|
</div>
|
||||||
block
|
|
||||||
type="submit"
|
<div className={style["login-container"]}>
|
||||||
color="primary"
|
{/* Logo和标题区域 */}
|
||||||
loading={loading}
|
<div className={style["login-header"]}>
|
||||||
size="large"
|
<div className={style["logo-section"]}>
|
||||||
|
<div className={style["logo-icon"]}>
|
||||||
|
<UserOutline />
|
||||||
|
</div>
|
||||||
|
<h1 className={style["app-name"]}>存客宝</h1>
|
||||||
|
</div>
|
||||||
|
<p className={style["welcome-text"]}>欢迎回来</p>
|
||||||
|
<p className={style["subtitle"]}>登录您的账户继续使用</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 登录表单 */}
|
||||||
|
<div className={style["form-container"]}>
|
||||||
|
{/* 标签页切换 */}
|
||||||
|
<div className={style["tab-container"]}>
|
||||||
|
<div
|
||||||
|
className={`${style["tab-item"]} ${
|
||||||
|
activeTab === "password" ? style["active"] : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setActiveTab("password")}
|
||||||
|
>
|
||||||
|
密码登录
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${style["tab-item"]} ${
|
||||||
|
activeTab === "verification" ? style["active"] : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setActiveTab("verification")}
|
||||||
|
>
|
||||||
|
验证码登录
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${style["tab-indicator"]} ${
|
||||||
|
activeTab === "verification" ? style["slide"] : ""
|
||||||
|
}`}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
className={style["login-form"]}
|
||||||
|
onFinish={handleLogin}
|
||||||
>
|
>
|
||||||
登录
|
{/* 手机号输入 */}
|
||||||
</Button>
|
<div className={style["input-group"]}>
|
||||||
}
|
<label className={style["input-label"]}>手机号</label>
|
||||||
>
|
<div className={style["input-wrapper"]}>
|
||||||
<Form.Item
|
<span className={style["input-prefix"]}>+86</span>
|
||||||
name="username"
|
<Input
|
||||||
label="账号"
|
name="phone"
|
||||||
rules={[{ required: true, message: "请输入账号" }]}
|
placeholder="请输入手机号"
|
||||||
>
|
clearable
|
||||||
<Input placeholder="请输入账号" clearable />
|
className={style["phone-input"]}
|
||||||
</Form.Item>
|
/>
|
||||||
<Form.Item
|
|
||||||
name="password"
|
|
||||||
label="密码"
|
|
||||||
rules={[{ required: true, message: "请输入密码" }]}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
placeholder="请输入密码"
|
|
||||||
clearable
|
|
||||||
type={visible ? "text" : "password"}
|
|
||||||
extra={
|
|
||||||
<div onClick={() => setVisible((v) => !v)}>
|
|
||||||
{visible ? <EyeOutline /> : <EyeInvisibleOutline />}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
/>
|
|
||||||
</Form.Item>
|
{/* 密码输入 */}
|
||||||
</Form>
|
{activeTab === "password" && (
|
||||||
|
<div className={style["input-group"]}>
|
||||||
|
<label className={style["input-label"]}>密码</label>
|
||||||
|
<div className={style["input-wrapper"]}>
|
||||||
|
<Input
|
||||||
|
name="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
clearable
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
className={style["password-input"]}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={style["eye-icon"]}
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOutline /> : <EyeInvisibleOutline />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 验证码输入 */}
|
||||||
|
{activeTab === "verification" && (
|
||||||
|
<div className={style["input-group"]}>
|
||||||
|
<label className={style["input-label"]}>验证码</label>
|
||||||
|
<div className={style["input-wrapper"]}>
|
||||||
|
<Input
|
||||||
|
name="verificationCode"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
clearable
|
||||||
|
className={style["code-input"]}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${style["send-code-btn"]} ${
|
||||||
|
countdown > 0 ? style["disabled"] : ""
|
||||||
|
}`}
|
||||||
|
onClick={handleSendVerificationCode}
|
||||||
|
disabled={loading || countdown > 0}
|
||||||
|
>
|
||||||
|
{countdown > 0 ? `${countdown}s` : "获取验证码"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 用户协议 */}
|
||||||
|
<div className={style["agreement-section"]}>
|
||||||
|
<Checkbox
|
||||||
|
checked={agreeToTerms}
|
||||||
|
onChange={setAgreeToTerms}
|
||||||
|
className={style["agreement-checkbox"]}
|
||||||
|
>
|
||||||
|
<span className={style["agreement-text"]}>
|
||||||
|
我已阅读并同意
|
||||||
|
<span className={style["agreement-link"]}>
|
||||||
|
《存客宝用户协议》
|
||||||
|
</span>
|
||||||
|
和
|
||||||
|
<span className={style["agreement-link"]}>《隐私政策》</span>
|
||||||
|
</span>
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 登录按钮 */}
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
loading={loading}
|
||||||
|
size="large"
|
||||||
|
className={style["login-btn"]}
|
||||||
|
>
|
||||||
|
{loading ? "登录中..." : "登录"}
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
{/* 分割线 */}
|
||||||
|
<div className={style["divider"]}>
|
||||||
|
<span>其他登录方式</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 第三方登录 */}
|
||||||
|
<div className={style["third-party-login"]}>
|
||||||
|
<div
|
||||||
|
className={style["third-party-item"]}
|
||||||
|
onClick={handleWechatLogin}
|
||||||
|
>
|
||||||
|
<div className={style["wechat-icon"]}>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
width="24"
|
||||||
|
className={style["wechat-icon"]}
|
||||||
|
>
|
||||||
|
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.81-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.595-6.348zM5.959 5.48c.609 0 1.104.498 1.104 1.112 0 .612-.495 1.11-1.104 1.11-.612 0-1.108-.498-1.108-1.11 0-.614.496-1.112 1.108-1.112zm5.315 0c.61 0 1.107.498 1.107 1.112 0 .612-.497 1.11-1.107 1.11-.611 0-1.105-.498-1.105-1.11 0-.614.494-1.112 1.105-1.112z"></path>
|
||||||
|
<path d="M23.002 15.816c0-3.309-3.136-6-7-6-3.863 0-7 2.691-7 6 0 3.31 3.137 6 7 6 .814 0 1.601-.099 2.338-.285a.7.7 0 0 1 .579.08l1.5.87a.267.267 0 0 0 .135.044c.13 0 .236-.108.236-.241 0-.06-.023-.118-.038-.17l-.309-1.167a.476.476 0 0 1 .172-.534c1.645-1.17 2.387-2.835 2.387-4.597zm-9.498-1.19c-.497 0-.9-.407-.9-.908a.905.905 0 0 1 .9-.91c.498 0 .9.408.9.91 0 .5-.402.908-.9.908zm4.998 0c-.497 0-.9-.407-.9-.908a.905.905 0 0 1 .9-.91c.498 0 .9.408.9.91 0 .5-.402.908-.9.908z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span>微信</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={style["third-party-item"]}
|
||||||
|
onClick={handleAppleLogin}
|
||||||
|
>
|
||||||
|
<div className={style["apple-icon"]}>
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span>Apple</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import Home from "@/pages/home/index";
|
|
||||||
import Login from "@/pages/login/login";
|
|
||||||
|
|
||||||
const homeRoutes = [
|
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
element: <Home />,
|
|
||||||
auth: false, // 不需要权限
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/login",
|
|
||||||
element: <Login />,
|
|
||||||
auth: false, // 不需要权限
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default homeRoutes;
|
|
||||||
35
nkebao/src/router/module/index.tsx
Normal file
35
nkebao/src/router/module/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import Home from "@/pages/home/index";
|
||||||
|
import Login from "@/pages/login/login";
|
||||||
|
import Scene from "@/pages/scene/index";
|
||||||
|
import Work from "@/pages/work/index";
|
||||||
|
import Mine from "@/pages/mine/index";
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <Home />,
|
||||||
|
auth: true, // 需要登录
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/login",
|
||||||
|
element: <Login />,
|
||||||
|
auth: false, // 不需要权限
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/scene",
|
||||||
|
element: <Scene />,
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/work",
|
||||||
|
element: <Work />,
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/mine",
|
||||||
|
element: <Mine />,
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
@@ -1,27 +1,45 @@
|
|||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
import { Navigate, useLocation } from "react-router-dom";
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
import { useUserStore } from "@/store/module/user"; // 假设你用 zustand 管理用户状态
|
import { useUserStore } from "@/store/module/user";
|
||||||
|
|
||||||
interface Props {
|
interface PermissionRouteProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
requiredRole?: string; // 可选:需要的角色
|
requiredRole?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PermissionRoute: React.FC<Props> = ({ children, requiredRole }) => {
|
const PermissionRoute: React.FC<PermissionRouteProps> = ({
|
||||||
const user = useUserStore((state) => state.user);
|
children,
|
||||||
|
requiredRole,
|
||||||
|
}) => {
|
||||||
|
const { user, isLoggedIn } = useUserStore();
|
||||||
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
// 未登录
|
useEffect(() => {
|
||||||
if (!user) {
|
// 检查是否已登录
|
||||||
return <Navigate to="/login" state={{ from: location }} replace />;
|
if (!isLoggedIn || !user) {
|
||||||
|
const currentPath = location.pathname + location.search;
|
||||||
|
navigate(`/login?returnUrl=${encodeURIComponent(currentPath)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查角色权限(如果需要)
|
||||||
|
if (requiredRole && user.role !== requiredRole) {
|
||||||
|
navigate("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [isLoggedIn, user, requiredRole, navigate, location]);
|
||||||
|
|
||||||
|
// 如果未登录,不渲染子组件
|
||||||
|
if (!isLoggedIn || !user) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 有角色要求但不满足
|
// 如果角色不匹配,不渲染子组件
|
||||||
if (requiredRole && user.role !== requiredRole) {
|
if (requiredRole && user.role !== requiredRole) {
|
||||||
return <Navigate to="/403" replace />;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通过
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,54 @@
|
|||||||
import { createPersistStore } from '@/store/createPersistStore';
|
import { createPersistStore } from '@/store/createPersistStore';
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
phone: string;
|
||||||
role: string;
|
role: string;
|
||||||
token: string;
|
token: string;
|
||||||
|
token_expired?: string;
|
||||||
|
s2_accountId?: string;
|
||||||
|
avatar?: string;
|
||||||
|
email?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserState {
|
interface UserState {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
|
isLoggedIn: boolean;
|
||||||
setUser: (user: User) => void;
|
setUser: (user: User) => void;
|
||||||
clearUser: () => void;
|
clearUser: () => void;
|
||||||
|
login: (token: string, userInfo: any) => void;
|
||||||
|
logout: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUserStore = createPersistStore<UserState>(
|
export const useUserStore = createPersistStore<UserState>(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
user: null,
|
user: null,
|
||||||
setUser: (user) => set({ user }),
|
isLoggedIn: false,
|
||||||
clearUser: () => set({ user: null }),
|
setUser: (user) => set({ user, isLoggedIn: true }),
|
||||||
|
clearUser: () => set({ user: null, isLoggedIn: false }),
|
||||||
|
login: (token, userInfo) => {
|
||||||
|
const user: User = {
|
||||||
|
id: userInfo.id,
|
||||||
|
name: userInfo.name || userInfo.nickname || userInfo.username,
|
||||||
|
phone: userInfo.phone,
|
||||||
|
role: userInfo.role || 'user',
|
||||||
|
token,
|
||||||
|
token_expired: userInfo.token_expired,
|
||||||
|
s2_accountId: userInfo.s2_accountId,
|
||||||
|
avatar: userInfo.avatar,
|
||||||
|
email: userInfo.email,
|
||||||
|
};
|
||||||
|
set({ user, isLoggedIn: true });
|
||||||
|
},
|
||||||
|
logout: () => {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('token_expired');
|
||||||
|
localStorage.removeItem('s2_accountId');
|
||||||
|
localStorage.removeItem('userInfo');
|
||||||
|
set({ user: null, isLoggedIn: false });
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
'user-store',
|
'user-store',
|
||||||
(state) => ({ user: state.user })
|
(state) => ({ user: state.user, isLoggedIn: state.isLoggedIn })
|
||||||
);
|
);
|
||||||
Reference in New Issue
Block a user