add(接口):添加授权、用户、好友关系服务接口
This commit is contained in:
parent
d85e906b39
commit
c67c666135
57
backend/IM_API/Dtos/BaseResponse.cs
Normal file
57
backend/IM_API/Dtos/BaseResponse.cs
Normal file
@ -0,0 +1,57 @@
|
||||
namespace IM_API.Dtos
|
||||
{
|
||||
public class BaseResponse<T>
|
||||
{
|
||||
//响应状态码
|
||||
public int Code { get; set; }
|
||||
//响应消息
|
||||
public string Message { get; set; }
|
||||
//响应数据
|
||||
public T? Data { get; set; }
|
||||
/// <summary>
|
||||
/// 默认成功响应返回
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="data"></param>
|
||||
public BaseResponse(string msg,T data)
|
||||
{
|
||||
this.Code = 0;
|
||||
this.Message = msg;
|
||||
this.Data = data;
|
||||
}
|
||||
/// <summary>
|
||||
/// 默认成功响应返回,不带数据
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="data"></param>
|
||||
public BaseResponse(string msg)
|
||||
{
|
||||
this.Code = 0;
|
||||
this.Message = msg;
|
||||
}
|
||||
/// <summary>
|
||||
/// 非成功响应且带数据
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
public BaseResponse(int code, string message, T? data)
|
||||
{
|
||||
Code = code;
|
||||
Message = message;
|
||||
Data = data;
|
||||
}
|
||||
/// <summary>
|
||||
/// 非成功响应且不带数据
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
public BaseResponse(int code, string message)
|
||||
{
|
||||
Code = code;
|
||||
Message = message;
|
||||
}
|
||||
public BaseResponse() { }
|
||||
}
|
||||
}
|
||||
9
backend/IM_API/Dtos/FriendRequestDto.cs
Normal file
9
backend/IM_API/Dtos/FriendRequestDto.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace IM_API.Dtos
|
||||
{
|
||||
public class FriendRequestDto
|
||||
{
|
||||
public int FromUserId { get; set; }
|
||||
public int ToUserId { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
}
|
||||
9
backend/IM_API/Dtos/LoginDto.cs
Normal file
9
backend/IM_API/Dtos/LoginDto.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace IM_API.Dtos
|
||||
{
|
||||
public class LoginDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Token { get; set; }
|
||||
public string RefreshToken { get; set; }
|
||||
}
|
||||
}
|
||||
8
backend/IM_API/Dtos/LoginRequestDto.cs
Normal file
8
backend/IM_API/Dtos/LoginRequestDto.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace IM_API.Dtos
|
||||
{
|
||||
public class LoginRequestDto
|
||||
{
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
||||
}
|
||||
8
backend/IM_API/Dtos/RegisterRequestDto.cs
Normal file
8
backend/IM_API/Dtos/RegisterRequestDto.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace IM_API.Dtos
|
||||
{
|
||||
public class RegisterRequestDto
|
||||
{
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
||||
}
|
||||
7
backend/IM_API/Dtos/UpdateUserDto.cs
Normal file
7
backend/IM_API/Dtos/UpdateUserDto.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace IM_API.Dtos
|
||||
{
|
||||
public class UpdateUserDto
|
||||
{
|
||||
public string? NickName { get; set; }
|
||||
}
|
||||
}
|
||||
38
backend/IM_API/Dtos/UserInfoDto.cs
Normal file
38
backend/IM_API/Dtos/UserInfoDto.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace IM_API.Dtos
|
||||
{
|
||||
public class UserInfoDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 唯一用户名
|
||||
/// </summary>
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户昵称
|
||||
/// </summary>
|
||||
public string NickName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户在线状态
|
||||
/// 0(默认):不在线
|
||||
/// 1:在线
|
||||
/// </summary>
|
||||
public sbyte OlineStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime Created { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 账户状态
|
||||
/// (0:未激活,1:正常,2:封禁)
|
||||
/// </summary>
|
||||
public sbyte Status { get; set; }
|
||||
}
|
||||
}
|
||||
@ -21,6 +21,7 @@
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
39
backend/IM_API/Interface/Services/IAuthService.cs
Normal file
39
backend/IM_API/Interface/Services/IAuthService.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using IM_API.Dtos;
|
||||
using IM_API.Models;
|
||||
|
||||
namespace IM_API.Interface.Services
|
||||
{
|
||||
public interface IAuthService
|
||||
{
|
||||
/// <summary>
|
||||
/// 登录
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
Task<LoginDto> LoginAsync(LoginRequestDto dto);
|
||||
/// <summary>
|
||||
/// 注册
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
Task<UserInfoDto> RegisterAsync(RegisterRequestDto dto);
|
||||
/// <summary>
|
||||
/// 生成登录凭证
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
string GenerateToken(User user);
|
||||
/// <summary>
|
||||
/// 验证登录凭证
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
int? ValidateToken(string token);
|
||||
/// <summary>
|
||||
/// 刷新令牌
|
||||
/// </summary>
|
||||
/// <param name="refreshToken"></param>
|
||||
/// <returns></returns>
|
||||
LoginDto RefreshToken(string refreshToken);
|
||||
}
|
||||
}
|
||||
32
backend/IM_API/Interface/Services/IFriendSerivce.cs
Normal file
32
backend/IM_API/Interface/Services/IFriendSerivce.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using IM_API.Dtos;
|
||||
using IM_API.Models;
|
||||
|
||||
namespace IM_API.Interface.Services
|
||||
{
|
||||
public interface IFriendSerivce
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取好友列表
|
||||
/// </summary>
|
||||
/// <param name="userId">指定用户</param>
|
||||
/// <param name="page">当前页</param>
|
||||
/// <param name="limit">分页大小</param>
|
||||
/// <returns></returns>
|
||||
Task<List<UserInfoDto>> GetFriendListAsync(int userId,int page,int limit);
|
||||
/// <summary>
|
||||
/// 新增好友请求
|
||||
/// </summary>
|
||||
/// <param name="friendRequest"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> SendFriendRequestAsync(FriendRequestDto friendRequest);
|
||||
/// <summary>
|
||||
/// 获取好友请求
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="isReceived">是否为接受请求方</param>
|
||||
/// <param name="page"></param>
|
||||
/// <param name="limit"></param>
|
||||
/// <returns></returns>
|
||||
Task<Friendrequest> GetFriendRequestListAsync(int userId,bool isReceived,int page,int limit);
|
||||
}
|
||||
}
|
||||
39
backend/IM_API/Interface/Services/IUserService.cs
Normal file
39
backend/IM_API/Interface/Services/IUserService.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using IM_API.Dtos;
|
||||
using IM_API.Models;
|
||||
|
||||
namespace IM_API.Interface.Services
|
||||
{
|
||||
public interface IUserService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取用户信息
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
Task<UserInfoDto> GetUserInfoAsync(int userId);
|
||||
/// <summary>
|
||||
/// 用户名查找用户
|
||||
/// </summary>
|
||||
/// <param name="username"></param>
|
||||
/// <returns></returns>
|
||||
Task<UserInfoDto> GetUserInfoByUsernameAsync(string username);
|
||||
/// <summary>
|
||||
/// 更新用户信息
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
Task<UserInfoDto> UpdateUserAsync(UpdateUserDto dto);
|
||||
/// <summary>
|
||||
/// 重置用户密码
|
||||
/// </summary>
|
||||
/// <param name="password"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> ResetPasswordAsync(string password);
|
||||
/// <summary>
|
||||
/// 更新用户在线状态
|
||||
/// </summary>
|
||||
/// <param name="onlineStatus"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> UpdateOlineStatusAsync(UserOnlineStatus onlineStatus);
|
||||
}
|
||||
}
|
||||
7
backend/IM_API/Models/FriendStatus.cs
Normal file
7
backend/IM_API/Models/FriendStatus.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace IM_API.Models
|
||||
{
|
||||
public enum FriendStatus:SByte
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@ -41,7 +41,7 @@ public partial class User
|
||||
/// 1:在线
|
||||
/// </summary>
|
||||
[Column(TypeName = "tinyint(4)")]
|
||||
public sbyte OlineStatus { get; set; }
|
||||
public UserOnlineStatus OlineStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
@ -60,7 +60,7 @@ public partial class User
|
||||
/// (0:未激活,1:正常,2:封禁)
|
||||
/// </summary>
|
||||
[Column(TypeName = "tinyint(4)")]
|
||||
public sbyte Status { get; set; }
|
||||
public UserStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 软删除标识
|
||||
|
||||
18
backend/IM_API/Models/UserOlineStatus.cs
Normal file
18
backend/IM_API/Models/UserOlineStatus.cs
Normal file
@ -0,0 +1,18 @@
|
||||
namespace IM_API.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户在线状态
|
||||
/// </summary>
|
||||
public enum UserOnlineStatus : sbyte
|
||||
{
|
||||
/// <summary>
|
||||
/// 不在线 (0)
|
||||
/// </summary>
|
||||
Offline = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 在线 (1)
|
||||
/// </summary>
|
||||
Online = 1
|
||||
}
|
||||
}
|
||||
9
backend/IM_API/Models/UserStatus.cs
Normal file
9
backend/IM_API/Models/UserStatus.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace IM_API.Models
|
||||
{
|
||||
public enum UserStatus:SByte
|
||||
{
|
||||
Inactive = 0,
|
||||
Normal = 1,
|
||||
Banned = 2
|
||||
}
|
||||
}
|
||||
22
backend/IM_API/Tools/PasswordHasher.cs
Normal file
22
backend/IM_API/Tools/PasswordHasher.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace IM_API.Tools
|
||||
{
|
||||
public static class PasswordHasher
|
||||
{
|
||||
public static string HashPassword(string password)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
var hashedBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(password));
|
||||
return Convert.ToBase64String(hashedBytes);
|
||||
}
|
||||
|
||||
public static bool VerifyPassword(string password, string hashedPassword)
|
||||
{
|
||||
var hashedInput = HashPassword(password);
|
||||
return hashedInput == hashedPassword;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,17 @@
|
||||
<script setup></script>
|
||||
|
||||
<template>
|
||||
<h1>You did it!</h1>
|
||||
<p>
|
||||
Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the
|
||||
documentation
|
||||
</p>
|
||||
<div id="app">
|
||||
<WindowLayout>
|
||||
|
||||
<RouterView></RouterView>
|
||||
</WindowLayout>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import WindowLayout from './components/Layout.vue'
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
262
frontend/web/src/components/Layout.vue
Normal file
262
frontend/web/src/components/Layout.vue
Normal file
@ -0,0 +1,262 @@
|
||||
<template>
|
||||
<div class="window-container">
|
||||
<div class="window">
|
||||
<!-- Windows 风格标题栏 -->
|
||||
<div class="window-header">
|
||||
<div class="window-title-area">
|
||||
<div class="window-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.5 1.5H1.5V6.5H6.5V1.5Z" fill="currentColor"/>
|
||||
<path d="M14.5 1.5H9.5V6.5H14.5V1.5Z" fill="currentColor"/>
|
||||
<path d="M6.5 9.5H1.5V14.5H6.5V9.5Z" fill="currentColor"/>
|
||||
<path d="M14.5 9.5H9.5V14.5H14.5V9.5Z" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="window-title">我的窗口</div>
|
||||
</div>
|
||||
<div class="window-controls">
|
||||
<button class="control-btn minimize" @click="minimize" title="最小化">
|
||||
<span class="control-icon"></span>
|
||||
</button>
|
||||
<button class="control-btn maximize" @click="maximize" title="最大化">
|
||||
<span class="control-icon"></span>
|
||||
</button>
|
||||
<button class="control-btn close" @click="close" title="关闭">
|
||||
<span class="control-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 窗口内容 -->
|
||||
<div class="window-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
function minimize() {
|
||||
console.log('最小化')
|
||||
}
|
||||
|
||||
function maximize() {
|
||||
console.log('最大化/还原')
|
||||
}
|
||||
|
||||
function close() {
|
||||
console.log('关闭窗口')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 外层容器,居中 - 保持不变 */
|
||||
.window-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background-color: #f0f0f0;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 窗口主体 - 保持不变 */
|
||||
.window {
|
||||
width: 800px;
|
||||
height: 550px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 窗口顶部栏 - 优化按钮样式 */
|
||||
.window-header {
|
||||
height: 42px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
background: linear-gradient(135deg, #1a73e8 0%, #4285f4 100%);
|
||||
color: #fff;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-weight: 600;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 添加顶栏的微光效果 */
|
||||
.window-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.4) 20%,
|
||||
rgba(255, 255, 255, 0.7) 50%,
|
||||
rgba(255, 255, 255, 0.4) 80%,
|
||||
transparent 100%);
|
||||
}
|
||||
|
||||
/* 标题区域 */
|
||||
.window-title-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 窗口图标 */
|
||||
.window-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
/* 窗口标题 */
|
||||
.window-title {
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.3px;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 标题栏按钮容器 - 增加间距 */
|
||||
.window-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 控制按钮通用样式 - 优化尺寸和交互 */
|
||||
.control-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.control-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0);
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.control-btn:hover::before {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.control-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* 控制按钮图标 - 使用CSS绘制精致图标 */
|
||||
.control-icon {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 最小化按钮图标 */
|
||||
.minimize .control-icon {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.minimize .control-icon::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 2px;
|
||||
background: currentColor;
|
||||
border-radius: 1px;
|
||||
bottom: 2px;
|
||||
}
|
||||
|
||||
/* 最大化按钮图标 */
|
||||
.maximize .control-icon {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.maximize .control-icon::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border: 1.5px solid currentColor;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
/* 关闭按钮图标 */
|
||||
.close .control-icon {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.close .control-icon::before,
|
||||
.close .control-icon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 1.5px;
|
||||
background: currentColor;
|
||||
border-radius: 1px;
|
||||
top: 4px;
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
.close .control-icon::before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.close .control-icon::after {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
/* 关闭按钮特殊样式 */
|
||||
.close:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.close:hover::before {
|
||||
background: rgba(232, 17, 35, 0.9);
|
||||
}
|
||||
|
||||
/* 窗口内容 - 保持不变 */
|
||||
.window-content {
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
background-color: #fdfdfd;
|
||||
}
|
||||
</style>
|
||||
@ -1,8 +1,10 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const routes = [{ path: '/auth/login', component: () => import('@/views/auth/Login.vue') }]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [],
|
||||
routes,
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
386
frontend/web/src/views/auth/Login.vue
Normal file
386
frontend/web/src/views/auth/Login.vue
Normal file
@ -0,0 +1,386 @@
|
||||
<template>
|
||||
<div class="login-content">
|
||||
<div class="login-header">
|
||||
<h2 class="login-title">欢迎登录</h2>
|
||||
<p class="login-subtitle">请输入您的账号信息</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleLogin" class="login-form">
|
||||
<div class="input-group">
|
||||
<div class="input-container">
|
||||
<input id="username" type="text" v-model="username" placeholder=" " required>
|
||||
<label for="username" class="floating-label">用户名</label>
|
||||
<div class="input-icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 21V19C20 16.7909 18.2091 15 16 15H8C5.79086 15 4 16.7909 4 19V21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<circle cx="12" cy="7" r="4" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<div class="input-container">
|
||||
<input id="password" type="password" v-model="password" placeholder=" " required>
|
||||
<label for="password" class="floating-label">密码</label>
|
||||
<div class="input-icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M7 11V7C7 4.23858 9.23858 2 12 2C14.7614 2 17 4.23858 17 7V11" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="options-row">
|
||||
<label class="remember-me">
|
||||
<input type="checkbox">
|
||||
<span class="checkmark"></span>
|
||||
记住我
|
||||
</label>
|
||||
<a href="#" class="forgot-link">忘记密码?</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="login-btn">
|
||||
<span>登录</span>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 12H19M19 12L12 5M19 12L12 19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<p v-if="errorMsg" class="error">{{ errorMsg }}</p>
|
||||
</form>
|
||||
|
||||
<div class="login-footer">
|
||||
<p>还没有账号? <a href="#" class="register-link">立即注册</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const errorMsg = ref('')
|
||||
|
||||
const handleLogin = () => {
|
||||
if (!username.value || !password.value) {
|
||||
errorMsg.value = '用户名或密码不能为空'
|
||||
return
|
||||
}
|
||||
|
||||
if (username.value === 'admin' && password.value === '123456') {
|
||||
alert('登录成功!')
|
||||
errorMsg.value = ''
|
||||
// TODO: 跳转到IM主界面
|
||||
} else {
|
||||
errorMsg.value = '用户名或密码错误'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-content {
|
||||
padding: 40px 30px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
border-radius: 0 0 8px 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 添加背景装饰元素 */
|
||||
.login-content::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -20%;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.05) 0%, rgba(37, 99, 235, 0.1) 100%);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.login-content::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -30%;
|
||||
left: -10%;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.05) 0%, rgba(37, 99, 235, 0.08) 100%);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
max-width: 320px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 输入组 */
|
||||
.input-group {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.input-container input {
|
||||
width: 100%;
|
||||
padding: 16px 16px 16px 48px;
|
||||
border: 1.5px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.input-container input:focus {
|
||||
border-color: #3b82f6;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1), 0 2px 6px rgba(59, 130, 246, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.input-container input:focus + .floating-label,
|
||||
.input-container input:not(:placeholder-shown) + .floating-label {
|
||||
top: -8px;
|
||||
left: 48px;
|
||||
font-size: 12px;
|
||||
color: #3b82f6;
|
||||
background: linear-gradient(180deg, #f8fafc 50%, #ffffff 50%);
|
||||
padding: 0 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.floating-label {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 48px;
|
||||
font-size: 15px;
|
||||
color: #64748b;
|
||||
pointer-events: none;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #94a3b8;
|
||||
transition: color 0.3s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.input-container input:focus ~ .input-icon {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
/* 选项行 */
|
||||
.options-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 10px 0 5px;
|
||||
}
|
||||
|
||||
.remember-me {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.remember-me input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid #cbd5e1;
|
||||
border-radius: 4px;
|
||||
margin-right: 8px;
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.remember-me:hover .checkmark {
|
||||
border-color: #94a3b8;
|
||||
}
|
||||
|
||||
.remember-me input:checked + .checkmark {
|
||||
background-color: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.remember-me input:checked + .checkmark::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 2px;
|
||||
width: 4px;
|
||||
height: 8px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.forgot-link {
|
||||
font-size: 14px;
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.forgot-link:hover {
|
||||
color: #2563eb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 按钮 */
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.25);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
||||
box-shadow: 0 6px 12px rgba(59, 130, 246, 0.35);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.login-btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.login-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
/* 错误提示 */
|
||||
.error {
|
||||
color: #ef4444;
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
background-color: #fef2f2;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #ef4444;
|
||||
box-shadow: 0 2px 4px rgba(239, 68, 68, 0.1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 登录页脚 */
|
||||
.login-footer {
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.register-link {
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: color 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.register-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: #3b82f6;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.register-link:hover {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.register-link:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user