diff --git a/backend/IM_API/Configs/ServiceCollectionExtensions.cs b/backend/IM_API/Configs/ServiceCollectionExtensions.cs index f811e2e..20a931d 100644 --- a/backend/IM_API/Configs/ServiceCollectionExtensions.cs +++ b/backend/IM_API/Configs/ServiceCollectionExtensions.cs @@ -1,5 +1,9 @@ -using IM_API.Interface.Services; +using IM_API.Dtos; +using IM_API.Interface.Services; using IM_API.Services; +using IM_API.Tools; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; namespace IM_API.Configs { @@ -7,6 +11,7 @@ namespace IM_API.Configs { public static IServiceCollection AddAllService(this IServiceCollection services, IConfiguration configuration) { + services.AddAutoMapper(typeof(MapperConfig)); services.AddTransient(); services.AddTransient(); @@ -14,8 +19,28 @@ namespace IM_API.Configs services.AddTransient(); services.AddTransient(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); return services; } - } + public static IServiceCollection AddModelValidation(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(options => + { + options.InvalidModelStateResponseFactory = context => + { + var errors = context.ModelState + .Where(e => e.Value.Errors.Count > 0) + .Select(e => new + { + Field = e.Key, + Message = e.Value.Errors.First().ErrorMessage + }); + + return new BadRequestObjectResult(new BaseResponse(CodeDefine.PARAMETER_ERROR.Code, errors.First().Message)); + }; + }); + return services; + } + + } } diff --git a/backend/IM_API/Controllers/ConversationController.cs b/backend/IM_API/Controllers/ConversationController.cs index 77db965..d04c63b 100644 --- a/backend/IM_API/Controllers/ConversationController.cs +++ b/backend/IM_API/Controllers/ConversationController.cs @@ -28,6 +28,20 @@ namespace IM_API.Controllers var res = new BaseResponse>(list); return Ok(res); } + [HttpPost] + public async Task Clear() + { + var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier); + await _conversationSerivice.ClearConversationsAsync(int.Parse(userIdStr)); + return Ok(new BaseResponse()); + } + [HttpPost] + public async Task Delete(int cid) + { + await _conversationSerivice.DeleteConversationAsync(cid); + return Ok(new BaseResponse()); + } + } } diff --git a/backend/IM_API/Controllers/UserController.cs b/backend/IM_API/Controllers/UserController.cs index cbd7ed9..074a9ca 100644 --- a/backend/IM_API/Controllers/UserController.cs +++ b/backend/IM_API/Controllers/UserController.cs @@ -83,7 +83,7 @@ namespace IM_API.Controllers { var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier); int userId = int.Parse(userIdStr); - await _userService.ResetPasswordAsync(userId,dto.oldPassword,dto.Password); + await _userService.ResetPasswordAsync(userId,dto.OldPassword,dto.Password); return Ok(new BaseResponse()); } /// diff --git a/backend/IM_API/Dtos/FriendDto.cs b/backend/IM_API/Dtos/FriendDto.cs index e21e06f..1a12449 100644 --- a/backend/IM_API/Dtos/FriendDto.cs +++ b/backend/IM_API/Dtos/FriendDto.cs @@ -1,7 +1,31 @@ using IM_API.Models; +using System.ComponentModel.DataAnnotations; namespace IM_API.Dtos { - public record FriendInfoDto(int Id, int UserId,int FriendId,FriendStatus StatusEnum,DateTime Created,string RemarkName,string? Avatar); - public record FriendRequestHandleDto(HandleFriendRequestAction action,string? remarkName); + public record FriendInfoDto + { + public int Id { get; init; } + + public int UserId { get; init; } + + public int FriendId { get; init; } + + public FriendStatus StatusEnum { get; init; } + + public DateTime Created { get; init; } + + public string RemarkName { get; init; } = string.Empty; + + public string? Avatar { get; init; } + } + + public record FriendRequestHandleDto + { + [Required(ErrorMessage = "操作必填")] + public HandleFriendRequestAction Action { get; init; } + + [StringLength(20, ErrorMessage = "备注名最大20个字符")] + public string? RemarkName { get; init; } + } } diff --git a/backend/IM_API/Dtos/FriendRequestDto.cs b/backend/IM_API/Dtos/FriendRequestDto.cs index e9c83da..34460a4 100644 --- a/backend/IM_API/Dtos/FriendRequestDto.cs +++ b/backend/IM_API/Dtos/FriendRequestDto.cs @@ -1,10 +1,15 @@ -namespace IM_API.Dtos +using System.ComponentModel.DataAnnotations; + +namespace IM_API.Dtos { public class FriendRequestDto { public int FromUserId { get; set; } public int ToUserId { get; set; } + [Required(ErrorMessage = "备注名必填")] + [StringLength(20, ErrorMessage = "备注名不能超过20位字符")] public string? RemarkName { get; set; } + [StringLength(50, ErrorMessage = "描述不能超过50字符")] public string? Description { get; set; } } } diff --git a/backend/IM_API/Dtos/LoginRequestDto.cs b/backend/IM_API/Dtos/LoginRequestDto.cs index b1c1b1f..16cd470 100644 --- a/backend/IM_API/Dtos/LoginRequestDto.cs +++ b/backend/IM_API/Dtos/LoginRequestDto.cs @@ -1,8 +1,17 @@ -namespace IM_API.Dtos +using IM_API.Tools; +using System.ComponentModel.DataAnnotations; + +namespace IM_API.Dtos { public class LoginRequestDto { + [Required(ErrorMessage = "用户名不能为空")] + [StringLength(20, ErrorMessage = "用户名不能超过20字符")] + [RegularExpression(@"^[A-Za-z0-9]+$",ErrorMessage = "用户名只能为英文或数字")] public string Username { get; set; } + + [Required(ErrorMessage = "密码不能为空")] + [StringLength(50, ErrorMessage = "密码不能超过50字符")] public string Password { get; set; } } } diff --git a/backend/IM_API/Dtos/RegisterRequestDto.cs b/backend/IM_API/Dtos/RegisterRequestDto.cs index 0bc05e0..83aaea4 100644 --- a/backend/IM_API/Dtos/RegisterRequestDto.cs +++ b/backend/IM_API/Dtos/RegisterRequestDto.cs @@ -1,9 +1,19 @@ -namespace IM_API.Dtos +using System.ComponentModel.DataAnnotations; + +namespace IM_API.Dtos { public class RegisterRequestDto { + [Required(ErrorMessage = "用户名不能为空")] + [MaxLength(20, ErrorMessage = "用户名不能超过20字符")] + [RegularExpression(@"^[A-Za-z0-9]+$", ErrorMessage = "用户名只能为英文或数字")] public string Username { get; set; } + [Required(ErrorMessage = "密码不能为空")] + [MaxLength(50, ErrorMessage = "密码不能超过50字符")] public string Password { get; set; } + [Required(ErrorMessage = "昵称不能为空")] + [MaxLength(20, ErrorMessage = "昵称不能超过20字符")] + public string? NickName { get; set; } } } diff --git a/backend/IM_API/Dtos/UpdateUserDto.cs b/backend/IM_API/Dtos/UpdateUserDto.cs index 86e2560..e7949e7 100644 --- a/backend/IM_API/Dtos/UpdateUserDto.cs +++ b/backend/IM_API/Dtos/UpdateUserDto.cs @@ -1,7 +1,10 @@ -namespace IM_API.Dtos +using System.ComponentModel.DataAnnotations; + +namespace IM_API.Dtos { public class UpdateUserDto { + [MaxLength(20, ErrorMessage = "昵称不能超过50字符")] public string? NickName { get; set; } public string? Avatar { get; set; } diff --git a/backend/IM_API/Dtos/UserDto.cs b/backend/IM_API/Dtos/UserDto.cs index 8d21169..655d1a1 100644 --- a/backend/IM_API/Dtos/UserDto.cs +++ b/backend/IM_API/Dtos/UserDto.cs @@ -1,7 +1,21 @@ using IM_API.Models; +using System.ComponentModel.DataAnnotations; namespace IM_API.Dtos { - public record PasswordResetDto(string oldPassword,string Password); - public record OnlineStatusSetDto(UserOnlineStatus OnlineStatus); + public record PasswordResetDto + { + [Required(ErrorMessage = "旧密码不能为空")] + public string OldPassword { get; init; } + + [Required(ErrorMessage = "新密码不能为空")] + [MaxLength(50, ErrorMessage = "密码不能超过50个字符")] + public string Password { get; init; } + } + + public record OnlineStatusSetDto + { + [Required] + public UserOnlineStatus OnlineStatus { get; init; } + } } diff --git a/backend/IM_API/Program.cs b/backend/IM_API/Program.cs index 954ebbc..9695881 100644 --- a/backend/IM_API/Program.cs +++ b/backend/IM_API/Program.cs @@ -102,6 +102,7 @@ namespace IM_API { options.Filters.Add(); }); + builder.Services.AddModelValidation(configuration); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); diff --git a/frontend/web/src/App.vue b/frontend/web/src/App.vue index 5d441c5..5668470 100644 --- a/frontend/web/src/App.vue +++ b/frontend/web/src/App.vue @@ -1,37 +1,21 @@ - - diff --git a/frontend/web/src/components/IconInput.vue b/frontend/web/src/components/IconInput.vue index 455e61c..eaffb74 100644 --- a/frontend/web/src/components/IconInput.vue +++ b/frontend/web/src/components/IconInput.vue @@ -109,55 +109,57 @@ label { text-decoration: none; } -/* 输入框样式:现代填充风格 */ +/* 输入框样式:现代线框风格 */ .field-wrap { position: relative; display: flex; align-items: center; } + .field-wrap .icon { - /* .icon 是 占位符,定位父容器 */ position: absolute; left: 12px; width: 18px; height: 18px; - color: #94a3b8; /* 默认图标颜色 */ + color: #94a3b8; transition: color 0.2s; - /* Feather icons 渲染后,SVG会继承这个颜色 */ + pointer-events: none; /* Icon shouldn't block input click */ + z-index: 10; } -/* 确保渲染后的 SVG 元素颜色能够正确继承 */ .field-wrap .icon > svg { - /* 确保 SVG 自身不被其他默认样式影响 */ display: block; width: 100%; height: 100%; - stroke: currentColor; /* 使用父元素的颜色 */ + stroke: currentColor; transition: stroke 0.2s; } .field-wrap input { padding: 12px 12px 12px 40px; - background: #f1f5f9; /* 浅灰底色 */ - border: 2px solid transparent; + background: #fff; + border: 1px solid #e2e8f0; border-radius: 8px; font-size: 14px; color: #1e293b; outline: none; transition: all 0.2s ease; + width: 100%; } /* 聚焦交互 */ .field-wrap input:focus { - background: #fff; - border-color: #4f46e5; /* 品牌色 */ - box-shadow: 0 4px 12px rgba(79, 70, 229, 0.1); + border-color: #6366f1; /* 品牌色 */ + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); /* 柔和光晕 */ +} + +/* 错误状态 (Optional, if you pass a prop later) */ +.field-wrap input.error { + border-color: #ef4444; } /* 🚨 关键:聚焦时图标颜色变化的选择器 */ -/* 当 input 聚焦时,选择它后面的所有兄弟元素 (~),其中类名为 .icon 的元素,改变它的颜色 */ -/* 由于我们在 .icon 元素上设置了 color,SVG 会自动继承 */ .field-wrap input:focus ~ .icon { - color: #4f46e5; /* 聚焦时的颜色 */ + color: #6366f1; /* 聚焦时的颜色 */ } \ No newline at end of file diff --git a/frontend/web/src/components/MyButton.vue b/frontend/web/src/components/MyButton.vue index e6ebd91..f7ea866 100644 --- a/frontend/web/src/components/MyButton.vue +++ b/frontend/web/src/components/MyButton.vue @@ -2,21 +2,22 @@
@@ -37,7 +38,8 @@ const props = defineProps({ 'variant': { type: String, default: 'primary', - validator: (value) => ['primary', 'secondary', 'danger', 'text'].includes(value) + + validator: (value) => ['primary', 'secondary', 'danger', 'text', 'pill', 'pill-green'].includes(value) }, // 明确接收 disabled 属性,以便在脚本中控制和类型检查 'disabled': { @@ -57,153 +59,129 @@ const attrs = useAttrs() \ No newline at end of file diff --git a/frontend/web/src/services/api.js b/frontend/web/src/services/api.js index dc5fd77..70d3c76 100644 --- a/frontend/web/src/services/api.js +++ b/frontend/web/src/services/api.js @@ -38,6 +38,11 @@ api.interceptors.response.use( message.error('未登录,请登录后操作。'); router.push('/auth/login') break; + case 400: + if (err.response.data && err.response.data.code == 1003) { + message.error(err.response.data.message); + break; + } default: message.error('请求错误,请检查网络。'); break; diff --git a/frontend/web/src/views/auth/Login.vue b/frontend/web/src/views/auth/Login.vue index 97f7b57..7e20aba 100644 --- a/frontend/web/src/views/auth/Login.vue +++ b/frontend/web/src/views/auth/Login.vue @@ -1,42 +1,44 @@