fix(frontend): 修复 TypeScript 类型错误,与后端 API 响应结构对齐
Some checks failed
Frontend CI / build-android-apk (push) Has been cancelled
Frontend CI / build-and-push-web (push) Has been cancelled
Frontend CI / ota-android (push) Has been cancelled

- 修复 UserDTO 类型定义,字段改为非 nullable 类型
- 修复 PostMapper 使用正确的 snake_case 字段名
- 修复 UserMapper 移除不存在的 is_blocked 和 updated_at 字段
- 修复 MessageMapper 使用 segments 替代已移除的字段
- 修复 MessageSyncService 正确处理 API 响应类型
- 修复 LocalDataSource SQLite 参数类型问题
- 修复导航相关类型错误
- 修复 PostDetailScreen 和 EditProfileScreen 的 null/undefined 类型不匹配
This commit is contained in:
lafay
2026-03-19 10:53:09 +08:00
parent a91637466c
commit de94447844
14 changed files with 146 additions and 148 deletions

View File

@@ -72,12 +72,7 @@ export class ApiDataSource implements IApiDataSource {
async delete<T>(url: string, data?: any): Promise<T> { async delete<T>(url: string, data?: any): Promise<T> {
try { try {
const fullUrl = this.buildUrl(url); const fullUrl = this.buildUrl(url);
// 处理带 request body 的 DELETE 请求 const response = await api.delete<T>(fullUrl, data);
if (data) {
const response = await api.request<T>('DELETE', fullUrl, undefined, data);
return response.data;
}
const response = await api.delete<T>(fullUrl);
return response.data; return response.data;
} catch (error) { } catch (error) {
this.handleError(error, 'DELETE'); this.handleError(error, 'DELETE');

View File

@@ -183,7 +183,10 @@ export class LocalDataSource implements ILocalDataSource {
async query<T>(sql: string, params?: any[]): Promise<T[]> { async query<T>(sql: string, params?: any[]): Promise<T[]> {
try { try {
const db = this.ensureDb(); const db = this.ensureDb();
return await db.getAllAsync<T>(sql, params); if (params && params.length > 0) {
return await db.getAllAsync<T>(sql, params as any);
}
return await db.getAllAsync<T>(sql);
} catch (error) { } catch (error) {
this.handleError(error, 'QUERY'); this.handleError(error, 'QUERY');
} }
@@ -192,7 +195,10 @@ export class LocalDataSource implements ILocalDataSource {
async getFirst<T>(sql: string, params?: any[]): Promise<T | null> { async getFirst<T>(sql: string, params?: any[]): Promise<T | null> {
try { try {
const db = this.ensureDb(); const db = this.ensureDb();
return await db.getFirstAsync<T>(sql, params); if (params && params.length > 0) {
return await db.getFirstAsync<T>(sql, params as any);
}
return await db.getFirstAsync<T>(sql);
} catch (error) { } catch (error) {
this.handleError(error, 'GET_FIRST'); this.handleError(error, 'GET_FIRST');
} }
@@ -210,7 +216,10 @@ export class LocalDataSource implements ILocalDataSource {
async run(sql: string, params?: any[]): Promise<{ lastInsertRowId: number; changes: number }> { async run(sql: string, params?: any[]): Promise<{ lastInsertRowId: number; changes: number }> {
try { try {
const db = this.ensureDb(); const db = this.ensureDb();
return await db.runAsync(sql, params); if (params && params.length > 0) {
return await db.runAsync(sql, params as any);
}
return await db.runAsync(sql);
} catch (error) { } catch (error) {
this.handleError(error, 'RUN'); this.handleError(error, 'RUN');
} }

View File

@@ -212,7 +212,7 @@ export class ConversationMapper {
id: String(user.id || ''), id: String(user.id || ''),
username: user.username || '', username: user.username || '',
nickname: user.nickname, nickname: user.nickname,
avatar: user.avatar, avatar: user.avatar ?? undefined,
}; };
} }
@@ -220,19 +220,31 @@ export class ConversationMapper {
* 映射群组 API 响应 * 映射群组 API 响应
*/ */
private static mapGroupFromApi(group: GroupResponse): GroupModel { private static mapGroupFromApi(group: GroupResponse): GroupModel {
// 将数字类型的 join_type 转换为字符串类型
// JoinType: 0 = 允许任何人, 1 = 需要审批, 2 = 不允许
const mapJoinType = (joinType: number): 'anyone' | 'approval' | 'invite' => {
switch (joinType) {
case 0: return 'anyone';
case 1: return 'approval';
case 2: return 'invite';
default: return 'approval';
}
};
return { return {
id: String(group.id || ''), id: String(group.id || ''),
name: group.name || '', name: group.name || '',
avatar: group.avatar, avatar: group.avatar,
description: group.description, description: group.description,
announcement: group.announcement, // GroupResponse 没有 announcement 和 updated_at 字段,使用默认值
announcement: undefined,
ownerId: String(group.owner_id || ''), ownerId: String(group.owner_id || ''),
memberCount: group.member_count || 0, memberCount: group.member_count || 0,
maxMemberCount: group.max_member_count || 500, maxMemberCount: group.max_members || 500,
joinType: group.join_type || 'approval', joinType: mapJoinType(group.join_type),
isMuted: group.mute_all || false, isMuted: group.mute_all || false,
createdAt: new Date(group.created_at || Date.now()), createdAt: new Date(group.created_at || Date.now()),
updatedAt: new Date(group.updated_at || Date.now()), updatedAt: new Date(group.created_at || Date.now()),
}; };
} }

View File

@@ -32,13 +32,16 @@ export class MessageMapper {
* 将 API 响应转换为应用模型 * 将 API 响应转换为应用模型
*/ */
static fromApiResponse(response: MessageResponse): MessageModel { static fromApiResponse(response: MessageResponse): MessageModel {
const textSegment = response.segments?.find(s => s.type === 'text');
const content = textSegment?.data?.text || textSegment?.data?.content || '';
return { return {
id: String(response.id || ''), id: String(response.id || ''),
conversationId: String(response.conversation_id || ''), conversationId: String(response.conversation_id || ''),
senderId: String(response.sender_id || ''), senderId: String(response.sender_id || ''),
content: response.content || '', content,
type: response.message_type || 'text', type: 'text',
isRead: response.is_read || false, isRead: false,
createdAt: new Date(response.created_at || Date.now()), createdAt: new Date(response.created_at || Date.now()),
seq: response.seq || 0, seq: response.seq || 0,
status: response.status || 'normal', status: response.status || 'normal',
@@ -169,15 +172,12 @@ export class MessageMapper {
} }
} }
/**
* 映射发送者信息
*/
private static mapSenderFromApi(sender: UserDTO): UserModel { private static mapSenderFromApi(sender: UserDTO): UserModel {
return { return {
id: String(sender.id || ''), id: String(sender.id || ''),
username: sender.username || '', username: sender.username || '',
nickname: sender.nickname, nickname: sender.nickname,
avatar: sender.avatar, avatar: sender.avatar || undefined,
}; };
} }
} }

View File

@@ -4,31 +4,28 @@
*/ */
import { PostModel, UserModel } from '../models'; import { PostModel, UserModel } from '../models';
import type { Post } from '../../types'; import type { PostDTO } from '../../types/dto';
export class PostMapper { export class PostMapper {
/** static fromApiResponse(response: PostDTO): PostModel {
* 将 API 响应转换为应用模型
*/
static fromApiResponse(response: Post): PostModel {
return { return {
id: String(response.id || ''), id: String(response.id || ''),
authorId: String(response.author_id || ''), authorId: String(response.user_id || ''),
author: response.author ? this.mapAuthorFromApi(response.author) : undefined, author: response.author ? this.mapAuthorFromApi(response.author) : undefined,
title: response.title || '', title: response.title || '',
content: response.content || '', content: response.content || '',
images: response.images || [], images: response.images?.map(img => img.url) || [],
likeCount: response.like_count || 0, likeCount: response.likes_count || 0,
commentCount: response.comment_count || 0, commentCount: response.comments_count || 0,
shareCount: response.share_count || 0, shareCount: response.shares_count || 0,
viewCount: response.view_count || 0, viewCount: response.views_count || 0,
favoriteCount: response.favorite_count || 0, favoriteCount: response.favorites_count || 0,
isLiked: response.is_liked || false, isLiked: response.is_liked || false,
isFavorited: response.is_favorited || false, isFavorited: response.is_favorited || false,
isTop: response.is_top || false, isTop: response.is_pinned || false,
status: response.status || 'published', status: (response.status || 'published') as 'published' | 'draft' | 'deleted',
communityId: response.community_id, communityId: response.community_id,
tags: response.tags || [], tags: [],
createdAt: new Date(response.created_at || Date.now()), createdAt: new Date(response.created_at || Date.now()),
updatedAt: new Date(response.updated_at || Date.now()), updatedAt: new Date(response.updated_at || Date.now()),
}; };
@@ -37,7 +34,7 @@ export class PostMapper {
/** /**
* 将 API 响应数组转换为应用模型数组 * 将 API 响应数组转换为应用模型数组
*/ */
static fromApiResponseList(responses: Post[]): PostModel[] { static fromApiResponseList(responses: PostDTO[]): PostModel[] {
return responses.map(r => this.fromApiResponse(r)); return responses.map(r => this.fromApiResponse(r));
} }

View File

@@ -4,7 +4,7 @@
*/ */
import { UserModel } from '../models'; import { UserModel } from '../models';
import type { UserDTO, User } from '../../types/dto'; import type { UserDTO } from '../../types/dto';
// 数据库用户记录类型 // 数据库用户记录类型
export interface UserDbRecord { export interface UserDbRecord {
@@ -14,9 +14,6 @@ export interface UserDbRecord {
} }
export class UserMapper { export class UserMapper {
/**
* 将 API DTO 转换为应用模型
*/
static fromDTO(dto: UserDTO): UserModel { static fromDTO(dto: UserDTO): UserModel {
return { return {
id: String(dto.id || ''), id: String(dto.id || ''),
@@ -32,39 +29,10 @@ export class UserMapper {
followingCount: dto.following_count, followingCount: dto.following_count,
postsCount: dto.posts_count, postsCount: dto.posts_count,
isFollowing: dto.is_following, isFollowing: dto.is_following,
isBlocked: dto.is_blocked,
createdAt: dto.created_at ? new Date(dto.created_at) : undefined, createdAt: dto.created_at ? new Date(dto.created_at) : undefined,
updatedAt: dto.updated_at ? new Date(dto.updated_at) : undefined,
}; };
} }
/**
* 将 User 类型转换为应用模型
*/
static fromUser(user: User): UserModel {
return {
id: String(user.id || ''),
username: user.username || '',
nickname: user.nickname,
avatar: user.avatar,
bio: user.bio,
website: user.website,
location: user.location,
email: user.email,
phone: user.phone,
followersCount: user.followers_count,
followingCount: user.following_count,
postsCount: user.posts_count,
isFollowing: user.is_following,
isBlocked: user.is_blocked,
createdAt: user.created_at ? new Date(user.created_at) : undefined,
updatedAt: user.updated_at ? new Date(user.updated_at) : undefined,
};
}
/**
* 将 DTO 数组转换为应用模型数组
*/
static fromDTOList(dtos: UserDTO[]): UserModel[] { static fromDTOList(dtos: UserDTO[]): UserModel[] {
return dtos.map(dto => this.fromDTO(dto)); return dtos.map(dto => this.fromDTO(dto));
} }
@@ -88,20 +56,19 @@ export class UserMapper {
const dto: UserDTO = { const dto: UserDTO = {
id: model.id, id: model.id,
username: model.username, username: model.username,
nickname: model.nickname, nickname: model.nickname || '',
avatar: model.avatar, avatar: model.avatar || '',
bio: model.bio, cover_url: '',
website: model.website, bio: model.bio || '',
location: model.location, website: model.website || '',
location: model.location || '',
email: model.email, email: model.email,
phone: model.phone, phone: model.phone,
followers_count: model.followersCount, followers_count: model.followersCount || 0,
following_count: model.followingCount, following_count: model.followingCount || 0,
posts_count: model.postsCount, posts_count: model.postsCount || 0,
is_following: model.isFollowing, is_following: model.isFollowing,
is_blocked: model.isBlocked, created_at: model.createdAt?.toISOString() || '',
created_at: model.createdAt?.toISOString(),
updated_at: model.updatedAt?.toISOString(),
}; };
return { return {
@@ -149,20 +116,19 @@ export class UserMapper {
return { return {
id: model.id, id: model.id,
username: model.username, username: model.username,
nickname: model.nickname, nickname: model.nickname || '',
avatar: model.avatar, avatar: model.avatar || '',
bio: model.bio, cover_url: '',
website: model.website, bio: model.bio || '',
location: model.location, website: model.website || '',
location: model.location || '',
email: model.email, email: model.email,
phone: model.phone, phone: model.phone,
followers_count: model.followersCount, followers_count: model.followersCount || 0,
following_count: model.followingCount, following_count: model.followingCount || 0,
posts_count: model.postsCount, posts_count: model.postsCount || 0,
is_following: model.isFollowing, is_following: model.isFollowing,
is_blocked: model.isBlocked, created_at: model.createdAt?.toISOString() || '',
created_at: model.createdAt?.toISOString(),
updated_at: model.updatedAt?.toISOString(),
}; };
} }
} }

View File

@@ -3,7 +3,7 @@
* 定义消息数据访问的抽象 * 定义消息数据访问的抽象
*/ */
import type { Message, Conversation, ConversationType } from '../../types/dto'; import type { Message, Conversation } from '../../../core/entities/Message';
export interface IMessageRepository { export interface IMessageRepository {
// ==================== 消息操作 ==================== // ==================== 消息操作 ====================

View File

@@ -53,7 +53,7 @@ class NavigationService {
if (!this.isNavigationReady()) { if (!this.isNavigationReady()) {
return null; return null;
} }
return this.navigationRef!.getCurrentRoute(); return this.navigationRef!.getCurrentRoute() ?? null;
} }
/** /**

View File

@@ -115,7 +115,7 @@ export function DesktopNavigator({ unreadCount = 0 }: DesktopNavigatorProps) {
> >
<View style={styles.sidebarIconContainer}> <View style={styles.sidebarIconContainer}>
<MaterialCommunityIcons <MaterialCommunityIcons
name={isActive ? item.icon : item.iconOutline} name={isActive ? item.icon as any : item.iconOutline as any}
size={24} size={24}
color={isActive ? colors.primary.main : colors.text.secondary} color={isActive ? colors.primary.main : colors.text.secondary}
/> />

View File

@@ -15,7 +15,6 @@ export type {
NavItemConfig, NavItemConfig,
} from './types'; } from './types';
// 导航器
export { AuthNavigator } from './AuthNavigator'; export { AuthNavigator } from './AuthNavigator';
export { HomeNavigator } from './HomeNavigator'; export { HomeNavigator } from './HomeNavigator';
export { MessageNavigator } from './MessageNavigator'; export { MessageNavigator } from './MessageNavigator';
@@ -24,7 +23,7 @@ export { ProfileNavigator } from './ProfileNavigator';
export { TabNavigator } from './TabNavigator'; export { TabNavigator } from './TabNavigator';
export { DesktopNavigator } from './DesktopNavigator'; export { DesktopNavigator } from './DesktopNavigator';
export { RootNavigator } from './RootNavigator'; export { RootNavigator } from './RootNavigator';
export { MainNavigator } from './MainNavigator'; export { default as MainNavigator } from './MainNavigator';
// 移动端简化导航 // 移动端简化导航
export { SimpleMobileTabNavigator } from './SimpleMobileTabNavigator'; export { SimpleMobileTabNavigator } from './SimpleMobileTabNavigator';

View File

@@ -635,13 +635,13 @@ export const PostDetailScreen: React.FC = () => {
id: '', id: '',
username: 'guest', username: 'guest',
nickname: '游客', nickname: '游客',
avatar: null, avatar: '',
cover_url: null, cover_url: '',
bio: null, bio: '',
website: null, website: '',
location: null, location: '',
phone: null, phone: undefined,
email: null, email: undefined,
posts_count: 0, posts_count: 0,
followers_count: 0, followers_count: 0,
following_count: 0, following_count: 0,

View File

@@ -225,11 +225,11 @@ export const EditProfileScreen: React.FC = () => {
if (updatedUser) { if (updatedUser) {
updateUser({ updateUser({
nickname: nickname.trim(), nickname: nickname.trim(),
bio: bio.trim() || null, bio: bio.trim() || undefined,
location: location.trim() || null, location: location.trim() || undefined,
website: website.trim() || null, website: website.trim() || undefined,
phone: phone.trim() || null, phone: phone.trim() || undefined,
email: email.trim() || null, email: email.trim() || undefined,
}); });
Alert.alert('成功', '资料已更新', [ Alert.alert('成功', '资料已更新', [

View File

@@ -6,7 +6,7 @@
import { messageService } from '../../services/messageService'; import { messageService } from '../../services/messageService';
import { messageRepository } from '../../data/repositories/MessageRepository'; import { messageRepository } from '../../data/repositories/MessageRepository';
import type { Message, Conversation } from '../../core/entities/Message'; import type { Message, Conversation } from '../../core/entities/Message';
import type { ConversationResponse, MessageResponse } from '../../types/dto'; import type { ConversationListResponse, MessageListResponse } from '../../types/dto';
export interface SyncOptions { export interface SyncOptions {
force?: boolean; force?: boolean;
@@ -32,8 +32,8 @@ export class MessageSyncService {
this.syncingConversations = true; this.syncingConversations = true;
try { try {
const response = await messageService.getConversations(); const response: ConversationListResponse = await messageService.getConversations();
const conversations = this.mapConversations(response); const conversations = this.mapConversations(response.list || []);
return conversations; return conversations;
} catch (error) { } catch (error) {
console.error('[MessageSyncService] Failed to sync conversations:', error); console.error('[MessageSyncService] Failed to sync conversations:', error);
@@ -59,10 +59,12 @@ export class MessageSyncService {
try { try {
const lastSeq = options.lastSeq ?? await messageRepository.getMaxSeq(conversationId); const lastSeq = options.lastSeq ?? await messageRepository.getMaxSeq(conversationId);
const response = await messageService.getMessages(conversationId, { const response: MessageListResponse = await messageService.getMessages(
after_seq: lastSeq, conversationId,
limit: 50, lastSeq,
}); undefined,
50
);
const messages = this.mapMessages(response.messages || []); const messages = this.mapMessages(response.messages || []);
@@ -73,7 +75,7 @@ export class MessageSyncService {
return { return {
conversations: [], conversations: [],
messages, messages,
hasMore: response.has_more || false, hasMore: messages.length >= 50,
}; };
} catch (error) { } catch (error) {
console.error('[MessageSyncService] Failed to sync messages:', error); console.error('[MessageSyncService] Failed to sync messages:', error);
@@ -89,10 +91,12 @@ export class MessageSyncService {
limit: number = 20 limit: number = 20
): Promise<Message[]> { ): Promise<Message[]> {
try { try {
const response = await messageService.getMessages(conversationId, { const response: MessageListResponse = await messageService.getMessages(
before_seq: beforeSeq, conversationId,
limit, undefined,
}); beforeSeq,
limit
);
const messages = this.mapMessages(response.messages || []); const messages = this.mapMessages(response.messages || []);
@@ -122,31 +126,45 @@ export class MessageSyncService {
return messageRepository.getMessagesBeforeSeq(conversationId, beforeSeq, limit); return messageRepository.getMessagesBeforeSeq(conversationId, beforeSeq, limit);
} }
private mapConversations(response: ConversationResponse[]): Conversation[] { private mapConversations(list: ConversationListResponse['list']): Conversation[] {
return response.map(conv => ({ return list.map(conv => ({
id: conv.id, id: conv.id,
type: conv.type || 'private', type: conv.type || 'private',
isPinned: conv.isPinned || false, isPinned: conv.is_pinned || false,
lastSeq: conv.lastSeq || conv.last_seq || 0, lastSeq: conv.last_seq || 0,
lastMessageAt: conv.lastMessageAt || conv.last_message_at || new Date().toISOString(), lastMessageAt: conv.last_message_at || new Date().toISOString(),
unreadCount: conv.unreadCount || conv.unread_count || 0, unreadCount: conv.unread_count || 0,
participants: conv.participants || [], participants: (conv.participants || []).map(p => ({
group: conv.group, id: p.id,
createdAt: conv.createdAt || conv.created_at || new Date().toISOString(), username: p.username,
updatedAt: conv.updatedAt || conv.updated_at || new Date().toISOString(), avatar: p.avatar || undefined,
nickname: p.nickname,
})),
group: conv.group ? {
id: String(conv.group.id),
name: conv.group.name,
avatar: conv.group.avatar,
} : undefined,
createdAt: conv.created_at || new Date().toISOString(),
updatedAt: conv.updated_at || new Date().toISOString(),
})); }));
} }
private mapMessages(response: MessageResponse[]): Message[] { private mapMessages(messages: MessageListResponse['messages']): Message[] {
return response.map(msg => ({ return messages.map(msg => ({
id: msg.id, id: msg.id,
conversationId: msg.conversationId || msg.conversation_id || '', conversationId: msg.conversation_id,
senderId: msg.senderId || msg.sender_id || '', senderId: msg.sender_id,
seq: msg.seq || 0, seq: msg.seq,
segments: msg.segments || [], segments: msg.segments || [],
createdAt: msg.createdAt || msg.created_at || new Date().toISOString(), createdAt: msg.created_at,
status: msg.status || 'normal', status: msg.status || 'normal',
sender: msg.sender, sender: msg.sender ? {
id: msg.sender.id,
username: msg.sender.username,
avatar: msg.sender.avatar || undefined,
nickname: msg.sender.nickname,
} : undefined,
})); }));
} }

View File

@@ -9,21 +9,20 @@ export interface UserDTO {
id: string; id: string;
username: string; username: string;
nickname: string; nickname: string;
avatar: string | null; avatar: string;
cover_url: string | null; // 头图URL cover_url: string;
bio: string | null; bio: string;
website: string | null; website: string;
location: string | null; location: string;
phone: string | null; phone?: string;
email: string | null; email?: string;
email_verified?: boolean; email_verified?: boolean;
posts_count: number; posts_count: number;
followers_count: number; followers_count: number;
following_count: number; following_count: number;
created_at: string; created_at: string;
// 额外字段 is_following?: boolean;
is_following?: boolean; // 当前用户是否关注了该用户 is_following_me?: boolean;
is_following_me?: boolean; // 该用户是否关注了当前用户
} }
export interface UserDetailDTO { export interface UserDetailDTO {
@@ -246,6 +245,8 @@ export interface ConversationResponse {
participants?: UserDTO[]; // 私聊时使用 participants?: UserDTO[]; // 私聊时使用
member_count?: number; // 群聊时使用 member_count?: number; // 群聊时使用
group?: GroupResponse; // 群聊会话的群组信息 group?: GroupResponse; // 群聊会话的群组信息
my_last_read_seq?: number; // 当前用户的已读位置
other_last_read_seq?: number; // 对方用户的已读位置
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }
@@ -269,6 +270,7 @@ export interface ConversationDetailResponse {
participants: UserDTO[]; participants: UserDTO[];
my_last_read_seq: number; // 当前用户的已读位置 my_last_read_seq: number; // 当前用户的已读位置
other_last_read_seq: number; // 对方用户的已读位置 other_last_read_seq: number; // 对方用户的已读位置
group?: GroupResponse; // 群聊会话的群组信息
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }