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="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" />
|
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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:在线
|
/// 1:在线
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Column(TypeName = "tinyint(4)")]
|
[Column(TypeName = "tinyint(4)")]
|
||||||
public sbyte OlineStatus { get; set; }
|
public UserOnlineStatus OlineStatus { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 创建时间
|
/// 创建时间
|
||||||
@ -60,7 +60,7 @@ public partial class User
|
|||||||
/// (0:未激活,1:正常,2:封禁)
|
/// (0:未激活,1:正常,2:封禁)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Column(TypeName = "tinyint(4)")]
|
[Column(TypeName = "tinyint(4)")]
|
||||||
public sbyte Status { get; set; }
|
public UserStatus Status { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
<script setup></script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1>You did it!</h1>
|
<div id="app">
|
||||||
<p>
|
<WindowLayout>
|
||||||
Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the
|
|
||||||
documentation
|
<RouterView></RouterView>
|
||||||
</p>
|
</WindowLayout>
|
||||||
|
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import WindowLayout from './components/Layout.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<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'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
const routes = [{ path: '/auth/login', component: () => import('@/views/auth/Login.vue') }]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [],
|
routes,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
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