main #69
@@ -4,13 +4,14 @@ using Moq;
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using IM_API.Services;
|
using IM_API.Services;
|
||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
using IM_API.Dtos;
|
|
||||||
using IM_API.Exceptions;
|
using IM_API.Exceptions;
|
||||||
using IM_API.Tools;
|
using IM_API.Tools;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using IM_API.Dtos.Auth;
|
||||||
|
using IM_API.Dtos.User;
|
||||||
|
|
||||||
public class AuthServiceTests
|
public class AuthServiceTests
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,25 +1,30 @@
|
|||||||
using Xunit;
|
using AutoMapper;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using IM_API.Domain.Events;
|
||||||
using Moq;
|
using IM_API.Dtos.Friend;
|
||||||
using AutoMapper;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System;
|
|
||||||
using IM_API.Services;
|
|
||||||
using IM_API.Models;
|
|
||||||
using IM_API.Dtos;
|
|
||||||
using IM_API.Exceptions;
|
using IM_API.Exceptions;
|
||||||
using IM_API.Tools;
|
using IM_API.Models;
|
||||||
|
using IM_API.Services;
|
||||||
|
using MassTransit; // 必须引入
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
public class FriendServiceTests
|
public class FriendServiceTests
|
||||||
{
|
{
|
||||||
|
private readonly Mock<IPublishEndpoint> _mockEndpoint = new();
|
||||||
|
private readonly Mock<ILogger<FriendService>> _mockLogger = new();
|
||||||
|
|
||||||
|
#region 辅助构造方法
|
||||||
private ImContext CreateDbContext()
|
private ImContext CreateDbContext()
|
||||||
{
|
{
|
||||||
var options = new DbContextOptionsBuilder<ImContext>()
|
var options = new DbContextOptionsBuilder<ImContext>()
|
||||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
.UseInMemoryDatabase(Guid.NewGuid().ToString()) // 确保每个测试数据库隔离
|
||||||
.Options;
|
.Options;
|
||||||
|
|
||||||
return new ImContext(options);
|
return new ImContext(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,132 +32,115 @@ public class FriendServiceTests
|
|||||||
{
|
{
|
||||||
var config = new MapperConfiguration(cfg =>
|
var config = new MapperConfiguration(cfg =>
|
||||||
{
|
{
|
||||||
|
// 补充你业务中实际需要的映射规则
|
||||||
cfg.CreateMap<Friend, FriendInfoDto>();
|
cfg.CreateMap<Friend, FriendInfoDto>();
|
||||||
cfg.CreateMap<FriendRequestDto, FriendRequest>();
|
cfg.CreateMap<FriendRequestDto, FriendRequest>();
|
||||||
cfg.CreateMap<HandleFriendRequestDto, FriendRequest>();
|
|
||||||
cfg.CreateMap<FriendRequest, Friend>()
|
cfg.CreateMap<FriendRequest, Friend>()
|
||||||
.ForMember(dest => dest.UserId, opt => opt.MapFrom(src => src.ResponseUser))
|
.ForMember(d => d.UserId, o => o.MapFrom(s => s.ResponseUser))
|
||||||
.ForMember(dest => dest.FriendId, opt => opt.MapFrom(src => src.RequestUser))
|
.ForMember(d => d.FriendId, o => o.MapFrom(s => s.RequestUser));
|
||||||
.ForMember(dest => dest.RemarkName, opt => opt.MapFrom(src => "AutoAdded"));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return config.CreateMapper();
|
return config.CreateMapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
private FriendService CreateService(ImContext context)
|
private FriendService CreateService(ImContext context)
|
||||||
{
|
{
|
||||||
var logger = new Mock<ILogger<FriendService>>();
|
// 注入 Mock 对象和真实的 Mapper/Context
|
||||||
return new FriendService(context, logger.Object, CreateMapper());
|
return new FriendService(context, _mockLogger.Object, CreateMapper(), _mockEndpoint.Object);
|
||||||
}
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
// --------------------------- 测试 BlockFriendAsync ---------------------------
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task BlockFriendAsync_Should_Set_Status_To_Blocked()
|
public async Task SendFriendRequestAsync_Success_ShouldSaveAndPublish()
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
var context = CreateDbContext();
|
var context = CreateDbContext();
|
||||||
context.Friends.Add(new Friend
|
context.Users.AddRange(
|
||||||
{
|
new User { Id = 1, Username = "Sender", Password = "..." },
|
||||||
Id = 1,
|
new User { Id = 2, Username = "Receiver", Password = "..." }
|
||||||
UserId = 10,
|
);
|
||||||
FriendId = 20,
|
|
||||||
StatusEnum = FriendStatus.Added,
|
|
||||||
RemarkName = "test remark"
|
|
||||||
});
|
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
var service = CreateService(context);
|
var service = CreateService(context);
|
||||||
|
var dto = new FriendRequestDto { ToUserId = 2, Description = "Hello" };
|
||||||
|
|
||||||
var result = await service.BlockeFriendAsync(1);
|
// Act
|
||||||
|
var result = await service.SendFriendRequestAsync(dto);
|
||||||
Assert.True(result);
|
|
||||||
Assert.Equal(FriendStatus.Blocked, context.Friends.Find(1).StatusEnum);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task BlockFriendAsync_Should_Throw_When_NotFound()
|
|
||||||
{
|
|
||||||
var service = CreateService(CreateDbContext());
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<BaseException>(() => service.BlockeFriendAsync(99));
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------- 删除好友关系 ---------------------------
|
|
||||||
[Fact]
|
|
||||||
public async Task DeleteFriendAsync_Should_Remove_Friend()
|
|
||||||
{
|
|
||||||
var context = CreateDbContext();
|
|
||||||
context.Friends.Add(new Friend
|
|
||||||
{
|
|
||||||
Id = 2,
|
|
||||||
UserId = 1,
|
|
||||||
FriendId = 3,
|
|
||||||
RemarkName = "remark",
|
|
||||||
StatusEnum = FriendStatus.Added
|
|
||||||
});
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
|
|
||||||
var service = CreateService(context);
|
|
||||||
|
|
||||||
var result = await service.DeleteFriendAsync(2);
|
|
||||||
|
|
||||||
Assert.True(result);
|
|
||||||
Assert.Empty(context.Friends);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------- 获取好友列表 ---------------------------
|
|
||||||
[Fact]
|
|
||||||
public async Task GetFriendListAsync_Should_Return_Only_Added_Friends()
|
|
||||||
{
|
|
||||||
var context = CreateDbContext();
|
|
||||||
context.Friends.AddRange(new List<Friend>
|
|
||||||
{
|
|
||||||
new Friend{ UserId = 1, FriendId = 2, RemarkName ="a1", StatusEnum = FriendStatus.Added },
|
|
||||||
new Friend{ UserId = 1, FriendId = 3, RemarkName ="a2", StatusEnum = FriendStatus.Blocked }
|
|
||||||
});
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
|
|
||||||
var service = CreateService(context);
|
|
||||||
|
|
||||||
var result = await service.GetFriendListAsync(1, 1, 10, false);
|
|
||||||
|
|
||||||
Assert.Single(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------- 发起好友请求 ---------------------------
|
|
||||||
[Fact]
|
|
||||||
public async Task SendFriendRequestAsync_Should_Succeed()
|
|
||||||
{
|
|
||||||
var context = CreateDbContext();
|
|
||||||
context.Users.Add(new User { Id = 10, Username = "A", Password = "123" });
|
|
||||||
context.Users.Add(new User { Id = 20, Username = "B", Password = "123" });
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
|
|
||||||
var service = CreateService(context);
|
|
||||||
|
|
||||||
var result = await service.SendFriendRequestAsync(new FriendRequestDto
|
|
||||||
{
|
|
||||||
FromUserId = 10,
|
|
||||||
ToUserId = 20
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Assert
|
||||||
Assert.True(result);
|
Assert.True(result);
|
||||||
Assert.Single(context.FriendRequests);
|
Assert.Single(context.FriendRequests);
|
||||||
Assert.Single(context.Friends);
|
|
||||||
|
// 验证事件是否发布到了 MQ
|
||||||
|
_mockEndpoint.Verify(x => x.Publish(
|
||||||
|
It.Is<RequestFriendEvent>(e => e.FromUserId == 1 && e.ToUserId == 2),
|
||||||
|
It.IsAny<CancellationToken>()),
|
||||||
|
Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SendFriendRequestAsync_Should_Throw_When_User_NotFound()
|
public async Task SendFriendRequestAsync_UserNotFound_ShouldThrow()
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
var context = CreateDbContext();
|
var context = CreateDbContext();
|
||||||
context.Users.Add(new User { Id = 10, Username = "A", Password = "123" });
|
var service = CreateService(context);
|
||||||
await context.SaveChangesAsync();
|
var dto = new FriendRequestDto { ToUserId = 99 }; // 不存在的用户
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await Assert.ThrowsAsync<BaseException>(() => service.SendFriendRequestAsync(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendFriendRequestAsync_AlreadyExists_ShouldThrow()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = CreateDbContext();
|
||||||
|
context.Users.Add(new User { Id = 2 });
|
||||||
|
context.FriendRequests.Add(new FriendRequest
|
||||||
|
{
|
||||||
|
RequestUser = 1,
|
||||||
|
ResponseUser = 2,
|
||||||
|
State = (sbyte)FriendRequestState.Pending
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
var service = CreateService(context);
|
var service = CreateService(context);
|
||||||
|
|
||||||
await Assert.ThrowsAsync<BaseException>(() => service.SendFriendRequestAsync(new FriendRequestDto
|
// Act & Assert
|
||||||
{
|
await Assert.ThrowsAsync<BaseException>(() => service.SendFriendRequestAsync(new FriendRequestDto { ToUserId = 2 }));
|
||||||
FromUserId = 10,
|
|
||||||
ToUserId = 99
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BlockFriendAsync_ValidId_ShouldUpdateStatus()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = CreateDbContext();
|
||||||
|
var friend = new Friend { Id = 50, UserId = 1, FriendId = 2, StatusEnum = FriendStatus.Added };
|
||||||
|
context.Friends.Add(friend);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
var service = CreateService(context);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await service.BlockeFriendAsync(50);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var updated = await context.Friends.FindAsync(50);
|
||||||
|
Assert.Equal(FriendStatus.Blocked, updated.StatusEnum);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetFriendListAsync_ShouldFilterByStatus()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = CreateDbContext();
|
||||||
|
context.Friends.AddRange(
|
||||||
|
new Friend { UserId = 1, FriendId = 2, StatusEnum = FriendStatus.Added },
|
||||||
|
new Friend { UserId = 1, FriendId = 3, StatusEnum = FriendStatus.Blocked }
|
||||||
|
);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
var service = CreateService(context);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await service.GetFriendListAsync(1, 1, 10, false);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(result); // 只应该拿到 Added 状态的
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,12 +4,12 @@ using Moq;
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using IM_API.Services;
|
using IM_API.Services;
|
||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
using IM_API.Dtos;
|
|
||||||
using IM_API.Exceptions;
|
using IM_API.Exceptions;
|
||||||
using IM_API.Tools;
|
using IM_API.Tools;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using IM_API.Dtos.User;
|
||||||
|
|
||||||
public class UserServiceTests
|
public class UserServiceTests
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,6 +55,42 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"coverlet.collector/6.0.0": {},
|
"coverlet.collector/6.0.0": {},
|
||||||
|
"MassTransit/8.5.5": {
|
||||||
|
"dependencies": {
|
||||||
|
"MassTransit.Abstractions": "8.5.5",
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||||
|
"Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.0",
|
||||||
|
"Microsoft.Extensions.Hosting.Abstractions": "8.0.1",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
||||||
|
"Microsoft.Extensions.Options": "8.0.2"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/MassTransit.dll": {
|
||||||
|
"assemblyVersion": "8.5.5.0",
|
||||||
|
"fileVersion": "8.5.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MassTransit.Abstractions/8.5.5": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/MassTransit.Abstractions.dll": {
|
||||||
|
"assemblyVersion": "8.5.5.0",
|
||||||
|
"fileVersion": "8.5.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MassTransit.RabbitMQ/8.5.5": {
|
||||||
|
"dependencies": {
|
||||||
|
"MassTransit": "8.5.5",
|
||||||
|
"RabbitMQ.Client": "7.1.2"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/MassTransit.RabbitMqTransport.dll": {
|
||||||
|
"assemblyVersion": "8.5.5.0",
|
||||||
|
"fileVersion": "8.5.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Microsoft.AspNetCore.Authentication.Abstractions/2.3.0": {
|
"Microsoft.AspNetCore.Authentication.Abstractions/2.3.0": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.AspNetCore.Http.Abstractions": "2.3.0",
|
"Microsoft.AspNetCore.Http.Abstractions": "2.3.0",
|
||||||
@@ -326,6 +362,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Microsoft.Extensions.Diagnostics.HealthChecks/8.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "8.0.0",
|
||||||
|
"Microsoft.Extensions.Hosting.Abstractions": "8.0.1",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
||||||
|
"Microsoft.Extensions.Options": "8.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/8.0.0": {},
|
||||||
"Microsoft.Extensions.FileProviders.Abstractions/8.0.0": {
|
"Microsoft.Extensions.FileProviders.Abstractions/8.0.0": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||||
@@ -843,6 +888,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"RabbitMQ.Client/7.1.2": {
|
||||||
|
"dependencies": {
|
||||||
|
"System.IO.Pipelines": "8.0.0",
|
||||||
|
"System.Threading.RateLimiting": "8.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/RabbitMQ.Client.dll": {
|
||||||
|
"assemblyVersion": "7.0.0.0",
|
||||||
|
"fileVersion": "7.1.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {},
|
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {},
|
||||||
"runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {},
|
"runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {},
|
||||||
"runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {},
|
"runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {},
|
||||||
@@ -1455,6 +1512,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"System.Threading.Channels/8.0.0": {},
|
"System.Threading.Channels/8.0.0": {},
|
||||||
|
"System.Threading.RateLimiting/8.0.0": {},
|
||||||
"System.Threading.Tasks/4.3.0": {
|
"System.Threading.Tasks/4.3.0": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||||
@@ -1573,6 +1631,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"AutoMapper": "12.0.1",
|
"AutoMapper": "12.0.1",
|
||||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "12.0.0",
|
"AutoMapper.Extensions.Microsoft.DependencyInjection": "12.0.0",
|
||||||
|
"MassTransit.RabbitMQ": "8.5.5",
|
||||||
"Microsoft.AspNetCore.Authentication.JwtBearer": "8.0.21",
|
"Microsoft.AspNetCore.Authentication.JwtBearer": "8.0.21",
|
||||||
"Microsoft.AspNetCore.SignalR": "1.2.0",
|
"Microsoft.AspNetCore.SignalR": "1.2.0",
|
||||||
"Microsoft.VisualStudio.Azure.Containers.Tools.Targets": "1.22.1",
|
"Microsoft.VisualStudio.Azure.Containers.Tools.Targets": "1.22.1",
|
||||||
@@ -1625,6 +1684,27 @@
|
|||||||
"path": "coverlet.collector/6.0.0",
|
"path": "coverlet.collector/6.0.0",
|
||||||
"hashPath": "coverlet.collector.6.0.0.nupkg.sha512"
|
"hashPath": "coverlet.collector.6.0.0.nupkg.sha512"
|
||||||
},
|
},
|
||||||
|
"MassTransit/8.5.5": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-bSg8k5q+rP1s+dIGXLLbctqDGdIkfDjdxwNWtCUH7xNCN9ZuM7mqSPQPIFgaYIi34e81m4FqAqo4CAHuWPkhRA==",
|
||||||
|
"path": "masstransit/8.5.5",
|
||||||
|
"hashPath": "masstransit.8.5.5.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"MassTransit.Abstractions/8.5.5": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-0mn2Ay17dD6z5tgSLjbVRlldSbL9iowzFEfVgVfBXVG5ttz9dSWeR4TrdD6pqH93GWXp4CvSmF8i1HqxLX7DZw==",
|
||||||
|
"path": "masstransit.abstractions/8.5.5",
|
||||||
|
"hashPath": "masstransit.abstractions.8.5.5.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"MassTransit.RabbitMQ/8.5.5": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-UxWn4o90YVMF9PBkJeoskOFPneh6YtnI1fLJHtvZiSAG0eoiRrWPGa+6FQCvjkQ/ljCKfjzok2eGZc/vmNZ01A==",
|
||||||
|
"path": "masstransit.rabbitmq/8.5.5",
|
||||||
|
"hashPath": "masstransit.rabbitmq.8.5.5.nupkg.sha512"
|
||||||
|
},
|
||||||
"Microsoft.AspNetCore.Authentication.Abstractions/2.3.0": {
|
"Microsoft.AspNetCore.Authentication.Abstractions/2.3.0": {
|
||||||
"type": "package",
|
"type": "package",
|
||||||
"serviceable": true,
|
"serviceable": true,
|
||||||
@@ -1870,6 +1950,20 @@
|
|||||||
"path": "microsoft.extensions.diagnostics.abstractions/8.0.1",
|
"path": "microsoft.extensions.diagnostics.abstractions/8.0.1",
|
||||||
"hashPath": "microsoft.extensions.diagnostics.abstractions.8.0.1.nupkg.sha512"
|
"hashPath": "microsoft.extensions.diagnostics.abstractions.8.0.1.nupkg.sha512"
|
||||||
},
|
},
|
||||||
|
"Microsoft.Extensions.Diagnostics.HealthChecks/8.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-P9SoBuVZhJPpALZmSq72aQEb9ryP67EdquaCZGXGrrcASTNHYdrUhnpgSwIipgM5oVC+dKpRXg5zxobmF9xr5g==",
|
||||||
|
"path": "microsoft.extensions.diagnostics.healthchecks/8.0.0",
|
||||||
|
"hashPath": "microsoft.extensions.diagnostics.healthchecks.8.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/8.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-AT2qqos3IgI09ok36Qag9T8bb6kHJ3uT9Q5ki6CySybFsK6/9JbvQAgAHf1pVEjST0/N4JaFaCbm40R5edffwg==",
|
||||||
|
"path": "microsoft.extensions.diagnostics.healthchecks.abstractions/8.0.0",
|
||||||
|
"hashPath": "microsoft.extensions.diagnostics.healthchecks.abstractions.8.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
"Microsoft.Extensions.FileProviders.Abstractions/8.0.0": {
|
"Microsoft.Extensions.FileProviders.Abstractions/8.0.0": {
|
||||||
"type": "package",
|
"type": "package",
|
||||||
"serviceable": true,
|
"serviceable": true,
|
||||||
@@ -2073,6 +2167,13 @@
|
|||||||
"path": "pomelo.entityframeworkcore.mysql/8.0.3",
|
"path": "pomelo.entityframeworkcore.mysql/8.0.3",
|
||||||
"hashPath": "pomelo.entityframeworkcore.mysql.8.0.3.nupkg.sha512"
|
"hashPath": "pomelo.entityframeworkcore.mysql.8.0.3.nupkg.sha512"
|
||||||
},
|
},
|
||||||
|
"RabbitMQ.Client/7.1.2": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-y3c6ulgULScWthHw5PLM1ShHRLhxg0vCtzX/hh61gRgNecL3ZC3WoBW2HYHoXOVRqTl99Br9E7CZEytGZEsCyQ==",
|
||||||
|
"path": "rabbitmq.client/7.1.2",
|
||||||
|
"hashPath": "rabbitmq.client.7.1.2.nupkg.sha512"
|
||||||
|
},
|
||||||
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {
|
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {
|
||||||
"type": "package",
|
"type": "package",
|
||||||
"serviceable": true,
|
"serviceable": true,
|
||||||
@@ -2605,6 +2706,13 @@
|
|||||||
"path": "system.threading.channels/8.0.0",
|
"path": "system.threading.channels/8.0.0",
|
||||||
"hashPath": "system.threading.channels.8.0.0.nupkg.sha512"
|
"hashPath": "system.threading.channels.8.0.0.nupkg.sha512"
|
||||||
},
|
},
|
||||||
|
"System.Threading.RateLimiting/8.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==",
|
||||||
|
"path": "system.threading.ratelimiting/8.0.0",
|
||||||
|
"hashPath": "system.threading.ratelimiting.8.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
"System.Threading.Tasks/4.3.0": {
|
"System.Threading.Tasks/4.3.0": {
|
||||||
"type": "package",
|
"type": "package",
|
||||||
"serviceable": true,
|
"serviceable": true,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"AutoMapper": "12.0.1",
|
"AutoMapper": "12.0.1",
|
||||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "12.0.0",
|
"AutoMapper.Extensions.Microsoft.DependencyInjection": "12.0.0",
|
||||||
|
"MassTransit.RabbitMQ": "8.5.5",
|
||||||
"Microsoft.AspNetCore.Authentication.JwtBearer": "8.0.21",
|
"Microsoft.AspNetCore.Authentication.JwtBearer": "8.0.21",
|
||||||
"Microsoft.AspNetCore.SignalR": "1.2.0",
|
"Microsoft.AspNetCore.SignalR": "1.2.0",
|
||||||
"Microsoft.EntityFrameworkCore.Design": "8.0.21",
|
"Microsoft.EntityFrameworkCore.Design": "8.0.21",
|
||||||
@@ -56,6 +57,42 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"MassTransit/8.5.5": {
|
||||||
|
"dependencies": {
|
||||||
|
"MassTransit.Abstractions": "8.5.5",
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||||
|
"Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.0",
|
||||||
|
"Microsoft.Extensions.Hosting.Abstractions": "8.0.1",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
||||||
|
"Microsoft.Extensions.Options": "8.0.2"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/MassTransit.dll": {
|
||||||
|
"assemblyVersion": "8.5.5.0",
|
||||||
|
"fileVersion": "8.5.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MassTransit.Abstractions/8.5.5": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/MassTransit.Abstractions.dll": {
|
||||||
|
"assemblyVersion": "8.5.5.0",
|
||||||
|
"fileVersion": "8.5.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MassTransit.RabbitMQ/8.5.5": {
|
||||||
|
"dependencies": {
|
||||||
|
"MassTransit": "8.5.5",
|
||||||
|
"RabbitMQ.Client": "7.1.2"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/MassTransit.RabbitMqTransport.dll": {
|
||||||
|
"assemblyVersion": "8.5.5.0",
|
||||||
|
"fileVersion": "8.5.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Microsoft.AspNetCore.Authentication.Abstractions/2.3.0": {
|
"Microsoft.AspNetCore.Authentication.Abstractions/2.3.0": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.AspNetCore.Http.Abstractions": "2.3.0",
|
"Microsoft.AspNetCore.Http.Abstractions": "2.3.0",
|
||||||
@@ -565,6 +602,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Microsoft.Extensions.Diagnostics.HealthChecks/8.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "8.0.0",
|
||||||
|
"Microsoft.Extensions.Hosting.Abstractions": "8.0.1",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
||||||
|
"Microsoft.Extensions.Options": "8.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/8.0.0": {},
|
||||||
"Microsoft.Extensions.FileProviders.Abstractions/8.0.0": {
|
"Microsoft.Extensions.FileProviders.Abstractions/8.0.0": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||||
@@ -764,6 +810,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"RabbitMQ.Client/7.1.2": {
|
||||||
|
"dependencies": {
|
||||||
|
"System.IO.Pipelines": "8.0.0",
|
||||||
|
"System.Threading.RateLimiting": "8.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/RabbitMQ.Client.dll": {
|
||||||
|
"assemblyVersion": "7.0.0.0",
|
||||||
|
"fileVersion": "7.1.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"StackExchange.Redis/2.9.32": {
|
"StackExchange.Redis/2.9.32": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
||||||
@@ -922,7 +980,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"System.Text.Encodings.Web/8.0.0": {},
|
"System.Text.Encodings.Web/8.0.0": {},
|
||||||
"System.Threading.Channels/8.0.0": {}
|
"System.Threading.Channels/8.0.0": {},
|
||||||
|
"System.Threading.RateLimiting/8.0.0": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"libraries": {
|
"libraries": {
|
||||||
@@ -952,6 +1011,27 @@
|
|||||||
"path": "humanizer.core/2.14.1",
|
"path": "humanizer.core/2.14.1",
|
||||||
"hashPath": "humanizer.core.2.14.1.nupkg.sha512"
|
"hashPath": "humanizer.core.2.14.1.nupkg.sha512"
|
||||||
},
|
},
|
||||||
|
"MassTransit/8.5.5": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-bSg8k5q+rP1s+dIGXLLbctqDGdIkfDjdxwNWtCUH7xNCN9ZuM7mqSPQPIFgaYIi34e81m4FqAqo4CAHuWPkhRA==",
|
||||||
|
"path": "masstransit/8.5.5",
|
||||||
|
"hashPath": "masstransit.8.5.5.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"MassTransit.Abstractions/8.5.5": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-0mn2Ay17dD6z5tgSLjbVRlldSbL9iowzFEfVgVfBXVG5ttz9dSWeR4TrdD6pqH93GWXp4CvSmF8i1HqxLX7DZw==",
|
||||||
|
"path": "masstransit.abstractions/8.5.5",
|
||||||
|
"hashPath": "masstransit.abstractions.8.5.5.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"MassTransit.RabbitMQ/8.5.5": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-UxWn4o90YVMF9PBkJeoskOFPneh6YtnI1fLJHtvZiSAG0eoiRrWPGa+6FQCvjkQ/ljCKfjzok2eGZc/vmNZ01A==",
|
||||||
|
"path": "masstransit.rabbitmq/8.5.5",
|
||||||
|
"hashPath": "masstransit.rabbitmq.8.5.5.nupkg.sha512"
|
||||||
|
},
|
||||||
"Microsoft.AspNetCore.Authentication.Abstractions/2.3.0": {
|
"Microsoft.AspNetCore.Authentication.Abstractions/2.3.0": {
|
||||||
"type": "package",
|
"type": "package",
|
||||||
"serviceable": true,
|
"serviceable": true,
|
||||||
@@ -1246,6 +1326,20 @@
|
|||||||
"path": "microsoft.extensions.diagnostics.abstractions/8.0.1",
|
"path": "microsoft.extensions.diagnostics.abstractions/8.0.1",
|
||||||
"hashPath": "microsoft.extensions.diagnostics.abstractions.8.0.1.nupkg.sha512"
|
"hashPath": "microsoft.extensions.diagnostics.abstractions.8.0.1.nupkg.sha512"
|
||||||
},
|
},
|
||||||
|
"Microsoft.Extensions.Diagnostics.HealthChecks/8.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-P9SoBuVZhJPpALZmSq72aQEb9ryP67EdquaCZGXGrrcASTNHYdrUhnpgSwIipgM5oVC+dKpRXg5zxobmF9xr5g==",
|
||||||
|
"path": "microsoft.extensions.diagnostics.healthchecks/8.0.0",
|
||||||
|
"hashPath": "microsoft.extensions.diagnostics.healthchecks.8.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/8.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-AT2qqos3IgI09ok36Qag9T8bb6kHJ3uT9Q5ki6CySybFsK6/9JbvQAgAHf1pVEjST0/N4JaFaCbm40R5edffwg==",
|
||||||
|
"path": "microsoft.extensions.diagnostics.healthchecks.abstractions/8.0.0",
|
||||||
|
"hashPath": "microsoft.extensions.diagnostics.healthchecks.abstractions.8.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
"Microsoft.Extensions.FileProviders.Abstractions/8.0.0": {
|
"Microsoft.Extensions.FileProviders.Abstractions/8.0.0": {
|
||||||
"type": "package",
|
"type": "package",
|
||||||
"serviceable": true,
|
"serviceable": true,
|
||||||
@@ -1393,6 +1487,13 @@
|
|||||||
"path": "pomelo.entityframeworkcore.mysql/8.0.3",
|
"path": "pomelo.entityframeworkcore.mysql/8.0.3",
|
||||||
"hashPath": "pomelo.entityframeworkcore.mysql.8.0.3.nupkg.sha512"
|
"hashPath": "pomelo.entityframeworkcore.mysql.8.0.3.nupkg.sha512"
|
||||||
},
|
},
|
||||||
|
"RabbitMQ.Client/7.1.2": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-y3c6ulgULScWthHw5PLM1ShHRLhxg0vCtzX/hh61gRgNecL3ZC3WoBW2HYHoXOVRqTl99Br9E7CZEytGZEsCyQ==",
|
||||||
|
"path": "rabbitmq.client/7.1.2",
|
||||||
|
"hashPath": "rabbitmq.client.7.1.2.nupkg.sha512"
|
||||||
|
},
|
||||||
"StackExchange.Redis/2.9.32": {
|
"StackExchange.Redis/2.9.32": {
|
||||||
"type": "package",
|
"type": "package",
|
||||||
"serviceable": true,
|
"serviceable": true,
|
||||||
@@ -1553,6 +1654,13 @@
|
|||||||
"sha512": "sha512-CMaFr7v+57RW7uZfZkPExsPB6ljwzhjACWW1gfU35Y56rk72B/Wu+sTqxVmGSk4SFUlPc3cjeKND0zktziyjBA==",
|
"sha512": "sha512-CMaFr7v+57RW7uZfZkPExsPB6ljwzhjACWW1gfU35Y56rk72B/Wu+sTqxVmGSk4SFUlPc3cjeKND0zktziyjBA==",
|
||||||
"path": "system.threading.channels/8.0.0",
|
"path": "system.threading.channels/8.0.0",
|
||||||
"hashPath": "system.threading.channels.8.0.0.nupkg.sha512"
|
"hashPath": "system.threading.channels.8.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"System.Threading.RateLimiting/8.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==",
|
||||||
|
"path": "system.threading.ratelimiting/8.0.0",
|
||||||
|
"hashPath": "system.threading.ratelimiting.8.0.0.nupkg.sha512"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -16,5 +16,11 @@
|
|||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Server=frp-era.com;Port=26582;Database=IM;User=product;Password=12345678;",
|
"DefaultConnection": "Server=frp-era.com;Port=26582;Database=IM;User=product;Password=12345678;",
|
||||||
"Redis": "192.168.5.100:6379"
|
"Redis": "192.168.5.100:6379"
|
||||||
|
},
|
||||||
|
"RabbitMQOptions": {
|
||||||
|
"Host": "192.168.5.100",
|
||||||
|
"Port": 5672,
|
||||||
|
"Username": "test",
|
||||||
|
"Password": "123456"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("IMTest")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("IMTest")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+7ebf23ddd8a0d5536167313c4cf37af1552a5057")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+22038345c32db146ffb78173c5410f954daa64e0")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("IMTest")]
|
[assembly: System.Reflection.AssemblyProductAttribute("IMTest")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("IMTest")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("IMTest")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
ca0390a4d5773daae2e747d7512c190701a7a942186c769e42327a4864733f0b
|
ee1fc45f192938903a153f1c2e3b53f60a2184cb806b87d9b57b487095b98264
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
is_global = true
|
is_global = true
|
||||||
build_property.TargetFramework = net8.0
|
build_property.TargetFramework = net8.0
|
||||||
build_property.TargetFrameworkIdentifier = .NETCoreApp
|
|
||||||
build_property.TargetFrameworkVersion = v8.0
|
|
||||||
build_property.TargetPlatformMinVersion =
|
build_property.TargetPlatformMinVersion =
|
||||||
build_property.UsingMicrosoftNETSdkWeb =
|
build_property.UsingMicrosoftNETSdkWeb =
|
||||||
build_property.ProjectTypeGuids =
|
build_property.ProjectTypeGuids =
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// <auto-generated/>
|
// <auto-generated/>
|
||||||
global using System;
|
global using global::System;
|
||||||
global using System.Collections.Generic;
|
global using global::System.Collections.Generic;
|
||||||
global using System.IO;
|
global using global::System.IO;
|
||||||
global using System.Linq;
|
global using global::System.Linq;
|
||||||
global using System.Net.Http;
|
global using global::System.Net.Http;
|
||||||
global using System.Threading;
|
global using global::System.Threading;
|
||||||
global using System.Threading.Tasks;
|
global using global::System.Threading.Tasks;
|
||||||
global using Xunit;
|
global using global::Xunit;
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
|
|||||||
b97252705aa63c43e1310042fe4fd7115206f72ef0f55d39109c2b849fe2a37e
|
97ad978fabe5fb8f7f258c9878659ee9a5f2492332ad5efd70b1d456ebc42a59
|
||||||
|
|||||||
@@ -140,3 +140,7 @@ C:\Users\nanxun\Documents\IM\backend\IMTest\obj\Debug\net8.0\refint\IMTest.dll
|
|||||||
C:\Users\nanxun\Documents\IM\backend\IMTest\obj\Debug\net8.0\IMTest.pdb
|
C:\Users\nanxun\Documents\IM\backend\IMTest\obj\Debug\net8.0\IMTest.pdb
|
||||||
C:\Users\nanxun\Documents\IM\backend\IMTest\obj\Debug\net8.0\IMTest.genruntimeconfig.cache
|
C:\Users\nanxun\Documents\IM\backend\IMTest\obj\Debug\net8.0\IMTest.genruntimeconfig.cache
|
||||||
C:\Users\nanxun\Documents\IM\backend\IMTest\obj\Debug\net8.0\ref\IMTest.dll
|
C:\Users\nanxun\Documents\IM\backend\IMTest\obj\Debug\net8.0\ref\IMTest.dll
|
||||||
|
C:\Users\nanxun\Documents\IM\backend\IMTest\bin\Debug\net8.0\MassTransit.dll
|
||||||
|
C:\Users\nanxun\Documents\IM\backend\IMTest\bin\Debug\net8.0\MassTransit.Abstractions.dll
|
||||||
|
C:\Users\nanxun\Documents\IM\backend\IMTest\bin\Debug\net8.0\MassTransit.RabbitMqTransport.dll
|
||||||
|
C:\Users\nanxun\Documents\IM\backend\IMTest\bin\Debug\net8.0\RabbitMQ.Client.dll
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -49,7 +49,7 @@
|
|||||||
"auditLevel": "low",
|
"auditLevel": "low",
|
||||||
"auditMode": "direct"
|
"auditMode": "direct"
|
||||||
},
|
},
|
||||||
"SdkAnalysisLevel": "10.0.100"
|
"SdkAnalysisLevel": "9.0.300"
|
||||||
},
|
},
|
||||||
"frameworks": {
|
"frameworks": {
|
||||||
"net8.0": {
|
"net8.0": {
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
"privateAssets": "all"
|
"privateAssets": "all"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.102/PortableRuntimeIdentifierGraph.json"
|
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
"auditLevel": "low",
|
"auditLevel": "low",
|
||||||
"auditMode": "direct"
|
"auditMode": "direct"
|
||||||
},
|
},
|
||||||
"SdkAnalysisLevel": "10.0.100"
|
"SdkAnalysisLevel": "9.0.300"
|
||||||
},
|
},
|
||||||
"frameworks": {
|
"frameworks": {
|
||||||
"net8.0": {
|
"net8.0": {
|
||||||
@@ -223,7 +223,7 @@
|
|||||||
"privateAssets": "all"
|
"privateAssets": "all"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.102/PortableRuntimeIdentifierGraph.json"
|
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
|
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
|
||||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\nanxun\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
|
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\nanxun\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
|
||||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
|
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.1</NuGetToolVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||||
<SourceRoot Include="C:\Users\nanxun\.nuget\packages\" />
|
<SourceRoot Include="C:\Users\nanxun\.nuget\packages\" />
|
||||||
|
|||||||
@@ -8805,7 +8805,7 @@
|
|||||||
"auditLevel": "low",
|
"auditLevel": "low",
|
||||||
"auditMode": "direct"
|
"auditMode": "direct"
|
||||||
},
|
},
|
||||||
"SdkAnalysisLevel": "10.0.100"
|
"SdkAnalysisLevel": "9.0.300"
|
||||||
},
|
},
|
||||||
"frameworks": {
|
"frameworks": {
|
||||||
"net8.0": {
|
"net8.0": {
|
||||||
@@ -8852,16 +8852,8 @@
|
|||||||
"privateAssets": "all"
|
"privateAssets": "all"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.102/PortableRuntimeIdentifierGraph.json"
|
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"logs": [
|
|
||||||
{
|
|
||||||
"code": "Undefined",
|
|
||||||
"level": "Warning",
|
|
||||||
"warningLevel": 1,
|
|
||||||
"message": "读取缓存文件 C:\\Users\\nanxun\\Documents\\IM\\backend\\IMTest\\obj\\project.nuget.cache 时遇到问题: '<' is an invalid start of a property name. Expected a '\"'. Path: $ | LineNumber: 2 | BytePositionInLine: 0."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dgSpecHash": "j7OjEXb1ZGE=",
|
"dgSpecHash": "tVGTA3KwBHQ=",
|
||||||
"success": true,
|
"success": true,
|
||||||
"projectFilePath": "C:\\Users\\nanxun\\Documents\\IM\\backend\\IMTest\\IMTest.csproj",
|
"projectFilePath": "C:\\Users\\nanxun\\Documents\\IM\\backend\\IMTest\\IMTest.csproj",
|
||||||
"expectedPackageFiles": [
|
"expectedPackageFiles": [
|
||||||
@@ -169,15 +169,5 @@
|
|||||||
"C:\\Users\\nanxun\\.nuget\\packages\\xunit.extensibility.execution\\2.5.3\\xunit.extensibility.execution.2.5.3.nupkg.sha512",
|
"C:\\Users\\nanxun\\.nuget\\packages\\xunit.extensibility.execution\\2.5.3\\xunit.extensibility.execution.2.5.3.nupkg.sha512",
|
||||||
"C:\\Users\\nanxun\\.nuget\\packages\\xunit.runner.visualstudio\\2.5.3\\xunit.runner.visualstudio.2.5.3.nupkg.sha512"
|
"C:\\Users\\nanxun\\.nuget\\packages\\xunit.runner.visualstudio\\2.5.3\\xunit.runner.visualstudio.2.5.3.nupkg.sha512"
|
||||||
],
|
],
|
||||||
"logs": [
|
"logs": []
|
||||||
{
|
|
||||||
"code": "Undefined",
|
|
||||||
"level": "Warning",
|
|
||||||
"message": "读取缓存文件 C:\\Users\\nanxun\\Documents\\IM\\backend\\IMTest\\obj\\project.nuget.cache 时遇到问题: '<' is an invalid start of a property name. Expected a '\"'. Path: $ | LineNumber: 2 | BytePositionInLine: 0.",
|
|
||||||
"projectPath": "C:\\Users\\nanxun\\Documents\\IM\\backend\\IMTest\\IMTest.csproj",
|
|
||||||
"warningLevel": 1,
|
|
||||||
"filePath": "C:\\Users\\nanxun\\Documents\\IM\\backend\\IMTest\\IMTest.csproj",
|
|
||||||
"targetGraphs": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
+12
-2
@@ -1,13 +1,23 @@
|
|||||||
using IM_API.Domain.Events;
|
using IM_API.Domain.Events;
|
||||||
|
using IM_API.Interface.Services;
|
||||||
using MassTransit;
|
using MassTransit;
|
||||||
|
|
||||||
namespace IM_API.Application.EventHandlers.FriendAddHandler
|
namespace IM_API.Application.EventHandlers.FriendAddHandler
|
||||||
{
|
{
|
||||||
public class FriendAddConversationHandler : IConsumer<FriendAddEvent>
|
public class FriendAddConversationHandler : IConsumer<FriendAddEvent>
|
||||||
{
|
{
|
||||||
public Task Consume(ConsumeContext<FriendAddEvent> context)
|
private readonly IConversationService _cService;
|
||||||
|
public FriendAddConversationHandler(IConversationService cService)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
_cService = cService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Consume(ConsumeContext<FriendAddEvent> context)
|
||||||
|
{
|
||||||
|
var @event = context.Message;
|
||||||
|
await _cService.MakeConversationAsync(@event.RequestUserId, @event.ResponseUserId, Models.ChatType.PRIVATE);
|
||||||
|
await _cService.MakeConversationAsync(@event.ResponseUserId, @event.RequestUserId, Models.ChatType.PRIVATE);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using IM_API.Domain.Events;
|
||||||
|
using IM_API.Interface.Services;
|
||||||
|
using MassTransit;
|
||||||
|
|
||||||
|
namespace IM_API.Application.EventHandlers.FriendAddHandler
|
||||||
|
{
|
||||||
|
public class FriendAddDBHandler : IConsumer<FriendAddEvent>
|
||||||
|
{
|
||||||
|
private readonly IFriendSerivce _friendService;
|
||||||
|
private readonly ILogger<FriendAddDBHandler> _logger;
|
||||||
|
public FriendAddDBHandler(IFriendSerivce friendService, ILogger<FriendAddDBHandler> logger)
|
||||||
|
{
|
||||||
|
_friendService = friendService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Consume(ConsumeContext<FriendAddEvent> context)
|
||||||
|
{
|
||||||
|
var @event = context.Message;
|
||||||
|
|
||||||
|
//为请求发起人添加好友记录
|
||||||
|
await _friendService.MakeFriendshipAsync(
|
||||||
|
@event.RequestUserId, @event.ResponseUserId, @event.RequestInfo.RemarkName);
|
||||||
|
//为接收人添加好友记录
|
||||||
|
await _friendService.MakeFriendshipAsync(
|
||||||
|
@event.ResponseUserId, @event.RequestUserId, @event.requestUserRemarkname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-10
@@ -1,28 +1,37 @@
|
|||||||
using IM_API.Domain.Events;
|
using IM_API.Domain.Events;
|
||||||
|
using IM_API.Dtos;
|
||||||
|
using IM_API.Hubs;
|
||||||
using IM_API.Interface.Services;
|
using IM_API.Interface.Services;
|
||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
using MassTransit;
|
using MassTransit;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
namespace IM_API.Application.EventHandlers.FriendAddHandler
|
namespace IM_API.Application.EventHandlers.FriendAddHandler
|
||||||
{
|
{
|
||||||
public class FriendAddSignalRHandler : IConsumer<FriendAddEvent>
|
public class FriendAddSignalRHandler : IConsumer<FriendAddEvent>
|
||||||
{
|
{
|
||||||
private readonly IFriendSerivce _friendService;
|
private readonly IHubContext<ChatHub> _chathub;
|
||||||
public FriendAddSignalRHandler(IFriendSerivce friendSerivce)
|
public FriendAddSignalRHandler(IHubContext<ChatHub> chathub)
|
||||||
{
|
{
|
||||||
_friendService = friendSerivce;
|
_chathub = chathub;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Consume(ConsumeContext<FriendAddEvent> context)
|
public async Task Consume(ConsumeContext<FriendAddEvent> context)
|
||||||
{
|
{
|
||||||
var @event = context.Message;
|
var @event = context.Message;
|
||||||
//为请求发起人添加好友记录
|
var usersList = new List<string> {
|
||||||
await _friendService.MakeFriendshipAsync(
|
@event.RequestUserId.ToString(), @event.ResponseUserId.ToString()
|
||||||
@event.RequestUser.Id, @event.ResponseUser.Id, @event.RequestInfo.RemarkName);
|
};
|
||||||
//为接收人添加好友记录
|
var res = new HubResponse<MessageBaseDto>("Event", new MessageBaseDto()
|
||||||
await _friendService.MakeFriendshipAsync(
|
{
|
||||||
@event.ResponseUser.Id, @event.RequestUser.Id, @event.requestUserRemarkname);
|
ChatType = ChatType.PRIVATE,
|
||||||
|
Content = "您有新的好友关系已添加",
|
||||||
|
MsgId = @event.EventId.ToString(),
|
||||||
|
ReceiverId = @event.ResponseUserId,
|
||||||
|
SenderId = @event.RequestUserId,
|
||||||
|
TimeStamp = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
await _chathub.Clients.Users(usersList).SendAsync("ReceiveMessage", res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
using IM_API.Domain.Events;
|
||||||
|
using MassTransit;
|
||||||
|
|
||||||
|
namespace IM_API.Application.EventHandlers.GroupRequestHandler
|
||||||
|
{
|
||||||
|
public class GroupRequestSignalRHandler : IConsumer<GroupRequestEvent>
|
||||||
|
{
|
||||||
|
Task IConsumer<GroupRequestEvent>.Consume(ConsumeContext<GroupRequestEvent> context)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -28,13 +28,13 @@ namespace IM_API.Application.EventHandlers.MessageCreatedHandler
|
|||||||
MessageBaseDto messageBaseDto = new MessageBaseDto
|
MessageBaseDto messageBaseDto = new MessageBaseDto
|
||||||
{
|
{
|
||||||
MsgId = @event.MessageId.ToString(),
|
MsgId = @event.MessageId.ToString(),
|
||||||
ChatType = @event.ChatType.ToString(),
|
ChatType = @event.ChatType,
|
||||||
Content = @event.MessageContent,
|
Content = @event.MessageContent,
|
||||||
GroupMemberId = null,
|
GroupMemberId = null,
|
||||||
ReceiverId = @event.MsgRecipientId,
|
ReceiverId = @event.MsgRecipientId,
|
||||||
SenderId = @event.MsgSenderId,
|
SenderId = @event.MsgSenderId,
|
||||||
TimeStamp = @event.MessageCreated,
|
TimeStamp = @event.MessageCreated,
|
||||||
Type = @event.MessageMsgType.ToString()
|
Type = @event.MessageMsgType
|
||||||
};
|
};
|
||||||
await _hub.Clients.Users(@event.MsgRecipientId.ToString()).SendAsync("ReceiveMessage", messageBaseDto);
|
await _hub.Clients.Users(@event.MsgRecipientId.ToString()).SendAsync("ReceiveMessage", messageBaseDto);
|
||||||
}
|
}
|
||||||
|
|||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
using IM_API.Domain.Events;
|
||||||
|
using IM_API.Dtos;
|
||||||
|
using IM_API.Dtos.Friend;
|
||||||
|
using IM_API.Hubs;
|
||||||
|
using IM_API.Interface.Services;
|
||||||
|
using MassTransit;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace IM_API.Application.EventHandlers.RequestFriendHandler
|
||||||
|
{
|
||||||
|
public class RequestFriendSignalRHandler:IConsumer<RequestFriendEvent>
|
||||||
|
{
|
||||||
|
private readonly IHubContext<ChatHub> _hub;
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
public RequestFriendSignalRHandler(IHubContext<ChatHub> hubContext, IUserService userService)
|
||||||
|
{
|
||||||
|
_hub = hubContext;
|
||||||
|
_userService = userService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Consume(ConsumeContext<RequestFriendEvent> context)
|
||||||
|
{
|
||||||
|
var @event = context.Message;
|
||||||
|
var userInfo = await _userService.GetUserInfoAsync(@event.FromUserId);
|
||||||
|
var res = new HubResponse<FriendRequestResDto>("Event", new FriendRequestResDto()
|
||||||
|
{
|
||||||
|
RequestUser = @event.FromUserId,
|
||||||
|
ResponseUser = @event.ToUserId,
|
||||||
|
Created = DateTime.UtcNow,
|
||||||
|
Description = @event.Description,
|
||||||
|
Avatar = userInfo.Avatar,
|
||||||
|
NickName = userInfo.NickName
|
||||||
|
});
|
||||||
|
await _hub.Clients.User(@event.ToUserId.ToString()).SendAsync("ReceiveMessage", res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using IM_API.Application.EventHandlers.FriendAddHandler;
|
using IM_API.Application.EventHandlers.FriendAddHandler;
|
||||||
using IM_API.Application.EventHandlers.MessageCreatedHandler;
|
using IM_API.Application.EventHandlers.MessageCreatedHandler;
|
||||||
|
using IM_API.Application.EventHandlers.RequestFriendHandler;
|
||||||
using MassTransit;
|
using MassTransit;
|
||||||
|
|
||||||
namespace IM_API.Configs
|
namespace IM_API.Configs
|
||||||
@@ -12,8 +13,10 @@ namespace IM_API.Configs
|
|||||||
{
|
{
|
||||||
x.AddConsumer<ConversationEventHandler>();
|
x.AddConsumer<ConversationEventHandler>();
|
||||||
x.AddConsumer<SignalREventHandler>();
|
x.AddConsumer<SignalREventHandler>();
|
||||||
x.AddConsumer<FriendAddConversationHandler>();
|
x.AddConsumer<FriendAddDBHandler>();
|
||||||
x.AddConsumer<FriendAddSignalRHandler>();
|
x.AddConsumer<FriendAddSignalRHandler>();
|
||||||
|
x.AddConsumer<RequestFriendSignalRHandler>();
|
||||||
|
x.AddConsumer<FriendAddConversationHandler>();
|
||||||
|
|
||||||
x.UsingRabbitMq((ctx,cfg) =>
|
x.UsingRabbitMq((ctx,cfg) =>
|
||||||
{
|
{
|
||||||
@@ -22,6 +25,7 @@ namespace IM_API.Configs
|
|||||||
h.Username(options.Username);
|
h.Username(options.Username);
|
||||||
h.Password(options.Password);
|
h.Password(options.Password);
|
||||||
});
|
});
|
||||||
|
cfg.ConfigureEndpoints(ctx);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using IM_API.Domain.Events;
|
using IM_API.Domain.Events;
|
||||||
using IM_API.Dtos;
|
using IM_API.Dtos;
|
||||||
|
using IM_API.Dtos.Auth;
|
||||||
|
using IM_API.Dtos.Conversation;
|
||||||
|
using IM_API.Dtos.Friend;
|
||||||
|
using IM_API.Dtos.Group;
|
||||||
|
using IM_API.Dtos.User;
|
||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
|
|
||||||
namespace IM_API.Configs
|
namespace IM_API.Configs
|
||||||
@@ -46,6 +51,13 @@ namespace IM_API.Configs
|
|||||||
.ForMember(dest => dest.StateEnum , opt => opt.MapFrom(src => FriendRequestState.Pending))
|
.ForMember(dest => dest.StateEnum , opt => opt.MapFrom(src => FriendRequestState.Pending))
|
||||||
.ForMember(dest => dest.Description , opt => opt.MapFrom(src => src.Description))
|
.ForMember(dest => dest.Description , opt => opt.MapFrom(src => src.Description))
|
||||||
;
|
;
|
||||||
|
|
||||||
|
CreateMap<FriendRequest, FriendRequestDto>()
|
||||||
|
.ForMember(dest => dest.ToUserId, opt => opt.MapFrom(src => src.ResponseUser))
|
||||||
|
.ForMember(dest => dest.FromUserId, opt => opt.MapFrom(src => src.RequestUser))
|
||||||
|
.ForMember(dest => dest.RemarkName, opt => opt.MapFrom(src => src.RemarkName))
|
||||||
|
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description))
|
||||||
|
;
|
||||||
//消息模型转换
|
//消息模型转换
|
||||||
CreateMap<Message, MessageBaseDto>()
|
CreateMap<Message, MessageBaseDto>()
|
||||||
.ForMember(dest => dest.Type , opt => opt.MapFrom(src => src.MsgTypeEnum.ToString()))
|
.ForMember(dest => dest.Type , opt => opt.MapFrom(src => src.MsgTypeEnum.ToString()))
|
||||||
@@ -59,8 +71,8 @@ namespace IM_API.Configs
|
|||||||
;
|
;
|
||||||
CreateMap<MessageBaseDto, Message>()
|
CreateMap<MessageBaseDto, Message>()
|
||||||
.ForMember(dest => dest.Sender, opt => opt.MapFrom(src => src.SenderId))
|
.ForMember(dest => dest.Sender, opt => opt.MapFrom(src => src.SenderId))
|
||||||
.ForMember(dest => dest.ChatTypeEnum,opt => opt.MapFrom(src => Enum.Parse<ChatType>(src.ChatType,true)))
|
.ForMember(dest => dest.ChatTypeEnum,opt => opt.MapFrom(src => src.ChatType))
|
||||||
.ForMember(dest => dest.MsgTypeEnum, opt => opt.MapFrom(src => Enum.Parse<MessageMsgType>(src.Type,true)))
|
.ForMember(dest => dest.MsgTypeEnum, opt => opt.MapFrom(src => src.Type))
|
||||||
.ForMember(dest => dest.Created, opt => opt.MapFrom(src => src.TimeStamp))
|
.ForMember(dest => dest.Created, opt => opt.MapFrom(src => src.TimeStamp))
|
||||||
.ForMember(dest => dest.Content, opt => opt.MapFrom(src => src.Content))
|
.ForMember(dest => dest.Content, opt => opt.MapFrom(src => src.Content))
|
||||||
.ForMember(dest => dest.Recipient, opt => opt.MapFrom(src => src.ReceiverId))
|
.ForMember(dest => dest.Recipient, opt => opt.MapFrom(src => src.ReceiverId))
|
||||||
@@ -124,6 +136,21 @@ namespace IM_API.Configs
|
|||||||
CreateMap<Group, ConversationDto>()
|
CreateMap<Group, ConversationDto>()
|
||||||
.ForMember(dest => dest.TargetAvatar, opt => opt.MapFrom(src => src.Avatar))
|
.ForMember(dest => dest.TargetAvatar, opt => opt.MapFrom(src => src.Avatar))
|
||||||
.ForMember(dest => dest.TargetName, opt => opt.MapFrom(src => src.Name));
|
.ForMember(dest => dest.TargetName, opt => opt.MapFrom(src => src.Name));
|
||||||
|
|
||||||
|
|
||||||
|
//群模型转换
|
||||||
|
CreateMap<Group, GroupInfoDto>()
|
||||||
|
.ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.StatusEnum))
|
||||||
|
.ForMember(dest => dest.AllMembersBanned, opt => opt.MapFrom(src => src.AllMembersBannedEnum))
|
||||||
|
.ForMember(dest => dest.Auhority, opt => opt.MapFrom(src => src.AuhorityEnum));
|
||||||
|
|
||||||
|
CreateMap<GroupCreateDto, Group>()
|
||||||
|
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name))
|
||||||
|
.ForMember(dest => dest.Avatar, opt => opt.MapFrom(src => src.Avatar))
|
||||||
|
.ForMember(dest => dest.Created, opt => opt.MapFrom(src => DateTime.UtcNow))
|
||||||
|
.ForMember(dest => dest.AllMembersBannedEnum, opt => opt.MapFrom(src => GroupAllMembersBanned.ALLOWED))
|
||||||
|
.ForMember(dest => dest.AuhorityEnum, opt => opt.MapFrom(src => GroupAuhority.REQUIRE_CONSENT))
|
||||||
|
.ForMember(dest => dest.StatusEnum, opt => opt.MapFrom(src => GroupStatus.Normal));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using IM_API.Dtos;
|
using IM_API.Dtos;
|
||||||
|
using IM_API.Dtos.Auth;
|
||||||
|
using IM_API.Dtos.User;
|
||||||
using IM_API.Interface.Services;
|
using IM_API.Interface.Services;
|
||||||
using IM_API.Tools;
|
using IM_API.Tools;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using IM_API.Dtos;
|
using IM_API.Dtos;
|
||||||
|
using IM_API.Dtos.Conversation;
|
||||||
using IM_API.Interface.Services;
|
using IM_API.Interface.Services;
|
||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using IM_API.Dtos;
|
using IM_API.Dtos;
|
||||||
|
using IM_API.Dtos.Friend;
|
||||||
using IM_API.Interface.Services;
|
using IM_API.Interface.Services;
|
||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@@ -60,7 +61,7 @@ namespace IM_API.Controllers
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> HandleRequest(
|
public async Task<IActionResult> HandleRequest(
|
||||||
[FromRoute]int id, [FromBody]FriendRequestHandleDto dto
|
[FromQuery]int id, [FromBody]FriendRequestHandleDto dto
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
await _friendService.HandleFriendRequestAsync(new HandleFriendRequestDto()
|
await _friendService.HandleFriendRequestAsync(new HandleFriendRequestDto()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using IM_API.Dtos;
|
using IM_API.Dtos;
|
||||||
|
using IM_API.Dtos.User;
|
||||||
using IM_API.Interface.Services;
|
using IM_API.Interface.Services;
|
||||||
using IM_API.Tools;
|
using IM_API.Tools;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using IM_API.Dtos;
|
using IM_API.Dtos.Friend;
|
||||||
|
|
||||||
namespace IM_API.Domain.Events
|
namespace IM_API.Domain.Events
|
||||||
{
|
{
|
||||||
@@ -8,12 +8,12 @@ namespace IM_API.Domain.Events
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 发起请求用户
|
/// 发起请求用户
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public UserInfoDto RequestUser { get; init; }
|
public int RequestUserId { get; init; }
|
||||||
public string? requestUserRemarkname { get; init; }
|
public string? requestUserRemarkname { get; init; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 接受请求用户
|
/// 接受请求用户
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public UserInfoDto ResponseUser { get; init; }
|
public int ResponseUserId { get; init; }
|
||||||
|
|
||||||
public FriendRequestDto RequestInfo { get; init; }
|
public FriendRequestDto RequestInfo { get; init; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace IM_API.Domain.Events
|
||||||
|
{
|
||||||
|
public record GroupInviteEvent : DomainEvent
|
||||||
|
{
|
||||||
|
public override string EventType => "IM.GROUPS_GROUP_INVITE";
|
||||||
|
public required List<int> Ids { get; init; }
|
||||||
|
public int GroupId { get; init; }
|
||||||
|
public int UserId { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using IM_API.Dtos.Group;
|
||||||
|
|
||||||
|
namespace IM_API.Domain.Events
|
||||||
|
{
|
||||||
|
public record GroupRequestEvent : DomainEvent
|
||||||
|
{
|
||||||
|
public override string EventType => "IM.GROUPS_GROUP_REQUEST";
|
||||||
|
public int GroupId { get; init; }
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace IM_API.Domain.Events
|
||||||
|
{
|
||||||
|
public record RequestFriendEvent : DomainEvent
|
||||||
|
{
|
||||||
|
public override string EventType => "IM.FRIENDS_FRIEND_REQUEST";
|
||||||
|
public int FromUserId { get; init; }
|
||||||
|
public int ToUserId { get; init; }
|
||||||
|
public string Description { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace IM_API.Dtos
|
namespace IM_API.Dtos.Auth
|
||||||
{
|
{
|
||||||
public record RefreshDto(string refreshToken);
|
public record RefreshDto(string refreshToken);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace IM_API.Dtos
|
using IM_API.Dtos.User;
|
||||||
|
|
||||||
|
namespace IM_API.Dtos.Auth
|
||||||
{
|
{
|
||||||
public class LoginDto
|
public class LoginDto
|
||||||
{
|
{
|
||||||
@@ -8,9 +10,9 @@
|
|||||||
public DateTime ExpireAt { get; set; }
|
public DateTime ExpireAt { get; set; }
|
||||||
public LoginDto(UserInfoDto userInfo,string token, string refreshToken, DateTime expireAt) {
|
public LoginDto(UserInfoDto userInfo,string token, string refreshToken, DateTime expireAt) {
|
||||||
this.userInfo = userInfo;
|
this.userInfo = userInfo;
|
||||||
this.Token = token;
|
Token = token;
|
||||||
this.RefreshToken = refreshToken;
|
RefreshToken = refreshToken;
|
||||||
this.ExpireAt = expireAt;
|
ExpireAt = expireAt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using IM_API.Tools;
|
using IM_API.Tools;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace IM_API.Dtos
|
namespace IM_API.Dtos.Auth
|
||||||
{
|
{
|
||||||
public class LoginRequestDto
|
public class LoginRequestDto
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace IM_API.Dtos
|
namespace IM_API.Dtos.Auth
|
||||||
{
|
{
|
||||||
public class RegisterRequestDto
|
public class RegisterRequestDto
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace IM_API.Dtos
|
namespace IM_API.Dtos.Conversation
|
||||||
{
|
{
|
||||||
public class ClearConversationsDto
|
public class ClearConversationsDto
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
|
|
||||||
namespace IM_API.Dtos
|
namespace IM_API.Dtos.Conversation
|
||||||
{
|
{
|
||||||
public class ConversationDto
|
public class ConversationDto
|
||||||
{
|
{
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
using IM_API.Models;
|
using IM_API.Dtos.User;
|
||||||
|
using IM_API.Models;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace IM_API.Dtos
|
namespace IM_API.Dtos.Friend
|
||||||
{
|
{
|
||||||
public record FriendInfoDto
|
public record FriendInfoDto
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace IM_API.Dtos
|
namespace IM_API.Dtos.Friend
|
||||||
{
|
{
|
||||||
public class FriendRequestDto
|
public class FriendRequestDto
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
|
|
||||||
namespace IM_API.Dtos
|
namespace IM_API.Dtos.Friend
|
||||||
{
|
{
|
||||||
public class FriendRequestResDto
|
public class FriendRequestResDto
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace IM_API.Dtos
|
namespace IM_API.Dtos.Friend
|
||||||
{
|
{
|
||||||
public class HandleFriendRequestDto
|
public class HandleFriendRequestDto
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using IM_API.Models;
|
||||||
|
|
||||||
|
namespace IM_API.Dtos.Group
|
||||||
|
{
|
||||||
|
public class GroupCreateDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 群聊名称
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 群头像
|
||||||
|
/// </summary>
|
||||||
|
public string Avatar { get; set; } = null!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using IM_API.Models;
|
||||||
|
|
||||||
|
namespace IM_API.Dtos.Group
|
||||||
|
{
|
||||||
|
public class GroupInfoDto
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 群聊名称
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 群主
|
||||||
|
/// </summary>
|
||||||
|
public int GroupMaster { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 群权限
|
||||||
|
/// (0:需管理员同意,1:任意人可加群,2:不允许任何人加入)
|
||||||
|
/// </summary>
|
||||||
|
public GroupAuhority Auhority { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全员禁言(0允许发言,2全员禁言)
|
||||||
|
/// </summary>
|
||||||
|
public GroupAllMembersBanned AllMembersBanned { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 群聊状态
|
||||||
|
/// (1:正常,2:封禁)
|
||||||
|
/// </summary>
|
||||||
|
public GroupStatus Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 群公告
|
||||||
|
/// </summary>
|
||||||
|
public string? Announcement { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 群聊创建时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime Created { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 群头像
|
||||||
|
/// </summary>
|
||||||
|
public string Avatar { get; set; } = null!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using IM_API.Tools;
|
||||||
|
|
||||||
|
namespace IM_API.Dtos
|
||||||
|
{
|
||||||
|
public class HubResponse<T>
|
||||||
|
{
|
||||||
|
public int Code { get; init; }
|
||||||
|
public string Method { get; init; }
|
||||||
|
public HubResponseType Type { get; init; }
|
||||||
|
public string Message { get; init; }
|
||||||
|
public T? Data { get; init; }
|
||||||
|
public HubResponse(string method)
|
||||||
|
{
|
||||||
|
Code = CodeDefine.SUCCESS.Code;
|
||||||
|
Message = CodeDefine.SUCCESS.Message;
|
||||||
|
Type = HubResponseType.ActionStatus;
|
||||||
|
}
|
||||||
|
public HubResponse(string method,T data)
|
||||||
|
{
|
||||||
|
Code = CodeDefine.SUCCESS.Code;
|
||||||
|
Message = CodeDefine.SUCCESS.Message;
|
||||||
|
Type = HubResponseType.ActionStatus;
|
||||||
|
Data = data;
|
||||||
|
}
|
||||||
|
public HubResponse(CodeDefine codedefine,string method)
|
||||||
|
{
|
||||||
|
Code = codedefine.Code;
|
||||||
|
Method = method;
|
||||||
|
Message = codedefine.Message;
|
||||||
|
Type = HubResponseType.ActionStatus;
|
||||||
|
}
|
||||||
|
public HubResponse(CodeDefine codeDefine, string method, HubResponseType type, T? data)
|
||||||
|
{
|
||||||
|
Code = codeDefine.Code;
|
||||||
|
Method = method;
|
||||||
|
Type = type;
|
||||||
|
Message = codeDefine.Message;
|
||||||
|
Data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public enum HubResponseType
|
||||||
|
{
|
||||||
|
ChatMsg = 1, // 聊天内容
|
||||||
|
SystemNotice = 2, // 系统通知(如:申请好友成功)
|
||||||
|
ActionStatus = 3 // 状态变更(如:对方正在输入、已读回执)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
namespace IM_API.Dtos
|
using IM_API.Models;
|
||||||
|
|
||||||
|
namespace IM_API.Dtos
|
||||||
{
|
{
|
||||||
public record MessageBaseDto
|
public record MessageBaseDto
|
||||||
{
|
{
|
||||||
// 使用 { get; init; } 确保对象创建后不可修改,且支持无参构造
|
// 使用 { get; init; } 确保对象创建后不可修改,且支持无参构造
|
||||||
public string Type { get; init; } = default!;
|
public MessageMsgType Type { get; init; } = default!;
|
||||||
public string ChatType { get; init; } = default!;
|
public ChatType ChatType { get; init; } = default!;
|
||||||
public string? MsgId { get; init; }
|
public string? MsgId { get; init; }
|
||||||
public int SenderId { get; init; }
|
public int SenderId { get; init; }
|
||||||
public int ReceiverId { get; init; }
|
public int ReceiverId { get; init; }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace IM_API.Dtos
|
namespace IM_API.Dtos.User
|
||||||
{
|
{
|
||||||
public class UpdateUserDto
|
public class UpdateUserDto
|
||||||
{
|
{
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace IM_API.Dtos
|
namespace IM_API.Dtos.User
|
||||||
{
|
{
|
||||||
public record PasswordResetDto
|
public record PasswordResetDto
|
||||||
{
|
{
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace IM_API.Dtos
|
namespace IM_API.Dtos.User
|
||||||
{
|
{
|
||||||
public class UserInfoDto
|
public class UserInfoDto
|
||||||
{
|
{
|
||||||
@@ -40,17 +40,17 @@ namespace IM_API.Hubs
|
|||||||
}
|
}
|
||||||
await base.OnConnectedAsync();
|
await base.OnConnectedAsync();
|
||||||
}
|
}
|
||||||
public async Task SendMessage(MessageBaseDto dto)
|
public async Task<HubResponse<MessageBaseDto?>> SendMessage(MessageBaseDto dto)
|
||||||
{
|
{
|
||||||
if (!Context.User.Identity.IsAuthenticated)
|
if (!Context.User.Identity.IsAuthenticated)
|
||||||
{
|
{
|
||||||
await Clients.Caller.SendAsync("ReceiveMessage", new BaseResponse<object?>(CodeDefine.AUTH_FAILED));
|
await Clients.Caller.SendAsync("ReceiveMessage", new BaseResponse<object?>(CodeDefine.AUTH_FAILED));
|
||||||
Context.Abort();
|
Context.Abort();
|
||||||
return;
|
return new HubResponse<MessageBaseDto?>(CodeDefine.AUTH_FAILED, "SendMessage");
|
||||||
}
|
}
|
||||||
var userIdStr = Context.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
var userIdStr = Context.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
MessageBaseDto msgInfo = null;
|
MessageBaseDto msgInfo = null;
|
||||||
if(dto.ChatType.ToLower() == ChatType.PRIVATE.ToString().ToLower())
|
if(dto.ChatType == ChatType.PRIVATE)
|
||||||
{
|
{
|
||||||
msgInfo = await _messageService.SendPrivateMessageAsync(int.Parse(userIdStr), dto.ReceiverId, dto);
|
msgInfo = await _messageService.SendPrivateMessageAsync(int.Parse(userIdStr), dto.ReceiverId, dto);
|
||||||
}
|
}
|
||||||
@@ -58,19 +58,19 @@ namespace IM_API.Hubs
|
|||||||
{
|
{
|
||||||
msgInfo = await _messageService.SendGroupMessageAsync(int.Parse(userIdStr), dto.ReceiverId, dto);
|
msgInfo = await _messageService.SendGroupMessageAsync(int.Parse(userIdStr), dto.ReceiverId, dto);
|
||||||
}
|
}
|
||||||
return;
|
return new HubResponse<MessageBaseDto?>("SendMessage", msgInfo);
|
||||||
}
|
}
|
||||||
public async Task ClearUnreadCount(int conversationId)
|
public async Task<HubResponse<object?>> ClearUnreadCount(int conversationId)
|
||||||
{
|
{
|
||||||
if (!Context.User.Identity.IsAuthenticated)
|
if (!Context.User.Identity.IsAuthenticated)
|
||||||
{
|
{
|
||||||
await Clients.Caller.SendAsync("ReceiveMessage", new BaseResponse<object?>(CodeDefine.AUTH_FAILED));
|
await Clients.Caller.SendAsync("ReceiveMessage", new BaseResponse<object?>(CodeDefine.AUTH_FAILED));
|
||||||
Context.Abort();
|
Context.Abort();
|
||||||
return;
|
return new HubResponse<object?>(CodeDefine.AUTH_FAILED, "ClearUnreadCount"); ;
|
||||||
}
|
}
|
||||||
var userIdStr = Context.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
var userIdStr = Context.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
await _conversationService.ClearUnreadCountAsync(int.Parse(userIdStr), conversationId);
|
await _conversationService.ClearUnreadCountAsync(int.Parse(userIdStr), conversationId);
|
||||||
return;
|
return new HubResponse<object?>("ClearUnreadCount");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,4 +31,8 @@
|
|||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Application\EventHandlers\GroupInviteHandler\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using IM_API.Dtos;
|
using IM_API.Dtos.Auth;
|
||||||
|
using IM_API.Dtos.User;
|
||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
|
|
||||||
namespace IM_API.Interface.Services
|
namespace IM_API.Interface.Services
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using IM_API.Dtos;
|
using IM_API.Dtos.Conversation;
|
||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
|
|
||||||
namespace IM_API.Interface.Services
|
namespace IM_API.Interface.Services
|
||||||
@@ -42,5 +42,13 @@ namespace IM_API.Interface.Services
|
|||||||
/// <param name="conversationId"></param>
|
/// <param name="conversationId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<bool> ClearUnreadCountAsync(int userId, int conversationId);
|
Task<bool> ClearUnreadCountAsync(int userId, int conversationId);
|
||||||
|
/// <summary>
|
||||||
|
/// 为用户创建会话
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userAId"></param>
|
||||||
|
/// <param name="userBId"></param>
|
||||||
|
/// <param name="chatType"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task MakeConversationAsync(int userAId, int userBId, ChatType chatType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using IM_API.Dtos;
|
using IM_API.Dtos.Friend;
|
||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
|
|
||||||
namespace IM_API.Interface.Services
|
namespace IM_API.Interface.Services
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using IM_API.Dtos.Group;
|
||||||
|
|
||||||
|
namespace IM_API.Interface.Services
|
||||||
|
{
|
||||||
|
public interface IGroupService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请好友入群
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">操作者ID</param>
|
||||||
|
/// <param name="groupId">群ID</param>
|
||||||
|
/// <param name="userIds">邀请的用户列表</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task InviteUsers(int userId,int groupId, List<int> userIds);
|
||||||
|
/// <summary>
|
||||||
|
/// 加入群聊
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">操作者ID</param>
|
||||||
|
/// <param name="groupId">群ID</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task JoinGroup(int userId,int groupId);
|
||||||
|
/// <summary>
|
||||||
|
/// 创建群聊
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">操作者ID</param>
|
||||||
|
/// <param name="groupCreateDto">群信息</param>
|
||||||
|
/// <param name="userIds">邀请用户列表</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<GroupInfoDto> CreateGroup(int userId, GroupCreateDto groupCreateDto, List<int> userIds);
|
||||||
|
/// <summary>
|
||||||
|
/// 删除群
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">操作者ID</param>
|
||||||
|
/// <param name="groupId">群ID</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task DeleteGroup(int userId, int groupId);
|
||||||
|
//Task UpdateGroupAuthori
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using IM_API.Dtos;
|
using IM_API.Dtos.User;
|
||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
|
|
||||||
namespace IM_API.Interface.Services
|
namespace IM_API.Interface.Services
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ public partial class FriendRequest
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sbyte State { get; set; }
|
public sbyte State { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注
|
||||||
|
/// </summary>
|
||||||
|
public string RemarkName { get; set; } = null!;
|
||||||
|
|
||||||
public virtual User RequestUserNavigation { get; set; } = null!;
|
public virtual User RequestUserNavigation { get; set; } = null!;
|
||||||
|
|
||||||
public virtual User ResponseUserNavigation { get; set; } = null!;
|
public virtual User ResponseUserNavigation { get; set; } = null!;
|
||||||
|
|||||||
@@ -53,5 +53,7 @@ public partial class Group
|
|||||||
|
|
||||||
public virtual User GroupMasterNavigation { get; set; } = null!;
|
public virtual User GroupMasterNavigation { get; set; } = null!;
|
||||||
|
|
||||||
|
public virtual ICollection<GroupMember> GroupMembers { get; set; } = new List<GroupMember>();
|
||||||
|
|
||||||
public virtual ICollection<GroupRequest> GroupRequests { get; set; } = new List<GroupRequest>();
|
public virtual ICollection<GroupRequest> GroupRequests { get; set; } = new List<GroupRequest>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public partial class GroupMember
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime Created { get; set; }
|
public DateTime Created { get; set; }
|
||||||
|
|
||||||
public virtual User Group { get; set; } = null!;
|
public virtual Group Group { get; set; } = null!;
|
||||||
|
|
||||||
public virtual User User { get; set; } = null!;
|
public virtual User User { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -281,6 +281,9 @@ public partial class ImContext : DbContext
|
|||||||
entity.Property(e => e.Description)
|
entity.Property(e => e.Description)
|
||||||
.HasComment("申请附言 ")
|
.HasComment("申请附言 ")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
entity.Property(e => e.RemarkName)
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasComment("备注");
|
||||||
entity.Property(e => e.RequestUser)
|
entity.Property(e => e.RequestUser)
|
||||||
.HasComment("申请人 ")
|
.HasComment("申请人 ")
|
||||||
.HasColumnType("int(11)");
|
.HasColumnType("int(11)");
|
||||||
@@ -429,12 +432,12 @@ public partial class ImContext : DbContext
|
|||||||
.HasComment("用户编号")
|
.HasComment("用户编号")
|
||||||
.HasColumnType("int(11)");
|
.HasColumnType("int(11)");
|
||||||
|
|
||||||
entity.HasOne(d => d.Group).WithMany(p => p.GroupMemberGroups)
|
entity.HasOne(d => d.Group).WithMany(p => p.GroupMembers)
|
||||||
.HasForeignKey(d => d.GroupId)
|
.HasForeignKey(d => d.GroupId)
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull)
|
.OnDelete(DeleteBehavior.ClientSetNull)
|
||||||
.HasConstraintName("group_member_ibfk_2");
|
.HasConstraintName("group_member_ibfk_2");
|
||||||
|
|
||||||
entity.HasOne(d => d.User).WithMany(p => p.GroupMemberUsers)
|
entity.HasOne(d => d.User).WithMany(p => p.GroupMembers)
|
||||||
.HasForeignKey(d => d.UserId)
|
.HasForeignKey(d => d.UserId)
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull)
|
.OnDelete(DeleteBehavior.ClientSetNull)
|
||||||
.HasConstraintName("group_member_ibfk_1");
|
.HasConstraintName("group_member_ibfk_1");
|
||||||
|
|||||||
@@ -73,9 +73,7 @@ public partial class User
|
|||||||
|
|
||||||
public virtual ICollection<GroupInvite> GroupInviteInvitedUserNavigations { get; set; } = new List<GroupInvite>();
|
public virtual ICollection<GroupInvite> GroupInviteInvitedUserNavigations { get; set; } = new List<GroupInvite>();
|
||||||
|
|
||||||
public virtual ICollection<GroupMember> GroupMemberGroups { get; set; } = new List<GroupMember>();
|
public virtual ICollection<GroupMember> GroupMembers { get; set; } = new List<GroupMember>();
|
||||||
|
|
||||||
public virtual ICollection<GroupMember> GroupMemberUsers { get; set; } = new List<GroupMember>();
|
|
||||||
|
|
||||||
public virtual ICollection<GroupRequest> GroupRequests { get; set; } = new List<GroupRequest>();
|
public virtual ICollection<GroupRequest> GroupRequests { get; set; } = new List<GroupRequest>();
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace IM_API
|
namespace IM_API
|
||||||
{
|
{
|
||||||
@@ -38,7 +40,12 @@ namespace IM_API
|
|||||||
|
|
||||||
builder.Services.AddAllService(configuration);
|
builder.Services.AddAllService(configuration);
|
||||||
|
|
||||||
builder.Services.AddSignalR();
|
builder.Services.AddSignalR().AddJsonProtocol(options =>
|
||||||
|
{
|
||||||
|
// 枚举输出字符串
|
||||||
|
options.PayloadSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||||
|
options.PayloadSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||||
|
});
|
||||||
//允许所有来源(跨域)
|
//允许所有来源(跨域)
|
||||||
builder.Services.AddCors(options =>
|
builder.Services.AddCors(options =>
|
||||||
{
|
{
|
||||||
@@ -110,6 +117,10 @@ namespace IM_API
|
|||||||
{
|
{
|
||||||
// 保持 ISO 8601 格式
|
// 保持 ISO 8601 格式
|
||||||
options.JsonSerializerOptions.Converters.Add(new UtcDateTimeConverter());
|
options.JsonSerializerOptions.Converters.Add(new UtcDateTimeConverter());
|
||||||
|
// 将枚举转换为字符串
|
||||||
|
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||||
|
// 建议:保持驼峰命名
|
||||||
|
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||||
});
|
});
|
||||||
builder.Services.AddModelValidation(configuration);
|
builder.Services.AddModelValidation(configuration);
|
||||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using IM_API.Dtos;
|
using IM_API.Dtos.Auth;
|
||||||
|
using IM_API.Dtos.User;
|
||||||
using IM_API.Exceptions;
|
using IM_API.Exceptions;
|
||||||
using IM_API.Interface.Services;
|
using IM_API.Interface.Services;
|
||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using IM_API.Dtos;
|
using IM_API.Dtos.Conversation;
|
||||||
using IM_API.Exceptions;
|
using IM_API.Exceptions;
|
||||||
using IM_API.Interface.Services;
|
using IM_API.Interface.Services;
|
||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
@@ -134,5 +134,27 @@ namespace IM_API.Services
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task MakeConversationAsync(int userAId, int userBId, ChatType chatType)
|
||||||
|
{
|
||||||
|
var userAcExist = await _context.Conversations.AnyAsync(x => x.UserId == userAId && x.TargetId == userBId);
|
||||||
|
if (userAcExist) return;
|
||||||
|
var streamKey = chatType == ChatType.PRIVATE ?
|
||||||
|
StreamKeyBuilder.Private(userAId, userBId) : StreamKeyBuilder.Group(userBId);
|
||||||
|
var conversation = new Conversation()
|
||||||
|
{
|
||||||
|
ChatType = (int)chatType,
|
||||||
|
LastMessage = "",
|
||||||
|
LastMessageTime = DateTime.UtcNow,
|
||||||
|
LastReadMessageId = -1,
|
||||||
|
StreamKey = streamKey,
|
||||||
|
TargetId = userBId,
|
||||||
|
UnreadCount = 0,
|
||||||
|
UserId = userAId
|
||||||
|
|
||||||
|
};
|
||||||
|
_context.Conversations.Add(conversation);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using IM_API.Dtos;
|
using IM_API.Domain.Events;
|
||||||
|
using IM_API.Dtos.Friend;
|
||||||
using IM_API.Exceptions;
|
using IM_API.Exceptions;
|
||||||
using IM_API.Interface.Services;
|
using IM_API.Interface.Services;
|
||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
using IM_API.Tools;
|
using IM_API.Tools;
|
||||||
|
using MassTransit;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace IM_API.Services
|
namespace IM_API.Services
|
||||||
@@ -13,11 +15,13 @@ namespace IM_API.Services
|
|||||||
private readonly ImContext _context;
|
private readonly ImContext _context;
|
||||||
private readonly ILogger<FriendService> _logger;
|
private readonly ILogger<FriendService> _logger;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
public FriendService(ImContext context, ILogger<FriendService> logger, IMapper mapper)
|
private readonly IPublishEndpoint _endpoint;
|
||||||
|
public FriendService(ImContext context, ILogger<FriendService> logger, IMapper mapper, IPublishEndpoint endpoint)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
|
_endpoint = endpoint;
|
||||||
}
|
}
|
||||||
#region 拉黑好友
|
#region 拉黑好友
|
||||||
public async Task<bool> BlockeFriendAsync(int friendId)
|
public async Task<bool> BlockeFriendAsync(int friendId)
|
||||||
@@ -113,27 +117,31 @@ namespace IM_API.Services
|
|||||||
var friend = await _context.Friends.FirstOrDefaultAsync(
|
var friend = await _context.Friends.FirstOrDefaultAsync(
|
||||||
x => x.UserId == friendRequest.RequestUser && x.FriendId == friendRequest.ResponseUser
|
x => x.UserId == friendRequest.RequestUser && x.FriendId == friendRequest.ResponseUser
|
||||||
);
|
);
|
||||||
if (friend is null) throw new BaseException(CodeDefine.FRIEND_RELATION_NOT_FOUND);
|
if (friend != null) throw new BaseException(CodeDefine.ALREADY_FRIENDS);
|
||||||
|
|
||||||
if (friend.StatusEnum != FriendStatus.Pending) throw new BaseException(CodeDefine.FRIEND_REQUEST_EXISTS);
|
|
||||||
//处理好友请求操作
|
//处理好友请求操作
|
||||||
switch (requestDto.Action)
|
switch (requestDto.Action)
|
||||||
{
|
{
|
||||||
//拒绝后标记
|
//拒绝后标记
|
||||||
case HandleFriendRequestAction.Reject:
|
case HandleFriendRequestAction.Reject:
|
||||||
friend.StatusEnum = FriendStatus.Declined;
|
|
||||||
friendRequest.StateEnum = FriendRequestState.Declined;
|
friendRequest.StateEnum = FriendRequestState.Declined;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
//同意后标记
|
//同意后标记
|
||||||
case HandleFriendRequestAction.Accept:
|
case HandleFriendRequestAction.Accept:
|
||||||
friend.StatusEnum = FriendStatus.Added;
|
|
||||||
friendRequest.StateEnum = FriendRequestState.Passed;
|
friendRequest.StateEnum = FriendRequestState.Passed;
|
||||||
|
await _endpoint.Publish(new FriendAddEvent()
|
||||||
|
{
|
||||||
|
AggregateId = friendRequest.Id.ToString(),
|
||||||
|
OccurredAt = DateTime.UtcNow,
|
||||||
|
Created = DateTime.UtcNow,
|
||||||
|
EventId = Guid.NewGuid(),
|
||||||
|
OperatorId = friendRequest.ResponseUser,
|
||||||
|
RequestInfo = _mapper.Map<FriendRequestDto>(friendRequest),
|
||||||
|
requestUserRemarkname = requestDto.RemarkName,
|
||||||
|
RequestUserId = friendRequest.RequestUser,
|
||||||
|
ResponseUserId = friendRequest.ResponseUser
|
||||||
|
|
||||||
//根据当前好友请求为被申请方添加一条好友记录(注意:好友记录为双向)
|
});
|
||||||
var ResponseFriend = _mapper.Map<Friend>(friendRequest);
|
|
||||||
if (!string.IsNullOrEmpty(requestDto.RemarkName)) ResponseFriend.RemarkName = requestDto.RemarkName;
|
|
||||||
_context.Friends.Add(ResponseFriend);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
//无效操作
|
//无效操作
|
||||||
@@ -172,6 +180,16 @@ namespace IM_API.Services
|
|||||||
var friendRequst = _mapper.Map<FriendRequest>(dto);
|
var friendRequst = _mapper.Map<FriendRequest>(dto);
|
||||||
_context.FriendRequests.Add(friendRequst);
|
_context.FriendRequests.Add(friendRequst);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
await _endpoint.Publish(new RequestFriendEvent()
|
||||||
|
{
|
||||||
|
AggregateId = friendRequst.Id.ToString(),
|
||||||
|
OccurredAt = friendRequst.Created,
|
||||||
|
Description = friendRequst.Description,
|
||||||
|
EventId = Guid.NewGuid(),
|
||||||
|
FromUserId = friendRequst.RequestUser,
|
||||||
|
ToUserId = friendRequst.ResponseUser,
|
||||||
|
OperatorId = friendRequst.RequestUser
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using IM_API.Domain.Events;
|
||||||
|
using IM_API.Dtos.Group;
|
||||||
|
using IM_API.Exceptions;
|
||||||
|
using IM_API.Interface.Services;
|
||||||
|
using IM_API.Models;
|
||||||
|
using IM_API.Tools;
|
||||||
|
using MassTransit;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace IM_API.Services
|
||||||
|
{
|
||||||
|
public class GroupService : IGroupService
|
||||||
|
{
|
||||||
|
private readonly ImContext _context;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
private readonly ILogger<GroupService> _logger;
|
||||||
|
private readonly IPublishEndpoint _endPoint;
|
||||||
|
public GroupService(ImContext context, IMapper mapper, ILogger<GroupService> logger, IPublishEndpoint publishEndpoint)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_mapper = mapper;
|
||||||
|
_logger = logger;
|
||||||
|
_endPoint = publishEndpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<GroupInvite>> GetGroupInvites(int userId, int groupId, List<int> ids)
|
||||||
|
{
|
||||||
|
DateTime dateTime = DateTime.UtcNow;
|
||||||
|
//验证被邀请用户是否为好友
|
||||||
|
var validFriendIds = await _context.Friends
|
||||||
|
.Where(f => f.UserId == userId && ids.Contains(f.FriendId))
|
||||||
|
.Select(f => f.FriendId)
|
||||||
|
.ToListAsync();
|
||||||
|
//创建群成员对象
|
||||||
|
return validFriendIds.Select(fid => new GroupInvite
|
||||||
|
{
|
||||||
|
Created = dateTime,
|
||||||
|
GroupId = groupId,
|
||||||
|
InvitedUser = fid,
|
||||||
|
StateEnum = GroupInviteState.Pending,
|
||||||
|
InviteUser = userId
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GroupInfoDto> CreateGroup(int userId, GroupCreateDto groupCreateDto, List<int> userIds)
|
||||||
|
{
|
||||||
|
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//先创建群
|
||||||
|
DateTime dateTime = DateTime.UtcNow;
|
||||||
|
Group group = _mapper.Map<Group>(groupCreateDto);
|
||||||
|
group.GroupMaster = userId;
|
||||||
|
_context.Groups.Add(group);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
var groupInvites = new List<GroupInvite>();
|
||||||
|
if (userIds.Count > 0)
|
||||||
|
{
|
||||||
|
groupInvites = await GetGroupInvites(userId,group.Id, userIds);
|
||||||
|
_context.GroupInvites.AddRange(groupInvites);
|
||||||
|
}
|
||||||
|
var groupMember = new GroupMember
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
Created = dateTime,
|
||||||
|
RoleEnum = GroupMemberRole.Master,
|
||||||
|
GroupId = group.Id
|
||||||
|
};
|
||||||
|
_context.GroupMembers.Add(groupMember);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
await transaction.CommitAsync();
|
||||||
|
await _endPoint.Publish(new GroupInviteEvent
|
||||||
|
{
|
||||||
|
AggregateId = userId.ToString(),
|
||||||
|
GroupId = group.Id,
|
||||||
|
EventId = Guid.NewGuid(),
|
||||||
|
OccurredAt = dateTime,
|
||||||
|
Ids = groupInvites.Select(x => x.Id).ToList(),
|
||||||
|
OperatorId = userId,
|
||||||
|
UserId = userId
|
||||||
|
});
|
||||||
|
return _mapper.Map<GroupInfoDto>(group);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DeleteGroup(int userId, int groupId)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InviteUsers(int userId, int groupId, List<int> userIds)
|
||||||
|
{
|
||||||
|
var group = await _context.Groups.FirstOrDefaultAsync(
|
||||||
|
x => x.Id == groupId) ?? throw new BaseException(CodeDefine.GROUP_NOT_FOUND);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task JoinGroup(int userId, int groupId)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using IM_API.Dtos;
|
using IM_API.Dtos.User;
|
||||||
using IM_API.Exceptions;
|
using IM_API.Exceptions;
|
||||||
using IM_API.Interface.Services;
|
using IM_API.Interface.Services;
|
||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
#VITE_API_BASE_URL = http://localhost:5202/api
|
#VITE_API_BASE_URL = http://localhost:5202/api
|
||||||
#VITE_SIGNALR_BASE_URL = http://localhost:5202/chat
|
#VITE_SIGNALR_BASE_URL = http://localhost:5202/chat/
|
||||||
VITE_API_BASE_URL = https://im.test.nxsir.cn/api
|
VITE_API_BASE_URL = https://im.test.nxsir.cn/api
|
||||||
VITE_SIGNALR_BASE_URL = https://im.test.nxsir.cn/chat/
|
VITE_SIGNALR_BASE_URL = https://im.test.nxsir.cn/chat/
|
||||||
@@ -67,12 +67,12 @@ const vClickOutside = {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* --- 以下是保持不动的原样部分 --- */
|
||||||
.add-menu-container {
|
.add-menu-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 加号按钮样式 */
|
|
||||||
.add-btn {
|
.add-btn {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@@ -87,14 +87,8 @@ const vClickOutside = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.add-btn:hover {
|
.add-btn:hover {
|
||||||
background: #cccbcb;
|
background: #cccbcb;
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
.add-btn.active {
|
|
||||||
background: #007aff;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
.plus-icon {
|
.plus-icon {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
@@ -102,20 +96,15 @@ const vClickOutside = {
|
|||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 弹出卡片容器 */
|
.pop-enter-active, .pop-leave-active {
|
||||||
.menu-card {
|
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
position: absolute;
|
}
|
||||||
top: 45px;
|
|
||||||
right: 0;
|
.pop-enter-from, .pop-leave-to {
|
||||||
width: 100px;
|
opacity: 0;
|
||||||
background: white;
|
transform: scale(0.9) translateY(-10px);
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
||||||
z-index: 100;
|
|
||||||
transform-origin: top right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 小三角 */
|
|
||||||
.arrow {
|
.arrow {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -6px;
|
top: -6px;
|
||||||
@@ -127,54 +116,65 @@ const vClickOutside = {
|
|||||||
border-bottom: 6px solid white;
|
border-bottom: 6px solid white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 列表样式 */
|
/* --- 重点优化:仅限菜单列表部分 --- */
|
||||||
|
|
||||||
|
.menu-card {
|
||||||
|
position: absolute;
|
||||||
|
top: 45px;
|
||||||
|
right: 0;
|
||||||
|
width: 120px; /* 稍微加宽,避免局促 */
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
/* 优化:使用更柔和的复合阴影,提升高级感 */
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 100;
|
||||||
|
transform-origin: top right;
|
||||||
|
padding: 4px; /* 增加内边距,让列表项不贴边 */
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05); /* 增加极细边框,防止白底背景重叠 */
|
||||||
|
}
|
||||||
|
|
||||||
.menu-list {
|
.menu-list {
|
||||||
padding: 6px 0;
|
padding: 0; /* 清除默认,改由父级 padding 控制 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px; /* 增加项与项之间的微小缝隙 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item {
|
.menu-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 5px 5px;
|
padding: 8px 10px; /* 增加点击区域和呼吸感 */
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s;
|
border-radius: 6px; /* 每一行也给圆角,悬浮时更好看 */
|
||||||
color: #333;
|
transition: all 0.2s;
|
||||||
|
color: #4a4a4a;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item:hover {
|
.menu-item:hover {
|
||||||
background: #f5f5f5;
|
background: #c6c6c6; /* 换成淡淡的品牌色背景 */
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item:first-child:hover {
|
|
||||||
border-top-left-radius: 10px;
|
|
||||||
border-top-right-radius: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移除旧的、生硬的边角覆盖逻辑,改用上面统一的 border-radius */
|
||||||
|
.menu-item:first-child:hover,
|
||||||
.menu-item:last-child:hover {
|
.menu-item:last-child:hover {
|
||||||
border-bottom-left-radius: 10px;
|
border-top-left-radius: 6px;
|
||||||
border-bottom-right-radius: 10px;
|
border-top-right-radius: 6px;
|
||||||
|
border-bottom-left-radius: 6px;
|
||||||
|
border-bottom-right-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin-right: 5px;
|
margin-right: 8px; /* 增加图标与文字的距离 */
|
||||||
font-size: 12px;
|
font-size: 14px; /* 稍微调大一点点 */
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
/*width: 20px;*/
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item span {
|
.menu-item span {
|
||||||
font-size: 12px;
|
font-size: 13px; /* 稍微增大字号,更容易阅读 */
|
||||||
/*font-weight: 400;*/
|
font-weight: 500; /* 增加字重,更有质感 */
|
||||||
}
|
|
||||||
|
|
||||||
/* 弹出动画 */
|
|
||||||
.pop-enter-active, .pop-leave-active {
|
|
||||||
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pop-enter-from, .pop-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.9) translateY(-10px);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<div v-for="c in props.contacts"
|
||||||
|
:key="c.id"
|
||||||
|
class="list-item"
|
||||||
|
:class="{active: activeContactId === c.id}"
|
||||||
|
@click="routeUserInfo(c.id)">
|
||||||
|
<img :src="c.userInfo.avatar" class="avatar-std" />
|
||||||
|
<div class="info">
|
||||||
|
<div class="name">{{ c.remarkName }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const activeContactId = ref(null)
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
contacts: {
|
||||||
|
type:String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const routeUserInfo = (id) => {
|
||||||
|
router.push(`/contacts/info/${id}`);
|
||||||
|
activeContactId.value = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
padding: 10px 12px;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
text-decoration: none; /* 去除下划线 */
|
||||||
|
color: inherit; /* 继承父元素的文本颜色 */
|
||||||
|
outline: none; /* 去除点击时的蓝框 */
|
||||||
|
-webkit-tap-highlight-color: transparent; /* 移动端点击高亮 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 去除 hover、active 等状态的效果 */
|
||||||
|
a:hover,
|
||||||
|
a:active,
|
||||||
|
a:focus {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit; /* 保持颜色不变 */
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item:hover { background: #e2e2e2; }
|
||||||
|
.list-item.active { background: #c6c6c6; }
|
||||||
|
|
||||||
|
.avatar-std {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-box {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.icon-box.orange { background: #faad14; }
|
||||||
|
.icon-box.green { background: #52c41a; }
|
||||||
|
.icon-box.blue { background: #1890ff; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({ modelValue: Boolean });
|
||||||
|
const emit = defineEmits(['update:modelValue', 'create']);
|
||||||
|
|
||||||
|
const friends = ref([
|
||||||
|
{ id: 1, name: '张三', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=1' },
|
||||||
|
{ id: 2, name: '李四', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=2' },
|
||||||
|
{ id: 3, name: '王五', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=3' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const groupName = ref('');
|
||||||
|
const selected = ref(new Set()); // 使用 Set 处理选中逻辑更简洁
|
||||||
|
|
||||||
|
const toggle = (id) => {
|
||||||
|
selected.value.has(id) ? selected.value.delete(id) : selected.value.add(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
emit('create', { name: groupName.value, members: Array.from(selected.value) });
|
||||||
|
emit('update:modelValue', false);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<div v-if="modelValue" class="overlay" @click.self="$emit('update:modelValue', false)">
|
||||||
|
<div class="mini-modal">
|
||||||
|
<header>
|
||||||
|
<span>发起群聊</span>
|
||||||
|
<button @click="$emit('update:modelValue', false)">✕</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<input v-model="groupName" placeholder="群组名称..." class="mini-input" />
|
||||||
|
|
||||||
|
<div class="list">
|
||||||
|
<div v-for="f in friends" :key="f.id" @click="toggle(f.id)" class="item">
|
||||||
|
<img :src="f.avatar" class="avatar" />
|
||||||
|
<span class="name">{{ f.name }}</span>
|
||||||
|
<input type="checkbox" :checked="selected.has(f.id)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<button @click="submit" :disabled="!groupName || !selected.size" class="btn">
|
||||||
|
创建 ({{ selected.size }})
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.overlay {
|
||||||
|
position: fixed; inset: 0; background: rgba(0,0,0,0.4);
|
||||||
|
display: flex; align-items: center; justify-content: center; z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-modal {
|
||||||
|
background: white; width: 300px; border-radius: 12px; overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding: 12px 16px; display: flex; justify-content: space-between;
|
||||||
|
background: #f9f9f9; font-weight: bold; font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header button { background: none; border: none; cursor: pointer; color: #999; }
|
||||||
|
|
||||||
|
main { padding: 12px; }
|
||||||
|
|
||||||
|
.mini-input {
|
||||||
|
width: 100%; padding: 8px; margin-bottom: 12px; border: 1px solid #eee;
|
||||||
|
border-radius: 4px; box-sizing: border-box; outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list { max-height: 200px; overflow-y: auto; }
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex; align-items: center; padding: 8px; cursor: pointer; border-radius: 6px;
|
||||||
|
}
|
||||||
|
.item:hover { background: #f5f5f5; }
|
||||||
|
|
||||||
|
.avatar { width: 32px; height: 32px; border-radius: 4px; margin-right: 10px; }
|
||||||
|
.name { flex: 1; font-size: 14px; }
|
||||||
|
|
||||||
|
footer { padding: 12px; }
|
||||||
|
.btn {
|
||||||
|
width: 100%; padding: 10px; background: #07c160; color: white;
|
||||||
|
border: none; border-radius: 6px; font-weight: bold; cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn:disabled { background: #e1e1e1; color: #999; cursor: not-allowed; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<div class="pure-group-list">
|
||||||
|
<div
|
||||||
|
v-for="group in groups"
|
||||||
|
:key="group.id"
|
||||||
|
class="group-item"
|
||||||
|
:class="{ active: activeId === group.id }"
|
||||||
|
@click="activeId = group.id; $emit('select', group)"
|
||||||
|
>
|
||||||
|
<img :src="group.avatar" class="group-avatar" />
|
||||||
|
|
||||||
|
<span class="group-name">{{ group.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
groups: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
// 数据结构仅需: { id, name, avatar }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['select']);
|
||||||
|
const activeId = ref(null);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pure-group-list {
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
/* 移除内边距,完全由外部容器控制 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
/* 避免文字选中 */
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 悬停反馈:轻微变暗 */
|
||||||
|
.group-item:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中态:建议使用稍微明显的底色 */
|
||||||
|
.group-item.active {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-avatar {
|
||||||
|
width: 32px; /* 进一步缩小,保持精致感 */
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 6px;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 12px;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
/* 防止名称过长换行 */
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,399 +1,175 @@
|
|||||||
<template>
|
|
||||||
<transition name="fade">
|
|
||||||
<div v-if="modelValue" class="modal-mask" @click.self="close">
|
|
||||||
<div class="modal-container">
|
|
||||||
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="header-title">
|
|
||||||
<button v-if="step === 2" class="btn-back-icon" @click="step = 1">←</button>
|
|
||||||
<h2>{{ step === 1 ? '添加好友' : '验证信息' }}</h2>
|
|
||||||
</div>
|
|
||||||
<button class="icon-close" @click="close">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="step === 1" class="step-wrapper">
|
|
||||||
<div class="search-box">
|
|
||||||
<input
|
|
||||||
v-model="keyword"
|
|
||||||
type="text"
|
|
||||||
placeholder="输入 ID / 手机号"
|
|
||||||
@keyup.enter="onSearch"
|
|
||||||
/>
|
|
||||||
<button class="search-btn" @click="onSearch" :disabled="loading">
|
|
||||||
<span v-if="!loading">搜索</span>
|
|
||||||
<span v-else class="spinner"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<transition name="fade-slide">
|
|
||||||
<div v-if="userResult" class="user-card">
|
|
||||||
<div class="user-info">
|
|
||||||
<img :src="userResult.avatar || defaultAvatar" class="avatar" />
|
|
||||||
<div class="detail">
|
|
||||||
<span class="name">{{ userResult.nickName }}</span>
|
|
||||||
<span class="id">ID: {{ userResult.username }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="add-action-btn" @click="goToAddForm">添加好友</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="hasSearched && !userResult" class="empty-state">
|
|
||||||
未找到该用户,请检查输入
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="step === 2" class="step-wrapper form-container">
|
|
||||||
<div class="form-item">
|
|
||||||
<label>备注名</label>
|
|
||||||
<input
|
|
||||||
v-model="form.remark"
|
|
||||||
placeholder="为好友起个备注吧"
|
|
||||||
class="form-input"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-item">
|
|
||||||
<label>验证信息</label>
|
|
||||||
<textarea
|
|
||||||
v-model="form.description"
|
|
||||||
placeholder="我是..."
|
|
||||||
rows="3"
|
|
||||||
class="form-textarea"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="btn-submit"
|
|
||||||
@click="submitAdd"
|
|
||||||
:disabled="submitting"
|
|
||||||
>
|
|
||||||
{{ submitting ? '发送中...' : '提交申请' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive } from 'vue';
|
import { ref, reactive } from 'vue';
|
||||||
import { friendService } from '@/services/friend';
|
import { friendService } from '@/services/friend';
|
||||||
import { useMessage } from '../messages/useAlert';
|
import { useMessage } from '../messages/useAlert';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({ modelValue: Boolean });
|
||||||
modelValue: Boolean
|
|
||||||
});
|
|
||||||
const emit = defineEmits(['update:modelValue', 'success']);
|
const emit = defineEmits(['update:modelValue', 'success']);
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
|
||||||
// 基础状态
|
// 状态管理
|
||||||
const step = ref(1);
|
const step = ref(1);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
const hasSearched = ref(false);
|
const hasSearched = ref(false);
|
||||||
const defaultAvatar = 'https://api.dicebear.com/7.x/adventurer/svg?seed=Lucky';
|
|
||||||
|
|
||||||
// 数据
|
|
||||||
const keyword = ref('');
|
|
||||||
const userResult = ref(null);
|
const userResult = ref(null);
|
||||||
const form = reactive({
|
|
||||||
remark: '',
|
const keyword = ref('');
|
||||||
description: '你好,想加你为好友'
|
const form = reactive({ remark: '', description: '你好,想加你为好友' });
|
||||||
});
|
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
emit('update:modelValue', false);
|
emit('update:modelValue', false);
|
||||||
// 延迟重置,避免动画没做完就变白
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
step.value = 1;
|
step.value = 1;
|
||||||
userResult.value = null;
|
userResult.value = null;
|
||||||
hasSearched.value = false;
|
|
||||||
keyword.value = '';
|
keyword.value = '';
|
||||||
form.remark = '';
|
hasSearched.value = false;
|
||||||
form.description = '你好,想加你为好友';
|
|
||||||
}, 300);
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 搜索逻辑
|
|
||||||
const onSearch = async () => {
|
const onSearch = async () => {
|
||||||
if (!keyword.value.trim()) return;
|
if (!keyword.value.trim()) return;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
hasSearched.value = false;
|
|
||||||
try {
|
try {
|
||||||
const res = await friendService.findUser(keyword.value);
|
const res = await friendService.findUser(keyword.value);
|
||||||
userResult.value = res.data;
|
userResult.value = res.data;
|
||||||
if (res.data) {
|
if (res.data) form.remark = res.data.nickName;
|
||||||
form.remark = res.data.nickName; // 默认备注为昵称
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
hasSearched.value = true;
|
hasSearched.value = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const goToAddForm = () => {
|
|
||||||
step.value = 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 提交逻辑
|
|
||||||
const submitAdd = async () => {
|
const submitAdd = async () => {
|
||||||
if (submitting.value) return;
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
const res = await friendService.requestFriend({
|
const res = await friendService.requestFriend({
|
||||||
toUserId: userResult.value.id, // 根据你后端字段调整
|
toUserId: userResult.value.id,
|
||||||
remarkName: form.remark,
|
remarkName: form.remark,
|
||||||
description: form.description
|
description: form.description
|
||||||
});
|
});
|
||||||
if(res.code == 0){
|
if (res.code == 0) message.success('已发送请求');
|
||||||
message.success('已发送好友请求');
|
else message.error(res.message);
|
||||||
}else{
|
submitting.value = false;
|
||||||
message.error(res.message);
|
close();
|
||||||
}
|
|
||||||
submitting.value = false;
|
|
||||||
close();
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<div v-if="modelValue" class="overlay" @click.self="close">
|
||||||
|
<div class="mini-modal">
|
||||||
|
<header>
|
||||||
|
<button v-if="step === 2" @click="step = 1" class="back-btn">←</button>
|
||||||
|
<span class="title">{{ step === 1 ? '添加好友' : '验证信息' }}</span>
|
||||||
|
<button @click="close" class="close-btn">✕</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div v-if="step === 1">
|
||||||
|
<div class="search-bar">
|
||||||
|
<input v-model="keyword" placeholder="搜索 ID / 手机号" @keyup.enter="onSearch" />
|
||||||
|
<button @click="onSearch" :disabled="loading">
|
||||||
|
{{ loading ? '...' : '搜索' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="userResult" class="result-card">
|
||||||
|
<img :src="userResult.avatar" class="mini-avatar" />
|
||||||
|
<div class="info">
|
||||||
|
<div class="name">{{ userResult.nickName }}</div>
|
||||||
|
<div class="id">ID: {{ userResult.username }}</div>
|
||||||
|
</div>
|
||||||
|
<button class="next-btn" @click="step = 2">添加</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="hasSearched" class="empty">未找到用户</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="step === 2" class="form">
|
||||||
|
<div class="f-item">
|
||||||
|
<label>备注</label>
|
||||||
|
<input v-model="form.remark" class="f-input" />
|
||||||
|
</div>
|
||||||
|
<div class="f-item">
|
||||||
|
<label>留言</label>
|
||||||
|
<textarea v-model="form.description" rows="2" class="f-input"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer v-if="step === 2">
|
||||||
|
<button class="submit-btn" :disabled="submitting" @click="submitAdd">
|
||||||
|
{{ submitting ? '发送中...' : '发送申请' }}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 1. 基础布局与遮罩 */
|
.overlay {
|
||||||
.modal-mask {
|
position: fixed; inset: 0; background: rgba(0,0,0,0.4);
|
||||||
position: fixed;
|
display: flex; align-items: center; justify-content: center; z-index: 1000;
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.2);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-container {
|
.mini-modal {
|
||||||
background: #ffffff;
|
background: white; width: 300px; border-radius: 12px; overflow: hidden;
|
||||||
width: 380px;
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||||
border-radius: 28px;
|
|
||||||
padding: 28px;
|
|
||||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 2. 头部样式 */
|
header {
|
||||||
.modal-header {
|
padding: 12px; display: flex; align-items: center;
|
||||||
display: flex;
|
border-bottom: 1px solid #f0f0f0; background: #fafafa;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-title {
|
.title { flex: 1; font-size: 14px; font-weight: bold; text-align: center; }
|
||||||
display: flex;
|
.back-btn, .close-btn { background: none; border: none; cursor: pointer; color: #999; padding: 4px; }
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
main { padding: 12px; }
|
||||||
|
|
||||||
|
/* 搜索条 */
|
||||||
|
.search-bar {
|
||||||
|
display: flex; background: #f0f0f0; border-radius: 6px; padding: 4px;
|
||||||
|
}
|
||||||
|
.search-bar input {
|
||||||
|
flex: 1; background: transparent; border: none; outline: none;
|
||||||
|
padding: 4px 8px; font-size: 13px;
|
||||||
|
}
|
||||||
|
.search-bar button {
|
||||||
|
background: #007aff; color: white; border: none;
|
||||||
|
padding: 4px 10px; border-radius: 4px; font-size: 12px; cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header h2 {
|
/* 结果卡片 */
|
||||||
font-size: 1.2rem;
|
.result-card {
|
||||||
font-weight: 700;
|
margin-top: 12px; display: flex; align-items: center;
|
||||||
color: #1d1d1f;
|
padding: 10px; background: #f9f9f9; border-radius: 8px;
|
||||||
margin: 0;
|
}
|
||||||
|
.mini-avatar { width: 40px; height: 40px; border-radius: 50%; margin-right: 10px; }
|
||||||
|
.info { flex: 1; }
|
||||||
|
.name { font-size: 14px; font-weight: bold; color: #333; }
|
||||||
|
.id { font-size: 11px; color: #999; }
|
||||||
|
.next-btn {
|
||||||
|
background: #007aff; color: white; border: none;
|
||||||
|
padding: 5px 12px; border-radius: 15px; font-size: 12px; cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-back-icon {
|
/* 表单区 */
|
||||||
background: none;
|
.f-item { margin-bottom: 10px; }
|
||||||
border: none;
|
.f-item label { display: block; font-size: 12px; color: #999; margin-bottom: 4px; }
|
||||||
font-size: 1.2rem;
|
.f-input {
|
||||||
cursor: pointer;
|
width: 100%; border: 1px solid #eee; border-radius: 4px;
|
||||||
color: #007aff;
|
padding: 8px; box-sizing: border-box; font-size: 13px; outline: none;
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
|
.f-input:focus { border-color: #007aff; }
|
||||||
|
|
||||||
.icon-close {
|
footer { padding: 0 12px 12px; }
|
||||||
background: #f5f5f7;
|
.submit-btn {
|
||||||
border: none;
|
width: 100%; padding: 10px; background: #007aff; color: white;
|
||||||
width: 28px;
|
border: none; border-radius: 6px; font-weight: bold; cursor: pointer;
|
||||||
height: 28px;
|
|
||||||
border-radius: 50%;
|
|
||||||
color: #86868b;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
}
|
||||||
|
.submit-btn:disabled { opacity: 0.6; }
|
||||||
|
|
||||||
.icon-close:hover {
|
.empty { text-align: center; padding: 20px; font-size: 12px; color: #ccc; }
|
||||||
background: #e8e8ed;
|
|
||||||
color: #1d1d1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 3. 搜索区域 */
|
|
||||||
.search-box {
|
|
||||||
display: flex;
|
|
||||||
background: #f5f5f7;
|
|
||||||
border-radius: 14px;
|
|
||||||
padding: 6px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-box:focus-within {
|
|
||||||
background: #fff;
|
|
||||||
border-color: #007aff;
|
|
||||||
box-shadow: 0 0 0 4px rgba(0, 122, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-box input {
|
|
||||||
flex: 1;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 14px;
|
|
||||||
outline: none;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #1d1d1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-btn {
|
|
||||||
background: #007aff;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 0 18px;
|
|
||||||
border-radius: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-btn:disabled {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 4. 用户卡片 */
|
|
||||||
.user-card {
|
|
||||||
background: #f5f5f7;
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: 72px;
|
|
||||||
height: 72px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 17px;
|
|
||||||
color: #1d1d1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.id {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #86868b;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-action-btn {
|
|
||||||
width: 100%;
|
|
||||||
background: #007aff;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-action-btn:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 5. 表单样式 */
|
|
||||||
.form-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-item label {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #86868b;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input, .form-textarea {
|
|
||||||
width: 100%;
|
|
||||||
background: #f5f5f7;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
font-size: 14px;
|
|
||||||
outline: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input:focus, .form-textarea:focus {
|
|
||||||
background: #fff;
|
|
||||||
border-color: #007aff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-submit {
|
|
||||||
background: #007aff;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 14px;
|
|
||||||
border-radius: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 6. 动画与反馈 */
|
|
||||||
.spinner {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border: 2px solid rgba(255,255,255,0.3);
|
|
||||||
border-top-color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
|
|
||||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
|
||||||
|
|
||||||
.fade-slide-enter-active {
|
|
||||||
transition: all 0.4s cubic-bezier(0.18, 0.89, 0.32, 1.28);
|
|
||||||
}
|
|
||||||
.fade-slide-enter-from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
color: #86868b;
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 30px 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export const FRIEND_ACTIONS = Object.freeze({
|
||||||
|
/**接受 */
|
||||||
|
Accept: 'Accept',
|
||||||
|
/**拒绝 */
|
||||||
|
Reject: 'Reject'
|
||||||
|
});
|
||||||
|
|
||||||
|
export const FRIEND_REQUEST_STATUS = Object.freeze({
|
||||||
|
/**待处理 */
|
||||||
|
Pending: 'Pending',
|
||||||
|
/**通过 */
|
||||||
|
Passed: 'Passed',
|
||||||
|
/**已拒绝 */
|
||||||
|
Declined: 'Declined',
|
||||||
|
/**已拉黑 */
|
||||||
|
Blocked: 'Blocked'
|
||||||
|
})
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export const SYSTEM_BASE_STATUS = Object.freeze({
|
||||||
|
SUCCESS: 0
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { request } from "./api";
|
import { request } from "./api";
|
||||||
|
import { FRIEND_ACTIONS } from "@/constants/friendAction";
|
||||||
|
|
||||||
export const friendService = {
|
export const friendService = {
|
||||||
|
|
||||||
@@ -28,5 +29,16 @@ export const friendService = {
|
|||||||
* @param {*} limit
|
* @param {*} limit
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
getFriendRequests: (page = 1, limit = 100) => request.get(`/friend/requests?page=${page}&limit=${limit}`)
|
getFriendRequests: (page = 1, limit = 100) => request.get(`/friend/requests?page=${page}&limit=${limit}`),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理好友请求
|
||||||
|
* @param {*} friendRequestId
|
||||||
|
* @param {typeof FRIEND_ACTIONS[keyof typeof FRIEND_ACTIONS]} action
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
handleFriendRequest: (friendRequestId, action, remarkname) => request.post(`/Friend/HandleRequest?id=${friendRequestId}`, {
|
||||||
|
remarkName: remarkname,
|
||||||
|
action: action
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@ const STORE_NAME = 'messages';
|
|||||||
const CONVERSARION_STORE_NAME = 'conversations';
|
const CONVERSARION_STORE_NAME = 'conversations';
|
||||||
const CONTACT_STORE_NAME = 'contacts';
|
const CONTACT_STORE_NAME = 'contacts';
|
||||||
|
|
||||||
export const dbPromise = openDB(DBNAME, 4, {
|
export const dbPromise = openDB(DBNAME, 5, {
|
||||||
upgrade(db) {
|
upgrade(db) {
|
||||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||||
const store = db.createObjectStore(STORE_NAME, { keyPath: 'msgId' });
|
const store = db.createObjectStore(STORE_NAME, { keyPath: 'msgId' });
|
||||||
@@ -21,6 +21,7 @@ export const dbPromise = openDB(DBNAME, 4, {
|
|||||||
const store = db.createObjectStore(CONTACT_STORE_NAME, { keyPath: 'id' });
|
const store = db.createObjectStore(CONTACT_STORE_NAME, { keyPath: 'id' });
|
||||||
store.createIndex('by-id', 'id');
|
store.createIndex('by-id', 'id');
|
||||||
store.createIndex('by-username', 'username');
|
store.createIndex('by-username', 'username');
|
||||||
|
store.createIndex('by-friendId', 'friendId', { unique: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -23,18 +23,13 @@
|
|||||||
<div class="name">标签</div>
|
<div class="name">标签</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="contactTab">
|
||||||
<div class="group-title">我的好友</div>
|
<button class="group-title" :class="{'group-title-active': contactTab === 0}" @click="contactTab = 0">我的好友</button>
|
||||||
<div v-for="c in filteredContacts"
|
<button class="group-title" :class="{'group-title-active': contactTab === 1}" @click="contactTab = 1">群聊</button>
|
||||||
:key="c.id"
|
|
||||||
class="list-item"
|
|
||||||
:class="{active: activeContactId === c.id}"
|
|
||||||
@click="routeUserInfo(c.id)">
|
|
||||||
<img :src="c.userInfo.avatar" class="avatar-std" />
|
|
||||||
<div class="info">
|
|
||||||
<div class="name">{{ c.remarkName }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<contactShow v-if="contactTab == 0" :contacts="filteredContacts"></contactShow>
|
||||||
|
<groupsShow v-if="contactTab == 1" :groups="myGroups"></groupsShow>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<RouterView></RouterView>
|
<RouterView></RouterView>
|
||||||
@@ -54,14 +49,36 @@ import GroupChatModal from '@/components/groups/GroupChatModal.vue'
|
|||||||
import feather from 'feather-icons';
|
import feather from 'feather-icons';
|
||||||
import { useContactStore } from '@/stores/contact';
|
import { useContactStore } from '@/stores/contact';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
import contactShow from '@/components/contacts/contactShow.vue';
|
||||||
|
import groupsShow from '@/components/groups/groupsShow.vue';
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const activeContactId = ref(null)
|
|
||||||
const contactStore = useContactStore();
|
const contactStore = useContactStore();
|
||||||
|
|
||||||
const groupModal = ref(false);
|
const groupModal = ref(false);
|
||||||
|
const contactTab = ref(0);
|
||||||
|
|
||||||
|
const myGroups = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "产品设计交流群",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=1",
|
||||||
|
lastMessage: "那个UI设计的初稿已经发在群文件了,大家记得看下。",
|
||||||
|
lastTime: "14:20",
|
||||||
|
unread: 3,
|
||||||
|
online: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "周五羽毛球小分队",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=2",
|
||||||
|
lastMessage: "这周五晚上 8 点,老地方见!",
|
||||||
|
lastTime: "昨天",
|
||||||
|
unread: 0,
|
||||||
|
online: false
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
const filteredContacts = computed(() => {
|
const filteredContacts = computed(() => {
|
||||||
@@ -82,10 +99,7 @@ const filteredContacts = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const routeUserInfo = (id) => {
|
|
||||||
router.push(`/contacts/info/${id}`);
|
|
||||||
activeContactId.value = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送事件给父组件(用于切换回聊天Tab并打开会话)
|
// 发送事件给父组件(用于切换回聊天Tab并打开会话)
|
||||||
const emit = defineEmits(['start-chat'])
|
const emit = defineEmits(['start-chat'])
|
||||||
@@ -95,6 +109,7 @@ const showGroupList = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await contactStore.loadContactList();
|
await contactStore.loadContactList();
|
||||||
})
|
})
|
||||||
@@ -145,11 +160,38 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.group-title {
|
.group-title {
|
||||||
padding: 8px 12px;
|
width: 40%;
|
||||||
|
padding: 5px 14px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #999;
|
margin: 5px;
|
||||||
|
border: none;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.group-title:hover {
|
||||||
|
color: #8e8e8e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-title-active {
|
||||||
|
background-color: white;
|
||||||
|
color: rgb(78, 78, 249);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixed-entries {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #dcdcdc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contactTab {
|
||||||
|
width: 90%;
|
||||||
|
margin: 10px auto;
|
||||||
|
background: #e0e0e0;
|
||||||
|
display: flex;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
.list-item {
|
.list-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
|
|||||||
@@ -17,20 +17,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<template v-if="item.state === 0 && item.requestUser != authStore.userInfo.id">
|
<template v-if="item.state === FRIEND_REQUEST_STATUS.Pending && item.requestUser != authStore.userInfo.id">
|
||||||
<button class="btn-text btn-reject" @click="item.status = 2">拒绝</button>
|
<button class="btn-text btn-reject" @click="confirmReject(item)">拒绝</button>
|
||||||
<button class="btn-text btn-accept" @click="item.status = 1">接受</button>
|
<button class="btn-text btn-accept" @click="handleOpenDialog(item)">接受</button>
|
||||||
</template>
|
</template>
|
||||||
<span v-else-if="item.state === 0" class="status-label">
|
<span v-else-if="item.state === FRIEND_REQUEST_STATUS.Pending" class="status-label">
|
||||||
待对方同意
|
待对方同意
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="item.state === 1" class="status-label">
|
<span v-else-if="item.state === FRIEND_REQUEST_STATUS.Declined" class="status-label">
|
||||||
{{item.requestUser != authStore.userInfo.id ? '已拒绝' : '对方拒绝'}}
|
{{item.requestUser != authStore.userInfo.id ? '已拒绝' : '对方拒绝'}}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="item.state === 2" class="status-label">
|
<span v-else-if="item.state === FRIEND_REQUEST_STATUS.Passed" class="status-label">
|
||||||
已添加
|
已添加
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="item.state === 3 && item.requestUser != authStore.userInfo.id" class="status-label">
|
<span v-else-if="item.state === FRIEND_REQUEST_STATUS.Blocked && item.requestUser != authStore.userInfo.id" class="status-label">
|
||||||
已拉黑
|
已拉黑
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="status-label">
|
<span v-else class="status-label">
|
||||||
@@ -40,6 +40,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="showDialog" class="modal-mask">
|
||||||
|
<div class="modal-box">
|
||||||
|
<div class="modal-header">添加备注</div>
|
||||||
|
<input v-model="remarkName" class="modal-input" placeholder="备注姓名" focus />
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="modal-btn-cancel" @click="showDialog = false">取消</button>
|
||||||
|
<button class="modal-btn-confirm" @click="confirmAccept">确定</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -49,6 +59,8 @@ import { friendService } from '@/services/friend';
|
|||||||
import { useMessage } from '@/components/messages/useAlert';
|
import { useMessage } from '@/components/messages/useAlert';
|
||||||
import { formatDate } from '@/utils/formatDate';
|
import { formatDate } from '@/utils/formatDate';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
import { FRIEND_ACTIONS, FRIEND_REQUEST_STATUS } from '@/constants/friendAction';
|
||||||
|
import { SYSTEM_BASE_STATUS } from '@/constants/systemBaseStatus';
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
@@ -64,6 +76,50 @@ const loadFriendRequests = async () => {
|
|||||||
requests.value = res.data;
|
requests.value = res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showDialog = ref(false);
|
||||||
|
const remarkName = ref('');
|
||||||
|
const activeItem = ref(null);
|
||||||
|
|
||||||
|
const handleOpenDialog = (item) => {
|
||||||
|
activeItem.value = item;
|
||||||
|
remarkName.value = item.nickName; // 默认备注为昵称
|
||||||
|
showDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmAccept = async () => {
|
||||||
|
if (!activeItem.value) return;
|
||||||
|
await handleFriendRequest(FRIEND_ACTIONS.Accept)
|
||||||
|
activeItem.value.state = FRIEND_REQUEST_STATUS.Passed;
|
||||||
|
showDialog.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmReject = async (item) => {
|
||||||
|
if(!item) return;
|
||||||
|
activeItem.value = item;
|
||||||
|
await handleFriendRequest(FRIEND_ACTIONS.Reject);
|
||||||
|
activeItem.value.state = FRIEND_REQUEST_STATUS.Declined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFriendRequest = async (action) => {
|
||||||
|
const res = await friendService.handleFriendRequest(activeItem.value.id,action,activeItem.value.remarkName);
|
||||||
|
if(res.code == SYSTEM_BASE_STATUS.SUCCESS){
|
||||||
|
switch(action){
|
||||||
|
case FRIEND_ACTIONS.Accept:
|
||||||
|
message.show('添加好友成功');
|
||||||
|
break;
|
||||||
|
case FRIEND_ACTIONS.Reject:
|
||||||
|
message.show('已拒绝');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
message.error('无效的操作');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
message.error(res.message);
|
||||||
|
console.log('好友请求处理异常:', res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadFriendRequests();
|
await loadFriendRequests();
|
||||||
})
|
})
|
||||||
@@ -79,6 +135,7 @@ onMounted(async () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-limit {
|
.content-limit {
|
||||||
@@ -197,4 +254,70 @@ onMounted(async () => {
|
|||||||
color: #d2d2d7;
|
color: #d2d2d7;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 弹窗遮罩:毛玻璃效果 */
|
||||||
|
.modal-mask {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.4);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗主体:延续你的极简白 */
|
||||||
|
.modal-box {
|
||||||
|
background: #fff;
|
||||||
|
width: 280px;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框:延续你的微灰色调 */
|
||||||
|
.modal-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: none;
|
||||||
|
background: #f5f5f7;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮:完全复用你原本的 btn-text 逻辑 */
|
||||||
|
.modal-btn-cancel {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border: none;
|
||||||
|
background: #f5f5f7;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #86868b;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn-confirm {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border: none;
|
||||||
|
background: #007aff;
|
||||||
|
color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -59,6 +59,7 @@ import { generateSessionId } from '@/utils/sessionIdTools';
|
|||||||
import { useSignalRStore } from '@/stores/signalr';
|
import { useSignalRStore } from '@/stores/signalr';
|
||||||
import { useConversationStore } from '@/stores/conversation';
|
import { useConversationStore } from '@/stores/conversation';
|
||||||
import feather from 'feather-icons';
|
import feather from 'feather-icons';
|
||||||
|
import { onBeforeRouteUpdate } from 'vue-router';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
id:{
|
id:{
|
||||||
@@ -142,13 +143,22 @@ async function loadConversation(conversationId) {
|
|||||||
conversationInfo.value = conversationStore.conversations.find(x => x.id == Number(conversationId));
|
conversationInfo.value = conversationStore.conversations.find(x => x.id == Number(conversationId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化时滚动到底部
|
const initChat = async (newId) => {
|
||||||
onMounted(async () => {
|
await loadConversation(newId);
|
||||||
await loadConversation(props.id);
|
|
||||||
const sessionid = generateSessionId(conversationInfo.value.userId, conversationInfo.value.targetId)
|
const sessionid = generateSessionId(conversationInfo.value.userId, conversationInfo.value.targetId)
|
||||||
await chatStore.swtichSession(sessionid,props.id);
|
await chatStore.swtichSession(sessionid,newId);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// 监听路由参数
|
||||||
|
watch(
|
||||||
|
() => props.id,
|
||||||
|
async (newId) => {
|
||||||
|
await initChat(newId)
|
||||||
|
},
|
||||||
|
{ immediate: true } // 组件第一次挂载(刷新页面进入)时会立即执行一次
|
||||||
|
)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
|
|
||||||
<RouterView></RouterView>
|
<RouterView></RouterView>
|
||||||
<SearchUser v-model="searchUserModal"/>
|
<SearchUser v-model="searchUserModal"/>
|
||||||
|
<CreateGroup v-model="createGroupModal"></CreateGroup>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ import { useConversationStore } from '@/stores/conversation'
|
|||||||
import AddMenu from '@/components/addMenu.vue'
|
import AddMenu from '@/components/addMenu.vue'
|
||||||
import feather from 'feather-icons'
|
import feather from 'feather-icons'
|
||||||
import SearchUser from '@/components/user/SearchUser.vue'
|
import SearchUser from '@/components/user/SearchUser.vue'
|
||||||
|
import CreateGroup from '@/components/groups/CreateGroup.vue'
|
||||||
import { useBrowserNotification } from '@/services/useBrowserNotification'
|
import { useBrowserNotification } from '@/services/useBrowserNotification'
|
||||||
|
|
||||||
const conversationStore = useConversationStore();
|
const conversationStore = useConversationStore();
|
||||||
@@ -56,25 +58,26 @@ const browserNotification = useBrowserNotification();
|
|||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const activeId = ref(1)
|
const activeId = ref(1)
|
||||||
const searchUserModal = ref(false);
|
const searchUserModal = ref(false);
|
||||||
|
const createGroupModal = ref(false);
|
||||||
const msgTitleShow = ref(false);
|
const msgTitleShow = ref(false);
|
||||||
const addMenuList = [
|
const addMenuList = [
|
||||||
{
|
{
|
||||||
text: '发起群聊',
|
text: '发起群聊',
|
||||||
action: 'createGroup',
|
action: 'createGroup',
|
||||||
// 气泡/对话图标
|
// 气泡/对话图标
|
||||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>`
|
icon: feather.icons['message-square'].toSvg()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: '添加朋友',
|
text: '添加朋友',
|
||||||
action: 'addFriend',
|
action: 'addFriend',
|
||||||
// 人像+号图标
|
// 人像+号图标
|
||||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="17" y1="11" x2="23" y2="11"></line></svg>`
|
icon: feather.icons['user-plus'].toSvg()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: '新建笔记',
|
text: '新建笔记',
|
||||||
action: 'newNote',
|
action: 'newNote',
|
||||||
// 书本/笔记本图标
|
// 书本/笔记本图标
|
||||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>`
|
icon: feather.icons['book-open'].toSvg()
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -90,6 +93,8 @@ function actionHandler(type){
|
|||||||
case 'addFriend':
|
case 'addFriend':
|
||||||
searchUserModal.value = true;
|
searchUserModal.value = true;
|
||||||
break;
|
break;
|
||||||
|
case 'createGroup':
|
||||||
|
createGroupModal.value = true;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,261 @@
|
|||||||
<template></template>
|
<template>
|
||||||
|
<div class="im-settings-container">
|
||||||
|
<section class="menu-sidebar">
|
||||||
|
<h2 class="sidebar-title">系统设置</h2>
|
||||||
|
<div class="menu-list">
|
||||||
|
<button
|
||||||
|
v-for="item in menuItems"
|
||||||
|
:key="item.id"
|
||||||
|
:class="['menu-item', { active: activeTab === item.id }]"
|
||||||
|
@click="activeTab = item.id"
|
||||||
|
>
|
||||||
|
<span class="item-icon" v-html="item.icon"></span>
|
||||||
|
<span class="item-text">{{ item.name }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-footer">版本 v2.4.0</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<main class="main-panel">
|
||||||
|
<header class="panel-header">
|
||||||
|
<h1>{{ currentMenuName }}</h1>
|
||||||
|
<p>配置您的个性化通讯偏好</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="panel-body">
|
||||||
|
<div v-if="activeTab === 'notifications'" class="setting-group">
|
||||||
|
<div v-for="(val, key) in notificationSettings" :key="key" class="setting-row">
|
||||||
|
<div class="row-info">
|
||||||
|
<div class="row-label">{{ key }}</div>
|
||||||
|
<div class="row-desc">开启后系统将实时同步您的通知偏好</div>
|
||||||
|
</div>
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" v-model="notificationSettings[key]">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="empty-placeholder">
|
||||||
|
<span class="icon">⚙️</span>
|
||||||
|
<p>{{ currentMenuName }} 功能开发中...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="panel-footer">
|
||||||
|
<button class="btn-cancel" @click="reset">重置</button>
|
||||||
|
<button class="btn-save" @click="save">保存更改</button>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed } from 'vue'
|
||||||
|
import feather from 'feather-icons'
|
||||||
|
|
||||||
|
const activeTab = ref('notifications')
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ id: 'profile', name: '个人资料', icon: feather.icons['user-check'].toSvg() },
|
||||||
|
{ id: 'notifications', name: '通知设置', icon: feather.icons['bell'].toSvg() },
|
||||||
|
{ id: 'security', name: '账号安全', icon: feather.icons['shield'].toSvg() },
|
||||||
|
{ id: 'general', name: '通用设置', icon: feather.icons['settings'].toSvg() }
|
||||||
|
]
|
||||||
|
|
||||||
|
const currentMenuName = computed(() => menuItems.find(i => i.id === activeTab.value)?.name)
|
||||||
|
|
||||||
|
const notificationSettings = reactive({
|
||||||
|
'声音提醒': true,
|
||||||
|
'桌面弹窗': true,
|
||||||
|
'仅在免打扰外提醒': false
|
||||||
|
})
|
||||||
|
|
||||||
|
const save = () => alert('设置已生效')
|
||||||
|
const reset = () => location.reload()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 变量定义 */
|
||||||
|
:component {
|
||||||
|
--active-color: #00a884;
|
||||||
|
--bg-sidebar: #f7f9fa;
|
||||||
|
--bg-hover: #f0f2f5;
|
||||||
|
--text-main: #111b21;
|
||||||
|
--text-dim: #667781;
|
||||||
|
--border: #e9edef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 核心容器:填满外部 */
|
||||||
|
.im-settings-container {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%; /* 绝对铺满 */
|
||||||
|
background: #ffffff;
|
||||||
|
font-family: sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧样式 */
|
||||||
|
.menu-sidebar {
|
||||||
|
width: 260px;
|
||||||
|
background: #eee;
|
||||||
|
border-right: 1px solid #d6d6d6;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
padding: 30px 24px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-main);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-list {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
transition: 0.2s;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover { background: #c6c6c6; }
|
||||||
|
|
||||||
|
.menu-item.active {
|
||||||
|
background: #c6c6c6;
|
||||||
|
color: var(--text-main);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-icon { margin-right: 12px; font-size: 18px; }
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧内容样式 */
|
||||||
|
.main-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
padding: 40px 50px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header h1 { font-size: 26px; color: var(--text-main); margin: 0 0 8px 0; }
|
||||||
|
.panel-header p { font-size: 14px; color: var(--text-dim); margin: 0; }
|
||||||
|
|
||||||
|
.panel-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 50px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24px 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-label { font-weight: 500; color: var(--text-main); margin-bottom: 4px; }
|
||||||
|
.row-desc { font-size: 13px; color: var(--text-dim); }
|
||||||
|
|
||||||
|
/* 开关组件 */
|
||||||
|
.toggle-switch {
|
||||||
|
position: relative;
|
||||||
|
width: 46px;
|
||||||
|
height: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.toggle-switch input { opacity: 0; width: 0; height: 0; }
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
inset: 0;
|
||||||
|
background-color: #d1d5db;
|
||||||
|
transition: .3s;
|
||||||
|
border-radius: 24px;
|
||||||
|
}
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
left: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
background-color: white;
|
||||||
|
transition: .3s;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
input:checked + .slider { background-color: #0075ae; }
|
||||||
|
input:checked + .slider:before { transform: translateX(22px); }
|
||||||
|
|
||||||
|
/* 底部操作 */
|
||||||
|
.panel-footer {
|
||||||
|
padding: 30px 50px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-dim);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save {
|
||||||
|
background: #000000;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 32px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 168, 132, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 200px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式:窄容器自动堆叠 */
|
||||||
|
@media (max-width: 650px) {
|
||||||
|
.im-settings-container { flex-direction: column; }
|
||||||
|
.menu-sidebar { width: 100%; border-right: none; border-bottom: 1px solid var(--border); }
|
||||||
|
.menu-list { display: flex; overflow-x: auto; padding: 10px; }
|
||||||
|
.menu-item { white-space: nowrap; width: auto; margin-right: 8px; margin-bottom: 0; }
|
||||||
|
.sidebar-title, .sidebar-footer { display: none; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user