Initial frontend repository commit.

Include app source and update .gitignore to exclude local release artifacts and signing files.

Made-with: Cursor
This commit is contained in:
2026-03-09 21:29:03 +08:00
commit 3968660048
129 changed files with 55599 additions and 0 deletions

867
src/types/dto.ts Normal file
View File

@@ -0,0 +1,867 @@
/**
* DTO Types - Data Transfer Object types for API communication
* 使用snake_case命名规范
*/
// ==================== User DTOs ====================
export interface UserDTO {
id: string;
username: string;
nickname: string;
avatar: string | null;
cover_url: string | null; // 头图URL
bio: string | null;
website: string | null;
location: string | null;
phone: string | null;
email: string | null;
email_verified?: boolean;
posts_count: number;
followers_count: number;
following_count: number;
created_at: string;
// 额外字段
is_following?: boolean; // 当前用户是否关注了该用户
is_following_me?: boolean; // 该用户是否关注了当前用户
}
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; // 该用户是否关注了当前用户
}
// ==================== Post DTOs ====================
export interface PostImageDTO {
id: string;
url: string;
thumbnail_url: string;
width: number;
height: number;
}
export interface PostDTO {
id: string;
user_id: string;
title: string;
content: string;
images: PostImageDTO[];
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;
author: UserDTO | null;
is_liked: boolean;
is_favorited: boolean;
// 额外字段
community_id?: string;
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;
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';
// 文本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;
}
// 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 };
// 消息响应
export interface MessageResponse {
id: string; // 雪花算法ID (使用string避免JavaScript精度丢失)
conversation_id: string;
sender_id: string; // UUID字符串
seq: number; // 消息序号
segments: MessageSegment[]; // 消息链
reply_to_id?: string; // 被回复消息的ID用于关联查找
status: MessageStatus;
category?: string; // 消息类别chat, notification, announcement
created_at: string;
sender?: UserDTO;
}
// 会话响应
export interface ConversationResponse {
id: string; // 雪花算法ID (使用string避免JavaScript精度丢失)
type: ConversationType;
is_pinned?: boolean;
last_seq: number;
last_message?: MessageResponse;
last_message_at: string;
unread_count: number;
participants?: UserDTO[]; // 私聊时使用
member_count?: number; // 群聊时使用
group?: GroupResponse; // 群聊会话的群组信息
created_at: string;
updated_at: string;
}
// 会话参与者响应
export interface ConversationParticipantResponse {
user_id: string;
last_read_seq: number;
muted: boolean;
}
// 会话详情响应
export interface ConversationDetailResponse {
id: string;
type: ConversationType;
is_pinned?: 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; // 对方用户的已读位置
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;
}
// ==================== Legacy Message DTOs (Deprecated) ====================
/**
* @deprecated 使用 MessageResponse 代替
*/
export interface MessageDTO {
id: string;
conversation_id: string;
sender_id: string;
content: string;
message_type: string;
is_read: boolean;
created_at: string;
sender: UserDTO;
}
/**
* @deprecated 使用 ConversationResponse 代替
*/
export interface ConversationDTO {
id: string;
user1_id: string;
user2_id: string;
last_message: MessageDTO | null;
last_message_at: string;
unread_count: number;
participant: UserDTO;
created_at: string;
updated_at: string;
}
// ==================== 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';
// 设备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 {
id: number;
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: number;
user_id: string; // UUID字符串
role: GroupRole;
nickname: string;
muted: boolean;
join_time: string;
user?: UserDTO;
}
// 群公告信息
export interface GroupAnnouncementResponse {
id: number;
group_id: number;
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;
}
// ==================== WebSocket Event Types ====================
// WebSocket 事件类型
export type WSEventType = 'message' | 'notice' | 'request' | 'meta';
// WebSocket 消息详细类型
export type WSDetailType = 'private' | 'group' | 'follow' | 'like' | 'comment' | 'request' | 'typing' | 'heartbeat' | 'read';
// WebSocket 事件消息段(与后端 message 字段一致)
export interface WSSegment {
type: string;
data: Record<string, any>;
}
// WebSocket 事件(后端推送的事件格式)
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
}
/**
* 从消息segments中提取纯文本内容
* 用于消息列表显示、通知等场景
*/
export function extractTextFromSegments(segments?: MessageSegment[]): string {
if (!segments || segments.length === 0) {
return '';
}
let result = '';
for (const segment of segments) {
switch (segment.type) {
case 'text':
// 兼容 content 和 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) {
result += `@${atData.nickname} `;
} else {
result += `@${atData.user_id} `;
}
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;
}
}
return result.trim();
}
// ==================== 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;
community_id?: string;
images?: string[];
vote_options: string[]; // 至少2个最多10个
}
/**
* 从消息segments中提取纯文本内容异步版本
* 用于会话列表等需要获取AT用户名的场景
* 优先从本地缓存获取用户信息fallback到API
*/
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;
}
}
return result.trim();
}

248
src/types/index.ts Normal file
View File

@@ -0,0 +1,248 @@
// ============================================
// Carrot BBS - Core Type Definitions
// 使用后端DTO类型保持snake_case命名规范
// ============================================
// 导出DTO类型作为主要类型
export * from './dto';
// 兼容旧类型(用于内部组件)
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,
} from './dto';
// 重新导出新的聊天系统类型
export type {
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,
};
// ============================================
// User Types
// ============================================
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 { VoteOptionDTO as VoteOption, VoteResultDTO as VoteResult, CreateVotePostRequest as CreateVotePostReq } from './dto';
export interface CreatePostInput {
title: string;
content: string;
images?: string[]; // 本地图片路径或 base64
}
// ============================================
// Comment Types
// ============================================
// 评论图片
export interface CommentImage {
url: string;
}
export interface Comment {
id: string;
post_id: string;
user_id: string;
parent_id: string | null;
root_id?: string | null;
content: string;
images?: CommentImage[];
likes_count: number;
replies_count: number;
is_liked?: boolean;
author: User;
replies?: Comment[];
target_id?: string | null; // 被回复的评论ID前端根据此ID找到被回复用户的昵称
created_at: string;
}
export interface CreateCommentInput {
postId: string;
parentId?: string;
content: string;
images?: string[]; // 图片URL列表
replyToUserId?: string;
}
// ============================================
// Notification Types
// ============================================
export type NotificationType =
| 'like_post'
| 'like_comment'
| 'comment'
| 'reply'
| 'follow'
| 'mention'
| 'system';
export interface Notification {
id: string;
userId: string;
type: NotificationType;
title: string;
content: string;
data: {
postId?: string;
commentId?: string;
userId?: string;
[key: string]: unknown;
};
isRead: boolean;
createdAt: string;
}
export interface NotificationBadge {
total: number;
likes: number;
comments: number;
follows: number;
system: number;
}
// ============================================
// Message Types (New Chat System)
// ============================================
/**
* 新版会话类型 - 使用雪花算法ID和seq机制
*/
export type Conversation = ConversationResponse;
/**
* 新版消息类型 - 使用雪花算法ID和seq机制
*/
export type Message = MessageResponse;
// ============================================
// Legacy Message Types (Deprecated)
// ============================================
/**
* @deprecated 使用 ConversationResponse 代替
*/
export interface LegacyConversation {
id: string;
participant: User;
lastMessage: LegacyMessage;
unreadCount: number;
updatedAt: string;
}
/**
* @deprecated 使用 MessageResponse 代替
*/
export interface LegacyMessage {
id: string;
conversationId: string;
senderId: string;
content: string;
type: 'text' | 'image' | 'system';
isRead: boolean;
createdAt: string;
}
// ============================================
// 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>;
};
}