【feat】 初步实现了账号密码登录
This commit is contained in:
parent
39b80c307a
commit
e50dfc2304
204
doc/salesforce/auth/Salesforce登录接口说明文档.md
Normal file
204
doc/salesforce/auth/Salesforce登录接口说明文档.md
Normal file
@ -0,0 +1,204 @@
|
||||
# Salesforce登录接口说明文档
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本文档详细描述了系统中提供的四种Salesforce登录策略的使用方法,包括每种策略所需的参数、调用方式以及异常处理机制。系统采用策略模式设计,方便扩展和维护。
|
||||
|
||||
## 2. 登录策略概览
|
||||
|
||||
系统目前支持以下四种登录策略:
|
||||
|
||||
| 策略名称 | 登录类型(loginType) | 描述 |
|
||||
|---------|-------------------|------|
|
||||
| OAuth2LoginStrategy | oauth2 | 支持OAuth 2.0的三种授权模式 |
|
||||
| LegacyCredentialLoginStrategy | legacy_credential | 使用用户名、密码和安全令牌的传统登录方式 |
|
||||
| SessionIdLoginStrategy | session_id | 使用已有的Salesforce Session ID进行登录 |
|
||||
| SalesforceCliLoginStrategy | salesforce_cli | 通过Salesforce CLI工具获取访问令牌 |
|
||||
|
||||
## 3. 各登录策略详细说明
|
||||
|
||||
### 3.1 OAuth2LoginStrategy (oauth2)
|
||||
|
||||
#### 3.1.1 支持的授权模式
|
||||
|
||||
该策略支持以下三种OAuth 2.0授权模式:
|
||||
|
||||
1. **密码模式 (Password Grant)** - 直接使用用户名和密码获取访问令牌
|
||||
2. **客户端凭证模式 (Client Credentials Grant)** - 使用客户端凭证获取访问令牌
|
||||
3. **授权码模式 (Authorization Code Grant)** - 通过用户授权获取访问令牌
|
||||
|
||||
#### 3.1.2 参数说明
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|-------|-----|-----|------|
|
||||
| loginType | String | 是 | 固定值:"oauth2" |
|
||||
| grantType | String | 是 | 授权类型:"password"、"client_credentials"或"authorization_code" |
|
||||
| username | String | 条件 | 密码模式必填 |
|
||||
| password | String | 条件 | 密码模式必填 |
|
||||
| securityToken | String | 条件 | 密码模式时的安全令牌 |
|
||||
| clientId | String | 是 | 应用的客户端ID |
|
||||
| clientSecret | String | 是 | 应用的客户端密钥 |
|
||||
| code | String | 条件 | 授权码模式必填 |
|
||||
| state | String | 条件 | 授权码模式必填,用于防止CSRF攻击 |
|
||||
|
||||
#### 3.1.3 调用示例
|
||||
|
||||
##### 密码模式示例:
|
||||
|
||||
```java
|
||||
SalesforceLoginRequest request = new SalesforceLoginRequest();
|
||||
request.setLoginType("oauth2");
|
||||
request.setGrantType("password");
|
||||
request.setUsername("user@example.com");
|
||||
request.setPassword("password");
|
||||
request.setSecurityToken("securityToken");
|
||||
request.setClientId("your_client_id");
|
||||
request.setClientSecret("your_client_secret");
|
||||
|
||||
SalesforceLoginResult result = loginService.login(request);
|
||||
```
|
||||
|
||||
##### 客户端凭证模式示例:
|
||||
|
||||
```java
|
||||
SalesforceLoginRequest request = new SalesforceLoginRequest();
|
||||
request.setLoginType("oauth2");
|
||||
request.setGrantType("client_credentials");
|
||||
request.setClientId("your_client_id");
|
||||
request.setClientSecret("your_client_secret");
|
||||
|
||||
SalesforceLoginResult result = loginService.login(request);
|
||||
```
|
||||
|
||||
##### 授权码模式示例:
|
||||
|
||||
```java
|
||||
// 第一步:生成授权URL并引导用户访问
|
||||
// 第二步:用户授权后回调获取code和state参数
|
||||
SalesforceLoginRequest request = new SalesforceLoginRequest();
|
||||
request.setLoginType("oauth2");
|
||||
request.setGrantType("authorization_code");
|
||||
request.setCode("authorization_code_from_callback");
|
||||
request.setState("state_from_callback");
|
||||
request.setClientId("your_client_id");
|
||||
request.setClientSecret("your_client_secret");
|
||||
|
||||
SalesforceLoginResult result = loginService.login(request);
|
||||
```
|
||||
|
||||
#### 3.1.4 异常处理
|
||||
|
||||
| 异常类 | 错误码 | 说明 |
|
||||
|--------|-------|------|
|
||||
| SalesforceOAuthException | OAUTH2_UNSUPPORTED_GRANT_TYPE | 不支持的授权类型 |
|
||||
| SalesforceOAuthException | OAUTH2_MISSING_REFRESH_TOKEN | 缺少刷新令牌 |
|
||||
| SalesforceOAuthException | OAUTH2_CONFIG_ERROR | 配置错误 |
|
||||
| SalesforceOAuthException | OAUTH2_MISSING_CLIENT_ID | 缺少客户端ID |
|
||||
| SalesforceOAuthException | OAUTH2_MISSING_CLIENT_SECRET | 缺少客户端密钥 |
|
||||
| SalesforceOAuthException | OAUTH2_MISSING_AUTHORIZATION_CODE | 缺少授权码 |
|
||||
| SalesforceOAuthException | OAUTH2_MISSING_STATE | 缺少state参数 |
|
||||
| SalesforceOAuthException | OAUTH2_INVALID_STATE | 无效的state参数 |
|
||||
| SalesforceOAuthException | OAUTH2_EXPIRED_STATE | state参数已过期 |
|
||||
|
||||
### 3.2 LegacyCredentialLoginStrategy (legacy_credential)
|
||||
|
||||
#### 3.2.1 参数说明
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|-------|-----|-----|------|
|
||||
| loginType | String | 是 | 固定值:"legacy_credential" |
|
||||
| username | String | 是 | Salesforce用户名 |
|
||||
| password | String | 是 | Salesforce密码 |
|
||||
| securityToken | String | 否 | 安全令牌(根据Salesforce设置可能必需) |
|
||||
|
||||
#### 3.2.2 调用示例
|
||||
|
||||
```java
|
||||
SalesforceLoginRequest request = new SalesforceLoginRequest();
|
||||
request.setLoginType("legacy_credential");
|
||||
request.setUsername("user@example.com");
|
||||
request.setPassword("password");
|
||||
request.setSecurityToken("securityToken");
|
||||
|
||||
SalesforceLoginResult result = loginService.login(request);
|
||||
```
|
||||
|
||||
#### 3.2.3 异常处理
|
||||
|
||||
| 异常类 | 错误码 | 说明 |
|
||||
|--------|-------|------|
|
||||
| SalesforceLegacyCredentialLoginException | MISSING_USERNAME | 缺少用户名 |
|
||||
| SalesforceLegacyCredentialLoginException | MISSING_PASSWORD | 缺少密码 |
|
||||
| SalesforceLegacyCredentialLoginException | CONFIG_NOT_FOUND | 配置未找到 |
|
||||
| SalesforceLegacyCredentialLoginException | SOAP_FAULT | SOAP故障 |
|
||||
|
||||
### 3.3 SessionIdLoginStrategy (session_id)
|
||||
|
||||
#### 3.3.1 参数说明
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|-------|-----|-----|------|
|
||||
| loginType | String | 是 | 固定值:"session_id" |
|
||||
| sessionId | String | 是 | Salesforce Session ID |
|
||||
|
||||
#### 3.3.2 调用示例
|
||||
|
||||
```java
|
||||
SalesforceLoginRequest request = new SalesforceLoginRequest();
|
||||
request.setLoginType("session_id");
|
||||
request.setSessionId("00Dxx0000000000!ARUAQH7...");
|
||||
|
||||
SalesforceLoginResult result = loginService.login(request);
|
||||
```
|
||||
|
||||
#### 3.3.3 异常处理
|
||||
|
||||
| 异常类 | 错误码 | 说明 |
|
||||
|--------|-------|------|
|
||||
| SalesforceSessionIdLoginException | SESSION_ID_EMPTY | Session ID为空 |
|
||||
| SalesforceSessionIdLoginException | CONFIG_NOT_FOUND | 配置未找到 |
|
||||
| SalesforceSessionIdLoginException | INVALID_SESSION_ID | 无效的Session ID |
|
||||
|
||||
### 3.4 SalesforceCliLoginStrategy (salesforce_cli)
|
||||
|
||||
#### 3.4.1 参数说明
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|-------|-----|-----|------|
|
||||
| loginType | String | 是 | 固定值:"salesforce_cli" |
|
||||
| orgAlias | String | 否 | Salesforce组织别名,如果不提供则使用默认组织 |
|
||||
|
||||
#### 3.4.2 调用示例
|
||||
|
||||
```java
|
||||
SalesforceLoginRequest request = new SalesforceLoginRequest();
|
||||
request.setLoginType("salesforce_cli");
|
||||
request.setOrgAlias("your_org_alias"); // 可选
|
||||
|
||||
SalesforceLoginResult result = loginService.login(request);
|
||||
```
|
||||
|
||||
#### 3.4.3 异常处理
|
||||
|
||||
| 异常类 | 错误码 | 说明 |
|
||||
|--------|-------|------|
|
||||
| SalesforceCliLoginException | CLI_NOT_INSTALLED | Salesforce CLI未安装 |
|
||||
| SalesforceCliLoginException | CLI_COMMAND_FAILED | CLI命令执行失败 |
|
||||
| SalesforceCliLoginException | CLI_PARSE_ERROR | CLI输出解析错误 |
|
||||
|
||||
## 4. 返回结果说明
|
||||
|
||||
所有登录方法都会返回[SalesforceLoginResult](file:///D:/idea_demo/datai/datai-scenes/datai-scene-salesforce/datai-salesforce-auth/src/main/java/com/datai/auth/domain/SalesforceLoginResult.java#L15-L152)对象,包含以下字段:
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|-------|-----|------|
|
||||
| success | boolean | 登录是否成功 |
|
||||
| accessToken | String | 访问令牌 |
|
||||
| refreshToken | String | 刷新令牌 |
|
||||
| instanceUrl | String | Salesforce实例URL |
|
||||
| organizationId | String | 组织ID |
|
||||
| userId | String | 用户ID |
|
||||
| tokenType | String | 令牌类型(通常是"Bearer") |
|
||||
| expiresIn | long | 令牌过期时间(秒) |
|
||||
| errorMessage | String | 错误消息 |
|
||||
| errorCode | String | 错误码 |
|
||||
488
doc/salesforce/auth/接口调用说明文档.md
Normal file
488
doc/salesforce/auth/接口调用说明文档.md
Normal file
@ -0,0 +1,488 @@
|
||||
# Salesforce登录认证接口调用说明文档
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本文档详细描述了Salesforce登录认证模块提供的RESTful API接口,包括各接口的功能、请求方式、参数说明、返回结果以及调用示例。
|
||||
|
||||
### 1.1 基础信息
|
||||
|
||||
- 基础路径:`/salesforce/login`
|
||||
- 控制器类:`DataISfLoginController`
|
||||
- 跨域支持:是
|
||||
|
||||
## 2. 接口详情
|
||||
|
||||
### 2.1 执行登录
|
||||
|
||||
#### 接口描述
|
||||
根据不同登录类型执行Salesforce登录操作。
|
||||
|
||||
#### 请求URL
|
||||
```
|
||||
POST /salesforce/login/execute
|
||||
```
|
||||
|
||||
#### 请求头
|
||||
```
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
#### 请求体参数 (SalesforceLoginRequest)
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|--------|------|------|------|
|
||||
| loginType | String | 是 | 登录类型,支持的类型有:oauth2、salesforce_cli、legacy_credential、session_id |
|
||||
| username | String | 否 | 用户名 |
|
||||
| password | String | 否 | 密码 |
|
||||
| securityToken | String | 否 | 安全令牌 |
|
||||
| clientId | String | 否 | OAuth客户端ID |
|
||||
| clientSecret | String | 否 | OAuth客户端密钥 |
|
||||
| grantType | String | 否 | OAuth授权类型 |
|
||||
| orgAlias | String | 否 | Salesforce CLI组织别名 |
|
||||
| privateKeyPath | String | 否 | 私有密钥路径 |
|
||||
| privateKeyPassword | String | 否 | 私有密钥密码 |
|
||||
| code | String | 否 | OAuth授权码 |
|
||||
| sessionId | String | 否 | Salesforce Session ID |
|
||||
|
||||
#### 不同登录类型所需参数说明
|
||||
|
||||
##### 1. oauth2 登录类型
|
||||
使用OAuth 2.0标准方式进行登录,适用于大多数现代应用集成场景。
|
||||
|
||||
必需参数:
|
||||
- loginType: "oauth2"
|
||||
- username: 用户名
|
||||
- password: 密码
|
||||
- clientId: OAuth客户端ID
|
||||
- clientSecret: OAuth客户端密钥
|
||||
- grantType: 授权类型,支持 "password"、"authorization_code" 或 "client_credentials"
|
||||
|
||||
示例请求:
|
||||
```json
|
||||
{
|
||||
"loginType": "oauth2",
|
||||
"username": "user@example.com",
|
||||
"password": "password123",
|
||||
"clientId": "your_client_id",
|
||||
"clientSecret": "your_client_secret",
|
||||
"grantType": "password"
|
||||
}
|
||||
```
|
||||
|
||||
##### 2. salesforce_cli 登录类型
|
||||
使用Salesforce CLI已经登录的会话进行身份验证,适用于开发和测试环境。
|
||||
|
||||
必需参数:
|
||||
- loginType: "salesforce_cli"
|
||||
- username: 用户名
|
||||
- orgAlias: (可选) 已登录的CLI组织别名
|
||||
|
||||
示例请求:
|
||||
```json
|
||||
{
|
||||
"loginType": "salesforce_cli",
|
||||
"username": "user@example.com",
|
||||
"orgAlias": "my-sandbox"
|
||||
}
|
||||
```
|
||||
|
||||
##### 3. legacy_credential 登录类型
|
||||
使用传统的用户名、密码和安全令牌组合进行登录,适用于较老的系统集成。
|
||||
|
||||
必需参数:
|
||||
- loginType: "legacy_credential"
|
||||
- username: 用户名
|
||||
- password: 密码
|
||||
- securityToken: 安全令牌
|
||||
|
||||
示例请求:
|
||||
```json
|
||||
{
|
||||
"loginType": "legacy_credential",
|
||||
"username": "user@example.com",
|
||||
"password": "password123",
|
||||
"securityToken": "securityToken123"
|
||||
}
|
||||
```
|
||||
|
||||
##### 4. session_id 登录类型
|
||||
使用已有的Salesforce Session ID进行登录,适用于已有有效会话的场景。
|
||||
|
||||
必需参数:
|
||||
- loginType: "session_id"
|
||||
- sessionId: Salesforce Session ID
|
||||
|
||||
示例请求:
|
||||
```json
|
||||
{
|
||||
"loginType": "session_id",
|
||||
"sessionId": "00Dxx0000001gEWEAY!ARwAQ.KBMrn123XYZ"
|
||||
}
|
||||
```
|
||||
|
||||
#### 请求示例
|
||||
```
|
||||
{
|
||||
"loginType": "oauth2",
|
||||
"username": "user@example.com",
|
||||
"password": "password123",
|
||||
"clientId": "your_client_id",
|
||||
"clientSecret": "your_client_secret",
|
||||
"grantType": "password"
|
||||
}
|
||||
```
|
||||
|
||||
#### 成功响应示例
|
||||
```
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "登录成功",
|
||||
"data": {
|
||||
"success": true,
|
||||
"accessToken": "00Dxx0000001gEWEAY!ARwAQ...",
|
||||
"refreshToken": "5Aep861z1k8KCNBg3KmH1...",
|
||||
"instanceUrl": "https://your-instance.salesforce.com",
|
||||
"organizationId": "00Dxx0000001gEWEAY",
|
||||
"userId": "005xx000001SwiQAAS",
|
||||
"tokenType": "Bearer",
|
||||
"expiresIn": 7200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 失败响应示例
|
||||
```
|
||||
{
|
||||
"code": 500,
|
||||
"msg": "登录失败: Invalid username, password, security token; or user locked out."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.2 刷新令牌
|
||||
|
||||
#### 接口描述
|
||||
使用刷新令牌获取新的访问令牌。
|
||||
|
||||
#### 请求URL
|
||||
```
|
||||
POST /salesforce/login/refresh-token
|
||||
```
|
||||
|
||||
#### 请求参数 (@RequestParam)
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|--------|------|------|------|
|
||||
| refreshToken | String | 是 | 刷新令牌 |
|
||||
| loginType | String | 是 | 登录类型,支持的类型有:oauth2、salesforce_cli、legacy_credential、session_id |
|
||||
|
||||
#### 请求示例
|
||||
```
|
||||
POST /salesforce/login/refresh-token?refreshToken=5Aep861z1k8KCNBg3KmH1...&loginType=oauth2
|
||||
```
|
||||
|
||||
#### 成功响应示例
|
||||
```
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "令牌刷新成功",
|
||||
"data": {
|
||||
"success": true,
|
||||
"accessToken": "00Dxx0000001gEWEAY!ARwAQ...",
|
||||
"refreshToken": "5Aep861z1k8KCNBg3KmH1...",
|
||||
"instanceUrl": "https://your-instance.salesforce.com",
|
||||
"organizationId": "00Dxx0000001gEWEAY",
|
||||
"userId": "005xx000001SwiQAAS",
|
||||
"tokenType": "Bearer",
|
||||
"expiresIn": 7200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 失败响应示例
|
||||
```
|
||||
{
|
||||
"code": 500,
|
||||
"msg": "刷新令牌失败: expired authorization code"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.3 执行登出
|
||||
|
||||
#### 接口描述
|
||||
退出登录,清理登录状态。
|
||||
|
||||
#### 请求URL
|
||||
```
|
||||
POST /salesforce/login/logout
|
||||
```
|
||||
|
||||
#### 请求参数 (@RequestParam)
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|--------|------|------|------|
|
||||
| accessToken | String | 是 | 访问令牌 |
|
||||
| loginType | String | 是 | 登录类型,支持的类型有:oauth2、salesforce_cli、legacy_credential、session_id |
|
||||
|
||||
#### 请求示例
|
||||
```
|
||||
POST /salesforce/login/logout?accessToken=00Dxx0000001gEWEAY!ARwAQ...&loginType=oauth2
|
||||
```
|
||||
|
||||
#### 成功响应示例
|
||||
```
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "登出成功"
|
||||
}
|
||||
```
|
||||
|
||||
#### 失败响应示例
|
||||
```
|
||||
{
|
||||
"code": 500,
|
||||
"msg": "登出失败"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.4 获取当前登录状态
|
||||
|
||||
#### 接口描述
|
||||
获取当前系统的登录状态。
|
||||
|
||||
#### 请求URL
|
||||
```
|
||||
GET /salesforce/login/status
|
||||
```
|
||||
|
||||
#### 请求示例
|
||||
```
|
||||
GET /salesforce/login/status
|
||||
```
|
||||
|
||||
#### 成功响应示例
|
||||
```
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "获取成功",
|
||||
"data": {
|
||||
"success": true,
|
||||
"accessToken": "00Dxx0000001gEWEAY!ARwAQ...",
|
||||
"refreshToken": "5Aep861z1k8KCNBg3KmH1...",
|
||||
"instanceUrl": "https://your-instance.salesforce.com",
|
||||
"organizationId": "00Dxx0000001gEWEAY",
|
||||
"userId": "005xx000001SwiQAAS",
|
||||
"tokenType": "Bearer",
|
||||
"expiresIn": 7200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 失败响应示例
|
||||
```
|
||||
{
|
||||
"code": 500,
|
||||
"msg": "获取登录状态失败: 未找到有效的登录状态"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.5 验证令牌
|
||||
|
||||
#### 接口描述
|
||||
验证访问令牌的有效性。
|
||||
|
||||
#### 请求URL
|
||||
```
|
||||
GET /salesforce/login/validate-token
|
||||
```
|
||||
|
||||
#### 请求参数 (@RequestParam)
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|--------|------|------|------|
|
||||
| accessToken | String | 是 | 访问令牌 |
|
||||
|
||||
#### 请求示例
|
||||
```
|
||||
GET /salesforce/login/validate-token?accessToken=00Dxx0000001gEWEAY!ARwAQ...
|
||||
```
|
||||
|
||||
#### 成功响应示例
|
||||
```
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "令牌有效"
|
||||
}
|
||||
```
|
||||
|
||||
#### 失败响应示例
|
||||
```
|
||||
{
|
||||
"code": 500,
|
||||
"msg": "令牌无效"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.6 绑定令牌
|
||||
|
||||
#### 接口描述
|
||||
将令牌绑定到指定设备和IP。
|
||||
|
||||
#### 请求URL
|
||||
```
|
||||
POST /salesforce/login/bind-token
|
||||
```
|
||||
|
||||
#### 请求参数 (@RequestParam)
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|--------|------|------|------|
|
||||
| accessToken | String | 是 | 访问令牌 |
|
||||
| deviceId | String | 否 | 设备ID |
|
||||
| ip | String | 否 | IP地址 |
|
||||
|
||||
#### 请求示例
|
||||
```
|
||||
POST /salesforce/login/bind-token?accessToken=00Dxx0000001gEWEAY!ARwAQ...&deviceId=device123&ip=192.168.1.100
|
||||
```
|
||||
|
||||
#### 成功响应示例
|
||||
```
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "令牌绑定成功"
|
||||
}
|
||||
```
|
||||
|
||||
#### 失败响应示例
|
||||
```
|
||||
{
|
||||
"code": 500,
|
||||
"msg": "绑定令牌失败: 令牌不存在"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.7 检查令牌绑定
|
||||
|
||||
#### 接口描述
|
||||
检查令牌是否与指定设备和IP匹配。
|
||||
|
||||
#### 请求URL
|
||||
```
|
||||
GET /salesforce/login/check-binding
|
||||
```
|
||||
|
||||
#### 请求参数 (@RequestParam)
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|--------|------|------|------|
|
||||
| accessToken | String | 是 | 访问令牌 |
|
||||
| deviceId | String | 否 | 设备ID |
|
||||
| ip | String | 否 | IP地址 |
|
||||
|
||||
#### 请求示例
|
||||
```
|
||||
GET /salesforce/login/check-binding?accessToken=00Dxx0000001gEWEAY!ARwAQ...&deviceId=device123&ip=192.168.1.100
|
||||
```
|
||||
|
||||
#### 成功响应示例
|
||||
```
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "令牌绑定匹配"
|
||||
}
|
||||
```
|
||||
|
||||
#### 失败响应示例
|
||||
```
|
||||
{
|
||||
"code": 500,
|
||||
"msg": "令牌绑定不匹配"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.8 获取当前会话信息
|
||||
|
||||
#### 接口描述
|
||||
获取当前登录成功的会话详细信息。
|
||||
|
||||
#### 请求URL
|
||||
```
|
||||
GET /salesforce/login/session
|
||||
```
|
||||
|
||||
#### 请求示例
|
||||
```
|
||||
GET /salesforce/login/session
|
||||
```
|
||||
|
||||
#### 成功响应示例
|
||||
```
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "获取成功",
|
||||
"data": {
|
||||
"sessionId": 1,
|
||||
"tenantId": "default",
|
||||
"username": "user@example.com",
|
||||
"loginType": "oauth2",
|
||||
"status": "active",
|
||||
"loginTime": "2025-12-14T10:30:00",
|
||||
"expireTime": "2025-12-14T12:30:00",
|
||||
"lastActivityTime": "2025-12-14T10:35:00",
|
||||
"loginIp": "192.168.1.100",
|
||||
"deviceInfo": "Windows PC",
|
||||
"browserInfo": "Chrome 98.0.4758.102",
|
||||
"sessionToken": "sess_abc123xyz",
|
||||
"tokenId": 1001
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 失败响应示例
|
||||
```
|
||||
{
|
||||
"code": 500,
|
||||
"msg": "当前没有活跃的登录会话"
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 返回码说明
|
||||
|
||||
| 返回码 | 说明 |
|
||||
|--------|------|
|
||||
| 200 | 操作成功 |
|
||||
| 500 | 系统内部错误 |
|
||||
|
||||
## 4. 注意事项
|
||||
|
||||
1. 所有接口均支持跨域调用
|
||||
2. 登录成功后,系统会自动将登录结果缓存到Redis中
|
||||
3. 令牌刷新成功后,系统会自动更新缓存中的登录状态
|
||||
4. 登出操作会清理相关的登录状态缓存
|
||||
5. 令牌绑定功能可以增强系统安全性,防止令牌被盗用
|
||||
6. 所有敏感操作都会记录日志,便于审计追踪
|
||||
|
||||
## 5. 错误处理
|
||||
|
||||
当发生错误时,系统会返回统一的错误格式:
|
||||
```
|
||||
{
|
||||
"code": 错误码,
|
||||
"msg": "错误描述信息"
|
||||
}
|
||||
```
|
||||
|
||||
常见错误情况包括但不限于:
|
||||
- 参数校验失败
|
||||
- 网络连接异常
|
||||
- Salesforce API调用失败
|
||||
- 缓存服务不可用
|
||||
- 数据库操作异常
|
||||
160
doc/salesforce/auth/获取当前会话信息接口说明文档.md
Normal file
160
doc/salesforce/auth/获取当前会话信息接口说明文档.md
Normal file
@ -0,0 +1,160 @@
|
||||
# 获取当前会话信息接口说明文档
|
||||
|
||||
## 1. 接口概述
|
||||
|
||||
### 1.1 接口名称
|
||||
获取当前会话信息
|
||||
|
||||
### 1.2 接口描述
|
||||
该接口用于获取当前用户登录成功的会话详细信息,包括用户基本信息、登录时间、会话状态等。通过此接口可以验证用户是否已登录以及获取当前登录会话的详细数据。
|
||||
|
||||
### 1.3 请求URL
|
||||
```
|
||||
GET /salesforce/login/session
|
||||
```
|
||||
|
||||
### 1.4 请求方法
|
||||
GET
|
||||
|
||||
### 1.5 是否需要认证
|
||||
否(但需确保用户已登录)
|
||||
|
||||
## 2. 请求参数
|
||||
|
||||
该接口无需传递任何请求参数。
|
||||
|
||||
## 3. 响应格式
|
||||
|
||||
### 3.1 响应结构
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "获取成功",
|
||||
"data": {
|
||||
// 会话详细信息对象
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 响应字段说明
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| code | Integer | 响应状态码,200表示成功 |
|
||||
| msg | String | 响应消息 |
|
||||
| data | Object | 响应数据,包含会话详细信息 |
|
||||
|
||||
### 3.3 会话信息对象字段说明
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| sessionId | Long | 会话ID |
|
||||
| tenantId | String | 租户编号 |
|
||||
| username | String | 用户名 |
|
||||
| loginType | String | 登录类型(oauth2、salesforce_cli、legacy_credential、session_id) |
|
||||
| status | String | 会话状态(ACTIVE、EXPIRED等) |
|
||||
| loginTime | String | 登录时间(ISO 8601格式) |
|
||||
| expireTime | String | 过期时间(ISO 8601格式) |
|
||||
| lastActivityTime | String | 最后活动时间(ISO 8601格式) |
|
||||
| loginIp | String | 登录IP地址 |
|
||||
| deviceInfo | String | 设备信息 |
|
||||
| browserInfo | String | 浏览器信息 |
|
||||
| sessionToken | String | 会话标识 |
|
||||
| tokenId | Long | 令牌ID |
|
||||
|
||||
## 4. 使用示例
|
||||
|
||||
### 4.1 请求示例
|
||||
```bash
|
||||
curl -X GET "http://localhost:8080/salesforce/login/session" \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
### 4.2 成功响应示例
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "获取成功",
|
||||
"data": {
|
||||
"sessionId": 10001,
|
||||
"tenantId": "default",
|
||||
"username": "user@example.com",
|
||||
"loginType": "oauth2",
|
||||
"status": "ACTIVE",
|
||||
"loginTime": "2025-12-14T10:30:00",
|
||||
"expireTime": "2025-12-14T12:30:00",
|
||||
"lastActivityTime": "2025-12-14T10:35:00",
|
||||
"loginIp": "192.168.1.100",
|
||||
"deviceInfo": "Windows PC",
|
||||
"browserInfo": "Chrome 98.0.4758.102",
|
||||
"sessionToken": "sess_abc123xyz",
|
||||
"tokenId": 20001
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 失败响应示例(无活跃会话)
|
||||
```json
|
||||
{
|
||||
"code": 500,
|
||||
"msg": "当前没有活跃的登录会话"
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 失败响应示例(系统异常)
|
||||
```json
|
||||
{
|
||||
"code": 500,
|
||||
"msg": "获取当前登录会话信息失败: 数据库连接异常"
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 业务逻辑说明
|
||||
|
||||
### 5.1 接口工作原理
|
||||
1. 接收到请求后,系统会查询数据库中所有状态为"ACTIVE"的登录会话记录
|
||||
2. 按照登录时间倒序排列,获取最新的会话记录
|
||||
3. 将会话信息缓存到Redis中以提高下次访问的性能
|
||||
4. 返回会话详细信息给客户端
|
||||
|
||||
### 5.2 会话状态说明
|
||||
- ACTIVE:活跃会话,用户当前已登录
|
||||
- EXPIRED:已过期会话,用户需要重新登录
|
||||
- REVOKED:已吊销会话,用户主动登出或被管理员强制登出
|
||||
|
||||
### 5.3 数据来源
|
||||
会话信息存储在[datai_sf_login_session](file:///D:/idea_demo/datai/datai-scenes/datai-scene-salesforce/datai-salesforce-auth/src/main/java/com/datai/auth/mapper/DataiSfLoginSessionMapper.java#L17-L17)表中,与[datai_sf_token](file:///D:/idea_demo/datai/datai-scenes/datai-scene-salesforce/datai-salesforce-auth/src/main/java/com/datai/auth/mapper/DataiSfTokenMapper.java#L17-L17)表通过[tokenId](file:///D:/idea_demo/datai/datai-scenes/datai-scene-salesforce/datai-salesforce-auth/src/main/java/com/datai/auth/domain/DataiSfLoginSession.java#L101-L101)字段关联。
|
||||
|
||||
## 6. 异常处理
|
||||
|
||||
### 6.1 常见异常情况
|
||||
| 异常情况 | 错误码 | 错误信息 | 处理建议 |
|
||||
|---------|--------|---------|---------|
|
||||
| 无活跃会话 | 500 | 当前没有活跃的登录会话 | 用户需要重新登录 |
|
||||
| 数据库异常 | 500 | 获取当前登录会话信息失败: [具体错误信息] | 检查数据库连接和服务状态 |
|
||||
| 系统内部错误 | 500 | 获取当前登录会话信息失败: [具体错误信息] | 查看系统日志,联系技术支持 |
|
||||
|
||||
### 6.2 日志记录
|
||||
接口调用过程中会产生以下日志:
|
||||
- INFO级别:记录接口调用开始和结束
|
||||
- DEBUG级别:记录详细的处理过程
|
||||
- ERROR级别:记录异常信息
|
||||
|
||||
## 7. 性能与安全
|
||||
|
||||
### 7.1 性能优化
|
||||
- 使用Redis缓存会话信息,减少数据库访问
|
||||
- 对数据库查询结果进行排序优化
|
||||
- 限制返回的会话记录数量(仅返回最新的活跃会话)
|
||||
|
||||
### 7.2 安全考虑
|
||||
- 接口不暴露敏感信息如密码、访问令牌等
|
||||
- 会话信息仅包含必要的元数据
|
||||
- 所有操作都记录审计日志
|
||||
|
||||
## 8. 相关接口
|
||||
|
||||
### 8.1 前置接口
|
||||
- [/salesforce/login/execute](file:///D:/idea_demo/datai/datai-scenes/datai-scene-salesforce/datai-salesforce-auth/src/main/java/com/datai/auth/controller/DataISfLoginController.java#L48-L74):执行登录,创建会话信息
|
||||
|
||||
|
||||
85
src/api/auth/register.js
Normal file
85
src/api/auth/register.js
Normal file
@ -0,0 +1,85 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 执行登录
|
||||
export function executeLogin(data) {
|
||||
return request({
|
||||
url: '/salesforce/login/execute',
|
||||
method: 'post',
|
||||
data: data,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 刷新访问令牌
|
||||
export function refreshToken(data) {
|
||||
return request({
|
||||
url: '/salesforce/login/refresh-token',
|
||||
method: 'post',
|
||||
data: data,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 执行登出
|
||||
export function logout(data) {
|
||||
return request({
|
||||
url: '/salesforce/login/logout',
|
||||
method: 'post',
|
||||
data: data,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取当前登录状态
|
||||
export function getLoginStatus() {
|
||||
return request({
|
||||
url: '/salesforce/login/status',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 验证令牌有效性
|
||||
export function validateToken(accessToken) {
|
||||
return request({
|
||||
url: '/salesforce/login/validate-token',
|
||||
method: 'get',
|
||||
params: {
|
||||
accessToken: accessToken
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 绑定令牌到设备/IP
|
||||
export function bindToken(data) {
|
||||
return request({
|
||||
url: '/salesforce/login/bind-token',
|
||||
method: 'post',
|
||||
data: data,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 检查令牌绑定
|
||||
export function checkBinding(params) {
|
||||
return request({
|
||||
url: '/salesforce/login/check-binding',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取当前登录会话信息
|
||||
export function getLoginSession() {
|
||||
return request({
|
||||
url: '/salesforce/login/session',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
372
src/views/auth/index.vue
Normal file
372
src/views/auth/index.vue
Normal file
@ -0,0 +1,372 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { Document, ChatDotRound, User } from '@element-plus/icons-vue';
|
||||
import { getConfigKey } from '@/api/system/config';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import Oauth from "./oauth.vue";
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { RoutesAlias } from '@/router/routesAlias';
|
||||
// 页面加载状态
|
||||
const pageLoaded = ref(false);
|
||||
const successCount = ref(0);
|
||||
const targetSuccessCount = 100;
|
||||
|
||||
// 数字增长动画
|
||||
function startCountAnimation() {
|
||||
const duration = 3000; // 动画持续时间(毫秒)
|
||||
const step = Math.ceil(targetSuccessCount / (duration / 50)); // 每50毫秒增加的数值
|
||||
const interval = setInterval(() => {
|
||||
successCount.value += step;
|
||||
if (successCount.value >= targetSuccessCount) {
|
||||
successCount.value = targetSuccessCount;
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
// 验证码开关
|
||||
const captchaEnabled = ref(false);
|
||||
// 注册开关
|
||||
const register = ref(false);
|
||||
|
||||
const route = useRoute();
|
||||
console.log(route);
|
||||
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
if (getToken()) {
|
||||
useRouter().push(RoutesAlias.Home)
|
||||
return;
|
||||
}
|
||||
|
||||
captchaEnabled.value = await getConfigKey("sys.account.captchaEnabled").then(res => res.msg === 'true')
|
||||
register.value = await getConfigKey("sys.account.registerUser").then(res => res.msg === 'true')
|
||||
// 设置页面加载状态为true,触发动画
|
||||
pageLoaded.value = true;
|
||||
startCountAnimation();
|
||||
});
|
||||
|
||||
const features = [
|
||||
{ icon: Document, text: '模块解耦' },
|
||||
{ icon: ChatDotRound, text: '前沿技术' },
|
||||
{ icon: User, text: '多端支持' }
|
||||
];
|
||||
|
||||
const method = ref("password");
|
||||
const methods = computed(() => {
|
||||
const text = route.name === 'Login' ? '登录' : '注册';
|
||||
return [
|
||||
{ label: `密码${text}`, value: "password" },
|
||||
{ label: `手机${text}`, value: "phone" },
|
||||
{ label: `邮箱${text}`, value: "email" }
|
||||
]
|
||||
});
|
||||
|
||||
const title = computed(() => import.meta.env.VITE_APP_TITLE || '后台管理系统');
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div class="auth">
|
||||
<div class="background-animation">
|
||||
<div v-for="i in 10" :key="i" class="floating-shape"></div>
|
||||
</div>
|
||||
<div class="container" :class="{ 'appear-animation': pageLoaded }">
|
||||
<div class="container-left">
|
||||
<h1>欢迎使用GEEK生态</h1>
|
||||
<p>打造高效、现代、可扩展的企业级开发平台</p>
|
||||
<div class="features">
|
||||
<div class="feature-item" v-for="(item, index) in features" :key="index">
|
||||
<el-icon>
|
||||
<component :is="item.icon" />
|
||||
</el-icon>
|
||||
<span>{{ item.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="success-stories">
|
||||
<div class="story-counter">
|
||||
<span class="counter">{{ successCount }}+</span>
|
||||
<span class="counter-label">成功案例</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-right" v-if="pageLoaded">
|
||||
<h3 class="title">{{ title }}</h3>
|
||||
<el-segmented v-model="method" :options="methods" block />
|
||||
<div class="container-form">
|
||||
<router-view v-slot="{ Component }">
|
||||
<component :is="Component" :register="register" :captchaEnabled="captchaEnabled" :method="method" />
|
||||
</router-view>
|
||||
</div>
|
||||
<Oauth />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部 -->
|
||||
<div class="el-auth-footer">
|
||||
<span>Copyright © 2018-2024 若依Geek后台管理系统 All Rights Reserved.</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang='scss' scoped>
|
||||
@use 'sass:list';
|
||||
@use 'sass:map';
|
||||
|
||||
.auth {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: linear-gradient(135deg, var(--el-bg-color) 0%, var(--el-fill-color) 100%);
|
||||
}
|
||||
|
||||
/* 背景动画元素 */
|
||||
.background-animation {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
|
||||
.floating-shape {
|
||||
position: absolute;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: var(--el-color-primary-light-8);
|
||||
animation: float 15s infinite linear;
|
||||
|
||||
@keyframes float {
|
||||
0% {
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-40px) scale(1.2);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用Sass循环简化浮动形状配置
|
||||
$shapes: (
|
||||
(top: 10%, left: 10%, w: 60px, h: 60px, delay: 0s, duration: 20s),
|
||||
(top: 20%, right: 20%, w: 100px, h: 100px, delay: 2s, duration: 22s),
|
||||
(bottom: 15%, left: 15%, w: 70px, h: 70px, delay: 4s, duration: 18s),
|
||||
(bottom: 25%, right: 10%, w: 50px, h: 50px, delay: 6s, duration: 25s),
|
||||
(top: 50%, left: 5%, w: 40px, h: 40px, delay: 8s, duration: 30s),
|
||||
(top: 40%, right: 5%, w: 90px, h: 90px, delay: 10s, duration: 28s),
|
||||
(top: 70%, left: 30%, w: 55px, h: 55px, delay: 12s, duration: 24s),
|
||||
(bottom: 40%, right: 25%, w: 65px, h: 65px, delay: 14s, duration: 26s),
|
||||
(top: 30%, left: 40%, w: 75px, h: 75px, delay: 16s, duration: 29s),
|
||||
(bottom: 10%, right: 40%, w: 45px, h: 45px, delay: 18s, duration: 27s)
|
||||
);
|
||||
|
||||
@for $i from 1 through list.length($shapes) {
|
||||
$shape: list.nth($shapes, $i);
|
||||
|
||||
&:nth-child(#{$i}) {
|
||||
@each $key, $value in $shape {
|
||||
@if $key ==top or $key ==bottom or $key ==left or $key ==right {
|
||||
#{$key}: #{$value};
|
||||
}
|
||||
}
|
||||
|
||||
width: map.get($shape, w);
|
||||
height: map.get($shape, h);
|
||||
animation-delay: map.get($shape, delay);
|
||||
animation-duration: map.get($shape, duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
width: 85%;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
max-width: 1200px;
|
||||
min-height: 600px;
|
||||
height: 70vh;
|
||||
background: var(--el-bg-color);
|
||||
border-radius: var(--el-border-radius-large);
|
||||
box-shadow: var(--el-box-shadow);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 1s ease;
|
||||
|
||||
&.appear-animation {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.container-left {
|
||||
width: 50%;
|
||||
background: linear-gradient(135deg, var(--el-color-primary-light-3) 0%, var(--el-color-primary-dark-3) 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
color: var(--el-color-white);
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 2px 4px var(--el-color-primary-dark-2);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 40px;
|
||||
opacity: 0.9;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 50px;
|
||||
width: 80%;
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-8px) scale(1.05);
|
||||
|
||||
.el-icon {
|
||||
box-shadow: 0 8px 16px var(--el-color-primary-dark-2);
|
||||
transform: rotate(5deg);
|
||||
}
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 15px;
|
||||
padding: 8px;
|
||||
background: var(--el-color-primary-light-8);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4px 12px var(--el-color-primary-light-5);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.success-stories {
|
||||
margin-top: 40px;
|
||||
|
||||
.story-counter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.counter {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: var(--el-color-white);
|
||||
text-shadow: 0 2px 8px var(--el-color-primary-dark-2);
|
||||
}
|
||||
|
||||
.counter-label {
|
||||
font-size: 1.2rem;
|
||||
margin-top: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-right {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle at center, rgba(255, 255, 255, 0) 70%, rgba(235, 235, 235, 0.5) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 30px auto 20px auto;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(45deg, var(--el-color-primary-light-3), var(--el-color-primary-dark-3));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.el-segmented {
|
||||
background-color: var(--el-color-primary);
|
||||
padding: 0;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
|
||||
:deep(.el-segmented__item) {
|
||||
border-radius: 0;
|
||||
background-color: var(--el-color-white);
|
||||
border-color: var(--el-color-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-form {
|
||||
width: 80%;
|
||||
max-width: 400px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.el-auth-footer {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-family: Arial;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
</style>
|
||||
245
src/views/auth/login.vue
Normal file
245
src/views/auth/login.vue
Normal file
@ -0,0 +1,245 @@
|
||||
<script setup lang="ts">
|
||||
import { encrypt, decrypt } from "@/utils/jsencrypt";
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import { useRouter } from "vue-router";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { getCodeImg, sendEmailCode, sendPhoneCode } from "@/api/login";
|
||||
import { RoutesAlias } from "@/router/routesAlias";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
|
||||
const props = defineProps<{
|
||||
register: boolean,
|
||||
captchaEnabled: boolean,
|
||||
method: 'password' | 'phone' | 'email'
|
||||
}>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter();
|
||||
|
||||
const loginForm = ref({
|
||||
username: "",
|
||||
password: "",
|
||||
email: '',
|
||||
phonenumber: '',
|
||||
code: "",
|
||||
uuid: "",
|
||||
rememberMe: false,
|
||||
autoRegister: false
|
||||
});
|
||||
|
||||
const loginFormState = useStorage('loginForm', loginForm.value);
|
||||
|
||||
const loginRules = {
|
||||
username: [{ required: true, trigger: "blur", message: "请输入您的账号" }],
|
||||
password: [{ required: true, trigger: "blur", message: "请输入您的密码" }],
|
||||
code: [{ required: true, trigger: "change", message: "请输入验证码" }]
|
||||
};
|
||||
|
||||
const codeUrl = ref("");
|
||||
const loading = ref(false);
|
||||
const redirect = ref(undefined);
|
||||
const loginRef = ref<any | null>(null)
|
||||
|
||||
function handleLogin() {
|
||||
loginRef.value?.validate((valid: any) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
userStore.login(loginForm.value, props.method).then(() => {
|
||||
router.push({ path: redirect.value || "/" });
|
||||
}).catch(() => {
|
||||
getCode();
|
||||
}).finally(() => {
|
||||
loading.value = false;
|
||||
if (loginForm.value.rememberMe) {
|
||||
loginFormState.value.username = loginForm.value.username;
|
||||
loginFormState.value.email = loginForm.value.email;
|
||||
loginFormState.value.phonenumber = loginForm.value.phonenumber;
|
||||
loginFormState.value.password = encrypt(loginForm.value.password);
|
||||
loginFormState.value.rememberMe = loginForm.value.rememberMe;
|
||||
} else {
|
||||
loginFormState.value.username = '';
|
||||
loginFormState.value.password = '';
|
||||
loginFormState.value.email = '';
|
||||
loginFormState.value.phonenumber = '';
|
||||
loginFormState.value.rememberMe = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getCode() {
|
||||
if (!props.captchaEnabled) return
|
||||
getCodeImg().then((res: any) => {
|
||||
codeUrl.value = "data:image/gif;base64," + res.img;
|
||||
loginForm.value.uuid = res.uuid;
|
||||
});
|
||||
}
|
||||
|
||||
function sendCode() {
|
||||
if (props.method === 'email') {
|
||||
sendEmailCode(loginForm.value, 'login')
|
||||
} else if (props.method === 'phone') {
|
||||
sendPhoneCode(loginForm.value, 'login')
|
||||
}
|
||||
}
|
||||
|
||||
function getCookie() {
|
||||
loginForm.value = {
|
||||
username: loginFormState.value.username ?? '',
|
||||
password: loginFormState.value.password ? decrypt(loginFormState.value.password) : '',
|
||||
email: loginFormState.value.email ?? '',
|
||||
phonenumber: loginFormState.value.phonenumber ?? '',
|
||||
code: "",
|
||||
uuid: "",
|
||||
rememberMe: !loginFormState.value.rememberMe ? false : loginFormState.value.rememberMe,
|
||||
autoRegister: false
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCookie();
|
||||
getCode();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
|
||||
<el-form-item prop="email" v-if="method === 'email'">
|
||||
<el-input v-model="loginForm.email" type="text" size="large" auto-complete="off" placeholder="邮箱">
|
||||
<template #prefix>
|
||||
<svg-icon icon-class="email" class="el-input__icon input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="phonenumber" v-else-if="method === 'phone'">
|
||||
<el-input v-model="loginForm.phonenumber" type="text" size="large" auto-complete="off" placeholder="手机号">
|
||||
<template #prefix>
|
||||
<svg-icon icon-class="phone" class="el-input__icon input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<div v-else>
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="loginForm.username" type="text" size="large" auto-complete="off" placeholder="账号">
|
||||
<template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="loginForm.password" type="password" size="large" auto-complete="off" placeholder="密码"
|
||||
show-password @keyup.enter="handleLogin">
|
||||
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item prop="code" v-if="method === 'email' || method === 'phone'">
|
||||
<el-input size="large" v-model="loginForm.code" auto-complete="off" placeholder="验证码" style="width: 63%"
|
||||
@keyup.enter="handleLogin">
|
||||
<template #prefix>
|
||||
<svg-icon icon-class="validCode" class="el-input__icon input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="login-code">
|
||||
<el-button class="login-code-img" @click="sendCode">发送验证码</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item prop="code" v-if="captchaEnabled && method === 'password'">
|
||||
<el-input v-model="loginForm.code" size="large" auto-complete="off" placeholder="验证码" style="width: 63%"
|
||||
@keyup.enter="handleLogin">
|
||||
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
<div class="login-code">
|
||||
<img :src="codeUrl" @click="getCode" class="login-code-img" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<div style="width:100%;display:flex;justify-content:space-between;align-items:center;">
|
||||
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住我!</el-checkbox>
|
||||
<el-checkbox v-if="method != 'password'" v-model="loginForm.autoRegister"
|
||||
style="margin:0px 0px 25px 0px;">若无账号自动注册~</el-checkbox>
|
||||
</div>
|
||||
<el-form-item style="width:100%;">
|
||||
<el-button :loading="loading" size="large" type="primary" style="width:100%;" @click.prevent="handleLogin">
|
||||
<span v-if="!loading">登 录</span>
|
||||
<span v-else>登 录 中...</span>
|
||||
</el-button>
|
||||
<div class="register-link" v-if="register">
|
||||
<span class="question-text">没有账号?</span>
|
||||
<router-link class="link-type" :to="RoutesAlias.Register">立即注册</router-link>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
<style lang='scss' scoped>
|
||||
.login-form {
|
||||
.el-input {
|
||||
height: 40px;
|
||||
|
||||
input {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
height: 39px;
|
||||
width: 14px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-code {
|
||||
width: 37%;
|
||||
height: 40px;
|
||||
float: right;
|
||||
|
||||
.login-code-img {
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
width: calc(100% - 12px);
|
||||
height: 40px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.register-link {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 5px;
|
||||
|
||||
.question-text {
|
||||
color: #606266;
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.link-type {
|
||||
color: #409EFF;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background-color: #409EFF;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #66b1ff;
|
||||
|
||||
&::after {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
171
src/views/auth/oauth.vue
Normal file
171
src/views/auth/oauth.vue
Normal file
@ -0,0 +1,171 @@
|
||||
<script setup lang="ts">
|
||||
import { getAction } from '@/utils/request';
|
||||
|
||||
function oauthLogin(provider: string) {
|
||||
console.log(`开始 ${provider} 登录流程`);
|
||||
// 打开链接 /system/auth
|
||||
getAction<string>(`/system/auth/login/${provider}`)
|
||||
.then((response) => {
|
||||
if (response && response.msg) {
|
||||
window.open(response.msg, '_blank');
|
||||
} else {
|
||||
console.error('登录链接获取失败', response);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`登录 ${provider} 失败`, error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<!-- 第三方登录区域 -->
|
||||
<div class="other-login">
|
||||
<div class="divider">
|
||||
<span class="divider-text">第三方账号登录</span>
|
||||
</div>
|
||||
<div class="oauth-buttons">
|
||||
<div class="oauth-btn" @click="oauthLogin('gitee')" data-tooltip="Gitee登录">
|
||||
<svg-icon icon-class="gitee" class="oauth-icon gitee" />
|
||||
<span class="oauth-name">Gitee</span>
|
||||
</div>
|
||||
<div class="oauth-btn" @click="oauthLogin('github')" data-tooltip="GitHub登录">
|
||||
<svg-icon icon-class="github" class="oauth-icon github" />
|
||||
<span class="oauth-name">GitHub</span>
|
||||
</div>
|
||||
<div class="oauth-btn" @click="oauthLogin('qq')" data-tooltip="QQ登录">
|
||||
<svg-icon icon-class="qq" class="oauth-icon qq" />
|
||||
<span class="oauth-name">QQ</span>
|
||||
</div>
|
||||
<div class="oauth-btn" @click="oauthLogin('wechat')" data-tooltip="微信登录">
|
||||
<svg-icon icon-class="wechat" class="oauth-icon wechat" />
|
||||
<span class="oauth-name">微信</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.other-login {
|
||||
max-width: 360px;
|
||||
padding: 0 20px;
|
||||
width: 100%;
|
||||
|
||||
.divider {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, #eaeaea, transparent);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.divider-text {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 0 12px;
|
||||
background-color: #fff;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.oauth-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
|
||||
.oauth-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.5s;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
transform: translateY(-5px);
|
||||
|
||||
&::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
bottom: -25px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.3s forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.oauth-icon {
|
||||
font-size: 28px;
|
||||
margin-bottom: 5px;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&.gitee {
|
||||
color: #c71d23;
|
||||
}
|
||||
|
||||
&.github {
|
||||
color: #24292e;
|
||||
}
|
||||
|
||||
&.qq {
|
||||
color: #12b7f5;
|
||||
}
|
||||
|
||||
&.wechat {
|
||||
color: #07c160;
|
||||
}
|
||||
}
|
||||
|
||||
.oauth-name {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 1px;
|
||||
background: currentColor;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.oauth-btn:hover & {
|
||||
font-weight: 500;
|
||||
|
||||
&::after {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
248
src/views/auth/register.vue
Normal file
248
src/views/auth/register.vue
Normal file
@ -0,0 +1,248 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessageBox } from "element-plus";
|
||||
import { getCodeImg, sendEmailCode, sendPhoneCode } from "@/api/login";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import useUserStore from "@/store/modules/user";
|
||||
import { RoutesAlias } from "@/router/routesAlias";
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore()
|
||||
const props = defineProps<{
|
||||
register: boolean,
|
||||
captchaEnabled: boolean,
|
||||
method: 'password' | 'phone' | 'email'
|
||||
}>()
|
||||
|
||||
|
||||
const registerForm = ref({
|
||||
username: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
email: '',
|
||||
phonenumber: '',
|
||||
code: "",
|
||||
uuid: "",
|
||||
});
|
||||
|
||||
const equalToPassword = (rule: any, value: any, callback: Function) => {
|
||||
if (registerForm.value.password !== value) {
|
||||
callback(new Error("两次输入的密码不一致"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const registerRules = {
|
||||
username: [
|
||||
{ required: true, trigger: "blur", message: "请输入您的账号" },
|
||||
{ min: 2, max: 20, message: "用户账号长度必须介于 2 和 20 之间", trigger: "blur" }
|
||||
],
|
||||
password: [
|
||||
{ required: true, trigger: "blur", message: "请输入您的密码" },
|
||||
{ min: 5, max: 20, message: "用户密码长度必须介于 5 和 20 之间", trigger: "blur" },
|
||||
{ pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur" }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, trigger: "blur", message: "请再次输入您的密码" },
|
||||
{ required: true, validator: equalToPassword, trigger: "blur" }
|
||||
],
|
||||
code: [{ required: true, trigger: "change", message: "请输入验证码" }]
|
||||
};
|
||||
|
||||
const codeUrl = ref("");
|
||||
const loading = ref(false);
|
||||
|
||||
const registerRef = ref<any | null>(null)
|
||||
function handleRegister() {
|
||||
registerRef.value?.validate((valid: any) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
userStore.register(registerForm.value, props.method).then(() => {
|
||||
const username = registerForm.value.username;
|
||||
ElMessageBox.alert("<font color='red'>恭喜你,您的账号 " + username + " 注册成功!</font>", "系统提示", {
|
||||
dangerouslyUseHTMLString: true,
|
||||
type: "success",
|
||||
}).then(() => {
|
||||
router.push(RoutesAlias.Login);
|
||||
}).catch(() => { });
|
||||
}).catch(() => {
|
||||
getCode();
|
||||
}).finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getCode() {
|
||||
if (!props.captchaEnabled) return
|
||||
getCodeImg().then((res: any) => {
|
||||
codeUrl.value = "data:image/gif;base64," + res.img;
|
||||
registerForm.value.uuid = res.uuid;
|
||||
});
|
||||
}
|
||||
|
||||
function sendCode() {
|
||||
if (props.method === 'email') {
|
||||
sendEmailCode(registerForm.value, 'register')
|
||||
} else if (props.method === 'phone') {
|
||||
sendPhoneCode(registerForm.value, 'register')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCode();
|
||||
})
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<el-form ref="registerRef" :model="registerForm" :rules="registerRules" class="register-form">
|
||||
<el-form-item prop="email" v-if="method === 'email'">
|
||||
<el-input v-model="registerForm.email" type="text" size="large" auto-complete="off" placeholder="邮箱">
|
||||
<template #prefix>
|
||||
<svg-icon icon-class="email" class="el-input__icon input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="phonenumber" v-else-if="method === 'phone'">
|
||||
<el-input v-model="registerForm.phonenumber" type="text" size="large" auto-complete="off" placeholder="手机号">
|
||||
<template #prefix>
|
||||
<svg-icon icon-class="phone" class="el-input__icon input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="username" v-else>
|
||||
<el-input v-model="registerForm.username" type="text" size="large" auto-complete="off" placeholder="账号">
|
||||
<template #prefix>
|
||||
<svg-icon icon-class="user" class="el-input__icon input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="registerForm.password" type="password" size="large" auto-complete="off" placeholder="密码"
|
||||
@keyup.enter="handleRegister">
|
||||
<template #prefix>
|
||||
<svg-icon icon-class="password" class="el-input__icon input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="confirmPassword">
|
||||
<el-input v-model="registerForm.confirmPassword" type="password" size="large" auto-complete="off"
|
||||
placeholder="确认密码" @keyup.enter="handleRegister">
|
||||
<template #prefix>
|
||||
<svg-icon icon-class="password" class="el-input__icon input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="code" v-if="method === 'email' || method === 'phone'">
|
||||
<el-input size="large" v-model="registerForm.code" auto-complete="off" placeholder="验证码" style="width: 63%"
|
||||
@keyup.enter="handleRegister">
|
||||
<template #prefix>
|
||||
<svg-icon icon-class="validCode" class="el-input__icon input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="register-code">
|
||||
<el-button class="register-code-img" @click="sendCode">发送验证码</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item prop="code" v-if="captchaEnabled && method === 'password'">
|
||||
<el-input size="large" v-model="registerForm.code" auto-complete="off" placeholder="验证码" style="width: 63%"
|
||||
@keyup.enter="handleRegister">
|
||||
<template #prefix>
|
||||
<svg-icon icon-class="validCode" class="el-input__icon input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="register-code">
|
||||
<img :src="codeUrl" @click="getCode" class="register-code-img" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item style="width:100%;">
|
||||
<el-button :loading="loading" size="large" type="primary" style="width:100%;" @click.prevent="handleRegister">
|
||||
<span v-if="!loading">注 册</span>
|
||||
<span v-else>注 册 中...</span>
|
||||
</el-button>
|
||||
<div class="login-link">
|
||||
<span class="question-text">已有账号?</span>
|
||||
<router-link class="link-type" :to="RoutesAlias.Login">立即登录</router-link>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.register-form {
|
||||
|
||||
.el-input {
|
||||
height: 40px;
|
||||
|
||||
input {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
height: 39px;
|
||||
width: 14px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.register-code {
|
||||
width: 37%;
|
||||
height: 40px;
|
||||
float: right;
|
||||
|
||||
.register-code-img {
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
width: calc(100% - 12px);
|
||||
height: 40px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-link {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 5px;
|
||||
|
||||
.question-text {
|
||||
color: #606266;
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.link-type {
|
||||
color: #409EFF;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background-color: #409EFF;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #66b1ff;
|
||||
|
||||
&::after {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
741
src/views/auth/register/index.vue
Normal file
741
src/views/auth/register/index.vue
Normal file
@ -0,0 +1,741 @@
|
||||
<template>
|
||||
<div class="salesforce-login-container">
|
||||
<div class="login-header">
|
||||
<h2>Salesforce 认证中心</h2>
|
||||
<p class="login-description">选择适合您的登录方式连接到 Salesforce</p>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<div class="login-section" v-if="!isLoggedIn">
|
||||
<el-card class="login-card" shadow="hover">
|
||||
<el-tabs v-model="activeTab" @tab-change="handleTabChange" class="login-tabs">
|
||||
<!-- OAuth 2.0 登录 -->
|
||||
<el-tab-pane label="OAuth 2.0" name="oauth2">
|
||||
<div class="tab-content">
|
||||
<div class="tab-description">
|
||||
<h3>OAuth 2.0 认证</h3>
|
||||
<p>使用标准的 OAuth 2.0 协议进行安全认证</p>
|
||||
</div>
|
||||
<el-form
|
||||
ref="oauth2FormRef"
|
||||
:model="oauth2Form"
|
||||
:rules="oauth2Rules"
|
||||
label-width="120px"
|
||||
class="login-form"
|
||||
>
|
||||
<el-form-item label="授权类型" prop="grantType">
|
||||
<el-select v-model="oauth2Form.grantType" placeholder="请选择授权类型" style="width: 100%">
|
||||
<el-option label="密码模式" value="password"></el-option>
|
||||
<el-option label="客户端凭证模式" value="client_credentials"></el-option>
|
||||
<el-option label="授权码模式" value="authorization_code"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="客户端ID" prop="clientId">
|
||||
<el-input v-model="oauth2Form.clientId" placeholder="请输入客户端ID"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="客户端密钥" prop="clientSecret">
|
||||
<el-input v-model="oauth2Form.clientSecret" type="password" placeholder="请输入客户端密钥" show-password></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="用户名" prop="username" v-show="oauth2Form.grantType === 'password'">
|
||||
<el-input v-model="oauth2Form.username" placeholder="请输入用户名"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="密码" prop="password" v-show="oauth2Form.grantType === 'password'">
|
||||
<el-input v-model="oauth2Form.password" type="password" placeholder="请输入密码" show-password></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="安全令牌" prop="securityToken" v-show="oauth2Form.grantType === 'password'">
|
||||
<el-input v-model="oauth2Form.securityToken" placeholder="请输入安全令牌(可选)"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="授权码" prop="code" v-show="oauth2Form.grantType === 'authorization_code'">
|
||||
<el-input v-model="oauth2Form.code" placeholder="请输入授权码"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="State" prop="state" v-show="oauth2Form.grantType === 'authorization_code'">
|
||||
<el-input v-model="oauth2Form.state" placeholder="请输入state参数"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item class="form-actions">
|
||||
<el-button type="primary" @click="handleOAuth2Login" :loading="loading" size="large">
|
||||
<el-icon><Connection /></el-icon>
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 传统凭据登录 -->
|
||||
<el-tab-pane label="传统凭据" name="legacy_credential">
|
||||
<div class="tab-content">
|
||||
<div class="tab-description">
|
||||
<h3>传统凭据认证</h3>
|
||||
<p>使用用户名和密码进行传统登录</p>
|
||||
</div>
|
||||
<el-form
|
||||
ref="legacyFormRef"
|
||||
:model="legacyForm"
|
||||
:rules="legacyRules"
|
||||
label-width="120px"
|
||||
class="login-form"
|
||||
>
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="legacyForm.username" placeholder="请输入用户名"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input v-model="legacyForm.password" type="password" placeholder="请输入密码" show-password></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="安全令牌" prop="securityToken">
|
||||
<el-input v-model="legacyForm.securityToken" placeholder="请输入安全令牌(可选)"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item class="form-actions">
|
||||
<el-button type="primary" @click="handleLegacyLogin" :loading="loading" size="large">
|
||||
<el-icon><User /></el-icon>
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Session ID 登录 -->
|
||||
<el-tab-pane label="Session ID" name="session_id">
|
||||
<div class="tab-content">
|
||||
<div class="tab-description">
|
||||
<h3>Session ID 认证</h3>
|
||||
<p>使用现有的 Session ID 直接登录</p>
|
||||
</div>
|
||||
<el-form
|
||||
ref="sessionIdFormRef"
|
||||
:model="sessionIdForm"
|
||||
:rules="sessionIdRules"
|
||||
label-width="120px"
|
||||
class="login-form"
|
||||
>
|
||||
<el-form-item label="Session ID" prop="sessionId">
|
||||
<el-input v-model="sessionIdForm.sessionId" placeholder="请输入Session ID" type="textarea" :rows="3"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item class="form-actions">
|
||||
<el-button type="primary" @click="handleSessionIdLogin" :loading="loading" size="large">
|
||||
<el-icon><Key /></el-icon>
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Salesforce CLI 登录 -->
|
||||
<el-tab-pane label="Salesforce CLI" name="salesforce_cli">
|
||||
<div class="tab-content">
|
||||
<div class="tab-description">
|
||||
<h3>Salesforce CLI 认证</h3>
|
||||
<p>使用已配置的 Salesforce CLI 凭据登录</p>
|
||||
</div>
|
||||
<el-form
|
||||
ref="cliFormRef"
|
||||
:model="cliForm"
|
||||
label-width="120px"
|
||||
class="login-form"
|
||||
>
|
||||
<el-form-item label="组织别名" prop="orgAlias">
|
||||
<el-input v-model="cliForm.orgAlias" placeholder="请输入组织别名(可选)"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item class="form-actions">
|
||||
<el-button type="primary" @click="handleCliLogin" :loading="loading" size="large">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 登录成功后的会话信息展示 -->
|
||||
<div v-if="isLoggedIn && sessionInfo" class="session-info-section">
|
||||
<el-card class="session-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h3>会话信息</h3>
|
||||
<el-tag :type="sessionInfo.status === 'active' ? 'success' : 'danger'" size="large">
|
||||
{{ sessionInfo.status === 'active' ? '活跃' : '非活跃' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-descriptions :column="2" border class="session-descriptions">
|
||||
<el-descriptions-item label="会话ID" span="2">
|
||||
<el-text class="session-id" copyable>{{ sessionInfo.sessionId }}</el-text>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="租户编号">{{ sessionInfo.tenantId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户名">{{ sessionInfo.username }}</el-descriptions-item>
|
||||
<el-descriptions-item label="登录类型">{{ sessionInfo.loginType }}</el-descriptions-item>
|
||||
<el-descriptions-item label="会话状态">
|
||||
<el-tag :type="sessionInfo.status === 'active' ? 'success' : 'danger'">
|
||||
{{ sessionInfo.status }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="登录时间">{{ sessionInfo.loginTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="过期时间">{{ sessionInfo.expireTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最后活动时间">{{ sessionInfo.lastActivityTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="登录IP" span="2">{{ sessionInfo.loginIp }}</el-descriptions-item>
|
||||
<el-descriptions-item label="设备信息" span="2">{{ sessionInfo.deviceInfo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="浏览器信息" span="2">{{ sessionInfo.browserInfo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="令牌ID" span="2">
|
||||
<el-text class="token-id" copyable>{{ sessionInfo.tokenId }}</el-text>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="session-actions">
|
||||
<el-button type="primary" @click="fetchSessionInfo" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新会话信息
|
||||
</el-button>
|
||||
<el-button type="danger" @click="logout">
|
||||
<el-icon><SwitchButton /></el-icon>
|
||||
退出登录
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { executeLogin as salesforceLogin, getLoginSession as getCurrentSession } from '@/api/auth/register'
|
||||
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
|
||||
import { Connection, User, Key, Monitor, Refresh, SwitchButton } from '@element-plus/icons-vue'
|
||||
|
||||
// 表单引用
|
||||
const oauth2FormRef = ref<FormInstance>()
|
||||
const legacyFormRef = ref<FormInstance>()
|
||||
const sessionIdFormRef = ref<FormInstance>()
|
||||
const cliFormRef = ref<FormInstance>()
|
||||
|
||||
// 当前激活的标签页
|
||||
const activeTab = ref('oauth2')
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 登录状态
|
||||
const isLoggedIn = ref(false)
|
||||
|
||||
// 会话信息
|
||||
const sessionInfo = ref<any>(null)
|
||||
|
||||
// OAuth2 表单数据
|
||||
const oauth2Form = reactive({
|
||||
loginType: 'oauth2',
|
||||
grantType: 'password',
|
||||
username: '',
|
||||
password: '',
|
||||
securityToken: '',
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
code: '',
|
||||
state: ''
|
||||
})
|
||||
|
||||
// 传统凭据表单数据
|
||||
const legacyForm = reactive({
|
||||
loginType: 'legacy_credential',
|
||||
username: '',
|
||||
password: '',
|
||||
securityToken: ''
|
||||
})
|
||||
|
||||
// Session ID 表单数据
|
||||
const sessionIdForm = reactive({
|
||||
loginType: 'session_id',
|
||||
sessionId: ''
|
||||
})
|
||||
|
||||
// CLI 表单数据
|
||||
const cliForm = reactive({
|
||||
loginType: 'salesforce_cli',
|
||||
orgAlias: ''
|
||||
})
|
||||
|
||||
// OAuth2 表单校验规则
|
||||
const oauth2Rules = reactive<FormRules>({
|
||||
grantType: [{ required: true, message: '请选择授权类型', trigger: 'change' }],
|
||||
clientId: [{ required: true, message: '请输入客户端ID', trigger: 'blur' }],
|
||||
clientSecret: [{ required: true, message: '请输入客户端密钥', trigger: 'blur' }],
|
||||
username: [{
|
||||
required: false,
|
||||
message: '请输入用户名',
|
||||
trigger: 'blur',
|
||||
validator: (rule, value, callback) => {
|
||||
if (oauth2Form.grantType === 'password' && !value) {
|
||||
callback(new Error('密码模式下用户名为必填项'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}],
|
||||
password: [{
|
||||
required: false,
|
||||
message: '请输入密码',
|
||||
trigger: 'blur',
|
||||
validator: (rule, value, callback) => {
|
||||
if (oauth2Form.grantType === 'password' && !value) {
|
||||
callback(new Error('密码模式下密码为必填项'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}],
|
||||
code: [{
|
||||
required: false,
|
||||
message: '请输入授权码',
|
||||
trigger: 'blur',
|
||||
validator: (rule, value, callback) => {
|
||||
if (oauth2Form.grantType === 'authorization_code' && !value) {
|
||||
callback(new Error('授权码模式下授权码为必填项'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}],
|
||||
state: [{
|
||||
required: false,
|
||||
message: '请输入state参数',
|
||||
trigger: 'blur',
|
||||
validator: (rule, value, callback) => {
|
||||
if (oauth2Form.grantType === 'authorization_code' && !value) {
|
||||
callback(new Error('授权码模式下state参数为必填项'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
// 传统凭据表单校验规则
|
||||
const legacyRules = reactive<FormRules>({
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
// Session ID 表单校验规则
|
||||
const sessionIdRules = reactive<FormRules>({
|
||||
sessionId: [{ required: true, message: '请输入Session ID', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
// 标签页切换处理
|
||||
const handleTabChange = () => {
|
||||
// 清空表单校验状态
|
||||
oauth2FormRef.value?.clearValidate()
|
||||
legacyFormRef.value?.clearValidate()
|
||||
sessionIdFormRef.value?.clearValidate()
|
||||
cliFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
// 通用登录处理函数
|
||||
const handleLogin = async (formRef: FormInstance | undefined, formData: any, loginType: string) => {
|
||||
if (!formRef) return
|
||||
|
||||
await formRef.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
try {
|
||||
// 处理CLI登录的特殊情况
|
||||
let loginParams = { ...formData }
|
||||
if (loginType === 'salesforce_cli' && !loginParams.orgAlias) {
|
||||
delete loginParams.orgAlias
|
||||
}
|
||||
|
||||
await salesforceLogin(loginParams)
|
||||
ElMessage.success('登录成功')
|
||||
await fetchSessionInfo()
|
||||
} catch (error: any) {
|
||||
console.error(`${loginType}登录失败:`, error)
|
||||
const errorMessage = error.response?.data?.message || error.message || '登录失败,请检查您的凭据'
|
||||
ElMessage.error(errorMessage)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// OAuth2 登录处理
|
||||
const handleOAuth2Login = async () => {
|
||||
if (!oauth2FormRef.value) return
|
||||
|
||||
await oauth2FormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
try {
|
||||
// 构造登录参数 - 所有OAuth2模式都需要客户端ID和密钥
|
||||
const loginParams: any = {
|
||||
loginType: oauth2Form.loginType,
|
||||
grantType: oauth2Form.grantType,
|
||||
clientId: oauth2Form.clientId,
|
||||
clientSecret: oauth2Form.clientSecret
|
||||
}
|
||||
|
||||
// 根据不同授权类型添加特定参数
|
||||
if (oauth2Form.grantType === 'password') {
|
||||
loginParams.username = oauth2Form.username
|
||||
loginParams.password = oauth2Form.password
|
||||
// 仅在有值时添加安全令牌
|
||||
if (oauth2Form.securityToken) {
|
||||
loginParams.securityToken = oauth2Form.securityToken
|
||||
}
|
||||
} else if (oauth2Form.grantType === 'authorization_code') {
|
||||
loginParams.code = oauth2Form.code
|
||||
loginParams.state = oauth2Form.state
|
||||
}
|
||||
// client_credentials 类型需要 clientId 和 clientSecret,已在基础参数中
|
||||
|
||||
await salesforceLogin(loginParams)
|
||||
ElMessage.success('登录成功')
|
||||
await fetchSessionInfo()
|
||||
} catch (error: any) {
|
||||
console.error('OAuth2登录失败:', error)
|
||||
const errorMessage = error.response?.data?.message || error.message || '登录失败,请检查您的凭据'
|
||||
ElMessage.error(errorMessage)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 传统凭据登录处理
|
||||
const handleLegacyLogin = () => handleLogin(legacyFormRef.value, legacyForm, '传统凭据')
|
||||
|
||||
// Session ID 登录处理
|
||||
const handleSessionIdLogin = () => handleLogin(sessionIdFormRef.value, sessionIdForm, 'Session ID')
|
||||
|
||||
// CLI 登录处理
|
||||
const handleCliLogin = () => {
|
||||
if (!cliFormRef.value) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
// 如果没有填写组织别名,则删除该字段
|
||||
const loginParams = { ...cliForm }
|
||||
if (!loginParams.orgAlias) {
|
||||
delete loginParams.orgAlias
|
||||
}
|
||||
|
||||
salesforceLogin(loginParams).then(() => {
|
||||
ElMessage.success('登录成功')
|
||||
fetchSessionInfo()
|
||||
}).catch((error: any) => {
|
||||
console.error('CLI登录失败:', error)
|
||||
const errorMessage = error.response?.data?.message || error.message || '登录失败,请检查您的CLI配置'
|
||||
ElMessage.error(errorMessage)
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('CLI登录失败:', error)
|
||||
const errorMessage = error.response?.data?.message || error.message || '登录失败,请检查您的CLI配置'
|
||||
ElMessage.error(errorMessage)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取会话信息
|
||||
const fetchSessionInfo = async () => {
|
||||
try {
|
||||
const response = await getCurrentSession()
|
||||
if (response.code === 200) {
|
||||
sessionInfo.value = response.data
|
||||
isLoggedIn.value = true
|
||||
} else {
|
||||
ElMessage.error(response.msg || '获取会话信息失败')
|
||||
isLoggedIn.value = false
|
||||
sessionInfo.value = null
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取会话信息失败:', error)
|
||||
const errorMessage = error.response?.data?.message || error.message || '获取会话信息失败'
|
||||
ElMessage.error(errorMessage)
|
||||
isLoggedIn.value = false
|
||||
sessionInfo.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时尝试获取会话信息
|
||||
onMounted(() => {
|
||||
fetchSessionInfo()
|
||||
})
|
||||
|
||||
// 退出登录
|
||||
const logout = () => {
|
||||
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
isLoggedIn.value = false
|
||||
sessionInfo.value = null
|
||||
ElMessage.success('已退出登录')
|
||||
}).catch(() => {
|
||||
// 取消退出
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.salesforce-login-container {
|
||||
padding: 24px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
|
||||
h2 {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.login-description {
|
||||
font-size: 16px;
|
||||
color: #606266;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.login-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.login-card {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
.login-tabs {
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-wrap) {
|
||||
&::after {
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
padding: 0 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 0 8px;
|
||||
|
||||
.tab-description {
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
:deep(.el-select .el-input .el-input__wrapper) {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 32px;
|
||||
text-align: center;
|
||||
|
||||
.el-button {
|
||||
padding: 12px 32px;
|
||||
font-size: 16px;
|
||||
border-radius: 6px;
|
||||
|
||||
.el-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.session-info-section {
|
||||
margin-top: 32px;
|
||||
|
||||
.session-card {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.session-descriptions {
|
||||
margin: 24px 0;
|
||||
|
||||
:deep(.el-descriptions__label) {
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__content) {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.session-id, .token-id {
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.session-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-top: 24px;
|
||||
|
||||
.el-button {
|
||||
padding: 10px 24px;
|
||||
border-radius: 6px;
|
||||
|
||||
.el-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.salesforce-login-container {
|
||||
padding: 16px;
|
||||
|
||||
.login-header {
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.login-description {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-section {
|
||||
.login-card {
|
||||
.login-tabs {
|
||||
:deep(.el-tabs__item) {
|
||||
font-size: 14px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
.tab-description {
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
max-width: 100%;
|
||||
|
||||
.form-actions {
|
||||
.el-button {
|
||||
padding: 10px 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.session-info-section {
|
||||
.session-card {
|
||||
.session-descriptions {
|
||||
:deep(.el-descriptions__label) {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__content) {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.session-actions {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.el-button {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user