refactor: restructure core architecture and responsive system
This commit implements a major architectural refactor to improve modularity, type safety, and maintainability across the codebase.
Key changes include:
- **Responsive System Refactor**: Migrated from a monolithic `useResponsive` hook to a set of specialized, granular hooks (`useBreakpoint`, `useOrientation`, `usePlatform`, etc.) located in `src/presentation/hooks/responsive`. This reduces unnecessary re-renders and improves developer experience.
- **Core Service Restructuring**: Introduced a new directory structure for services, including dedicated folders for `datasources`, `mappers`, and domain-specific types (`message`, `post`).
- **Data Layer Improvements**:
- Centralized JSON parsing logic in `src/database/core/jsonUtils.ts`.
- Cleaned up repository implementations by removing redundant local utility functions.
- Refactored `MessageMapper` to use the new centralized JSON utility.
- **Type System Cleanup**:
- Decomposed the large `src/types/dto.ts` into a modular `src/types/dto/` directory.
- Simplified `src/types/index.ts` and introduced backward-compatible aliases for core entities.
- **Utility Consolidation**:
- Created a centralized `src/utils/formatTime.ts` to replace fragmented date formatting logic across various screens and components.
- Removed deprecated responsive utility files in favor of the new hook-based system.
- **Service Logic Refinement**: Refactored `ApiClient` and `WebSocketService` to use a centralized `showVerificationModal` service, removing duplicated state management for verification prompts.
This commit is contained in:
998
src/types/dto.ts
998
src/types/dto.ts
@@ -1,998 +0,0 @@
|
||||
/**
|
||||
* DTO Types - Data Transfer Object types for API communication
|
||||
* 使用snake_case命名规范
|
||||
*/
|
||||
|
||||
// ==================== User DTOs ====================
|
||||
|
||||
export interface UserDTO {
|
||||
id: string;
|
||||
username: string;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
cover_url: string;
|
||||
bio: string;
|
||||
website: string;
|
||||
location: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
email_verified?: boolean;
|
||||
posts_count: number;
|
||||
followers_count: number;
|
||||
following_count: number;
|
||||
created_at: string;
|
||||
is_following?: boolean;
|
||||
is_following_me?: boolean;
|
||||
identity?: UserIdentity;
|
||||
verification_status?: VerificationStatus;
|
||||
}
|
||||
|
||||
export interface UserDetailDTO {
|
||||
id: string;
|
||||
username: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
email_verified?: boolean;
|
||||
avatar: string | null;
|
||||
bio: string | null;
|
||||
website: string | null;
|
||||
location: string | null;
|
||||
posts_count: number;
|
||||
followers_count: number;
|
||||
following_count: number;
|
||||
is_verified: boolean;
|
||||
created_at: string;
|
||||
// 额外字段
|
||||
is_following?: boolean; // 当前用户是否关注了该用户
|
||||
is_following_me?: boolean; // 该用户是否关注了当前用户
|
||||
identity?: UserIdentity;
|
||||
verification_status?: VerificationStatus;
|
||||
}
|
||||
|
||||
// ==================== Post DTOs ====================
|
||||
|
||||
export interface PostImageDTO {
|
||||
id: string;
|
||||
url: string;
|
||||
thumbnail_url: string;
|
||||
preview_url: string; // 列表/网格预览图
|
||||
preview_url_large: string; // 详情页预览图
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/** 帖子所属频道(列表卡片展示) */
|
||||
export interface PostChannelBriefDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface PostDTO {
|
||||
id: string;
|
||||
user_id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
segments?: MessageSegment[];
|
||||
images: PostImageDTO[];
|
||||
status?: string;
|
||||
likes_count: number;
|
||||
comments_count: number;
|
||||
favorites_count: number;
|
||||
shares_count: number;
|
||||
views_count: number;
|
||||
is_pinned: boolean;
|
||||
is_locked: boolean;
|
||||
is_vote: boolean;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
/** 用户编辑正文/标题/图片的时间;优先用于「已编辑」展示 */
|
||||
content_edited_at?: string;
|
||||
author: UserDTO | null;
|
||||
is_liked: boolean;
|
||||
is_favorited: boolean;
|
||||
// 额外字段
|
||||
channel_id?: string;
|
||||
/** 频道摘要(与 channel_id 对应,服务端批量填充) */
|
||||
channel?: PostChannelBriefDTO | null;
|
||||
top_comment?: CommentDTO | null;
|
||||
}
|
||||
|
||||
export interface PostDetailDTO extends PostDTO {
|
||||
status: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// ==================== Comment DTOs ====================
|
||||
|
||||
// 评论图片响应
|
||||
export interface CommentImageDTO {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface CommentDTO {
|
||||
id: string;
|
||||
post_id: string;
|
||||
user_id: string;
|
||||
parent_id: string | null;
|
||||
root_id: string | null;
|
||||
content: string;
|
||||
segments?: MessageSegment[];
|
||||
images: CommentImageDTO[];
|
||||
likes_count: number;
|
||||
replies_count: number;
|
||||
created_at: string;
|
||||
author: UserDTO;
|
||||
is_liked: boolean;
|
||||
target_id?: string | null; // 被回复的评论ID,前端根据此ID找到被回复用户的昵称
|
||||
replies?: CommentDTO[]; // 子回复列表(扁平化,所有层级都在这里)
|
||||
}
|
||||
|
||||
// ==================== Notification DTOs ====================
|
||||
|
||||
export interface NotificationDTO {
|
||||
id: string;
|
||||
user_id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
content: string;
|
||||
data: string;
|
||||
is_read: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// ==================== Chat System Types ====================
|
||||
|
||||
// 会话类型
|
||||
export type ConversationType = 'private' | 'group';
|
||||
|
||||
// 消息内容类型
|
||||
export type MessageContentType = 'text' | 'image' | 'video' | 'audio' | 'file';
|
||||
|
||||
// 消息状态
|
||||
export type MessageStatus = 'normal' | 'recalled' | 'deleted';
|
||||
|
||||
// ==================== Message Segment Types ====================
|
||||
|
||||
// Segment类型
|
||||
export type SegmentType = 'text' | 'image' | 'voice' | 'video' | 'file' | 'at' | 'reply' | 'face' | 'link' | 'vote' | 'post_ref';
|
||||
|
||||
// 文本Segment数据
|
||||
export interface TextSegmentData {
|
||||
content?: string; // 旧格式字段
|
||||
text?: string; // 新格式字段(用于 REST API)
|
||||
}
|
||||
|
||||
// 图片Segment数据
|
||||
export interface ImageSegmentData {
|
||||
url: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
thumbnail_url?: string;
|
||||
file_size?: number;
|
||||
}
|
||||
|
||||
// 语音Segment数据
|
||||
export interface VoiceSegmentData {
|
||||
url: string;
|
||||
duration?: number; // 秒
|
||||
file_size?: number;
|
||||
}
|
||||
|
||||
// 视频Segment数据
|
||||
export interface VideoSegmentData {
|
||||
url: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
duration?: number; // 秒
|
||||
thumbnail_url?: string;
|
||||
file_size?: number;
|
||||
}
|
||||
|
||||
// 文件Segment数据
|
||||
export interface FileSegmentData {
|
||||
url: string;
|
||||
name: string;
|
||||
size?: number;
|
||||
mime_type?: string;
|
||||
}
|
||||
|
||||
// @Segment数据
|
||||
export interface AtSegmentData {
|
||||
user_id: string; // "all" 表示@所有人
|
||||
nickname?: string;
|
||||
}
|
||||
|
||||
// 回复Segment数据
|
||||
export interface ReplySegmentData {
|
||||
id: string; // 被回复消息的ID
|
||||
seq?: number; // 被回复消息的序号(可选,用于快速查找)
|
||||
}
|
||||
|
||||
// 表情Segment数据
|
||||
export interface FaceSegmentData {
|
||||
id: number;
|
||||
name?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
// 链接Segment数据
|
||||
export interface LinkSegmentData {
|
||||
url: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
// 投票选项数据
|
||||
export interface VoteOptionData {
|
||||
id: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
// 投票Segment数据
|
||||
export interface VoteSegmentData {
|
||||
options: VoteOptionData[];
|
||||
max_choices?: number;
|
||||
is_multi_choice?: boolean;
|
||||
}
|
||||
|
||||
// 帖子内链Segment数据
|
||||
export interface PostRefSegmentData {
|
||||
post_id: string;
|
||||
title?: string;
|
||||
author?: {
|
||||
id: string;
|
||||
nickname: string;
|
||||
avatar?: string;
|
||||
};
|
||||
status?: string;
|
||||
accessible?: boolean;
|
||||
click_count?: number;
|
||||
}
|
||||
|
||||
// MessageSegment 联合类型 - 使用 discriminated union
|
||||
export type MessageSegment =
|
||||
| { type: 'text'; data: TextSegmentData }
|
||||
| { type: 'image'; data: ImageSegmentData }
|
||||
| { type: 'voice'; data: VoiceSegmentData }
|
||||
| { type: 'video'; data: VideoSegmentData }
|
||||
| { type: 'file'; data: FileSegmentData }
|
||||
| { type: 'at'; data: AtSegmentData }
|
||||
| { type: 'reply'; data: ReplySegmentData }
|
||||
| { type: 'face'; data: FaceSegmentData }
|
||||
| { type: 'link'; data: LinkSegmentData }
|
||||
| { type: 'vote'; data: VoteSegmentData }
|
||||
| { type: 'post_ref'; data: PostRefSegmentData };
|
||||
|
||||
// 消息响应
|
||||
export interface MessageResponse {
|
||||
id: string;
|
||||
conversation_id: string;
|
||||
sender_id: string;
|
||||
seq: number;
|
||||
segments: MessageSegment[];
|
||||
reply_to_id?: string;
|
||||
status: MessageStatus;
|
||||
category?: string;
|
||||
created_at: string;
|
||||
sender?: UserDTO;
|
||||
is_system_notice?: boolean;
|
||||
notice_content?: string;
|
||||
}
|
||||
|
||||
// 会话响应
|
||||
export interface ConversationResponse {
|
||||
id: string; // 雪花算法ID (使用string避免JavaScript精度丢失)
|
||||
type: ConversationType;
|
||||
is_pinned?: boolean;
|
||||
notification_muted?: boolean;
|
||||
last_seq: number;
|
||||
last_message?: MessageResponse;
|
||||
last_message_at: string;
|
||||
unread_count: number;
|
||||
participants?: UserDTO[]; // 私聊时使用
|
||||
member_count?: number; // 群聊时使用
|
||||
group?: GroupResponse; // 群聊会话的群组信息
|
||||
my_last_read_seq?: number; // 当前用户的已读位置
|
||||
other_last_read_seq?: number; // 对方用户的已读位置
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 会话参与者响应
|
||||
export interface ConversationParticipantResponse {
|
||||
user_id: string;
|
||||
last_read_seq: number;
|
||||
notification_muted: boolean;
|
||||
is_pinned: boolean;
|
||||
}
|
||||
|
||||
// 会话详情响应
|
||||
export interface ConversationDetailResponse {
|
||||
id: string;
|
||||
type: ConversationType;
|
||||
is_pinned?: boolean;
|
||||
notification_muted?: boolean;
|
||||
last_seq: number;
|
||||
last_message?: MessageResponse;
|
||||
last_message_at: string;
|
||||
unread_count: number;
|
||||
participants: UserDTO[];
|
||||
my_last_read_seq: number; // 当前用户的已读位置
|
||||
other_last_read_seq: number; // 对方用户的已读位置
|
||||
group?: GroupResponse; // 群聊会话的群组信息
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 创建会话请求
|
||||
export interface CreateConversationRequest {
|
||||
user_id: string; // 目标用户ID (UUID格式)
|
||||
}
|
||||
|
||||
// 发送消息请求
|
||||
export interface SendMessageRequest {
|
||||
segments?: MessageSegment[]; // 消息链(可选)
|
||||
content?: string; // 纯文本内容(可选,用于兼容旧版本)
|
||||
reply_to_id?: string; // 回复的消息ID (string类型)
|
||||
}
|
||||
|
||||
// 标记已读请求
|
||||
export interface MarkReadRequest {
|
||||
last_read_seq: number; // 已读到的seq位置
|
||||
}
|
||||
|
||||
// 未读数响应
|
||||
export interface UnreadCountResponse {
|
||||
total_unread_count: number; // 所有会话的未读总数
|
||||
}
|
||||
|
||||
// 单个会话未读数响应
|
||||
export interface ConversationUnreadCountResponse {
|
||||
conversation_id: string;
|
||||
unread_count: number;
|
||||
}
|
||||
|
||||
// 会话列表响应
|
||||
export interface ConversationListResponse {
|
||||
list: ConversationResponse[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
// 消息列表响应
|
||||
export interface MessageListResponse {
|
||||
messages: MessageResponse[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
// 消息同步响应(增量同步)
|
||||
export interface MessageSyncResponse {
|
||||
messages: MessageResponse[];
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
// ==================== Auth DTOs ====================
|
||||
|
||||
export interface LoginDTO {
|
||||
user: UserDTO;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
export interface RegisterDTO {
|
||||
user: UserDTO;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
export interface RefreshTokenDTO {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
// ==================== Common DTOs ====================
|
||||
|
||||
export interface SuccessDTO {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface AvailableDTO {
|
||||
available: boolean;
|
||||
}
|
||||
|
||||
export interface CountDTO {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface URLDTO {
|
||||
url: string;
|
||||
}
|
||||
|
||||
// ==================== API Response Types ====================
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface PaginatedData<T> {
|
||||
list: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
// ==================== System Message Types ====================
|
||||
|
||||
// 消息分类
|
||||
export type MessageCategory = 'chat' | 'notification' | 'announcement' | 'marketing';
|
||||
|
||||
// 系统消息类型
|
||||
export type SystemMessageType =
|
||||
| 'like_post'
|
||||
| 'like_comment'
|
||||
| 'like_reply'
|
||||
| 'favorite_post'
|
||||
| 'comment'
|
||||
| 'reply'
|
||||
| 'follow'
|
||||
| 'mention'
|
||||
| 'system'
|
||||
| 'announcement'
|
||||
| 'announce'
|
||||
| 'group_invite'
|
||||
| 'group_join_apply'
|
||||
| 'group_join_approved'
|
||||
| 'group_join_rejected';
|
||||
|
||||
// 系统消息额外数据
|
||||
export interface SystemMessageExtraData {
|
||||
// 后端返回的字段(使用 actor_ 前缀)
|
||||
actor_id?: number; // 操作者ID (数字格式,兼容旧数据)
|
||||
actor_id_str?: string; // 操作者ID (UUID字符串格式)
|
||||
actor_name?: string; // 操作者名称
|
||||
avatar_url?: string; // 操作者头像
|
||||
target_id?: string; // 目标ID (UUID字符串格式)
|
||||
target_title?: string; // 目标标题(如帖子标题)
|
||||
target_type?: string; // 目标类型
|
||||
action_url?: string; // 操作URL
|
||||
action_time?: string; // 操作时间
|
||||
group_id?: string;
|
||||
group_name?: string;
|
||||
group_avatar?: string;
|
||||
group_description?: string;
|
||||
flag?: string;
|
||||
request_type?: 'invite' | 'join_apply';
|
||||
request_status?: 'pending' | 'accepted' | 'rejected' | 'cancelled' | 'expired';
|
||||
reason?: string;
|
||||
target_user_id?: string;
|
||||
target_user_name?: string;
|
||||
target_user_avatar?: string;
|
||||
|
||||
// 兼容旧字段(使用 operator_ 前缀)
|
||||
operator_id?: number; // 操作者ID
|
||||
operator_name?: string; // 操作者名称
|
||||
operator_avatar?: string; // 操作者头像
|
||||
post_id?: number; // 帖子ID
|
||||
post_title?: string; // 帖子标题
|
||||
comment_id?: number; // 评论ID
|
||||
comment_preview?: string; // 评论预览
|
||||
reply_id?: number; // 回复ID
|
||||
}
|
||||
|
||||
// 系统消息响应
|
||||
export interface SystemMessageResponse {
|
||||
id: string; // 后端返回字符串格式
|
||||
sender_id: string;
|
||||
receiver_id: string;
|
||||
content: string;
|
||||
category: MessageCategory;
|
||||
system_type: SystemMessageType;
|
||||
extra_data?: SystemMessageExtraData;
|
||||
is_read?: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// 系统消息列表响应
|
||||
export interface SystemMessageListResponse {
|
||||
messages: SystemMessageResponse[];
|
||||
total: number;
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
// 系统消息未读数响应
|
||||
export interface SystemUnreadCountResponse {
|
||||
unread_count: number;
|
||||
}
|
||||
|
||||
// ==================== Push Device Types ====================
|
||||
|
||||
// 设备类型
|
||||
export type DeviceType = 'ios' | 'android' | 'web';
|
||||
|
||||
// ==================== 游标分页相关 DTO ====================
|
||||
|
||||
/**
|
||||
* 游标分页请求参数
|
||||
*/
|
||||
export interface CursorPaginationRequest {
|
||||
cursor?: string;
|
||||
direction?: 'forward' | 'backward';
|
||||
page_size?: number;
|
||||
post_type?: 'follow' | 'hot' | 'latest';
|
||||
channel_id?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 游标分页响应
|
||||
*/
|
||||
export interface CursorPaginationResponse<T> {
|
||||
/** 数据项列表 */
|
||||
list: T[];
|
||||
/** 下一页游标 */
|
||||
next_cursor: string | null;
|
||||
/** 上一页游标 */
|
||||
prev_cursor: string | null;
|
||||
/** 是否有更多数据 */
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
// 设备Token响应
|
||||
export interface DeviceTokenResponse {
|
||||
id: number;
|
||||
device_id: string;
|
||||
device_type: DeviceType;
|
||||
is_active: boolean;
|
||||
device_name?: string;
|
||||
last_used_at: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// 注册设备请求
|
||||
export interface RegisterDeviceRequest {
|
||||
device_id: string;
|
||||
device_type: DeviceType;
|
||||
push_token?: string;
|
||||
device_name?: string;
|
||||
}
|
||||
|
||||
// 推送记录响应
|
||||
export interface PushRecordResponse {
|
||||
id: number;
|
||||
message_id: number;
|
||||
push_channel: string;
|
||||
push_status: string;
|
||||
retry_count: number;
|
||||
pushed_at?: string;
|
||||
delivered_at?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// ==================== Group Chat Types ====================
|
||||
|
||||
// 群组加入类型
|
||||
export type JoinType = 0 | 1 | 2; // 0:允许任何人 1:需要审批 2:不允许
|
||||
|
||||
// 群组成员角色
|
||||
export type GroupRole = 'owner' | 'admin' | 'member';
|
||||
|
||||
// 群组信息
|
||||
export interface GroupResponse {
|
||||
/** 应用内统一为十进制字符串(groupService 从 API 归一化),避免大整型精度问题 */
|
||||
id: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
description: string;
|
||||
owner_id: string; // UUID字符串
|
||||
member_count: number;
|
||||
max_members: number;
|
||||
join_type: JoinType;
|
||||
mute_all: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// 群组成员信息
|
||||
export interface GroupMemberResponse {
|
||||
id: number;
|
||||
group_id: string;
|
||||
user_id: string; // UUID字符串
|
||||
role: GroupRole;
|
||||
nickname: string;
|
||||
muted: boolean;
|
||||
join_time: string;
|
||||
user?: UserDTO;
|
||||
}
|
||||
|
||||
// 群公告信息
|
||||
export interface GroupAnnouncementResponse {
|
||||
id: number;
|
||||
group_id: string;
|
||||
content: string;
|
||||
author_id: string; // UUID字符串
|
||||
is_pinned: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// 创建群组请求
|
||||
export interface CreateGroupRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
member_ids?: string[]; // UUID字符串数组
|
||||
}
|
||||
|
||||
// 更新群组请求
|
||||
export interface UpdateGroupRequest {
|
||||
name?: string;
|
||||
description?: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
// 邀请成员请求
|
||||
export interface InviteMembersRequest {
|
||||
member_ids: string[]; // UUID字符串数组
|
||||
}
|
||||
|
||||
export interface HandleGroupRequestAction {
|
||||
flag: string;
|
||||
approve: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
// 转让群主请求
|
||||
export interface TransferOwnerRequest {
|
||||
new_owner_id: string; // UUID字符串
|
||||
}
|
||||
|
||||
// 设置成员角色请求
|
||||
export interface SetRoleRequest {
|
||||
role: 'admin' | 'member';
|
||||
}
|
||||
|
||||
// 设置群昵称请求
|
||||
export interface SetNicknameRequest {
|
||||
nickname: string;
|
||||
}
|
||||
|
||||
// 禁言成员请求
|
||||
export interface MuteMemberRequest {
|
||||
muted: boolean;
|
||||
}
|
||||
|
||||
// 全员禁言请求
|
||||
export interface SetMuteAllRequest {
|
||||
mute_all: boolean;
|
||||
}
|
||||
|
||||
// 当前用户群成员信息响应
|
||||
export interface MyMemberInfoResponse {
|
||||
member: GroupMemberResponse;
|
||||
mute_all: boolean;
|
||||
is_muted: boolean;
|
||||
can_speak: boolean;
|
||||
}
|
||||
|
||||
// 设置加群方式请求
|
||||
export interface SetJoinTypeRequest {
|
||||
join_type: JoinType;
|
||||
}
|
||||
|
||||
// 设置群头像请求
|
||||
export interface SetGroupAvatarRequest {
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
// 创建群公告请求
|
||||
export interface CreateAnnouncementRequest {
|
||||
content: string;
|
||||
}
|
||||
|
||||
// 群组列表响应
|
||||
export interface GroupListResponse {
|
||||
list: GroupResponse[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
// 群组成员列表响应
|
||||
export interface GroupMemberListResponse {
|
||||
list: GroupMemberResponse[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
// 群公告列表响应
|
||||
export interface GroupAnnouncementListResponse {
|
||||
list: GroupAnnouncementResponse[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
// ==================== WS Event Types ====================
|
||||
|
||||
// WS 事件类型
|
||||
export type WSEventType = 'message' | 'notice' | 'request' | 'meta';
|
||||
|
||||
// WS 消息详细类型
|
||||
export type WSDetailType = 'private' | 'group' | 'follow' | 'like' | 'comment' | 'request' | 'typing' | 'heartbeat' | 'read';
|
||||
|
||||
// WS 事件消息段(与后端 message 字段一致)
|
||||
export interface WSSegment {
|
||||
type: string;
|
||||
data: Record<string, any>;
|
||||
}
|
||||
|
||||
// WS 事件(后端推送的事件格式)
|
||||
export interface WSEvent {
|
||||
id: string; // 事件唯一ID
|
||||
time: number; // 事件时间戳(毫秒)
|
||||
type: WSEventType; // 事件类型: message(消息), notice(通知), request(请求), meta(元事件)
|
||||
detail_type: WSDetailType; // 详细类型: private(私聊), group(群聊), follow(关注), like(点赞)等
|
||||
seq: string; // 消息序号
|
||||
message?: WSSegment[]; // 消息内容数组
|
||||
conversation_id: string; // 会话ID
|
||||
}
|
||||
|
||||
// 旧的类型名称,保持向后兼容
|
||||
export type SSEEventType = WSEventType;
|
||||
export type SSEDetailType = WSDetailType;
|
||||
export interface SSESegment extends WSSegment {}
|
||||
export interface SSEEvent extends WSEvent {}
|
||||
|
||||
/**
|
||||
* 从消息segments中提取纯文本内容
|
||||
* 用于消息列表显示、通知等场景
|
||||
*/
|
||||
export function extractTextFromSegments(segments?: MessageSegment[], memberMap?: Map<string, { nickname: string; user_id: string }>): string {
|
||||
if (!segments || segments.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let result = '';
|
||||
|
||||
for (const segment of segments) {
|
||||
switch (segment.type) {
|
||||
case 'text':
|
||||
const textData = segment.data as TextSegmentData;
|
||||
result += textData.content ?? textData.text ?? '';
|
||||
break;
|
||||
case 'at':
|
||||
const atData = segment.data as AtSegmentData;
|
||||
if (atData.user_id === 'all') {
|
||||
result += '@所有人 ';
|
||||
} else {
|
||||
const member = memberMap?.get(atData.user_id);
|
||||
const displayName = member?.nickname || atData.nickname || atData.user_id;
|
||||
result += `@${displayName} `;
|
||||
}
|
||||
break;
|
||||
case 'image':
|
||||
result += '[图片]';
|
||||
break;
|
||||
case 'voice':
|
||||
result += '[语音]';
|
||||
break;
|
||||
case 'video':
|
||||
result += '[视频]';
|
||||
break;
|
||||
case 'file':
|
||||
const fileData = segment.data as FileSegmentData;
|
||||
result += `[文件: ${fileData.name || '未知'}]`;
|
||||
break;
|
||||
case 'face':
|
||||
const faceData = segment.data as FaceSegmentData;
|
||||
result += faceData.name ? `[${faceData.name}]` : '[表情]';
|
||||
break;
|
||||
case 'link':
|
||||
const linkData = segment.data as LinkSegmentData;
|
||||
result += linkData.title ? `[链接: ${linkData.title}]` : '[链接]';
|
||||
break;
|
||||
case 'reply':
|
||||
// 回复不显示在预览中
|
||||
break;
|
||||
case 'post_ref':
|
||||
const refData = segment.data as PostRefSegmentData;
|
||||
result += refData.title ? `[帖子: ${refData.title}]` : '[帖子引用]';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
export type VisibilityLevel = 'everyone' | 'following' | 'self';
|
||||
|
||||
export interface PrivacySettingsDTO {
|
||||
followers_visibility: VisibilityLevel;
|
||||
following_visibility: VisibilityLevel;
|
||||
posts_visibility: VisibilityLevel;
|
||||
favorites_visibility: VisibilityLevel;
|
||||
}
|
||||
|
||||
export interface PrivacySettingsRequestDTO {
|
||||
followers_visibility?: VisibilityLevel;
|
||||
following_visibility?: VisibilityLevel;
|
||||
posts_visibility?: VisibilityLevel;
|
||||
favorites_visibility?: VisibilityLevel;
|
||||
}
|
||||
|
||||
export interface DeletionStatusDTO {
|
||||
is_pending_deletion: boolean;
|
||||
deletion_requested_at?: string;
|
||||
cool_down_days?: number;
|
||||
}
|
||||
|
||||
// ==================== Vote DTOs ====================
|
||||
|
||||
export interface VoteOptionDTO {
|
||||
id: string;
|
||||
content: string;
|
||||
votes_count: number;
|
||||
}
|
||||
|
||||
export interface VoteResultDTO {
|
||||
options: VoteOptionDTO[];
|
||||
total_votes: number;
|
||||
has_voted: boolean;
|
||||
voted_option_id?: string;
|
||||
}
|
||||
|
||||
export interface CreateVotePostRequest {
|
||||
title: string;
|
||||
content?: string;
|
||||
images?: string[];
|
||||
vote_options: Array<{
|
||||
content: string;
|
||||
}>;
|
||||
channel_id?: string;
|
||||
}
|
||||
|
||||
// ==================== Verification DTOs ====================
|
||||
|
||||
export type VerificationStatus = 'not_submitted' | 'pending' | 'approved' | 'rejected' | 'none';
|
||||
export type UserIdentity = 'general' | 'student' | 'teacher' | 'staff' | 'alumni' | 'external';
|
||||
|
||||
export interface VerificationStatusDTO {
|
||||
verification_status: VerificationStatus;
|
||||
identity?: UserIdentity;
|
||||
has_pending_request?: boolean;
|
||||
rejected_reason?: string;
|
||||
verified_at?: string;
|
||||
latest_record?: {
|
||||
id: string;
|
||||
reject_reason?: string;
|
||||
real_name?: string;
|
||||
department?: string;
|
||||
created_at?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface VerificationRecordDTO {
|
||||
id: string;
|
||||
user_id: string;
|
||||
identity: UserIdentity;
|
||||
status: VerificationStatus;
|
||||
real_name: string;
|
||||
student_id?: string;
|
||||
employee_id?: string;
|
||||
department?: string;
|
||||
proof_materials: string;
|
||||
reviewer_id?: string;
|
||||
review_note?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface SubmitVerificationRequest {
|
||||
identity: UserIdentity;
|
||||
real_name: string;
|
||||
student_id?: string;
|
||||
employee_id?: string;
|
||||
department?: string;
|
||||
proof_materials: string;
|
||||
}
|
||||
|
||||
export interface RequestDeletionRequestDTO {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export async function extractTextFromSegmentsAsync(
|
||||
segments?: MessageSegment[],
|
||||
getUserCache?: (userId: string) => Promise<UserDTO | null>,
|
||||
getUserById?: (userId: string) => Promise<UserDTO | null>
|
||||
): Promise<string> {
|
||||
if (!segments || segments.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let result = '';
|
||||
|
||||
for (const segment of segments) {
|
||||
switch (segment.type) {
|
||||
case 'text':
|
||||
const textData = segment.data as TextSegmentData;
|
||||
result += textData.content ?? textData.text ?? '';
|
||||
break;
|
||||
case 'at':
|
||||
const atData = segment.data as AtSegmentData;
|
||||
if (atData.user_id === 'all') {
|
||||
result += '@所有人 ';
|
||||
} else if (atData.nickname) {
|
||||
// 优先使用已有的 nickname
|
||||
result += `@${atData.nickname} `;
|
||||
} else {
|
||||
// 异步获取用户名
|
||||
let nickname = atData.user_id; // 默认显示 user_id
|
||||
try {
|
||||
// 先尝试从缓存获取
|
||||
if (getUserCache) {
|
||||
const cachedUser = await getUserCache(atData.user_id);
|
||||
if (cachedUser?.nickname) {
|
||||
nickname = cachedUser.nickname;
|
||||
} else if (getUserById) {
|
||||
// 缓存没有,从 API 获取
|
||||
const user = await getUserById(atData.user_id);
|
||||
if (user?.nickname) {
|
||||
nickname = user.nickname;
|
||||
}
|
||||
}
|
||||
} else if (getUserById) {
|
||||
// 没有缓存函数,直接从 API 获取
|
||||
const user = await getUserById(atData.user_id);
|
||||
if (user?.nickname) {
|
||||
nickname = user.nickname;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 获取失败,使用 user_id 作为 fallback
|
||||
console.warn('[extractTextFromSegmentsAsync] 获取用户信息失败:', error);
|
||||
}
|
||||
result += `@${nickname} `;
|
||||
}
|
||||
break;
|
||||
case 'image':
|
||||
result += '[图片]';
|
||||
break;
|
||||
case 'voice':
|
||||
result += '[语音]';
|
||||
break;
|
||||
case 'video':
|
||||
result += '[视频]';
|
||||
break;
|
||||
case 'file':
|
||||
const fileData = segment.data as FileSegmentData;
|
||||
result += `[文件: ${fileData.name || '未知'}]`;
|
||||
break;
|
||||
case 'face':
|
||||
const faceData = segment.data as FaceSegmentData;
|
||||
result += faceData.name ? `[${faceData.name}]` : '[表情]';
|
||||
break;
|
||||
case 'link':
|
||||
const linkData = segment.data as LinkSegmentData;
|
||||
result += linkData.title ? `[链接: ${linkData.title}]` : '[链接]';
|
||||
break;
|
||||
case 'reply':
|
||||
// 回复不显示在预览中
|
||||
break;
|
||||
case 'post_ref':
|
||||
const refDataAsync = segment.data as PostRefSegmentData;
|
||||
result += refDataAsync.title ? `[帖子: ${refDataAsync.title}]` : '[帖子引用]';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result.trim();
|
||||
}
|
||||
210
src/types/dto/common.ts
Normal file
210
src/types/dto/common.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Auth, Notification & System DTOs
|
||||
*/
|
||||
|
||||
import type { UserDTO } from './user';
|
||||
import type { MessageSegment, AtSegmentData, FileSegmentData, FaceSegmentData, LinkSegmentData, PostRefSegmentData } from './message';
|
||||
|
||||
// ==================== Auth DTOs ====================
|
||||
|
||||
export interface LoginDTO {
|
||||
user: UserDTO;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
export interface RegisterDTO {
|
||||
user: UserDTO;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
export interface RefreshTokenDTO {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
// ==================== Notification DTOs ====================
|
||||
|
||||
export interface NotificationDTO {
|
||||
id: string;
|
||||
user_id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
content: string;
|
||||
data: string;
|
||||
is_read: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// ==================== System Message Types ====================
|
||||
|
||||
export type MessageCategory = 'chat' | 'notification' | 'announcement' | 'marketing';
|
||||
|
||||
export type SystemMessageType =
|
||||
| 'like_post'
|
||||
| 'like_comment'
|
||||
| 'like_reply'
|
||||
| 'favorite_post'
|
||||
| 'comment'
|
||||
| 'reply'
|
||||
| 'follow'
|
||||
| 'mention'
|
||||
| 'system'
|
||||
| 'announcement'
|
||||
| 'announce'
|
||||
| 'group_invite'
|
||||
| 'group_join_apply'
|
||||
| 'group_join_approved'
|
||||
| 'group_join_rejected';
|
||||
|
||||
export interface SystemMessageExtraData {
|
||||
actor_id?: number;
|
||||
actor_id_str?: string;
|
||||
actor_name?: string;
|
||||
avatar_url?: string;
|
||||
target_id?: string;
|
||||
target_title?: string;
|
||||
target_type?: string;
|
||||
action_url?: string;
|
||||
action_time?: string;
|
||||
group_id?: string;
|
||||
group_name?: string;
|
||||
group_avatar?: string;
|
||||
group_description?: string;
|
||||
flag?: string;
|
||||
request_type?: 'invite' | 'join_apply';
|
||||
request_status?: 'pending' | 'accepted' | 'rejected' | 'cancelled' | 'expired';
|
||||
reason?: string;
|
||||
target_user_id?: string;
|
||||
target_user_name?: string;
|
||||
target_user_avatar?: string;
|
||||
operator_id?: number;
|
||||
operator_name?: string;
|
||||
operator_avatar?: string;
|
||||
post_id?: number;
|
||||
post_title?: string;
|
||||
comment_id?: number;
|
||||
comment_preview?: string;
|
||||
reply_id?: number;
|
||||
}
|
||||
|
||||
export interface SystemMessageResponse {
|
||||
id: string;
|
||||
sender_id: string;
|
||||
receiver_id: string;
|
||||
content: string;
|
||||
category: MessageCategory;
|
||||
system_type: SystemMessageType;
|
||||
extra_data?: SystemMessageExtraData;
|
||||
is_read?: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface SystemMessageListResponse {
|
||||
messages: SystemMessageResponse[];
|
||||
total: number;
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
export interface SystemUnreadCountResponse {
|
||||
unread_count: number;
|
||||
}
|
||||
|
||||
// ==================== Push Device Types ====================
|
||||
|
||||
export type DeviceType = 'ios' | 'android' | 'web';
|
||||
|
||||
export interface DeviceTokenResponse {
|
||||
id: number;
|
||||
device_id: string;
|
||||
device_type: DeviceType;
|
||||
is_active: boolean;
|
||||
device_name?: string;
|
||||
last_used_at: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface RegisterDeviceRequest {
|
||||
device_id: string;
|
||||
device_type: DeviceType;
|
||||
push_token?: string;
|
||||
device_name?: string;
|
||||
}
|
||||
|
||||
export interface PushRecordResponse {
|
||||
id: number;
|
||||
message_id: number;
|
||||
push_channel: string;
|
||||
push_status: string;
|
||||
retry_count: number;
|
||||
pushed_at?: string;
|
||||
delivered_at?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// ==================== WS Event Types ====================
|
||||
|
||||
export type WSEventType = 'message' | 'notice' | 'request' | 'meta';
|
||||
export type WSDetailType = 'private' | 'group' | 'follow' | 'like' | 'comment' | 'request' | 'typing' | 'heartbeat' | 'read';
|
||||
|
||||
export interface WSSegment {
|
||||
type: string;
|
||||
data: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface WSEvent {
|
||||
id: string;
|
||||
time: number;
|
||||
type: WSEventType;
|
||||
detail_type: WSDetailType;
|
||||
seq: string;
|
||||
message?: WSSegment[];
|
||||
conversation_id: string;
|
||||
}
|
||||
|
||||
// ==================== Verification DTOs ====================
|
||||
|
||||
import type { VerificationStatus, UserIdentity } from './user';
|
||||
|
||||
export type { VerificationStatus, UserIdentity } from './user';
|
||||
|
||||
export interface VerificationStatusDTO {
|
||||
verification_status: VerificationStatus;
|
||||
identity?: UserIdentity;
|
||||
has_pending_request?: boolean;
|
||||
rejected_reason?: string;
|
||||
verified_at?: string;
|
||||
latest_record?: {
|
||||
id: string;
|
||||
reject_reason?: string;
|
||||
real_name?: string;
|
||||
department?: string;
|
||||
created_at?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface VerificationRecordDTO {
|
||||
id: string;
|
||||
user_id: string;
|
||||
identity: UserIdentity;
|
||||
status: VerificationStatus;
|
||||
real_name: string;
|
||||
student_id?: string;
|
||||
employee_id?: string;
|
||||
department?: string;
|
||||
proof_materials: string;
|
||||
reviewer_id?: string;
|
||||
review_note?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface SubmitVerificationRequest {
|
||||
identity: UserIdentity;
|
||||
real_name: string;
|
||||
student_id?: string;
|
||||
employee_id?: string;
|
||||
department?: string;
|
||||
proof_materials: string;
|
||||
}
|
||||
126
src/types/dto/group.ts
Normal file
126
src/types/dto/group.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Group Chat DTOs
|
||||
*/
|
||||
|
||||
import type { UserDTO } from './user';
|
||||
|
||||
export type JoinType = 0 | 1 | 2;
|
||||
export type GroupRole = 'owner' | 'admin' | 'member';
|
||||
|
||||
export interface GroupResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
description: string;
|
||||
owner_id: string;
|
||||
member_count: number;
|
||||
max_members: number;
|
||||
join_type: JoinType;
|
||||
mute_all: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface GroupMemberResponse {
|
||||
id: number;
|
||||
group_id: string;
|
||||
user_id: string;
|
||||
role: GroupRole;
|
||||
nickname: string;
|
||||
muted: boolean;
|
||||
join_time: string;
|
||||
user?: UserDTO;
|
||||
}
|
||||
|
||||
export interface GroupAnnouncementResponse {
|
||||
id: number;
|
||||
group_id: string;
|
||||
content: string;
|
||||
author_id: string;
|
||||
is_pinned: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface CreateGroupRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
member_ids?: string[];
|
||||
}
|
||||
|
||||
export interface UpdateGroupRequest {
|
||||
name?: string;
|
||||
description?: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
export interface InviteMembersRequest {
|
||||
member_ids: string[];
|
||||
}
|
||||
|
||||
export interface HandleGroupRequestAction {
|
||||
flag: string;
|
||||
approve: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface TransferOwnerRequest {
|
||||
new_owner_id: string;
|
||||
}
|
||||
|
||||
export interface SetRoleRequest {
|
||||
role: 'admin' | 'member';
|
||||
}
|
||||
|
||||
export interface SetNicknameRequest {
|
||||
nickname: string;
|
||||
}
|
||||
|
||||
export interface MuteMemberRequest {
|
||||
muted: boolean;
|
||||
}
|
||||
|
||||
export interface SetMuteAllRequest {
|
||||
mute_all: boolean;
|
||||
}
|
||||
|
||||
export interface MyMemberInfoResponse {
|
||||
member: GroupMemberResponse;
|
||||
mute_all: boolean;
|
||||
is_muted: boolean;
|
||||
can_speak: boolean;
|
||||
}
|
||||
|
||||
export interface SetJoinTypeRequest {
|
||||
join_type: JoinType;
|
||||
}
|
||||
|
||||
export interface SetGroupAvatarRequest {
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
export interface CreateAnnouncementRequest {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface GroupListResponse {
|
||||
list: GroupResponse[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
export interface GroupMemberListResponse {
|
||||
list: GroupMemberResponse[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
export interface GroupAnnouncementListResponse {
|
||||
list: GroupAnnouncementResponse[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
}
|
||||
11
src/types/dto/index.ts
Normal file
11
src/types/dto/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* DTO Types - Re-exports from individual modules
|
||||
*/
|
||||
|
||||
export * from './user';
|
||||
export * from './post';
|
||||
export * from './message';
|
||||
export * from './group';
|
||||
export * from './common';
|
||||
export * from './pagination';
|
||||
export * from './segmentUtils';
|
||||
211
src/types/dto/message.ts
Normal file
211
src/types/dto/message.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Message Segment Types & Chat DTOs
|
||||
*/
|
||||
|
||||
import type { UserDTO } from './user';
|
||||
import type { GroupResponse } from './group';
|
||||
|
||||
export type ConversationType = 'private' | 'group';
|
||||
export type MessageContentType = 'text' | 'image' | 'video' | 'audio' | 'file';
|
||||
export type MessageStatus = 'normal' | 'recalled' | 'deleted';
|
||||
|
||||
// ==================== Segment Types ====================
|
||||
|
||||
export type SegmentType = 'text' | 'image' | 'voice' | 'video' | 'file' | 'at' | 'reply' | 'face' | 'link' | 'vote' | 'post_ref';
|
||||
|
||||
export interface TextSegmentData {
|
||||
content?: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export interface ImageSegmentData {
|
||||
url: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
thumbnail_url?: string;
|
||||
file_size?: number;
|
||||
}
|
||||
|
||||
export interface VoiceSegmentData {
|
||||
url: string;
|
||||
duration?: number;
|
||||
file_size?: number;
|
||||
}
|
||||
|
||||
export interface VideoSegmentData {
|
||||
url: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
duration?: number;
|
||||
thumbnail_url?: string;
|
||||
file_size?: number;
|
||||
}
|
||||
|
||||
export interface FileSegmentData {
|
||||
url: string;
|
||||
name: string;
|
||||
size?: number;
|
||||
mime_type?: string;
|
||||
}
|
||||
|
||||
export interface AtSegmentData {
|
||||
user_id: string;
|
||||
nickname?: string;
|
||||
}
|
||||
|
||||
export interface ReplySegmentData {
|
||||
id: string;
|
||||
seq?: number;
|
||||
}
|
||||
|
||||
export interface FaceSegmentData {
|
||||
id: number;
|
||||
name?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface LinkSegmentData {
|
||||
url: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export interface VoteOptionData {
|
||||
id: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface VoteSegmentData {
|
||||
options: VoteOptionData[];
|
||||
max_choices?: number;
|
||||
is_multi_choice?: boolean;
|
||||
}
|
||||
|
||||
export interface PostRefSegmentData {
|
||||
post_id: string;
|
||||
title?: string;
|
||||
author?: {
|
||||
id: string;
|
||||
nickname: string;
|
||||
avatar?: string;
|
||||
};
|
||||
status?: string;
|
||||
accessible?: boolean;
|
||||
click_count?: number;
|
||||
}
|
||||
|
||||
export type MessageSegment =
|
||||
| { type: 'text'; data: TextSegmentData }
|
||||
| { type: 'image'; data: ImageSegmentData }
|
||||
| { type: 'voice'; data: VoiceSegmentData }
|
||||
| { type: 'video'; data: VideoSegmentData }
|
||||
| { type: 'file'; data: FileSegmentData }
|
||||
| { type: 'at'; data: AtSegmentData }
|
||||
| { type: 'reply'; data: ReplySegmentData }
|
||||
| { type: 'face'; data: FaceSegmentData }
|
||||
| { type: 'link'; data: LinkSegmentData }
|
||||
| { type: 'vote'; data: VoteSegmentData }
|
||||
| { type: 'post_ref'; data: PostRefSegmentData };
|
||||
|
||||
// ==================== Chat DTOs ====================
|
||||
|
||||
export interface MessageResponse {
|
||||
id: string;
|
||||
conversation_id: string;
|
||||
sender_id: string;
|
||||
seq: number;
|
||||
segments: MessageSegment[];
|
||||
reply_to_id?: string;
|
||||
status: MessageStatus;
|
||||
category?: string;
|
||||
created_at: string;
|
||||
sender?: UserDTO;
|
||||
is_system_notice?: boolean;
|
||||
notice_content?: string;
|
||||
}
|
||||
|
||||
export interface ConversationResponse {
|
||||
id: string;
|
||||
type: ConversationType;
|
||||
is_pinned?: boolean;
|
||||
notification_muted?: boolean;
|
||||
last_seq: number;
|
||||
last_message?: MessageResponse;
|
||||
last_message_at: string;
|
||||
unread_count: number;
|
||||
participants?: UserDTO[];
|
||||
member_count?: number;
|
||||
group?: GroupResponse;
|
||||
my_last_read_seq?: number;
|
||||
other_last_read_seq?: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ConversationParticipantResponse {
|
||||
user_id: string;
|
||||
last_read_seq: number;
|
||||
notification_muted: boolean;
|
||||
is_pinned: boolean;
|
||||
}
|
||||
|
||||
export interface ConversationDetailResponse {
|
||||
id: string;
|
||||
type: ConversationType;
|
||||
is_pinned?: boolean;
|
||||
notification_muted?: boolean;
|
||||
last_seq: number;
|
||||
last_message?: MessageResponse;
|
||||
last_message_at: string;
|
||||
unread_count: number;
|
||||
participants: UserDTO[];
|
||||
my_last_read_seq: number;
|
||||
other_last_read_seq: number;
|
||||
group?: GroupResponse;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateConversationRequest {
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
export interface SendMessageRequest {
|
||||
segments?: MessageSegment[];
|
||||
content?: string;
|
||||
reply_to_id?: string;
|
||||
}
|
||||
|
||||
export interface MarkReadRequest {
|
||||
last_read_seq: number;
|
||||
}
|
||||
|
||||
export interface UnreadCountResponse {
|
||||
total_unread_count: number;
|
||||
}
|
||||
|
||||
export interface ConversationUnreadCountResponse {
|
||||
conversation_id: string;
|
||||
unread_count: number;
|
||||
}
|
||||
|
||||
export interface ConversationListResponse {
|
||||
list: ConversationResponse[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
export interface MessageListResponse {
|
||||
messages: MessageResponse[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
export interface MessageSyncResponse {
|
||||
messages: MessageResponse[];
|
||||
has_more: boolean;
|
||||
}
|
||||
92
src/types/dto/pagination.ts
Normal file
92
src/types/dto/pagination.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Pagination, Vote, Privacy & Other DTOs
|
||||
*/
|
||||
|
||||
// ==================== Common ====================
|
||||
|
||||
export interface SuccessDTO {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface AvailableDTO {
|
||||
available: boolean;
|
||||
}
|
||||
|
||||
export interface CountDTO {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface URLDTO {
|
||||
url: string;
|
||||
}
|
||||
|
||||
// ==================== Cursor Pagination ====================
|
||||
|
||||
export interface CursorPaginationRequest {
|
||||
cursor?: string;
|
||||
direction?: 'forward' | 'backward';
|
||||
page_size?: number;
|
||||
post_type?: 'follow' | 'hot' | 'latest';
|
||||
channel_id?: string;
|
||||
}
|
||||
|
||||
export interface CursorPaginationResponse<T> {
|
||||
list: T[];
|
||||
next_cursor: string | null;
|
||||
prev_cursor: string | null;
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
// ==================== Vote DTOs ====================
|
||||
|
||||
export interface VoteOptionDTO {
|
||||
id: string;
|
||||
content: string;
|
||||
votes_count: number;
|
||||
}
|
||||
|
||||
export interface VoteResultDTO {
|
||||
options: VoteOptionDTO[];
|
||||
total_votes: number;
|
||||
has_voted: boolean;
|
||||
voted_option_id?: string;
|
||||
}
|
||||
|
||||
export interface CreateVotePostRequest {
|
||||
title: string;
|
||||
content?: string;
|
||||
images?: string[];
|
||||
vote_options: Array<{
|
||||
content: string;
|
||||
}>;
|
||||
channel_id?: string;
|
||||
}
|
||||
|
||||
// ==================== Privacy & Account ====================
|
||||
|
||||
export type VisibilityLevel = 'everyone' | 'following' | 'self';
|
||||
|
||||
export interface PrivacySettingsDTO {
|
||||
followers_visibility: VisibilityLevel;
|
||||
following_visibility: VisibilityLevel;
|
||||
posts_visibility: VisibilityLevel;
|
||||
favorites_visibility: VisibilityLevel;
|
||||
}
|
||||
|
||||
export interface PrivacySettingsRequestDTO {
|
||||
followers_visibility?: VisibilityLevel;
|
||||
following_visibility?: VisibilityLevel;
|
||||
posts_visibility?: VisibilityLevel;
|
||||
favorites_visibility?: VisibilityLevel;
|
||||
}
|
||||
|
||||
export interface DeletionStatusDTO {
|
||||
is_pending_deletion: boolean;
|
||||
deletion_requested_at?: string;
|
||||
cool_down_days?: number;
|
||||
}
|
||||
|
||||
export interface RequestDeletionRequestDTO {
|
||||
password: string;
|
||||
}
|
||||
75
src/types/dto/post.ts
Normal file
75
src/types/dto/post.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Post & Comment DTOs
|
||||
*/
|
||||
|
||||
import type { UserDTO } from './user';
|
||||
import type { MessageSegment } from './message';
|
||||
|
||||
export interface PostImageDTO {
|
||||
id: string;
|
||||
url: string;
|
||||
thumbnail_url: string;
|
||||
preview_url: string;
|
||||
preview_url_large: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface PostChannelBriefDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface PostDTO {
|
||||
id: string;
|
||||
user_id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
segments?: MessageSegment[];
|
||||
images: PostImageDTO[];
|
||||
status?: string;
|
||||
likes_count: number;
|
||||
comments_count: number;
|
||||
favorites_count: number;
|
||||
shares_count: number;
|
||||
views_count: number;
|
||||
is_pinned: boolean;
|
||||
is_locked: boolean;
|
||||
is_vote: boolean;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
content_edited_at?: string;
|
||||
author: UserDTO | null;
|
||||
is_liked: boolean;
|
||||
is_favorited: boolean;
|
||||
channel_id?: string;
|
||||
channel?: PostChannelBriefDTO | null;
|
||||
top_comment?: CommentDTO | null;
|
||||
}
|
||||
|
||||
export interface PostDetailDTO extends PostDTO {
|
||||
status: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CommentImageDTO {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface CommentDTO {
|
||||
id: string;
|
||||
post_id: string;
|
||||
user_id: string;
|
||||
parent_id: string | null;
|
||||
root_id: string | null;
|
||||
content: string;
|
||||
segments?: MessageSegment[];
|
||||
images: CommentImageDTO[];
|
||||
likes_count: number;
|
||||
replies_count: number;
|
||||
created_at: string;
|
||||
author: UserDTO;
|
||||
is_liked: boolean;
|
||||
target_id?: string | null;
|
||||
replies?: CommentDTO[];
|
||||
}
|
||||
108
src/types/dto/segmentUtils.ts
Normal file
108
src/types/dto/segmentUtils.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Segment text extraction utilities
|
||||
*/
|
||||
|
||||
import type { MessageSegment, TextSegmentData, AtSegmentData, FileSegmentData, FaceSegmentData, LinkSegmentData, PostRefSegmentData } from './message';
|
||||
import type { UserDTO } from './user';
|
||||
|
||||
/**
|
||||
* Extract plain text from message segments for display
|
||||
*/
|
||||
export function extractTextFromSegments(segments?: MessageSegment[], memberMap?: Map<string, { nickname: string; user_id: string }>): string {
|
||||
if (!segments || segments.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let result = '';
|
||||
|
||||
for (const segment of segments) {
|
||||
switch (segment.type) {
|
||||
case 'text':
|
||||
const textData = segment.data as TextSegmentData;
|
||||
result += textData.content ?? textData.text ?? '';
|
||||
break;
|
||||
case 'at':
|
||||
const atData = segment.data as AtSegmentData;
|
||||
if (atData.user_id === 'all') {
|
||||
result += '@所有人 ';
|
||||
} else {
|
||||
const member = memberMap?.get(atData.user_id);
|
||||
const displayName = member?.nickname || atData.nickname || atData.user_id;
|
||||
result += `@${displayName} `;
|
||||
}
|
||||
break;
|
||||
case 'image':
|
||||
result += '[图片]';
|
||||
break;
|
||||
case 'voice':
|
||||
result += '[语音]';
|
||||
break;
|
||||
case 'video':
|
||||
result += '[视频]';
|
||||
break;
|
||||
case 'file':
|
||||
const fileData = segment.data as FileSegmentData;
|
||||
result += `[文件: ${fileData.name || '未知'}]`;
|
||||
break;
|
||||
case 'face':
|
||||
const faceData = segment.data as FaceSegmentData;
|
||||
result += faceData.name ? `[${faceData.name}]` : '[表情]';
|
||||
break;
|
||||
case 'link':
|
||||
const linkData = segment.data as LinkSegmentData;
|
||||
result += linkData.title ? `[链接: ${linkData.title}]` : '[链接]';
|
||||
break;
|
||||
case 'reply':
|
||||
break;
|
||||
case 'post_ref':
|
||||
const refData = segment.data as PostRefSegmentData;
|
||||
result += refData.title ? `[帖子: ${refData.title}]` : '[帖子引用]';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Async version that resolves missing @at nicknames
|
||||
*/
|
||||
export async function extractTextFromSegmentsAsync(
|
||||
segments?: MessageSegment[],
|
||||
getUserCache?: (userId: string) => Promise<UserDTO | null>,
|
||||
getUserById?: (userId: string) => Promise<UserDTO | null>
|
||||
): Promise<string> {
|
||||
if (!segments || segments.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const resolvedSegments = [...segments];
|
||||
for (let i = 0; i < resolvedSegments.length; i++) {
|
||||
const segment = resolvedSegments[i];
|
||||
if (segment.type === 'at') {
|
||||
const atData = segment.data as AtSegmentData;
|
||||
if (atData.user_id !== 'all' && !atData.nickname) {
|
||||
try {
|
||||
let nickname = atData.user_id;
|
||||
if (getUserCache) {
|
||||
const cachedUser = await getUserCache(atData.user_id);
|
||||
if (cachedUser?.nickname) {
|
||||
nickname = cachedUser.nickname;
|
||||
} else if (getUserById) {
|
||||
const user = await getUserById(atData.user_id);
|
||||
if (user?.nickname) nickname = user.nickname;
|
||||
}
|
||||
} else if (getUserById) {
|
||||
const user = await getUserById(atData.user_id);
|
||||
if (user?.nickname) nickname = user.nickname;
|
||||
}
|
||||
resolvedSegments[i] = { ...segment, data: { ...atData, nickname } };
|
||||
} catch (error) {
|
||||
console.warn('[extractTextFromSegmentsAsync] 获取用户信息失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extractTextFromSegments(resolvedSegments);
|
||||
}
|
||||
49
src/types/dto/user.ts
Normal file
49
src/types/dto/user.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* User DTOs
|
||||
*/
|
||||
|
||||
export type VerificationStatus = 'not_submitted' | 'pending' | 'approved' | 'rejected' | 'none';
|
||||
export type UserIdentity = 'general' | 'student' | 'teacher' | 'staff' | 'alumni' | 'external';
|
||||
|
||||
export interface UserDTO {
|
||||
id: string;
|
||||
username: string;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
cover_url: string;
|
||||
bio: string;
|
||||
website: string;
|
||||
location: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
email_verified?: boolean;
|
||||
posts_count: number;
|
||||
followers_count: number;
|
||||
following_count: number;
|
||||
created_at: string;
|
||||
is_following?: boolean;
|
||||
is_following_me?: boolean;
|
||||
identity?: UserIdentity;
|
||||
verification_status?: VerificationStatus;
|
||||
}
|
||||
|
||||
export interface UserDetailDTO {
|
||||
id: string;
|
||||
username: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
email_verified?: boolean;
|
||||
avatar: string | null;
|
||||
bio: string | null;
|
||||
website: string | null;
|
||||
location: string | null;
|
||||
posts_count: number;
|
||||
followers_count: number;
|
||||
following_count: number;
|
||||
is_verified: boolean;
|
||||
created_at: string;
|
||||
is_following?: boolean;
|
||||
is_following_me?: boolean;
|
||||
identity?: UserIdentity;
|
||||
verification_status?: VerificationStatus;
|
||||
}
|
||||
@@ -1,73 +1,30 @@
|
||||
// ============================================
|
||||
// WithYou - Core Type Definitions
|
||||
// 使用后端DTO类型,保持snake_case命名规范
|
||||
// ============================================
|
||||
/**
|
||||
* WithYou - Core Type Definitions
|
||||
*/
|
||||
|
||||
// 导出DTO类型作为主要类型
|
||||
export * from './dto';
|
||||
export * from './schedule';
|
||||
export * from './material';
|
||||
export * from './trade';
|
||||
|
||||
// 兼容旧类型(用于内部组件)
|
||||
import type {
|
||||
UserDTO,
|
||||
PostDTO,
|
||||
PostImageDTO,
|
||||
// 新的聊天系统类型
|
||||
ConversationType,
|
||||
MessageContentType,
|
||||
MessageStatus,
|
||||
MessageResponse,
|
||||
ConversationResponse,
|
||||
ConversationParticipantResponse,
|
||||
ConversationDetailResponse,
|
||||
CreateConversationRequest,
|
||||
SendMessageRequest,
|
||||
MarkReadRequest,
|
||||
UnreadCountResponse,
|
||||
ConversationUnreadCountResponse,
|
||||
ConversationListResponse,
|
||||
MessageListResponse,
|
||||
MessageSyncResponse,
|
||||
// Segment 类型
|
||||
SegmentType,
|
||||
MessageSegment,
|
||||
TextSegmentData,
|
||||
ImageSegmentData,
|
||||
VoiceSegmentData,
|
||||
VideoSegmentData,
|
||||
FileSegmentData,
|
||||
AtSegmentData,
|
||||
ReplySegmentData,
|
||||
FaceSegmentData,
|
||||
LinkSegmentData,
|
||||
VoteSegmentData,
|
||||
VoteOptionData,
|
||||
ConversationResponse,
|
||||
MessageResponse,
|
||||
} from './dto';
|
||||
|
||||
// ============================================
|
||||
// User Types
|
||||
// Backward-compat aliases — prefer DTO names directly
|
||||
// ============================================
|
||||
|
||||
export type User = UserDTO;
|
||||
|
||||
export interface UserProfile extends User {
|
||||
favorites_count: number;
|
||||
liked_posts_count: number;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Post Types
|
||||
// ============================================
|
||||
|
||||
export type Post = PostDTO;
|
||||
export type PostDetail = PostDTO;
|
||||
export type PostImage = PostImageDTO;
|
||||
|
||||
// ============================================
|
||||
// Vote Types
|
||||
// ============================================
|
||||
export type Conversation = ConversationResponse;
|
||||
export type Message = MessageResponse;
|
||||
|
||||
export type { VoteOptionDTO as VoteOption, VoteResultDTO as VoteResult, CreateVotePostRequest as CreateVotePostReq } from './dto';
|
||||
|
||||
@@ -83,7 +40,6 @@ export interface CreatePostInput {
|
||||
// Comment Types
|
||||
// ============================================
|
||||
|
||||
// 评论图片
|
||||
export interface CommentImage {
|
||||
url: string;
|
||||
}
|
||||
@@ -102,7 +58,7 @@ export interface Comment {
|
||||
is_liked?: boolean;
|
||||
author: User;
|
||||
replies?: Comment[];
|
||||
target_id?: string | null; // 被回复的评论ID,前端根据此ID找到被回复用户的昵称
|
||||
target_id?: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@@ -110,7 +66,7 @@ export interface CreateCommentInput {
|
||||
postId: string;
|
||||
parentId?: string;
|
||||
content: string;
|
||||
images?: string[]; // 图片URL列表
|
||||
images?: string[];
|
||||
replyToUserId?: string;
|
||||
}
|
||||
|
||||
@@ -149,49 +105,4 @@ export interface NotificationBadge {
|
||||
comments: number;
|
||||
follows: number;
|
||||
system: number;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Message Types (New Chat System)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 新版会话类型 - 使用雪花算法ID和seq机制
|
||||
*/
|
||||
export type Conversation = ConversationResponse;
|
||||
|
||||
/**
|
||||
* 新版消息类型 - 使用雪花算法ID和seq机制
|
||||
*/
|
||||
export type Message = MessageResponse;
|
||||
|
||||
// ============================================
|
||||
// API Response Types
|
||||
// ============================================
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
success: boolean;
|
||||
data: T[];
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
hasMore: boolean;
|
||||
nextCursor?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
success: false;
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: Record<string, string>;
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user