后端:
add(GroupNotification):合并群通知表
This commit is contained in:
parent
f7223dc590
commit
eb8455e141
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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+2ecaa28091b41de707825db3628d380b62fa727f")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f7223dc5904286fdf8e117b56c37f58dc431d68b")]
|
||||||
[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 @@
|
|||||||
ed4980dfc7aff253176b260ed9015f9a80b52e92cbf3095eff3ed06865ea6e0d
|
3c0a5335719f892c65744a9df7eae9fd7b12de9067131f5c9f4cb65f2b27b8d4
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -15,7 +15,7 @@ namespace IM_API.Application.EventHandlers.GroupInviteActionUpdateHandler
|
|||||||
public async Task Consume(ConsumeContext<GroupInviteActionUpdateEvent> context)
|
public async Task Consume(ConsumeContext<GroupInviteActionUpdateEvent> context)
|
||||||
{
|
{
|
||||||
var @event = context.Message;
|
var @event = context.Message;
|
||||||
if(@event.Action == Models.GroupInviteState.Passed)
|
if(@event.Action == Models.GroupRequestState.Passed)
|
||||||
{
|
{
|
||||||
await _groupService.MakeGroupRequestAsync(@event.UserId, @event.InviteUserId,@event.GroupId);
|
await _groupService.MakeGroupRequestAsync(@event.UserId, @event.InviteUserId,@event.GroupId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -229,6 +229,20 @@ namespace IM_API.Configs
|
|||||||
.ForMember(dest => dest.AllMembersBanned, opt => opt.MapFrom(src => src.AllMembersBannedEnum))
|
.ForMember(dest => dest.AllMembersBanned, opt => opt.MapFrom(src => src.AllMembersBannedEnum))
|
||||||
.ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.StatusEnum))
|
.ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.StatusEnum))
|
||||||
;
|
;
|
||||||
|
|
||||||
|
//群通知模型转换
|
||||||
|
CreateMap<GroupRequest, GroupNotificationVo>()
|
||||||
|
.ForMember(dest => dest.UserId, opt => opt.MapFrom(src => src.UserId))
|
||||||
|
.ForMember(dest => dest.GroupId, opt => opt.MapFrom(src => src.GroupId))
|
||||||
|
.ForMember(dest => dest.InviteUser, opt => opt.MapFrom(src => src.InviteUserId))
|
||||||
|
.ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.StateEnum))
|
||||||
|
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description))
|
||||||
|
.ForMember(dest => dest.RequestId, opt => opt.MapFrom(src => src.Id))
|
||||||
|
//.ForAllMembers(opt => opt.Ignore())
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,5 +96,14 @@ namespace IM_API.Controllers
|
|||||||
var group = await _groupService.GetGroupInfoAsync(groupId);
|
var group = await _groupService.GetGroupInfoAsync(groupId);
|
||||||
return Ok(new BaseResponse<GroupInfoVo>(group));
|
return Ok(new BaseResponse<GroupInfoVo>(group));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(BaseResponse<List<GroupNotificationVo>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<IActionResult> GetGroupNotification([FromQuery]int groupId)
|
||||||
|
{
|
||||||
|
string userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier)!;
|
||||||
|
var data = await _groupService.GetGroupNotificationAsync(int.Parse(userIdStr));
|
||||||
|
return Ok(new BaseResponse<List<GroupNotificationVo>>(data));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,6 @@ namespace IM_API.Domain.Events
|
|||||||
public int InviteUserId { get; set; }
|
public int InviteUserId { get; set; }
|
||||||
public int InviteId { get; set; }
|
public int InviteId { get; set; }
|
||||||
public int GroupId { get; set; }
|
public int GroupId { get; set; }
|
||||||
public GroupInviteState Action { get; set; }
|
public GroupRequestState Action { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,6 @@ namespace IM_API.Dtos.Group
|
|||||||
public class HandleGroupInviteDto
|
public class HandleGroupInviteDto
|
||||||
{
|
{
|
||||||
public int InviteId { get; set; }
|
public int InviteId { get; set; }
|
||||||
public GroupInviteState Action { get; set; }
|
public GroupRequestState Action { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,5 +56,12 @@ namespace IM_API.Interface.Services
|
|||||||
Task<GroupInfoVo> UpdateGroupInfoAsync(int userId, int groupId, GroupUpdateDto updateDto);
|
Task<GroupInfoVo> UpdateGroupInfoAsync(int userId, int groupId, GroupUpdateDto updateDto);
|
||||||
|
|
||||||
Task<GroupInfoVo> GetGroupInfoAsync(int groupId);
|
Task<GroupInfoVo> GetGroupInfoAsync(int groupId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取群聊通知
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<List<GroupNotificationVo>> GetGroupNotificationAsync(int userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1174
backend/IM_API/Migrations/20260309065303_update-group-announcement.Designer.cs
generated
Normal file
1174
backend/IM_API/Migrations/20260309065303_update-group-announcement.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,54 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace IM_API.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class updategroupannouncement : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "groups",
|
||||||
|
keyColumn: "Announcement",
|
||||||
|
keyValue: null,
|
||||||
|
column: "Announcement",
|
||||||
|
value: "");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Announcement",
|
||||||
|
table: "groups",
|
||||||
|
type: "text",
|
||||||
|
nullable: false,
|
||||||
|
comment: "群公告",
|
||||||
|
collation: "utf8mb4_general_ci",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "text",
|
||||||
|
oldNullable: true,
|
||||||
|
oldComment: "群公告")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("Relational:Collation", "utf8mb4_general_ci");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Announcement",
|
||||||
|
table: "groups",
|
||||||
|
type: "text",
|
||||||
|
nullable: true,
|
||||||
|
comment: "群公告",
|
||||||
|
collation: "utf8mb4_general_ci",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "text",
|
||||||
|
oldComment: "群公告")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("Relational:Collation", "utf8mb4_general_ci");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1101
backend/IM_API/Migrations/20260312094604_group-invite-request-merge.Designer.cs
generated
Normal file
1101
backend/IM_API/Migrations/20260312094604_group-invite-request-merge.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,87 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace IM_API.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class groupinviterequestmerge : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "group_invite");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "InviteUserId",
|
||||||
|
table: "group_request",
|
||||||
|
type: "int",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "InviteUserId",
|
||||||
|
table: "group_request");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "GroupId",
|
||||||
|
table: "group_request",
|
||||||
|
newName: "GroupId1");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "group_invite",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
ID = table.Column<int>(type: "int(11)", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
GroupId = table.Column<int>(type: "int(11)", nullable: false, comment: "群聊编号"),
|
||||||
|
InvitedUser = table.Column<int>(type: "int(11)", nullable: true, comment: "被邀请用户"),
|
||||||
|
InviteUser = table.Column<int>(type: "int(11)", nullable: true, comment: "邀请用户"),
|
||||||
|
Created = table.Column<DateTimeOffset>(type: "datetime", nullable: true, comment: "创建时间"),
|
||||||
|
State = table.Column<sbyte>(type: "tinyint(4)", nullable: true, comment: "当前状态(0:待被邀请人同意\r\n1:被邀请人已同意)")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PRIMARY", x => x.ID);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "group_invite_ibfk_1",
|
||||||
|
column: x => x.InviteUser,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "ID");
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "group_invite_ibfk_2",
|
||||||
|
column: x => x.GroupId,
|
||||||
|
principalTable: "groups",
|
||||||
|
principalColumn: "ID");
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "group_invite_ibfk_3",
|
||||||
|
column: x => x.InvitedUser,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "ID");
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.Annotation("Relational:Collation", "utf8mb4_general_ci");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "GroupId",
|
||||||
|
table: "group_invite",
|
||||||
|
column: "GroupId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "InvitedUser",
|
||||||
|
table: "group_invite",
|
||||||
|
column: "InvitedUser");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "InviteUser",
|
||||||
|
table: "group_invite",
|
||||||
|
column: "InviteUser");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -333,6 +333,7 @@ namespace IM_API.Migrations
|
|||||||
.HasComment("全员禁言(0允许发言,2全员禁言)");
|
.HasComment("全员禁言(0允许发言,2全员禁言)");
|
||||||
|
|
||||||
b.Property<string>("Announcement")
|
b.Property<string>("Announcement")
|
||||||
|
.IsRequired()
|
||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasComment("群公告");
|
.HasComment("群公告");
|
||||||
|
|
||||||
@ -392,50 +393,6 @@ namespace IM_API.Migrations
|
|||||||
MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci");
|
MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("IM_API.Models.GroupInvite", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("int(11)")
|
|
||||||
.HasColumnName("ID");
|
|
||||||
|
|
||||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("Created")
|
|
||||||
.HasColumnType("datetime")
|
|
||||||
.HasComment("创建时间");
|
|
||||||
|
|
||||||
b.Property<int>("GroupId")
|
|
||||||
.HasColumnType("int(11)")
|
|
||||||
.HasComment("群聊编号");
|
|
||||||
|
|
||||||
b.Property<int?>("InviteUser")
|
|
||||||
.HasColumnType("int(11)")
|
|
||||||
.HasComment("邀请用户");
|
|
||||||
|
|
||||||
b.Property<int?>("InvitedUser")
|
|
||||||
.HasColumnType("int(11)")
|
|
||||||
.HasComment("被邀请用户");
|
|
||||||
|
|
||||||
b.Property<sbyte?>("State")
|
|
||||||
.HasColumnType("tinyint(4)")
|
|
||||||
.HasComment("当前状态(0:待被邀请人同意\r\n1:被邀请人已同意)");
|
|
||||||
|
|
||||||
b.HasKey("Id")
|
|
||||||
.HasName("PRIMARY");
|
|
||||||
|
|
||||||
b.HasIndex(new[] { "GroupId" }, "GroupId");
|
|
||||||
|
|
||||||
b.HasIndex(new[] { "InviteUser" }, "InviteUser");
|
|
||||||
|
|
||||||
b.HasIndex(new[] { "InvitedUser" }, "InvitedUser");
|
|
||||||
|
|
||||||
b.ToTable("group_invite", (string)null);
|
|
||||||
|
|
||||||
MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4");
|
|
||||||
MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("IM_API.Models.GroupMember", b =>
|
modelBuilder.Entity("IM_API.Models.GroupMember", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@ -502,6 +459,9 @@ namespace IM_API.Migrations
|
|||||||
.HasColumnType("int(11)")
|
.HasColumnType("int(11)")
|
||||||
.HasComment("群聊编号\r\n");
|
.HasComment("群聊编号\r\n");
|
||||||
|
|
||||||
|
b.Property<int?>("InviteUserId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<sbyte>("State")
|
b.Property<sbyte>("State")
|
||||||
.HasColumnType("tinyint(4)")
|
.HasColumnType("tinyint(4)")
|
||||||
.HasComment("申请状态(0:待管理员同意,1:已拒绝,2:已同意)");
|
.HasComment("申请状态(0:待管理员同意,1:已拒绝,2:已同意)");
|
||||||
@ -515,8 +475,7 @@ namespace IM_API.Migrations
|
|||||||
|
|
||||||
b.HasIndex("UserId");
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
b.HasIndex(new[] { "GroupId" }, "GroupId")
|
b.HasIndex(new[] { "GroupId" }, "GroupId");
|
||||||
.HasDatabaseName("GroupId1");
|
|
||||||
|
|
||||||
b.ToTable("group_request", (string)null);
|
b.ToTable("group_request", (string)null);
|
||||||
|
|
||||||
@ -991,31 +950,6 @@ namespace IM_API.Migrations
|
|||||||
b.Navigation("GroupMasterNavigation");
|
b.Navigation("GroupMasterNavigation");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("IM_API.Models.GroupInvite", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("IM_API.Models.Group", "Group")
|
|
||||||
.WithMany("GroupInvites")
|
|
||||||
.HasForeignKey("GroupId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasConstraintName("group_invite_ibfk_2");
|
|
||||||
|
|
||||||
b.HasOne("IM_API.Models.User", "InviteUserNavigation")
|
|
||||||
.WithMany("GroupInviteInviteUserNavigations")
|
|
||||||
.HasForeignKey("InviteUser")
|
|
||||||
.HasConstraintName("group_invite_ibfk_1");
|
|
||||||
|
|
||||||
b.HasOne("IM_API.Models.User", "InvitedUserNavigation")
|
|
||||||
.WithMany("GroupInviteInvitedUserNavigations")
|
|
||||||
.HasForeignKey("InvitedUser")
|
|
||||||
.HasConstraintName("group_invite_ibfk_3");
|
|
||||||
|
|
||||||
b.Navigation("Group");
|
|
||||||
|
|
||||||
b.Navigation("InviteUserNavigation");
|
|
||||||
|
|
||||||
b.Navigation("InvitedUserNavigation");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("IM_API.Models.GroupMember", b =>
|
modelBuilder.Entity("IM_API.Models.GroupMember", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("IM_API.Models.Group", "Group")
|
b.HasOne("IM_API.Models.Group", "Group")
|
||||||
@ -1108,8 +1042,6 @@ namespace IM_API.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("IM_API.Models.Group", b =>
|
modelBuilder.Entity("IM_API.Models.Group", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("GroupInvites");
|
|
||||||
|
|
||||||
b.Navigation("GroupMembers");
|
b.Navigation("GroupMembers");
|
||||||
|
|
||||||
b.Navigation("GroupRequests");
|
b.Navigation("GroupRequests");
|
||||||
@ -1148,10 +1080,6 @@ namespace IM_API.Migrations
|
|||||||
|
|
||||||
b.Navigation("FriendUsers");
|
b.Navigation("FriendUsers");
|
||||||
|
|
||||||
b.Navigation("GroupInviteInviteUserNavigations");
|
|
||||||
|
|
||||||
b.Navigation("GroupInviteInvitedUserNavigations");
|
|
||||||
|
|
||||||
b.Navigation("GroupMembers");
|
b.Navigation("GroupMembers");
|
||||||
|
|
||||||
b.Navigation("GroupRequests");
|
b.Navigation("GroupRequests");
|
||||||
|
|||||||
@ -38,7 +38,7 @@ public partial class Group
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 群公告
|
/// 群公告
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Announcement { get; set; }
|
public string Announcement { get; set; } = "暂无群公告,点击编辑添加。";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 群聊创建时间
|
/// 群聊创建时间
|
||||||
@ -56,7 +56,6 @@ public partial class Group
|
|||||||
public DateTimeOffset LastUpdateTime { get; set; } = DateTime.UtcNow;
|
public DateTimeOffset LastUpdateTime { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
|
||||||
public virtual ICollection<GroupInvite> GroupInvites { get; set; } = new List<GroupInvite>();
|
|
||||||
|
|
||||||
public virtual User GroupMasterNavigation { get; set; } = null!;
|
public virtual User GroupMasterNavigation { get; set; } = null!;
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
namespace IM_API.Models
|
|
||||||
{
|
|
||||||
public partial class GroupInvite
|
|
||||||
{
|
|
||||||
public GroupInviteState StateEnum
|
|
||||||
{
|
|
||||||
get => (GroupInviteState)State;
|
|
||||||
set => State = (sbyte)value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
namespace IM_API.Models
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 群邀请状态
|
|
||||||
/// </summary>
|
|
||||||
public enum GroupInviteState
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 待处理
|
|
||||||
/// </summary>
|
|
||||||
Pending = 0,
|
|
||||||
/// <summary>
|
|
||||||
/// 已同意
|
|
||||||
/// </summary>
|
|
||||||
Passed = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,12 +7,20 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Pending = 0,
|
Pending = 0,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 已拒绝
|
/// 管理员已拒绝
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Declined = 1,
|
Declined = 1,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 已同意
|
/// 管理员已同意
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Passed = 2
|
Passed = 2,
|
||||||
|
/// <summary>
|
||||||
|
/// 待对方同意
|
||||||
|
/// </summary>
|
||||||
|
TargetPending = 3,
|
||||||
|
/// <summary>
|
||||||
|
/// 对方拒绝
|
||||||
|
/// </summary>
|
||||||
|
TargetDeclined = 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,41 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace IM_API.Models;
|
|
||||||
|
|
||||||
public partial class GroupInvite
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 群聊编号
|
|
||||||
/// </summary>
|
|
||||||
public int GroupId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 被邀请用户
|
|
||||||
/// </summary>
|
|
||||||
public int? InvitedUser { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 邀请用户
|
|
||||||
/// </summary>
|
|
||||||
public int? InviteUser { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 当前状态(0:待被邀请人同意
|
|
||||||
/// 1:被邀请人已同意)
|
|
||||||
/// </summary>
|
|
||||||
public sbyte? State { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 创建时间
|
|
||||||
/// </summary>
|
|
||||||
public DateTimeOffset? Created { get; set; }
|
|
||||||
|
|
||||||
public virtual Group Group { get; set; } = null!;
|
|
||||||
|
|
||||||
public virtual User? InviteUserNavigation { get; set; }
|
|
||||||
|
|
||||||
public virtual User? InvitedUserNavigation { get; set; }
|
|
||||||
}
|
|
||||||
@ -19,8 +19,10 @@ public partial class GroupRequest
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
|
|
||||||
|
public int? InviteUserId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 申请状态(0:待管理员同意,1:已拒绝,2:已同意)
|
/// 申请状态(0:待管理员同意,1:管理员已拒绝,2:管理员已同意,3:待对方同意,4:对方拒绝)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sbyte State { get; set; }
|
public sbyte State { get; set; }
|
||||||
|
|
||||||
|
|||||||
@ -49,11 +49,6 @@ namespace IM_API.Models
|
|||||||
entity.Ignore(e => e.AuhorityEnum);
|
entity.Ignore(e => e.AuhorityEnum);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<GroupInvite>(entity =>
|
|
||||||
{
|
|
||||||
entity.Ignore(e => e.StateEnum);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity<GroupMember>(entity =>
|
modelBuilder.Entity<GroupMember>(entity =>
|
||||||
{
|
{
|
||||||
entity.Ignore(e => e.RoleEnum);
|
entity.Ignore(e => e.RoleEnum);
|
||||||
|
|||||||
@ -27,7 +27,6 @@ public partial class ImContext : DbContext
|
|||||||
|
|
||||||
public virtual DbSet<Group> Groups { get; set; }
|
public virtual DbSet<Group> Groups { get; set; }
|
||||||
|
|
||||||
public virtual DbSet<GroupInvite> GroupInvites { get; set; }
|
|
||||||
|
|
||||||
public virtual DbSet<GroupMember> GroupMembers { get; set; }
|
public virtual DbSet<GroupMember> GroupMembers { get; set; }
|
||||||
|
|
||||||
@ -362,53 +361,6 @@ public partial class ImContext : DbContext
|
|||||||
.HasConstraintName("groups_ibfk_1");
|
.HasConstraintName("groups_ibfk_1");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<GroupInvite>(entity =>
|
|
||||||
{
|
|
||||||
entity.HasKey(e => e.Id).HasName("PRIMARY");
|
|
||||||
|
|
||||||
entity
|
|
||||||
.ToTable("group_invite")
|
|
||||||
.HasCharSet("utf8mb4")
|
|
||||||
.UseCollation("utf8mb4_general_ci");
|
|
||||||
|
|
||||||
entity.HasIndex(e => e.GroupId, "GroupId");
|
|
||||||
|
|
||||||
entity.HasIndex(e => e.InviteUser, "InviteUser");
|
|
||||||
|
|
||||||
entity.HasIndex(e => e.InvitedUser, "InvitedUser");
|
|
||||||
|
|
||||||
entity.Property(e => e.Id)
|
|
||||||
.HasColumnType("int(11)")
|
|
||||||
.HasColumnName("ID");
|
|
||||||
entity.Property(e => e.Created)
|
|
||||||
.HasComment("创建时间")
|
|
||||||
.HasColumnType("datetime");
|
|
||||||
entity.Property(e => e.GroupId)
|
|
||||||
.HasComment("群聊编号")
|
|
||||||
.HasColumnType("int(11)");
|
|
||||||
entity.Property(e => e.InviteUser)
|
|
||||||
.HasComment("邀请用户")
|
|
||||||
.HasColumnType("int(11)");
|
|
||||||
entity.Property(e => e.InvitedUser)
|
|
||||||
.HasComment("被邀请用户")
|
|
||||||
.HasColumnType("int(11)");
|
|
||||||
entity.Property(e => e.State)
|
|
||||||
.HasComment("当前状态(0:待被邀请人同意\r\n1:被邀请人已同意)")
|
|
||||||
.HasColumnType("tinyint(4)");
|
|
||||||
|
|
||||||
entity.HasOne(d => d.Group).WithMany(p => p.GroupInvites)
|
|
||||||
.HasForeignKey(d => d.GroupId)
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull)
|
|
||||||
.HasConstraintName("group_invite_ibfk_2");
|
|
||||||
|
|
||||||
entity.HasOne(d => d.InviteUserNavigation).WithMany(p => p.GroupInviteInviteUserNavigations)
|
|
||||||
.HasForeignKey(d => d.InviteUser)
|
|
||||||
.HasConstraintName("group_invite_ibfk_1");
|
|
||||||
|
|
||||||
entity.HasOne(d => d.InvitedUserNavigation).WithMany(p => p.GroupInviteInvitedUserNavigations)
|
|
||||||
.HasForeignKey(d => d.InvitedUser)
|
|
||||||
.HasConstraintName("group_invite_ibfk_3");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity<GroupMember>(entity =>
|
modelBuilder.Entity<GroupMember>(entity =>
|
||||||
{
|
{
|
||||||
|
|||||||
@ -73,10 +73,6 @@ public partial class User
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public virtual ICollection<Friend> FriendUsers { get; set; } = new List<Friend>();
|
public virtual ICollection<Friend> FriendUsers { get; set; } = new List<Friend>();
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public virtual ICollection<GroupInvite> GroupInviteInviteUserNavigations { get; set; } = new List<GroupInvite>();
|
|
||||||
[JsonIgnore]
|
|
||||||
public virtual ICollection<GroupInvite> GroupInviteInvitedUserNavigations { get; set; } = new List<GroupInvite>();
|
|
||||||
[JsonIgnore]
|
|
||||||
public virtual ICollection<GroupMember> GroupMembers { get; set; } = new List<GroupMember>();
|
public virtual ICollection<GroupMember> GroupMembers { get; set; } = new List<GroupMember>();
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public virtual ICollection<GroupRequest> GroupRequests { get; set; } = new List<GroupRequest>();
|
public virtual ICollection<GroupRequest> GroupRequests { get; set; } = new List<GroupRequest>();
|
||||||
|
|||||||
@ -79,6 +79,7 @@ namespace IM_API.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Task DeleteGroupAsync(int userId, int groupId)
|
public Task DeleteGroupAsync(int userId, int groupId)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
@ -90,15 +91,15 @@ namespace IM_API.Services
|
|||||||
x => x.Id == groupId) ?? throw new BaseException(CodeDefine.GROUP_NOT_FOUND);
|
x => x.Id == groupId) ?? throw new BaseException(CodeDefine.GROUP_NOT_FOUND);
|
||||||
//过滤非好友
|
//过滤非好友
|
||||||
var groupInviteIds = await validFriendshipAsync(userId, userIds);
|
var groupInviteIds = await validFriendshipAsync(userId, userIds);
|
||||||
var inviteList = groupInviteIds.Select(id => new GroupInvite
|
var inviteList = groupInviteIds.Select(id => new GroupRequest
|
||||||
{
|
{
|
||||||
Created = DateTime.UtcNow,
|
Created = DateTime.UtcNow,
|
||||||
GroupId = group.Id,
|
GroupId = group.Id,
|
||||||
InviteUser = userId,
|
UserId = id,
|
||||||
InvitedUser = id,
|
InviteUserId = userId,
|
||||||
StateEnum = GroupInviteState.Pending
|
StateEnum = GroupRequestState.TargetPending
|
||||||
}).ToList();
|
}).ToList();
|
||||||
_context.GroupInvites.AddRange(inviteList);
|
_context.GroupRequests.AddRange(inviteList);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
await _endPoint.Publish(new GroupInviteEvent
|
await _endPoint.Publish(new GroupInviteEvent
|
||||||
{
|
{
|
||||||
@ -132,7 +133,7 @@ namespace IM_API.Services
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<GroupInfoDto>> GetGroupListAsync(int userId, int page, int limit, bool desc)
|
public async Task<List<GroupInfoDto>> GetGroupListAsync(int userId, int page = 1, int limit = 50, bool desc = false)
|
||||||
{
|
{
|
||||||
var query = _context.GroupMembers
|
var query = _context.GroupMembers
|
||||||
.Where(x => x.UserId == userId)
|
.Where(x => x.UserId == userId)
|
||||||
@ -159,14 +160,18 @@ namespace IM_API.Services
|
|||||||
_context.Groups.Update(group);
|
_context.Groups.Update(group);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task HandleGroupInviteAsync(int userid, HandleGroupInviteDto dto)
|
public async Task HandleGroupInviteAsync(int userid, HandleGroupInviteDto dto)
|
||||||
{
|
{
|
||||||
var user = _userService.GetUserInfoAsync(userid);
|
var user = _userService.GetUserInfoAsync(userid);
|
||||||
var inviteInfo = await _context.GroupInvites.FirstOrDefaultAsync(x => x.Id == dto.InviteId)
|
var inviteInfo = await _context.GroupRequests
|
||||||
|
.FirstOrDefaultAsync(x => x.UserId == userid && x.StateEnum == GroupRequestState.TargetPending)
|
||||||
?? throw new BaseException(CodeDefine.INVALID_ACTION);
|
?? throw new BaseException(CodeDefine.INVALID_ACTION);
|
||||||
if (inviteInfo.InvitedUser != userid) throw new BaseException(CodeDefine.AUTH_FAILED);
|
if (!(dto.Action == GroupRequestState.TargetPending ||
|
||||||
|
dto.Action == GroupRequestState.TargetDeclined))
|
||||||
|
return;
|
||||||
inviteInfo.StateEnum = dto.Action;
|
inviteInfo.StateEnum = dto.Action;
|
||||||
_context.GroupInvites.Update(inviteInfo);
|
_context.GroupRequests.Update(inviteInfo);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
await _endPoint.Publish(new GroupInviteActionUpdateEvent
|
await _endPoint.Publish(new GroupInviteActionUpdateEvent
|
||||||
{
|
{
|
||||||
@ -176,12 +181,13 @@ namespace IM_API.Services
|
|||||||
EventId = Guid.NewGuid(),
|
EventId = Guid.NewGuid(),
|
||||||
GroupId = inviteInfo.GroupId,
|
GroupId = inviteInfo.GroupId,
|
||||||
InviteId = inviteInfo.Id,
|
InviteId = inviteInfo.Id,
|
||||||
InviteUserId = inviteInfo.InviteUser.Value,
|
InviteUserId = inviteInfo.InviteUserId!.Value,
|
||||||
OperatorId = userid,
|
OperatorId = userid,
|
||||||
UserId = userid
|
UserId = userid
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task HandleGroupRequestAsync(int userid, HandleGroupRequestDto dto)
|
public async Task HandleGroupRequestAsync(int userid, HandleGroupRequestDto dto)
|
||||||
{
|
{
|
||||||
var user = _userService.GetUserInfoAsync(userid);
|
var user = _userService.GetUserInfoAsync(userid);
|
||||||
@ -302,5 +308,68 @@ namespace IM_API.Services
|
|||||||
|
|
||||||
return _mapper.Map<GroupInfoVo>(groupInfo);
|
return _mapper.Map<GroupInfoVo>(groupInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<GroupNotificationVo>> GetGroupNotificationAsync(int userId)
|
||||||
|
{
|
||||||
|
// 1. 查询群请求记录
|
||||||
|
var groupList = await _context.GroupMembers
|
||||||
|
.Where(x => x.UserId == userId &&
|
||||||
|
(x.Role == (sbyte)GroupMemberRole.Master || x.Role == (sbyte)GroupMemberRole.Administrator))
|
||||||
|
.Select(s => s.GroupId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var groupRequest = await _context.GroupRequests
|
||||||
|
.Where(x => groupList.Contains(x.GroupId) || x.UserId == userId || x.InviteUserId == userId)
|
||||||
|
.OrderByDescending(o => o.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (!groupRequest.Any()) return new List<GroupNotificationVo>();
|
||||||
|
|
||||||
|
// 2. 收集所有需要的 ID 并去重
|
||||||
|
var userIds = groupRequest.Select(s => s.UserId).Distinct().ToList();
|
||||||
|
var inviteUserIds = groupRequest.Where(x => x.InviteUserId != null).Select(s => s.InviteUserId.Value).Distinct().ToList();
|
||||||
|
var groupIds = groupRequest.Select(s => s.GroupId).Distinct().ToList();
|
||||||
|
|
||||||
|
var userList = await _userService.GetUserInfoListAsync(userIds);
|
||||||
|
var inviteUserList = await _userService.GetUserInfoListAsync(inviteUserIds);
|
||||||
|
var groupInfoList = await _context.Groups
|
||||||
|
.Where(x => groupIds.Contains(x.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// 2. 转换为字典
|
||||||
|
var userDict = userList.ToDictionary(u => u.Id);
|
||||||
|
var inviteUserDict = inviteUserList.ToDictionary(u => u.Id);
|
||||||
|
var groupDict = groupInfoList.ToDictionary(g => g.Id);
|
||||||
|
|
||||||
|
// 3. 组装数据 (Select 逻辑不变)
|
||||||
|
return groupRequest.Select(g =>
|
||||||
|
{
|
||||||
|
var gnv = _mapper.Map<GroupNotificationVo>(g);
|
||||||
|
|
||||||
|
// 匹配用户信息
|
||||||
|
if (userDict.TryGetValue(g.UserId, out var u))
|
||||||
|
{
|
||||||
|
gnv.UserAvatar = u.Avatar;
|
||||||
|
gnv.NickName = u.NickName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配邀请人信息
|
||||||
|
if (g.InviteUserId.HasValue && inviteUserDict.TryGetValue(g.InviteUserId.Value, out var i))
|
||||||
|
{
|
||||||
|
gnv.InviteUserAvatar = i.Avatar;
|
||||||
|
gnv.InviteUserNickname = i.NickName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配群信息
|
||||||
|
if (groupDict.TryGetValue(g.GroupId, out var gi))
|
||||||
|
{
|
||||||
|
gnv.GroupAvatar = gi.Avatar;
|
||||||
|
gnv.GroupName = gi.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return gnv;
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,6 @@ namespace IM_API.VOs.Group
|
|||||||
public int InviteUserId { get; set; }
|
public int InviteUserId { get; set; }
|
||||||
public int InvitedUserId { get; set; }
|
public int InvitedUserId { get; set; }
|
||||||
public int InviteId { get; set; }
|
public int InviteId { get; set; }
|
||||||
public GroupInviteState Action { get; set; }
|
public GroupRequestState Action { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
backend/IM_API/VOs/Group/GroupNotificationVo.cs
Normal file
21
backend/IM_API/VOs/Group/GroupNotificationVo.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using IM_API.Models;
|
||||||
|
|
||||||
|
namespace IM_API.VOs.Group
|
||||||
|
{
|
||||||
|
public class GroupNotificationVo
|
||||||
|
{
|
||||||
|
public int RequestId { get; set; }
|
||||||
|
public int? UserId { get; set; }
|
||||||
|
public string? NickName { get; set; }
|
||||||
|
public string? UserAvatar { get; set; }
|
||||||
|
public int GroupId { get; set; }
|
||||||
|
public string? GroupAvatar { get; set; }
|
||||||
|
public string? GroupName { get; set; }
|
||||||
|
|
||||||
|
public GroupRequestState Status { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public int? InviteUser { get; set; }
|
||||||
|
public string? InviteUserNickname { get; set; }
|
||||||
|
public string? InviteUserAvatar { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
# 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/
|
||||||
|
|
||||||
|
|
||||||
VITE_API_BASE_URL = http://192.168.5.116:7070/api
|
# VITE_API_BASE_URL = http://192.168.5.116:7070/api
|
||||||
VITE_SIGNALR_BASE_URL = http://192.168.5.116:7070/chat/
|
# VITE_SIGNALR_BASE_URL = http://192.168.5.116:7070/chat/
|
||||||
|
|||||||
8
frontend/pc/IM/package-lock.json
generated
8
frontend/pc/IM/package-lock.json
generated
@ -16,6 +16,7 @@
|
|||||||
"@vuelidate/core": "^2.0.3",
|
"@vuelidate/core": "^2.0.3",
|
||||||
"@vuelidate/validators": "^2.0.4",
|
"@vuelidate/validators": "^2.0.4",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
"electron-updater": "^6.3.9",
|
"electron-updater": "^6.3.9",
|
||||||
"feather-icons": "^4.29.2",
|
"feather-icons": "^4.29.2",
|
||||||
"hevue-img-preview": "^7.1.3",
|
"hevue-img-preview": "^7.1.3",
|
||||||
@ -3993,6 +3994,13 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crypto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/crypto/-/crypto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
|
||||||
|
"deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/cssesc": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
|
|||||||
@ -25,6 +25,7 @@
|
|||||||
"@vuelidate/core": "^2.0.3",
|
"@vuelidate/core": "^2.0.3",
|
||||||
"@vuelidate/validators": "^2.0.4",
|
"@vuelidate/validators": "^2.0.4",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
"electron-updater": "^6.3.9",
|
"electron-updater": "^6.3.9",
|
||||||
"feather-icons": "^4.29.2",
|
"feather-icons": "^4.29.2",
|
||||||
"hevue-img-preview": "^7.1.3",
|
"hevue-img-preview": "^7.1.3",
|
||||||
|
|||||||
13
frontend/pc/IM/src/cache/cacheDir.js
vendored
Normal file
13
frontend/pc/IM/src/cache/cacheDir.js
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { app } from "electron";
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export const CACHE_ROOT = path.join(app.getPath('userData'), 'resource_cache')
|
||||||
|
|
||||||
|
export const PROTOCOL_HEAD = 'ql-im://'
|
||||||
|
|
||||||
|
export const DIRS = {
|
||||||
|
Image: 'images',
|
||||||
|
Video: 'videos',
|
||||||
|
Voice: 'voices',
|
||||||
|
File: 'files',
|
||||||
|
}
|
||||||
35
frontend/pc/IM/src/cache/cacheHandler.js
vendored
Normal file
35
frontend/pc/IM/src/cache/cacheHandler.js
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import path from "path";
|
||||||
|
import { FILE_TYPE } from "../renderer/src/constants/fileTypeDefine";
|
||||||
|
import { DIRS, CACHE_ROOT, PROTOCOL_HEAD } from "./cacheDir";
|
||||||
|
import crypto from 'crypto'
|
||||||
|
import fs from 'fs-extra'
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const getCacheResorce = async (url, type = FILE_TYPE.Image) => {
|
||||||
|
const hash = crypto.createHash('md5').update(url).digest('hex')
|
||||||
|
const subDir = hash.substring(0,2);
|
||||||
|
const targetPath = path.join(DIRS[type], subDir)
|
||||||
|
|
||||||
|
const filePath = path.join(targetPath, hash)
|
||||||
|
|
||||||
|
if(await fs.pathExists(path.join(CACHE_ROOT, filePath))){
|
||||||
|
return PROTOCOL_HEAD + filePath.replaceAll('\\', '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.ensureDir(path.join(CACHE_ROOT, targetPath))
|
||||||
|
|
||||||
|
const writer = fs.createWriteStream(path.join(CACHE_ROOT, filePath))
|
||||||
|
|
||||||
|
const response = await axios({
|
||||||
|
url,
|
||||||
|
method: 'GET',
|
||||||
|
responseType: 'stream'
|
||||||
|
})
|
||||||
|
|
||||||
|
response.data.pipe(writer)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
writer.on('finish', () => resolve(PROTOCOL_HEAD + filePath.replaceAll('\\', '/')));
|
||||||
|
writer.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
13
frontend/pc/IM/src/cache/protocolReg.js
vendored
Normal file
13
frontend/pc/IM/src/cache/protocolReg.js
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { net, protocol } from 'electron'
|
||||||
|
import { CACHE_ROOT } from './cacheDir'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export const addProtocolHandler = () => {
|
||||||
|
protocol.handle('ql-im', (request) => {
|
||||||
|
const url = request.url.replace('ql-im://', '')
|
||||||
|
|
||||||
|
const filePath = path.join(CACHE_ROOT, url.replaceAll('/', '\\'))
|
||||||
|
|
||||||
|
return net.fetch(`file://${filePath}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -4,6 +4,8 @@ import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
|||||||
import icon from '../../resources/icon.png?asset'
|
import icon from '../../resources/icon.png?asset'
|
||||||
import { registerWindowHandler } from './ipcHandlers/window'
|
import { registerWindowHandler } from './ipcHandlers/window'
|
||||||
import { createTry } from './trayHandler'
|
import { createTry } from './trayHandler'
|
||||||
|
import { registerCacheHandler } from './ipcHandlers/cache'
|
||||||
|
import { addProtocolHandler } from '../cache/protocolReg'
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
// Create the browser window.
|
// Create the browser window.
|
||||||
@ -49,6 +51,8 @@ app.whenReady().then(() => {
|
|||||||
// Set app user model id for windows
|
// Set app user model id for windows
|
||||||
electronApp.setAppUserModelId('com.electron')
|
electronApp.setAppUserModelId('com.electron')
|
||||||
|
|
||||||
|
addProtocolHandler()
|
||||||
|
|
||||||
// Default open or close DevTools by F12 in development
|
// Default open or close DevTools by F12 in development
|
||||||
// and ignore CommandOrControl + R in production.
|
// and ignore CommandOrControl + R in production.
|
||||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||||
@ -60,6 +64,7 @@ app.whenReady().then(() => {
|
|||||||
ipcMain.on('ping', () => console.log('pong'))
|
ipcMain.on('ping', () => console.log('pong'))
|
||||||
|
|
||||||
registerWindowHandler()
|
registerWindowHandler()
|
||||||
|
registerCacheHandler()
|
||||||
|
|
||||||
createWindow()
|
createWindow()
|
||||||
|
|
||||||
|
|||||||
8
frontend/pc/IM/src/main/ipcHandlers/cache.js
Normal file
8
frontend/pc/IM/src/main/ipcHandlers/cache.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { ipcMain } from "electron";
|
||||||
|
import { getCacheResorce } from "../../cache/cacheHandler";
|
||||||
|
|
||||||
|
export function registerCacheHandler(){
|
||||||
|
ipcMain.handle('cache-get', (event, url, type) => {
|
||||||
|
return getCacheResorce(url, type)
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -11,6 +11,9 @@ const api = {
|
|||||||
isMaximized: () => ipcRenderer.send('window-action', 'isMaximized'),
|
isMaximized: () => ipcRenderer.send('window-action', 'isMaximized'),
|
||||||
newWindow: (route, data) => ipcRenderer.send('window-new', { route, data }),
|
newWindow: (route, data) => ipcRenderer.send('window-new', { route, data }),
|
||||||
getWindowData: (winId) => ipcRenderer.invoke('get-window-data', winId)
|
getWindowData: (winId) => ipcRenderer.invoke('get-window-data', winId)
|
||||||
|
},
|
||||||
|
cache: {
|
||||||
|
getCache: (url, type) => ipcRenderer.invoke('cache-get', url, type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,13 +5,13 @@
|
|||||||
<title>Electron</title>
|
<title>Electron</title>
|
||||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||||
<meta http-equiv="Content-Security-Policy"
|
<meta http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self';
|
content="default-src 'self' http://192.168.5.116:7070 ql-im:;
|
||||||
script-src 'self' 'unsafe-inline';
|
script-src 'self' 'unsafe-inline';
|
||||||
style-src 'self' 'unsafe-inline';
|
style-src 'self' 'unsafe-inline';
|
||||||
connect-src 'self' http://localhost:5202 ws://localhost:5202 http://192.168.5.116:7070 ws://192.168.5.116:7070;
|
connect-src 'self' http://localhost:5202 ws://localhost:5202 http://192.168.5.116:7070 ws://192.168.5.116:7070;
|
||||||
img-src 'self' data: blob: https: http:;
|
img-src 'self' data: blob: https: http: ql-im:;
|
||||||
font-src 'self' data:;
|
font-src 'self' data:;
|
||||||
media-src 'self' blob:;">
|
media-src 'self' blob: http://192.168.5.116:7070; ql-im:">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
BIN
frontend/pc/IM/src/renderer/src/assets/loading_img.png
Normal file
BIN
frontend/pc/IM/src/renderer/src/assets/loading_img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
40
frontend/pc/IM/src/renderer/src/components/AsyncImage.vue
Normal file
40
frontend/pc/IM/src/renderer/src/components/AsyncImage.vue
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="img-container">
|
||||||
|
<img :src="finalUrl" alt="" v-bind="$attrs" @error="imgLoadErrHandler">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useCacheStore } from '../stores/cache';
|
||||||
|
import { FILE_TYPE } from '../constants/fileTypeDefine';
|
||||||
|
import default_avatar from '@/assets/default_avatar.png'
|
||||||
|
import loading_img from '@/assets/loading_img.png'
|
||||||
|
|
||||||
|
const cacheStore = useCacheStore()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
rawUrl: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
noAvatar: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const finalUrl = ref(props.noAvatar ? loading_img : default_avatar)
|
||||||
|
|
||||||
|
const imgLoadErrHandler = (e) => {
|
||||||
|
e.target.src = loading_img
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!props.rawUrl || props.rawUrl == '') return
|
||||||
|
finalUrl.value = await cacheStore.getCache(props.rawUrl, FILE_TYPE.Image)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@ -27,11 +27,13 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
import { ref } from 'vue'
|
import { ref, defineEmits } from 'vue'
|
||||||
import { isElectron } from '../utils/electronHelper'
|
import { isElectron } from '../utils/electronHelper'
|
||||||
|
|
||||||
const isMaximized = ref(false)
|
const isMaximized = ref(false)
|
||||||
|
|
||||||
|
const emits = defineEmits(['close'])
|
||||||
|
|
||||||
function minimize() {
|
function minimize() {
|
||||||
window.api.window.minimize();
|
window.api.window.minimize();
|
||||||
}
|
}
|
||||||
@ -41,6 +43,7 @@ function toggleMaximize() {
|
|||||||
}
|
}
|
||||||
function close() {
|
function close() {
|
||||||
window.api.window.close()
|
window.api.window.close()
|
||||||
|
emits('close')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<WindowControls/>
|
<div></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { previewImages } from 'hevue-img-preview/v3'
|
import { previewImages } from 'hevue-img-preview/v3'
|
||||||
import {onMounted, ref } from 'vue';
|
import {onMounted, ref } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import WindowControls from '../WindowControls.vue';
|
// import WindowControls from '../WindowControls.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,148 @@
|
|||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<div class="video-overlay">
|
||||||
|
|
||||||
|
<div class="video-dialog" :style="isElectron() ? 'width:100vw;height:100vh' : ''">
|
||||||
|
<WindowControls v-if="isElectron()" @close="windowCloseHandler"/>
|
||||||
|
<div class="close-bar" v-if="!isElectron()">
|
||||||
|
<span>正在播放视频</span>
|
||||||
|
<button class="close-btn" @click="webCloseHandler">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="player-wrapper" :class="{'electron-play-container': isElectron()}">
|
||||||
|
<vue3-video-player
|
||||||
|
v-if="videoLoaded"
|
||||||
|
:src="videoInfo"
|
||||||
|
poster="https://xxx.jpg"
|
||||||
|
:controls="true"
|
||||||
|
:autoplay="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref, defineEmits } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { isElectron } from '../../utils/electronHelper';
|
||||||
|
import WindowControls from '../WindowControls.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
videoData: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits(['close'])
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const videoInfo = ref(null);
|
||||||
|
|
||||||
|
const videoLoaded = ref(false)
|
||||||
|
|
||||||
|
const winId = ref(null)
|
||||||
|
|
||||||
|
const windowCloseHandler = () => {
|
||||||
|
window.api.window.closeThis()
|
||||||
|
}
|
||||||
|
|
||||||
|
const webCloseHandler = () => {
|
||||||
|
emits('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (isElectron()) {
|
||||||
|
winId.value = route.query.winId;
|
||||||
|
const data = await window.api.window.getWindowData(winId.value);
|
||||||
|
videoInfo.value = data;
|
||||||
|
videoLoaded.value = true;
|
||||||
|
}else{
|
||||||
|
videoInfo.value = props.videoData
|
||||||
|
videoLoaded.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
/* 遮罩层:全屏、黑色半透明、固定定位 */
|
||||||
|
.video-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: rgba(0, 0, 0, 0.85);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
/* 确保在最顶层 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 播放器弹窗主体 */
|
||||||
|
.video-dialog {
|
||||||
|
position: relative;
|
||||||
|
width: 90%;
|
||||||
|
/* max-width: 1000px; */
|
||||||
|
background: #000;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部状态栏(包含关闭按钮) */
|
||||||
|
.close-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #eee;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
/* 锁定 16:9 比例 */
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 进场动画 */
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.electron-play-container {
|
||||||
|
height: calc(100vh - 30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -1,14 +1,33 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, onMounted, defineEmits } from 'vue';
|
||||||
import { useContactStore } from '@/stores/contact';
|
import { friendService } from '../../services/friend';
|
||||||
import { groupService } from '@/services/group';
|
import { groupService } from '@/services/group';
|
||||||
import { SYSTEM_BASE_STATUS } from '@/constants/systemBaseStatus';
|
import { SYSTEM_BASE_STATUS } from '@/constants/systemBaseStatus';
|
||||||
import { useMessage } from '../messages/useAlert';
|
import { useMessage } from '../messages/useAlert';
|
||||||
|
|
||||||
const contactStore = useContactStore();
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
|
||||||
const props = defineProps({ modelValue: Boolean });
|
const isLoaded = ref(false);
|
||||||
|
|
||||||
|
const isError = ref(false)
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: Boolean,
|
||||||
|
type: {
|
||||||
|
/**@type {"CreateGroup" | "InviteUser"} */
|
||||||
|
type: String,
|
||||||
|
default: 'CreateGroup',
|
||||||
|
validator: (value) => {
|
||||||
|
return ['CreateGroup', 'InviteUser'].includes(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '创建群聊'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(['submit'])
|
||||||
|
|
||||||
const friends = ref([])
|
const friends = ref([])
|
||||||
|
|
||||||
@ -20,21 +39,12 @@ const toggle = (id) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
const res = await groupService.createGroup({
|
|
||||||
name: groupName.value,
|
|
||||||
avatar: "https://baidu.com",
|
|
||||||
userIDs: [...selected.value]
|
|
||||||
});
|
|
||||||
|
|
||||||
if(res.code == SYSTEM_BASE_STATUS.SUCCESS){
|
emits('submit', selected.value, groupName.value)
|
||||||
message.show('群聊创建成功。');
|
|
||||||
}else{
|
|
||||||
message.error(res.message);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () =>{
|
onMounted(async () =>{
|
||||||
friends.value = contactStore.contacts;
|
friends.value = (await friendService.getFriendList()).data;
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -43,12 +53,12 @@ onMounted(async () =>{
|
|||||||
<div v-if="modelValue" class="overlay" @click.self="$emit('update:modelValue', false)">
|
<div v-if="modelValue" class="overlay" @click.self="$emit('update:modelValue', false)">
|
||||||
<div class="mini-modal">
|
<div class="mini-modal">
|
||||||
<header>
|
<header>
|
||||||
<span>发起群聊</span>
|
<span>{{ props.title }}</span>
|
||||||
<button @click="$emit('update:modelValue', false)">✕</button>
|
<button @click="$emit('update:modelValue', false)">✕</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<input v-model="groupName" placeholder="群组名称..." class="mini-input" />
|
<input v-if="props.type == 'CreateGroup'" v-model="groupName" placeholder="群组名称..." class="mini-input" />
|
||||||
|
|
||||||
<div class="list">
|
<div class="list">
|
||||||
<div v-for="f in friends" :key="f.friendId" @click="toggle(f.friendId)" class="item">
|
<div v-for="f in friends" :key="f.friendId" @click="toggle(f.friendId)" class="item">
|
||||||
@ -60,8 +70,8 @@ onMounted(async () =>{
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<button @click="submit" :disabled="!groupName || !selected.size" class="btn">
|
<button @click="submit" :disabled="(!groupName&& props.type == 'CreateGroup') || !selected.size" class="btn">
|
||||||
创建 ({{ selected.size }})
|
{{ props.type == 'CreateGroup' ? '创建' : '确定' }} ({{ selected.size }})
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -19,26 +19,26 @@
|
|||||||
<button v-if="isAdmin" class="text-link">编辑</button>
|
<button v-if="isAdmin" class="text-link">编辑</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="announcement-box">
|
<div class="announcement-box">
|
||||||
{{ groupData.announcement || '暂无群公告,点击编辑添加。' }}
|
{{ groupInfo ? groupInfo.announcement : '暂无群公告,点击编辑添加。' }}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-if="chatType == MESSAGE_TYPE.GROUP" class="info-card">
|
<section v-if="chatType == MESSAGE_TYPE.GROUP" class="info-card">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3 class="section-label">群成员 <span class="count-tag">{{ groupData.members?.length || 0 }}</span></h3>
|
<h3 class="section-label">群成员 <span class="count-tag">{{ groupInfo.members?.length || 0 }}</span></h3>
|
||||||
<button class="text-link" @click="$emit('viewAll')">查看全部</button>
|
<button class="text-link" @click="$emit('viewAll')">查看全部</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="member-grid">
|
<div class="member-grid">
|
||||||
<div class="member-item add-btn">
|
<div class="member-item add-btn">
|
||||||
<div class="member-avatar-box dashed">
|
<div class="member-avatar-box dashed" @click="inviteHandler">
|
||||||
<span>+</span>
|
<span>+</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="member-nick">邀请</span>
|
<span class="member-nick">邀请</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="member in groupData.members?.slice(0, 11)"
|
v-for="member in groupInfo.members?.slice(0, 11)"
|
||||||
:key="member.id"
|
:key="member.id"
|
||||||
class="member-item"
|
class="member-item"
|
||||||
>
|
>
|
||||||
@ -69,12 +69,13 @@
|
|||||||
<button class="danger-btn">删除并退出</button>
|
<button class="danger-btn">删除并退出</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<create-group v-model="groupInviteModal" type="InviteUser" title="邀请好友" @submit="inviteUserHandler"/>
|
||||||
</aside>
|
</aside>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, useTemplateRef } from 'vue';
|
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||||
import { MESSAGE_TYPE } from '../../constants/MessageType';
|
import { MESSAGE_TYPE } from '../../constants/MessageType';
|
||||||
import feather from 'feather-icons';
|
import feather from 'feather-icons';
|
||||||
import { GROUP_MEMBER_ROLE } from '../../constants/GroupDefine';
|
import { GROUP_MEMBER_ROLE } from '../../constants/GroupDefine';
|
||||||
@ -83,6 +84,7 @@ import { groupService } from '../../services/group';
|
|||||||
import { SYSTEM_BASE_STATUS } from '../../constants/systemBaseStatus';
|
import { SYSTEM_BASE_STATUS } from '../../constants/systemBaseStatus';
|
||||||
import { useMessage } from './useAlert';
|
import { useMessage } from './useAlert';
|
||||||
import { getFileHash } from '../../utils/uploadTools';
|
import { getFileHash } from '../../utils/uploadTools';
|
||||||
|
import CreateGroup from '../groups/CreateGroup.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
chatType: {
|
chatType: {
|
||||||
@ -111,6 +113,12 @@ const input = useTemplateRef('input')
|
|||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
|
||||||
|
const groupInfo = ref({
|
||||||
|
members: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const groupInviteModal = ref(false)
|
||||||
|
|
||||||
defineEmits(['close', 'viewAll']);
|
defineEmits(['close', 'viewAll']);
|
||||||
|
|
||||||
const uploadGroupAvatar = () => {
|
const uploadGroupAvatar = () => {
|
||||||
@ -119,7 +127,7 @@ const uploadGroupAvatar = () => {
|
|||||||
|
|
||||||
const fileUploadHandler = async (e) => {
|
const fileUploadHandler = async (e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
const hash = getFileHash(file)
|
const hash = await getFileHash(file)
|
||||||
const { data } = await uploadService.uploadSmallFile(file, hash);
|
const { data } = await uploadService.uploadSmallFile(file, hash);
|
||||||
const res = await groupService.updateGroupInfo(props.groupData.targetId, {
|
const res = await groupService.updateGroupInfo(props.groupData.targetId, {
|
||||||
avatar: data.url
|
avatar: data.url
|
||||||
@ -132,11 +140,38 @@ const fileUploadHandler = async (e) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inviteHandler = () => {
|
||||||
|
groupInviteModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const inviteUserHandler = async (selectedUsers) => {
|
||||||
|
const res = await groupService.inviteUser(props.groupData.targetId, [...selectedUsers])
|
||||||
|
if (res.code != SYSTEM_BASE_STATUS.SUCCESS) return message.error(res.message)
|
||||||
|
message.success('成功')
|
||||||
|
}
|
||||||
|
|
||||||
// 判断当前用户是否为管理员
|
// 判断当前用户是否为管理员
|
||||||
const isAdmin = computed(() => {
|
const isAdmin = computed(() => {
|
||||||
// 逻辑:在 members 数组中找到当前用户并检查 role
|
// 逻辑:在 members 数组中找到当前用户并检查 role
|
||||||
return true; // 演示用,默认 true
|
return true; // 演示用,默认 true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.groupData.id,
|
||||||
|
async (newVal, oldVal) => {
|
||||||
|
if (props.chatType == MESSAGE_TYPE.GROUP && newVal != oldVal) {
|
||||||
|
groupInfo.value = (await groupService.getGroupInfo(props.groupData.targetId)).data
|
||||||
|
|
||||||
|
groupInfo.value.members = (await groupService.getGroupMember(props.groupData.targetId)).data
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -148,7 +183,7 @@ const isAdmin = computed(() => {
|
|||||||
right: 0;
|
right: 0;
|
||||||
width: 320px;
|
width: 320px;
|
||||||
background-color: #f5f5f5; /* 背景色改为浅灰,突出白色卡片 */
|
background-color: #f5f5f5; /* 背景色改为浅灰,突出白色卡片 */
|
||||||
z-index: 1000;
|
z-index: 100;
|
||||||
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.05);
|
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.05);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@ -3,3 +3,19 @@ export const GROUP_MEMBER_ROLE = Object.freeze({
|
|||||||
ADMIN: 'Administrator',
|
ADMIN: 'Administrator',
|
||||||
MASTER: 'Master'
|
MASTER: 'Master'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群请求状态枚举 (对应后端 String 输出)
|
||||||
|
*/
|
||||||
|
export const GROUP_REQUEST_STATUS = Object.freeze({
|
||||||
|
/** 待管理员处理 */
|
||||||
|
PENDING: 'Pending',
|
||||||
|
/** 管理员已拒绝 */
|
||||||
|
DECLINED: 'Declined',
|
||||||
|
/** 管理员已同意 */
|
||||||
|
PASSED: 'Passed',
|
||||||
|
/** 待对方同意 */
|
||||||
|
TARGET_PENDING: 'TargetPending',
|
||||||
|
/** 对方拒绝 */
|
||||||
|
TARGET_DECLINED: 'TargetDeclined'
|
||||||
|
});
|
||||||
|
|||||||
@ -36,5 +36,6 @@ export const FILE_TYPE = Object.freeze({
|
|||||||
Image: 'Image',
|
Image: 'Image',
|
||||||
Video: 'Video',
|
Video: 'Video',
|
||||||
Voice: 'Voice',
|
Voice: 'Voice',
|
||||||
File: 'File'
|
File: 'File',
|
||||||
|
TEXT: 'Text'
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
export const GROUP_REQUEST_TYPE = Object.freeze({
|
||||||
|
//**邀请对方 */
|
||||||
|
INVITE: 'invite',
|
||||||
|
/**被邀请 */
|
||||||
|
INVITED: 'invited',
|
||||||
|
/**入群请求 */
|
||||||
|
IS_GROUP: 'is-group',
|
||||||
|
//**我的申请入群 */
|
||||||
|
IS_USER: 'is-user',
|
||||||
|
})
|
||||||
@ -57,6 +57,11 @@ const routes = [
|
|||||||
path: '/contacts/requests',
|
path: '/contacts/requests',
|
||||||
name: 'friendRequests',
|
name: 'friendRequests',
|
||||||
component: () => import('@/views/contact/FriendRequestList.vue')
|
component: () => import('@/views/contact/FriendRequestList.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/contacts/grouphandle',
|
||||||
|
name: 'grouphandle',
|
||||||
|
component: () => import('@/views/contact/GroupRequest.vue')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -72,6 +77,9 @@ const routes = [
|
|||||||
{ path: '/test', component: TestView },
|
{ path: '/test', component: TestView },
|
||||||
{
|
{
|
||||||
path: '/imgpre', component: () => import('@/components/electron/ImagePreview.vue')
|
path: '/imgpre', component: () => import('@/components/electron/ImagePreview.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/videopre', component: () => import('@/components/electron/VideoPreview.vue')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -21,5 +21,26 @@ export const groupService = {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
|
|
||||||
updateGroupInfo: (groupId, params) => request.post(`/Group/UpdateGroup?groupId=${groupId}`, params)
|
updateGroupInfo: (groupId, params) => request.post(`/Group/UpdateGroup?groupId=${groupId}`, params),
|
||||||
|
/**
|
||||||
|
* 查询群组信息
|
||||||
|
* @param {*} groupId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
getGroupInfo: (groupId) => request.get(`/Group/GetGroupInfo?groupId=${groupId}`),
|
||||||
|
/**
|
||||||
|
* 邀请入群
|
||||||
|
* @param {*} groupId
|
||||||
|
* @param {*} users
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
inviteUser: (groupId, users) =>
|
||||||
|
request.post('/Group/InviteUser', { groupId: groupId, ids: users }),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取群聊通知
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getGroupNotification: () => request.get('/Group/GetGroupNotification')
|
||||||
}
|
}
|
||||||
|
|||||||
29
frontend/pc/IM/src/renderer/src/stores/cache.js
Normal file
29
frontend/pc/IM/src/renderer/src/stores/cache.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { isElectron } from '../utils/electronHelper'
|
||||||
|
|
||||||
|
export const useCacheStore = defineStore('cache', {
|
||||||
|
state: () => ({
|
||||||
|
cacheMap: new Map()
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
/**
|
||||||
|
* 获取文件地址
|
||||||
|
* @param {String} url 网络路径
|
||||||
|
* @param {String} type 文件类型
|
||||||
|
* @returns {Promise} 本地路径
|
||||||
|
*/
|
||||||
|
async getCache(url, type) {
|
||||||
|
if (!isElectron()) return url
|
||||||
|
|
||||||
|
if (this.cacheMap.has(url)) {
|
||||||
|
return this.cacheMap.get(url)
|
||||||
|
}
|
||||||
|
const localPath = await window.api.cache.getCache(url, type)
|
||||||
|
|
||||||
|
this.cacheMap.set(url, localPath)
|
||||||
|
|
||||||
|
return localPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { messagesDb } from "@/utils/db/messageDB";
|
import { messagesDb } from "@/utils/db/messageDB";
|
||||||
import { messageService } from "@/services/message";
|
import { messageService } from "@/services/message";
|
||||||
import { useConversationStore } from "./conversation";
|
|
||||||
|
|
||||||
export const useChatStore = defineStore('chat', {
|
export const useChatStore = defineStore('chat', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
|
|||||||
27
frontend/pc/IM/src/renderer/src/stores/groupRequest.js
Normal file
27
frontend/pc/IM/src/renderer/src/stores/groupRequest.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { groupService } from '../services/group'
|
||||||
|
import { useMessage } from '../components/messages/useAlert'
|
||||||
|
import { SYSTEM_BASE_STATUS } from '../constants/systemBaseStatus'
|
||||||
|
import { groupRequestDb } from '../utils/db/groupRequestDb'
|
||||||
|
|
||||||
|
export const useGroupRequestStore = defineStore('groupRequest', {
|
||||||
|
state: () => ({
|
||||||
|
groupRequest: []
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
async loadGroupRequest() {
|
||||||
|
this.groupRequest = await groupRequestDb.getAll()
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const res = await groupService.getGroupNotification()
|
||||||
|
if (res.code != SYSTEM_BASE_STATUS.SUCCESS) return message.error(res.message)
|
||||||
|
|
||||||
|
this.groupRequest = res.data
|
||||||
|
|
||||||
|
res.data.forEach((element) => {
|
||||||
|
groupRequestDb.save(element)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -1,29 +1,36 @@
|
|||||||
import { openDB } from "idb";
|
import { openDB } from 'idb'
|
||||||
|
|
||||||
const DBNAME = 'IM_DB';
|
const DBNAME = 'IM_DB'
|
||||||
const STORE_NAME = 'messages';
|
const STORE_NAME = 'messages'
|
||||||
const CONVERSARION_STORE_NAME = 'conversations';
|
const CONVERSARION_STORE_NAME = 'conversations'
|
||||||
const CONTACT_STORE_NAME = 'contacts';
|
const CONTACT_STORE_NAME = 'contacts'
|
||||||
|
const GROUP_REQUEST_STORE_NAME = 'groupRequests'
|
||||||
|
|
||||||
export const dbPromise = openDB(DBNAME, 7, {
|
export const dbPromise = openDB(DBNAME, 7, {
|
||||||
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' })
|
||||||
store.createIndex('by-sessionId', 'sessionId');
|
store.createIndex('by-sessionId', 'sessionId')
|
||||||
store.createIndex('by-time', 'timeStamp');
|
store.createIndex('by-time', 'timeStamp')
|
||||||
store.createIndex('by-sequenceId', 'sequenceId');
|
store.createIndex('by-sequenceId', 'sequenceId')
|
||||||
store.createIndex('by-session-sequenceId', ['sessionId', 'sequenceId']);
|
store.createIndex('by-session-sequenceId', ['sessionId', 'sequenceId'])
|
||||||
|
|
||||||
}
|
}
|
||||||
if (!db.objectStoreNames.contains(CONVERSARION_STORE_NAME)) {
|
if (!db.objectStoreNames.contains(CONVERSARION_STORE_NAME)) {
|
||||||
const store = db.createObjectStore(CONVERSARION_STORE_NAME, { keyPath: 'id' });
|
const store = db.createObjectStore(CONVERSARION_STORE_NAME, { keyPath: 'id' })
|
||||||
store.createIndex('by-id', 'id');
|
store.createIndex('by-id', 'id')
|
||||||
}
|
}
|
||||||
if (!db.objectStoreNames.contains(CONTACT_STORE_NAME)) {
|
if (!db.objectStoreNames.contains(CONTACT_STORE_NAME)) {
|
||||||
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 });
|
store.createIndex('by-friendId', 'friendId', { unique: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!db.objectStoreNames.contains(GROUP_REQUEST_STORE_NAME)) {
|
||||||
|
const store = db.createObjectStore(GROUP_REQUEST_STORE_NAME, { keyPath: 'requestId' })
|
||||||
|
store.createIndex('by-id', 'requestId')
|
||||||
|
store.createIndex('by-userid', 'userId')
|
||||||
|
store.createIndex('by-groupid', 'groupId')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
12
frontend/pc/IM/src/renderer/src/utils/db/groupRequestDb.js
Normal file
12
frontend/pc/IM/src/renderer/src/utils/db/groupRequestDb.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { dbPromise } from "./baseDb";
|
||||||
|
|
||||||
|
const STORE_NAME = 'groupRequests'
|
||||||
|
|
||||||
|
export const groupRequestDb = {
|
||||||
|
async save(request){
|
||||||
|
(await dbPromise).put(STORE_NAME, request)
|
||||||
|
},
|
||||||
|
async getAll(){
|
||||||
|
(await dbPromise).getAll(STORE_NAME)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,8 @@
|
|||||||
<div class="im-container">
|
<div class="im-container">
|
||||||
<nav class="nav-sidebar">
|
<nav class="nav-sidebar">
|
||||||
<div class="user-self">
|
<div class="user-self">
|
||||||
<img :src="myInfo?.avatar ?? defaultAvatar" class="avatar-std" />
|
<async-image :raw-url="myInfo?.avatar" class="avatar-std"/>
|
||||||
|
<!-- <img :src="myInfo?.avatar ?? defaultAvatar" class="avatar-std" /> -->
|
||||||
</div>
|
</div>
|
||||||
<router-link class="nav-item" to="/messages" active-class="active">
|
<router-link class="nav-item" to="/messages" active-class="active">
|
||||||
<i class="menuIcon" v-html="feather.icons['message-square'].toSvg()"></i>
|
<i class="menuIcon" v-html="feather.icons['message-square'].toSvg()"></i>
|
||||||
@ -20,11 +21,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted } from 'vue'
|
import { watch, onMounted } from 'vue'
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import defaultAvatar from '@/assets/default_avatar.png'
|
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import feather from 'feather-icons';
|
import feather from 'feather-icons';
|
||||||
|
import AsyncImage from '../components/AsyncImage.vue';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
@ -195,7 +196,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 头像样式统一 */
|
/* 头像样式统一 */
|
||||||
.avatar-std { width: 40px; height: 40px; border-radius: 4px; object-fit: cover; }
|
:deep(.avatar-std) { width: 40px; height: 40px; border-radius: 4px; object-fit: cover; }
|
||||||
.avatar-chat { width: 38px; height: 38px; border-radius: 4px; object-fit: cover; flex-shrink: 0; }
|
.avatar-chat { width: 38px; height: 38px; border-radius: 4px; object-fit: cover; flex-shrink: 0; }
|
||||||
|
|
||||||
/* 未读气泡 */
|
/* 未读气泡 */
|
||||||
|
|||||||
@ -1,206 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
|
||||||
<div v-if="visible" class="mask" @click.self="close">
|
|
||||||
|
|
||||||
<!-- 工具栏 -->
|
<AsyncImage raw-url="http://192.168.5.116:7070/uploads/files/IM/2026/03/2/e6c407f60c68.jpg" :type="FILE_TYPE.Image"/>
|
||||||
<div class="toolbar" @click.stop>
|
<button @click="test">click</button>
|
||||||
<button @click="prev" title="上一张" v-html="feather.icons['arrow-left'].toSvg({width:15,height15})"></button>
|
|
||||||
<button @click="next" title="下一张">▶</button>
|
|
||||||
<button @click="zoomOut" title="缩小">-</button>
|
|
||||||
<button @click="zoomIn" title="放大">+</button>
|
|
||||||
<button @click="rotate" title="旋转">⟳</button>
|
|
||||||
<button @click="download" title="下载">⬇</button>
|
|
||||||
<button @click="close" title="关闭">✕</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 图片 -->
|
|
||||||
<img
|
|
||||||
:src="current"
|
|
||||||
class="img"
|
|
||||||
:style="style"
|
|
||||||
@mousedown="onDown"
|
|
||||||
@wheel.prevent="onWheel"
|
|
||||||
@dblclick="toggleZoom"
|
|
||||||
draggable="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 索引 -->
|
|
||||||
<div class="indicator">{{ index + 1 }} / {{ list.length }}</div>
|
|
||||||
|
|
||||||
<!-- 左右翻页大按钮 -->
|
|
||||||
<div class="nav left" @click.stop="prev">‹</div>
|
|
||||||
<div class="nav right" @click.stop="next">›</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
import { ref } from 'vue';
|
||||||
import feather from 'feather-icons'
|
import { useCacheStore } from '../stores/cache';
|
||||||
|
import { FILE_TYPE } from '../constants/fileTypeDefine';
|
||||||
|
import AsyncImage from '../components/AsyncImage.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: Boolean,
|
|
||||||
list: { type: Array, default: ['http://localhost:5202/uploads/files/IM/2026/02/2/b92f0a4ba0f0.jpg', 'http://localhost:5202/uploads/files/IM/2026/02/2/b92f0a4ba0f0.jpg'] },
|
|
||||||
start: { type: Number, default: 0 }
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const url = ref('')
|
||||||
|
|
||||||
const visible = ref(true)
|
const test = async () => {
|
||||||
const index = ref(0)
|
|
||||||
const scale = ref(1)
|
|
||||||
const rotateDeg = ref(0)
|
|
||||||
const offset = ref({ x: 0, y: 0 })
|
|
||||||
|
|
||||||
let dragging = false
|
const cacheStore = useCacheStore();
|
||||||
let startPos = { x: 0, y: 0 }
|
|
||||||
|
|
||||||
watch(() => props.modelValue, v => {
|
const localPath = await cacheStore.getCache('http://192.168.5.116:7070/uploads/files/IM/2026/03/2/e6c407f60c68.jpg', FILE_TYPE.Image)
|
||||||
visible.value = v
|
console.log(localPath)
|
||||||
if (v) {
|
url.value = localPath
|
||||||
index.value = props.start
|
|
||||||
reset()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const current = computed(() => props.list[index.value] || '')
|
|
||||||
|
|
||||||
const style = computed(() => ({
|
|
||||||
transform: `translate(${offset.value.x}px, ${offset.value.y}px) scale(${scale.value}) rotate(${rotateDeg.value}deg)`
|
|
||||||
}))
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
scale.value = 1
|
|
||||||
rotateDeg.value = 0
|
|
||||||
offset.value = { x: 0, y: 0 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() { emit('update:modelValue', false) }
|
|
||||||
function prev() { if(index.value>0){ index.value--; reset() } }
|
|
||||||
function next() { if(index.value<props.list.length-1){ index.value++; reset() } }
|
|
||||||
function zoomIn() { scale.value = Math.min(scale.value + 0.2, 5) }
|
|
||||||
function zoomOut() { scale.value = Math.max(scale.value - 0.2, 0.3) }
|
|
||||||
function toggleZoom() { scale.value = scale.value === 1 ? 2 : 1 }
|
|
||||||
function rotate() { rotateDeg.value = (rotateDeg.value + 90) % 360 }
|
|
||||||
|
|
||||||
function onWheel(e){
|
|
||||||
if(e.ctrlKey){ e.deltaY>0?zoomOut():zoomIn() }
|
|
||||||
else { e.deltaY>0?next():prev() }
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDown(e){
|
|
||||||
dragging = true
|
|
||||||
startPos = { x: e.clientX - offset.value.x, y: e.clientY - offset.value.y }
|
|
||||||
window.addEventListener('mousemove', onMove)
|
|
||||||
window.addEventListener('mouseup', onUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMove(e){ if(!dragging) return; offset.value={ x:e.clientX-startPos.x, y:e.clientY-startPos.y } }
|
|
||||||
function onUp(){ dragging=false; window.removeEventListener('mousemove',onMove); window.removeEventListener('mouseup',onUp) }
|
|
||||||
|
|
||||||
async function download(){
|
|
||||||
try{
|
|
||||||
const res = await fetch(current.value)
|
|
||||||
const blob = await res.blob()
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.href = URL.createObjectURL(blob)
|
|
||||||
a.download = current.value.split('/').pop()
|
|
||||||
a.click()
|
|
||||||
URL.revokeObjectURL(a.href)
|
|
||||||
}catch(e){ console.error(e) }
|
|
||||||
}
|
|
||||||
|
|
||||||
function onKey(e){
|
|
||||||
if(!visible.value) return
|
|
||||||
if(e.key==='Escape') close()
|
|
||||||
if(e.key==='ArrowLeft') prev()
|
|
||||||
if(e.key==='ArrowRight') next()
|
|
||||||
if(e.key==='ArrowUp') zoomIn()
|
|
||||||
if(e.key==='ArrowDown') zoomOut()
|
|
||||||
if(e.key==='r'||e.key==='R') rotate()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(()=> window.addEventListener('keydown',onKey))
|
|
||||||
onUnmounted(()=> window.removeEventListener('keydown',onKey))
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.mask {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0,0,0,0.95);
|
|
||||||
z-index: 9999;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 图片 */
|
|
||||||
.img {
|
|
||||||
max-width: 90vw;
|
|
||||||
max-height: 90vh;
|
|
||||||
cursor: grab;
|
|
||||||
user-select: none;
|
|
||||||
transition: transform 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 工具栏 */
|
|
||||||
.toolbar {
|
|
||||||
position: absolute;
|
|
||||||
top: 24px;
|
|
||||||
display: flex;
|
|
||||||
gap: 14px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: rgba(20,20,20,0.85);
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar button {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
font-size: 22px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: rgba(255,255,255,0.08);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all .15s ease;
|
|
||||||
}
|
|
||||||
.toolbar button:hover { background: rgba(255,255,255,.2); transform: scale(1.05);}
|
|
||||||
.toolbar button:active { transform: scale(0.95); }
|
|
||||||
|
|
||||||
/* 左右翻页按钮 */
|
|
||||||
.nav {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
margin-top: -32px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba(0,0,0,.6);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 36px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.nav.left { left: 32px; }
|
|
||||||
.nav.right { right: 32px; }
|
|
||||||
.nav:hover { background: rgba(255,255,255,.2); }
|
|
||||||
|
|
||||||
/* 底部索引 */
|
|
||||||
.indicator {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 28px;
|
|
||||||
font-size: 16px;
|
|
||||||
color: #ddd;
|
|
||||||
background: rgba(0,0,0,.5);
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -14,10 +14,10 @@
|
|||||||
<div class="icon-box orange" v-html="feather.icons['user-plus'].toSvg()"></div>
|
<div class="icon-box orange" v-html="feather.icons['user-plus'].toSvg()"></div>
|
||||||
<div class="name">新的朋友</div>
|
<div class="name">新的朋友</div>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<div class="list-item mini" @click="showGroupList">
|
<RouterLink class="list-item mini" to="/contacts/grouphandle">
|
||||||
<div class="icon-box green" v-html="feather.icons['users'].toSvg()"></div>
|
<div class="icon-box green" v-html="feather.icons['users'].toSvg()"></div>
|
||||||
<div class="name">群聊</div>
|
<div class="name">群聊通知</div>
|
||||||
</div>
|
</RouterLink>
|
||||||
<div class="list-item mini">
|
<div class="list-item mini">
|
||||||
<div class="icon-box blue" v-html="feather.icons['tag'].toSvg()"></div>
|
<div class="icon-box blue" v-html="feather.icons['tag'].toSvg()"></div>
|
||||||
<div class="name">标签</div>
|
<div class="name">标签</div>
|
||||||
@ -104,9 +104,8 @@ const filteredContacts = computed(() => {
|
|||||||
// 发送事件给父组件(用于切换回聊天Tab并打开会话)
|
// 发送事件给父组件(用于切换回聊天Tab并打开会话)
|
||||||
const emit = defineEmits(['start-chat'])
|
const emit = defineEmits(['start-chat'])
|
||||||
|
|
||||||
const showGroupList = () => {
|
// const showGroupList = () => {
|
||||||
groupModal.value = true;
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="minimal-page">
|
<div class="minimal-page">
|
||||||
|
<WindowControls/>
|
||||||
|
<div class="request-container">
|
||||||
|
|
||||||
<div class="content-limit">
|
<div class="content-limit">
|
||||||
|
|
||||||
<div class="section-title">申请列表</div>
|
<div class="section-title">申请列表</div>
|
||||||
|
|
||||||
<div class="request-group">
|
<div class="request-group">
|
||||||
@ -51,6 +55,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -61,6 +68,7 @@ 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 { FRIEND_ACTIONS, FRIEND_REQUEST_STATUS } from '@/constants/friendAction';
|
||||||
import { SYSTEM_BASE_STATUS } from '@/constants/systemBaseStatus';
|
import { SYSTEM_BASE_STATUS } from '@/constants/systemBaseStatus';
|
||||||
|
import WindowControls from '../../components/WindowControls.vue';
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
@ -132,6 +140,11 @@ onMounted(async () => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #f5f5f5; /* 极致白 */
|
background-color: #f5f5f5; /* 极致白 */
|
||||||
|
}
|
||||||
|
.request-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #f5f5f5; /* 极致白 */
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
289
frontend/pc/IM/src/renderer/src/views/contact/GroupRequest.vue
Normal file
289
frontend/pc/IM/src/renderer/src/views/contact/GroupRequest.vue
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
<template>
|
||||||
|
<div class="minimal-page">
|
||||||
|
<WindowControls />
|
||||||
|
<div class="request-container">
|
||||||
|
<div class="content-limit">
|
||||||
|
<div class="section-title">群聊通知</div>
|
||||||
|
|
||||||
|
<div class="request-group">
|
||||||
|
<div v-for="item in groupRequestStore.groupRequest" :key="item.requestId" class="minimal-item">
|
||||||
|
|
||||||
|
<div class="avatar-wrapper">
|
||||||
|
<img :src="item.avatar" :class="['avatar', item.type === 'invite' ? 'is-group' : 'is-user']" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<div class="title-row">
|
||||||
|
<span class="name">{{ item.name }}</span>
|
||||||
|
<span :class="['type-tag', item.type === 'apply' ? 'tag-orange' : 'tag-green']">
|
||||||
|
{{ item.type === 'apply' ? '申请加入' : '邀请你加入' }}
|
||||||
|
</span>
|
||||||
|
<span class="date">14:20</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group-info">
|
||||||
|
<span class="label">目标群聊:</span>
|
||||||
|
<span class="group-name">微信产品经理交流群</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="sub-text">{{ item.desc }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn-text btn-reject">忽略</button>
|
||||||
|
<button class="btn-text btn-accept">去处理</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-hint">仅保留最近 30 天的通知记录</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import WindowControls from '../../components/WindowControls.vue';
|
||||||
|
import { useGroupRequestStore } from '../../stores/groupRequest';
|
||||||
|
import { useAuthStore } from '../../stores/auth';
|
||||||
|
import { GROUP_REQUEST_TYPE } from '../../constants/groupRequestTypeDefine';
|
||||||
|
import { GROUP_REQUEST_STATUS } from '../../constants/GroupDefine';
|
||||||
|
|
||||||
|
const groupRequestStore = useGroupRequestStore()
|
||||||
|
const myInfo = useAuthStore().userInfo
|
||||||
|
// 纯样式模拟数据
|
||||||
|
const mockRequests = [
|
||||||
|
{ id: 1, name: '张小龙', type: 'apply', desc: '久仰大名,请求入群交流产品心法。', avatar: 'https://i.pravatar.cc/100?img=1' },
|
||||||
|
{ id: 2, name: '李彦宏', type: 'invite', desc: '诚邀您加入百度大模型技术研讨组。', avatar: 'https://i.pravatar.cc/100?img=2' },
|
||||||
|
{ id: 3, name: '马斯克', type: 'apply', desc: 'I want to talk about Mars.', avatar: 'https://i.pravatar.cc/100?img=3' }
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
const getRequestType = (request) => {
|
||||||
|
if (
|
||||||
|
request.inviteUser &&
|
||||||
|
request.inviteUser == myInfo.id &&
|
||||||
|
[GROUP_REQUEST_STATUS.TARGET_DECLINED, GROUP_REQUEST_STATUS.TARGET_PENDING].includes(
|
||||||
|
request.status
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return GROUP_REQUEST_TYPE.INVITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
request.inviteUser &&
|
||||||
|
request.userId == myInfo.id &&
|
||||||
|
[GROUP_REQUEST_STATUS.TARGET_DECLINED, GROUP_REQUEST_STATUS.TARGET_PENDING].includes(
|
||||||
|
request.status
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return GROUP_REQUEST_TYPE.INVITED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.userId == myInfo.id) {
|
||||||
|
return GROUP_REQUEST_TYPE.IS_USER;
|
||||||
|
}
|
||||||
|
if (request.inviteUser != myInfo.id && request.userId != myInfo.id) {
|
||||||
|
return GROUP_REQUEST_TYPE.IS_GROUP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await groupRequestStore.loadGroupRequest()
|
||||||
|
console.log(groupRequestStore.groupRequest)
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 容器基础环境 */
|
||||||
|
.minimal-page {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #f5f5f7;
|
||||||
|
/* Apple 官网经典的背景灰 */
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-limit {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #86868b;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列表项设计 */
|
||||||
|
.minimal-item {
|
||||||
|
display: flex;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 18px;
|
||||||
|
/* 较圆润的倒角 */
|
||||||
|
margin-bottom: 12px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimal-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头像差异化处理 */
|
||||||
|
.avatar-wrapper {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #f2f2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-user {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户是圆的 */
|
||||||
|
.is-group {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 群组是方圆的 */
|
||||||
|
|
||||||
|
/* 信息排版 */
|
||||||
|
.info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d1d1f;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态标签 */
|
||||||
|
.type-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-orange {
|
||||||
|
background: #fff4e5;
|
||||||
|
color: #ff9500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-green {
|
||||||
|
background: #e8f7ed;
|
||||||
|
color: #34c759;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #c1c1c6;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 关键信息:群聊名称展示 */
|
||||||
|
.group-info {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #86868b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-name {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #007aff;
|
||||||
|
/* 链接蓝,暗示可点击 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #86868b;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0;
|
||||||
|
/* 文字截断 */
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮样式:去掉了生硬的边框,采用色块感 */
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
/* 垂直排列,更具操作仪式感 */
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-text {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-accept {
|
||||||
|
background-color: #007aff;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-accept:hover {
|
||||||
|
background-color: #0063cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reject {
|
||||||
|
background-color: #f2f2f7;
|
||||||
|
color: #86868b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reject:hover {
|
||||||
|
background-color: #e5e5ea;
|
||||||
|
color: #1d1d1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-hint {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #c1c1c6;
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -19,7 +19,8 @@
|
|||||||
<div v-for="s in filteredSessions" :key="s.id"
|
<div v-for="s in filteredSessions" :key="s.id"
|
||||||
class="list-item" :class="{active: activeId == s.id}" @click="selectSession(s)">
|
class="list-item" :class="{active: activeId == s.id}" @click="selectSession(s)">
|
||||||
<div class="avatar-container">
|
<div class="avatar-container">
|
||||||
<img :src="s.targetAvatar ? s.targetAvatar : defaultAvatar" class="avatar-std" />
|
<AsyncImage :raw-url="s.targetAvatar" class="avatar-std"/>
|
||||||
|
<!-- <img :src="s.targetAvatar ? s.targetAvatar : defaultAvatar" class="avatar-std" /> -->
|
||||||
<span v-if="s.unreadCount > 0" class="unread-badge">{{ s.unreadCount ?? 0 }}</span>
|
<span v-if="s.unreadCount > 0" class="unread-badge">{{ s.unreadCount ?? 0 }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
@ -35,7 +36,7 @@
|
|||||||
|
|
||||||
<RouterView></RouterView>
|
<RouterView></RouterView>
|
||||||
<SearchUser v-model="searchUserModal"/>
|
<SearchUser v-model="searchUserModal"/>
|
||||||
<CreateGroup v-model="createGroupModal"></CreateGroup>
|
<CreateGroup v-model="createGroupModal" @submit="createGroupSubmitHandler"></CreateGroup>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -51,10 +52,17 @@ import SearchUser from '@/components/user/SearchUser.vue'
|
|||||||
import CreateGroup from '@/components/groups/CreateGroup.vue'
|
import CreateGroup from '@/components/groups/CreateGroup.vue'
|
||||||
import { useBrowserNotification } from '@/services/useBrowserNotification'
|
import { useBrowserNotification } from '@/services/useBrowserNotification'
|
||||||
import { useChatStore } from '@/stores/chat'
|
import { useChatStore } from '@/stores/chat'
|
||||||
|
import { groupService } from '../../services/group'
|
||||||
|
import { SYSTEM_BASE_STATUS } from '../../constants/systemBaseStatus'
|
||||||
|
import { useMessage } from '../../components/messages/useAlert'
|
||||||
|
import { useCacheStore } from '../../stores/cache'
|
||||||
|
import AsyncImage from '../../components/AsyncImage.vue'
|
||||||
|
|
||||||
const conversationStore = useConversationStore();
|
const conversationStore = useConversationStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const browserNotification = useBrowserNotification();
|
const browserNotification = useBrowserNotification();
|
||||||
|
const message = useMessage()
|
||||||
|
const cacheStore = useCacheStore()
|
||||||
|
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const activeId = ref(0)
|
const activeId = ref(0)
|
||||||
@ -82,6 +90,25 @@ const addMenuList = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
const createGroupSubmitHandler = async (selectedUsers, groupName) => {
|
||||||
|
const res = await groupService.createGroup({
|
||||||
|
name: groupName,
|
||||||
|
avatar: "http://192.168.5.116:7070/uploads/files/IM/2026/03/2/bf1a0f691220.jpg",
|
||||||
|
userIDs: [...selectedUsers]
|
||||||
|
})
|
||||||
|
|
||||||
|
if(res.code == SYSTEM_BASE_STATUS.SUCCESS){
|
||||||
|
message.success('群聊创建成功')
|
||||||
|
}else{
|
||||||
|
message.error(res.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlHandler = async () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
const filteredSessions = computed(() => conversationStore.sortedConversations.filter(s => s.targetName.includes(searchQuery.value)))
|
const filteredSessions = computed(() => conversationStore.sortedConversations.filter(s => s.targetName.includes(searchQuery.value)))
|
||||||
|
|
||||||
function selectSession(s) {
|
function selectSession(s) {
|
||||||
@ -272,7 +299,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 头像样式统一 */
|
/* 头像样式统一 */
|
||||||
.avatar-std { width: 40px; height: 40px; border-radius: 4px; object-fit: cover; }
|
:deep(.avatar-std) { width: 40px; height: 40px; border-radius: 4px; object-fit: cover; }
|
||||||
.avatar-chat { width: 38px; height: 38px; border-radius: 4px; object-fit: cover; flex-shrink: 0; }
|
.avatar-chat { width: 38px; height: 38px; border-radius: 4px; object-fit: cover; flex-shrink: 0; }
|
||||||
|
|
||||||
/* 未读气泡 */
|
/* 未读气泡 */
|
||||||
|
|||||||
@ -18,29 +18,19 @@
|
|||||||
@retry="loadHistoryMsg" />
|
@retry="loadHistoryMsg" />
|
||||||
<UserHoverCard ref="userHoverCardRef" />
|
<UserHoverCard ref="userHoverCardRef" />
|
||||||
<ContextMenu ref="menuRef" />
|
<ContextMenu ref="menuRef" />
|
||||||
<Teleport to="body">
|
|
||||||
<Transition name="fade">
|
|
||||||
<div v-if="videoOpen" class="video-overlay" @click.self="videoOpen = false">
|
|
||||||
|
|
||||||
<div class="video-dialog">
|
<VideoPreview v-if="videoOpen" :videoData="videoUrl" @close="videoClose" />
|
||||||
<div class="close-bar" @click="videoOpen = false">
|
|
||||||
<span>正在播放视频</span>
|
|
||||||
<button class="close-btn">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="player-wrapper">
|
|
||||||
<vue3-video-player :src="videoUrl" poster="https://xxx.jpg" :controls="true" :autoplay="true" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</Teleport>
|
|
||||||
|
|
||||||
<div v-for="m in chatStore.messages" :key="m.id" :class="['msg', m.senderId == myInfo.id ? 'mine' : 'other']">
|
<div v-for="m in chatStore.messages" :key="m.id" :class="['msg', m.senderId == myInfo.id ? 'mine' : 'other']">
|
||||||
<img @mouseenter="(e) => handleHoverCard(e, m)" @mouseleave="closeHoverCard"
|
<!-- <img @mouseenter="(e) => handleHoverCard(e, m)" @mouseleave="closeHoverCard"
|
||||||
:src="(m.senderId == myInfo.id ? (myInfo?.avatar || defaultAvatar) : m.chatType == MESSAGE_TYPE.GROUP ? m.senderAvatar : conversationInfo?.targetAvatar) ?? defaultAvatar"
|
:src="(m.senderId == myInfo.id ? myInfo?.avatar : m.chatType == MESSAGE_TYPE.GROUP ? m.senderAvatar : conversationInfo?.targetAvatar)"
|
||||||
class="avatar-chat" />
|
class="avatar-chat" /> -->
|
||||||
|
<AsyncImage :raw-url="m.senderId == myInfo.id
|
||||||
|
? myInfo?.avatar
|
||||||
|
: m.chatType == MESSAGE_TYPE.GROUP
|
||||||
|
? m.senderAvatar
|
||||||
|
: conversationInfo?.targetAvatar
|
||||||
|
" @mouseenter="(e) => handleHoverCard(e, m)" class="avatar-chat" @mouseleave="closeHoverCard" />
|
||||||
|
|
||||||
<div class="msg-content">
|
<div class="msg-content">
|
||||||
<div class="group-sendername" v-if="m.chatType == MESSAGE_TYPE.GROUP && m.senderId != myInfo.id">{{
|
<div class="group-sendername" v-if="m.chatType == MESSAGE_TYPE.GROUP && m.senderId != myInfo.id">{{
|
||||||
@ -50,8 +40,11 @@
|
|||||||
<div v-if="m.type === 'Text'">{{ m.content }}</div>
|
<div v-if="m.type === 'Text'">{{ m.content }}</div>
|
||||||
<div v-else-if="m.type === 'emoji'" class="emoji-msg">{{ m.content }}</div>
|
<div v-else-if="m.type === 'emoji'" class="emoji-msg">{{ m.content }}</div>
|
||||||
<div v-else-if="m.type === FILE_TYPE.Image" class="image-msg-container" :style="getImageStyle(m.content)">
|
<div v-else-if="m.type === FILE_TYPE.Image" class="image-msg-container" :style="getImageStyle(m.content)">
|
||||||
<img class="image-msg-content" :src="m.isImgLoading || m.isLoading ? m.localUrl : m.content.thumb"
|
<!-- <img class="image-msg-content" :src="m.isImgLoading || m.isLoading ? m.localUrl : m.content.thumb"
|
||||||
alt="图片消息" @click="imagePreview(m)">
|
alt="图片消息" @click="imagePreview(m)"> -->
|
||||||
|
<AsyncImage class="image-msg-content" :noAvatar="true"
|
||||||
|
:rawUrl="m.isImgLoading || m.isLoading ? m.localUrl : m.content.thumb" alt="图片消息"
|
||||||
|
@click="imagePreview($event, m)" />
|
||||||
|
|
||||||
<div v-if="m.isImgLoading || m.isError" class="image-overlay">
|
<div v-if="m.isImgLoading || m.isError" class="image-overlay">
|
||||||
<div v-if="m.isImgLoading" class="progress-box">
|
<div v-if="m.isImgLoading" class="progress-box">
|
||||||
@ -89,7 +82,8 @@
|
|||||||
|
|
||||||
<footer class="chat-footer">
|
<footer class="chat-footer">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<button class="tool-btn" @click="toggleEmoji" v-html="feather.icons['smile'].toSvg({ width: 25, height: 25 })">
|
<button class="tool-btn" @click="toggleEmoji"
|
||||||
|
v-html="feather.icons['smile'].toSvg({ width: 25, height: 25 })">
|
||||||
</button>
|
</button>
|
||||||
<label class="tool-btn">
|
<label class="tool-btn">
|
||||||
<i v-html="feather.icons['file'].toSvg({ width: 25, height: 25 })"></i>
|
<i v-html="feather.icons['file'].toSvg({ width: 25, height: 25 })"></i>
|
||||||
@ -136,6 +130,9 @@ import WindowControls from '../../../components/WindowControls.vue';
|
|||||||
import InfoSidebar from '../../../components/messages/InfoSidebar.vue';
|
import InfoSidebar from '../../../components/messages/InfoSidebar.vue';
|
||||||
import { isElectron } from '../../../utils/electronHelper';
|
import { isElectron } from '../../../utils/electronHelper';
|
||||||
import { groupService } from '../../../services/group';
|
import { groupService } from '../../../services/group';
|
||||||
|
import VideoPreview from '../../../components/electron/VideoPreview.vue';
|
||||||
|
import { useRightClickHandler } from './hooks/useRightClickHandler';
|
||||||
|
import AsyncImage from '../../../components/AsyncImage.vue';
|
||||||
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -177,16 +174,11 @@ const videoUrl = ref(null);
|
|||||||
const videoOpen = ref(false)
|
const videoOpen = ref(false)
|
||||||
|
|
||||||
const infoShowHandler = async () => {
|
const infoShowHandler = async () => {
|
||||||
if (infoSideBarShow.value){
|
if (infoSideBarShow.value) {
|
||||||
infoSideBarShow.value = false;
|
infoSideBarShow.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
groupData = conversationInfo.value
|
groupData = conversationInfo.value
|
||||||
if (conversationInfo.value.chatType == MESSAGE_TYPE.GROUP) {
|
|
||||||
const { data } = await groupService.getGroupMember(groupData.targetId)
|
|
||||||
groupData.members = data
|
|
||||||
}
|
|
||||||
|
|
||||||
infoSideBarShow.value = true
|
infoSideBarShow.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,17 +209,18 @@ const getImageStyle = (content) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const imagePreview = (m) => {
|
const imagePreview = (e, m) => {
|
||||||
|
e.stopPropagation();
|
||||||
const imageList = chatStore.messages
|
const imageList = chatStore.messages
|
||||||
.filter(x => x.type == 'Image')
|
.filter(x => x.type == 'Image')
|
||||||
;
|
;
|
||||||
const index = imageList.indexOf(m);
|
const index = imageList.indexOf(m);
|
||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
const safeData = JSON.parse(JSON.stringify( {
|
const safeData = JSON.parse(JSON.stringify({
|
||||||
imageList,
|
imageList,
|
||||||
index
|
index
|
||||||
}));
|
}));
|
||||||
window.api.window.newWindow('imgpre',safeData);
|
window.api.window.newWindow('imgpre', safeData);
|
||||||
} else {
|
} else {
|
||||||
previewImages({
|
previewImages({
|
||||||
imgList: imageList.map(m => m.content.url),
|
imgList: imageList.map(m => m.content.url),
|
||||||
@ -279,9 +272,17 @@ const stopRecord = async () => {
|
|||||||
|
|
||||||
const playHandler = (m) => {
|
const playHandler = (m) => {
|
||||||
videoUrl.value = m.content.url
|
videoUrl.value = m.content.url
|
||||||
|
if (isElectron()) {
|
||||||
|
window.api.window.newWindow('videopre', videoUrl.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
videoOpen.value = true
|
videoOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const videoClose = () => {
|
||||||
|
videoOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
const loadHistoryMsg = async () => {
|
const loadHistoryMsg = async () => {
|
||||||
// 1. 如果正在加载,或者已经彻底没数据了,才拦截
|
// 1. 如果正在加载,或者已经彻底没数据了,才拦截
|
||||||
if (isLoading.value || isFinished.value) return;
|
if (isLoading.value || isFinished.value) return;
|
||||||
@ -326,34 +327,7 @@ const closeHoverCard = () => {
|
|||||||
|
|
||||||
const handleRightClick = (e, m) => {
|
const handleRightClick = (e, m) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const items = [
|
menuRef.value.show(e, useRightClickHandler(e, m));
|
||||||
{
|
|
||||||
label: '复制',
|
|
||||||
action: () => console.log('打开之前的悬浮卡片', user)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '转发',
|
|
||||||
action: () => console.log('进入私聊', user.id)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '多选',
|
|
||||||
action: () => { }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '翻译',
|
|
||||||
action: () => { }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '引用',
|
|
||||||
action: () => { }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '删除',
|
|
||||||
type: 'danger',
|
|
||||||
action: () => alert('删除成功')
|
|
||||||
}
|
|
||||||
];
|
|
||||||
menuRef.value.show(e, items);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@ -593,75 +567,7 @@ onUnmounted(() => {
|
|||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 遮罩层:全屏、黑色半透明、固定定位 */
|
|
||||||
.video-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: rgba(0, 0, 0, 0.85);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 9999;
|
|
||||||
/* 确保在最顶层 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 播放器弹窗主体 */
|
|
||||||
.video-dialog {
|
|
||||||
position: relative;
|
|
||||||
width: 90%;
|
|
||||||
max-width: 1000px;
|
|
||||||
background: #000;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 顶部状态栏(包含关闭按钮) */
|
|
||||||
.close-bar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px 20px;
|
|
||||||
background: #1a1a1a;
|
|
||||||
color: #eee;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 28px;
|
|
||||||
cursor: pointer;
|
|
||||||
line-height: 1;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn:hover {
|
|
||||||
transform: scale(1.2);
|
|
||||||
color: #ff4d4f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
/* 锁定 16:9 比例 */
|
|
||||||
background: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 进场动画 */
|
|
||||||
.fade-enter-active,
|
|
||||||
.fade-leave-active {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-from,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-btn {
|
.tool-btn {
|
||||||
/* 允许拖动整个窗口 */
|
/* 允许拖动整个窗口 */
|
||||||
@ -724,7 +630,7 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 图片:填满容器但不拉伸 */
|
/* 图片:填满容器但不拉伸 */
|
||||||
.image-msg-content {
|
:deep(.image-msg-content) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
@ -875,7 +781,7 @@ onUnmounted(() => {
|
|||||||
/* background: #95ec69; */
|
/* background: #95ec69; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-chat {
|
:deep(.avatar-chat) {
|
||||||
width: 38px;
|
width: 38px;
|
||||||
height: 38px;
|
height: 38px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
@ -1,21 +1,41 @@
|
|||||||
export function useRightClickHandler() {
|
import { FILE_TYPE } from '../../../../constants/fileTypeDefine'
|
||||||
const items = [
|
|
||||||
|
export function useRightClickHandler(e, m) {
|
||||||
|
const textRightItem = [
|
||||||
{
|
{
|
||||||
label: '查看资料',
|
label: '复制',
|
||||||
action: () => console.log('打开之前的悬浮卡片', user)
|
action: async () => {
|
||||||
|
await navigator.clipboard.writeText(e.target.innerText)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '发送消息',
|
label: '引用',
|
||||||
action: () => console.log('进入私聊', user.id)
|
action: () => console.log('进入私聊')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '修改备注',
|
label: '转发',
|
||||||
action: () => { }
|
action: () => {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '删除好友',
|
label: '删除',
|
||||||
type: 'danger',
|
type: 'danger',
|
||||||
action: () => alert('删除成功')
|
action: () => alert('删除成功')
|
||||||
}
|
}
|
||||||
];
|
]
|
||||||
|
|
||||||
|
const imgRightItem = [
|
||||||
|
{
|
||||||
|
label: '复制',
|
||||||
|
action: () => {
|
||||||
|
console.log(e.target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
switch (m.type) {
|
||||||
|
case FILE_TYPE.TEXT:
|
||||||
|
return textRightItem
|
||||||
|
case FILE_TYPE.Image:
|
||||||
|
case FILE_TYPE.Video:
|
||||||
|
return imgRightItem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user