腾讯云IMSDK的封装


[MODE: RESEARCH]

快乐程序员,我来详细讲解 useIMManager.ts 中的核心方法!这个文件是腾讯云IM SDK的封装,提供了完整的即时通讯功能。

🏗️ 整体架构设计

1. 单例模式设计

export class IMManager {
    private static chatInstance: IMManager
    
    public static getInstance(): IMManager {
        if (!this.chatInstance) {
            this.chatInstance = new IMManager()
        }
        return this.chatInstance
    }
}
  • 设计目的: 确保全局只有一个IM实例,避免重复初始化
  • 优势: 节省内存,统一管理连接状态
  • 使用场景: 多组件共享同一个IM连接

2. 状态管理系统

export interface IMState {
    connectionState: ConnectionState    // 连接状态
    networkState: NetworkState        // 网络状态
    isReady: boolean                  // SDK是否就绪
    isLoggedIn: boolean              // 是否已登录
    errorCount: number               // 错误计数
    reconnectAttempts: number         // 重连次数
    lastHeartbeat: number            // 最后心跳时间
    timestamp: number                // 状态更新时间
}

🔧 核心方法详解

1. 初始化与连接管理

构造函数 - 系统初始化

constructor() {
    try {
        // 创建腾讯云IM实例
        this.chat = TencentCloudChat.create({ 
            SDKAppID, 
            unlimitedAVChatRoom: true 
        })
        
        // 设置日志级别
        this.chat.setLogLevel(0)
        
        // 注册上传插件
        this.chat.registerPlugin({ 'tim-upload-plugin': TIMUploadPlugin })
        
        // 初始化状态
        this.state = this.createInitialState()
        
        // 注册事件监听器
        this.registerEventListeners()
        
        // 启动健康检查
        this.startHealthCheck()
    } catch (error) {
        throw new IMError('IM SDK 初始化失败', 'INIT_FAILED', error)
    }
}

核心功能:

  • 🎯 SDK初始化: 创建腾讯云IM实例
  • 🔧 插件注册: 支持文件上传功能
  • 📊 状态初始化: 设置初始状态
  • 👂 事件监听: 注册所有必要的事件处理器
  • ❤️ 健康检查: 启动心跳和健康监控

事件监听器注册

private registerEventListeners(): void {
    // 基础消息事件
    this.chat.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, this.handleMessageReceived.bind(this))
    this.chat.on(TencentCloudChat.EVENT.SDK_READY, this.handleSDKReady.bind(this))
    this.chat.on(TencentCloudChat.EVENT.MESSAGE_REVOKED, (revokedMessage: MessageRevokedEvent) => {
        this.emitter.emit(IMEventType.MSG_REVOKED, revokedMessage.data)
    })
    
    // 网络状态监控
    this.chat.on(TencentCloudChat.EVENT.NET_STATE_CHANGE, this.handleNetworkStateChanged.bind(this))
    
    // 错误事件监控
    this.chat.on(TencentCloudChat.EVENT.ERROR, this.handleError.bind(this))
    this.chat.on(TencentCloudChat.EVENT.KICKED_OUT, this.handleKickedOut.bind(this))
    
    // 群组相关事件
    this.chat.on(TencentCloudChat.EVENT.GROUP_ATTRIBUTES_UPDATED, this.handleGroupAttributesUpdated.bind(this))
}

2. 用户认证系统

登录方法 - 用户身份验证

async login(userID: string, userSig: string): Promise<void> {
    try {
        // 参数验证
        this.validateNotEmpty(userID)
        this.validateNotEmpty(userSig)
        
        // 检查是否已登录
        if (this.isUserLoggedIn) {
            console.log(LOG_PRIFIX, 'IM 已经登录')
            return
        }
        
        // 执行登录
        await this.chat.login({ userID, userSig })
        
        // 更新状态
        this.isUserLoggedIn = true
        this.currentUserID = userID
        
        this.updateState({
            isLoggedIn: true,
        })
        
        console.log(LOG_PRIFIX, 'IM 登录成功')
    } catch (error) {
        // 错误处理
        this.isUserLoggedIn = false
        this.currentUserID = null
        
        this.updateState({
            isLoggedIn: false,
        })
        
        // 处理腾讯云IM的错误码
        if (error && typeof error === 'object' && 'code' in error) {
            const errorCode = (error as any).code
            let message = '登录失败'
            
            switch (errorCode) {
                case 7001: message = '用户签名错误'; break
                case 7002: message = '用户ID格式错误'; break
                case 7003: message = '用户已登录'; break
                case 7004: message = '网络连接失败'; break
                default: message = `登录失败: ${errorCode}`
            }
            
            throw new IMError(message, 'LOGIN_FAILED', error)
        }
        
        throw new IMError('登录失败', 'LOGIN_FAILED', error)
    }
}

核心功能:

  • 🔐 身份验证: 使用userID和userSig进行身份验证
  • 🛡️ 重复登录检查: 防止重复登录
  • 📊 状态更新: 实时更新登录状态
  • 🚨 错误处理: 详细的错误码处理和用户友好的错误信息

登出方法 - 安全退出

async logout(): Promise<void> {
    if (!this.isUserLoggedIn) {
        console.log(LOG_PRIFIX, 'IM 未登录,无需登出')
        return
    }
    
    try {
        // 清理已加入群组
        this.joinedGroups.clear()
        
        // 执行登出
        await this.chat.logout()
        
        // 更新状态
        this.isUserLoggedIn = false
        this.currentUserID = null
        this.connectionState = ConnectionState.DISCONNECTED
        
        this.updateState({
            isLoggedIn: false,
            connectionState: ConnectionState.DISCONNECTED,
        })
        
        console.log(LOG_PRIFIX, 'IM 退出成功')
    } catch (error) {
        // 错误处理...
        throw new IMError('退出失败', 'LOGOUT_FAILED', error)
    }
}

3. 群组管理系统

创建群组 - 建立聊天室

public async createGroup(groupID: string): Promise<any> {
    try {
        // 参数验证
        this.validateGroupID(groupID)
        this.confirmReady()
        
        // 创建群组
        const group = await this.chat.createGroup({
            type: TencentCloudChat.TYPES.GRP_AVCHATROOM,  // 音视频聊天室
            name: groupID,
            isSupportTopic: true,  // 支持话题功能
            groupID,
        })
        
        console.log(LOG_PRIFIX, '直播群组创建成功', group)
        return group
    } catch (error) {
        // 错误处理
        if (error && typeof error === 'object' && 'code' in error) {
            const errorCode = (error as any).code
            let message = '创建群组失败'
            
            switch (errorCode) {
                case 10013: message = '群组已存在'; break
                case 10014: message = '群组ID格式错误'; break
                case 10015: message = '群组名称过长'; break
                default: message = `创建群组失败: ${errorCode}`
            }
            
            throw new IMError(message, 'CREATE_GROUP_FAILED', error)
        }
        
        throw new IMError('创建群组失败', 'CREATE_GROUP_FAILED', error)
    }
}

核心功能:

  • 🏗️ 群组创建: 创建音视频聊天室
  • 🎯 类型选择: 支持不同类型的群组
  • 🔧 功能配置: 支持话题、音视频等功能
  • 🚨 错误处理: 详细的错误码处理

加入群组 - 参与聊天

public async joinGroup(groupID: string): Promise<any> {
    try {
        // 参数验证
        this.validateGroupID(groupID)
        this.confirmReady()
        
        // 检查是否已加入
        if (this.joinedGroups.has(groupID)) {
            console.log(LOG_PRIFIX, `已加入群组 ${groupID},跳过重复加入`)
            return
        }
        
        // 加入群组
        const result = await this.chat.joinGroup({ 
            groupID, 
            type: TencentCloudChat.TYPES.GRP_AVCHATROOM 
        })
        
        // 记录已加入群组
        this.joinedGroups.add(groupID)
        
        console.log(LOG_PRIFIX, `已加入群组 ${groupID}`)
        return result
    } catch (error) {
        // 错误处理...
        throw new IMError('加入群组失败', 'JOIN_GROUP_FAILED', error)
    }
}

退出群组 - 离开聊天

public async quitGroup(groupID: string): Promise<void> {
    try {
        // 参数验证
        this.validateGroupID(groupID)
        this.confirmReady()
        
        // 检查是否已加入
        if (!this.joinedGroups.has(groupID)) {
            console.log(LOG_PRIFIX, `未加入群组 ${groupID},无需退出`)
            return
        }
        
        // 从记录中移除
        this.joinedGroups.delete(groupID)
        
        // 执行退出
        await this.chat.quitGroup(groupID)
        
        console.log(LOG_PRIFIX, `已退出群组 ${groupID}`)
    } catch (error) {
        throw new IMError('退出群组失败', 'QUIT_GROUP_FAILED', error)
    }
}

4. 消息系统

发送群组消息 - 群聊功能

async sendGroupTextMessage(groupID: string, text: string): Promise<number> {
    try {
        // 参数验证
        this.validateGroupID(groupID)
        this.confirmReady()
        
        if (!text || typeof text !== 'string' || text.trim().length === 0) {
            throw new IMError('消息内容不能为空', 'INVALID_MESSAGE_TEXT', { text })
        }
        
        // 创建消息对象
        const message = this.chat.createTextMessage({
            to: groupID,
            conversationType: TencentCloudChat.TYPES.CONV_GROUP,
            payload: { text },
        })
        
        // 发送消息
        await this.chat.sendMessage(message)
        
        console.log(LOG_PRIFIX, `群组消息发送成功: ${groupID}`)
        return 0
    } catch (error) {
        console.error(LOG_PRIFIX, `群组消息发送失败: ${groupID}`, error)
        throw new IMError('群组消息发送失败', 'SEND_GROUP_MESSAGE_FAILED', error)
    }
}

发送私聊消息 - 一对一聊天

async sendPrivateTextMessage(userID: string, text: string): Promise<number> {
    try {
        // 参数验证
        this.validateNotEmpty(userID)
        this.confirmReady()
        
        if (!text || typeof text !== 'string' || text.trim().length === 0) {
            throw new IMError('消息内容不能为空', 'INVALID_MESSAGE_TEXT', { text })
        }
        
        // 创建私聊消息
        const message = this.chat.createTextMessage({
            to: userID,
            conversationType: TencentCloudChat.TYPES.CONV_C2C,
            payload: { text },
        })
        
        // 发送消息
        await this.chat.sendMessage(message)
        
        console.log(LOG_PRIFIX, `私聊消息发送成功: ${userID}`)
        return 0
    } catch (error) {
        console.error(LOG_PRIFIX, `私聊消息发送失败: ${userID}`, error)
        throw new IMError('私聊消息发送失败', 'SEND_PRIVATE_MESSAGE_FAILED', error)
    }
}

消息接收处理 - 实时消息

private handleMessageReceived(event: { data: Message[] }): void {
    try {
        if (!event || !event.data || !Array.isArray(event.data)) {
            console.warn(LOG_PRIFIX, '收到无效的消息事件:', event)
            return
        }
        
        console.log('DEBUG_LOG:call handleMessageReceived', event.data)
        
        // 处理每条消息
        event.data.forEach(message => {
            try {
                const conversationType = message.conversationType
                
                // 群组消息处理
                if (conversationType === TencentCloudChat.TYPES.CONV_GROUP) {
                    // 群组消息处理逻辑
                }
                
                // 触发消息接收事件
                this.emitter.emit(IMEventType.CHAT_MSG, message)
            } catch (error) {
                console.error(LOG_PRIFIX, '处理单条消息失败:', error, message)
            }
        })
    } catch (error) {
        console.error(LOG_PRIFIX, '处理消息事件失败:', error)
    }
}

5. 历史消息系统

获取历史消息 - 消息记录

async getHistoryMessages(options: HistoryMessageOptions): Promise<{
    messageList: Message[]
    isCompleted: boolean
    nextReqMessageID?: string
}> {
    try {
        this.confirmReady()
        this.validateConversationID(options.conversationID)
        
        const { conversationID, nextReqMessageID } = options
        
        console.log(LOG_PRIFIX, `开始获取历史消息: ${conversationID}`, options)
        
        // 构建获取参数
        const getMessageListParams: any = {
            conversationID: `${conversationID}`,
        }
        
        // 分页参数
        if (nextReqMessageID) {
            getMessageListParams.nextReqMessageID = nextReqMessageID
        }
        
        // 获取消息列表
        const result = await this.chat.getMessageList(getMessageListParams)
        
        console.log(LOG_PRIFIX, `获取历史消息成功: ${conversationID}`, {
            messageList: result.data.messageList,
            isCompleted: result.data.isCompleted,
            nextReqMessageID: result.data.nextReqMessageID,
        })
        
        return {
            messageList: result.data.messageList || [],
            isCompleted: result.data.isCompleted,
            nextReqMessageID: result.data.nextReqMessageID,
        }
    } catch (error) {
        console.error(LOG_PRIFIX, `获取历史消息失败: ${options.conversationID}`, error)
        throw new IMError('获取历史消息失败', 'GET_HISTORY_MESSAGES_FAILED', error)
    }
}

获取最近消息 - 智能消息获取

async getRecentMessages(
    conversationID: string,
    targetCount: number = 50,
    maxRetries: number = 10,
): Promise<Message[]> {
    try {
        this.confirmReady()
        this.validateConversationID(conversationID)
        
        if (targetCount <= 0) {
            throw new IMError('目标消息数量必须大于0', 'INVALID_TARGET_COUNT', { targetCount })
        }
        
        if (maxRetries < 0) {
            throw new IMError('最大重试次数不能为负数', 'INVALID_MAX_RETRIES', { maxRetries })
        }
        
        const allMessages: Message[] = []
        let nextReqMessageID: string | undefined
        let retryCount = 0
        
        console.log(LOG_PRIFIX, `开始获取最近 ${targetCount} 条消息: ${conversationID}`)
        
        // 循环获取消息直到达到目标数量
        while (allMessages.length < targetCount) {
            try {
                const result = await this.getHistoryMessages({
                    conversationID,
                    nextReqMessageID,
                })
                
                // 如果没有获取到消息,退出循环
                if (!result.messageList || result.messageList.length === 0) {
                    console.log(LOG_PRIFIX, `没有更多消息,停止获取: ${conversationID}`)
                    break
                }
                
                // 添加消息到列表
                allMessages.push(...result.messageList)
                console.log(LOG_PRIFIX, `已获取 ${allMessages.length} 条消息,目标: ${targetCount}`)
                
                // 如果已经完成或没有下一页标识,退出循环
                if (result.isCompleted || !result.nextReqMessageID) {
                    console.log(LOG_PRIFIX, `消息获取完成: ${conversationID}`)
                    break
                }
                
                // 设置下一页标识
                nextReqMessageID = result.nextReqMessageID
            } catch (error) {
                retryCount++
                console.warn(LOG_PRIFIX, `获取消息失败,重试第 ${retryCount} 次: ${conversationID}`, error)
                
                // 如果达到最大重试次数,抛出异常
                if (retryCount >= maxRetries) {
                    console.error(LOG_PRIFIX, `达到最大重试次数 ${maxRetries},停止重试: ${conversationID}`)
                    throw new IMError('获取消息重试次数超限', 'MAX_RETRIES_EXCEEDED', {
                        conversationID,
                        retryCount,
                        maxRetries,
                        error,
                    })
                }
                
                // 等待一段时间后重试
                await new Promise(resolve => setTimeout(resolve, 1000 * retryCount))
            }
        }
        
        console.log(LOG_PRIFIX, `最终获取到 ${allMessages.length} 条消息: ${conversationID}`)
        return allMessages
    } catch (error) {
        console.error(LOG_PRIFIX, `获取最近消息失败: ${conversationID}`, error)
        throw new IMError('获取最近消息失败', 'GET_RECENT_MESSAGES_FAILED', error)
    }
}

6. 状态监控系统

健康检查机制

// 启动健康检查
private startHealthCheck(): void {
    // 心跳检查 - 每30秒
    this.heartbeatInterval = setInterval(() => {
        this.performHeartbeat()
    }, 30000)
    
    // 健康状态检查 - 每60秒
    this.healthCheckInterval = setInterval(() => {
        this.performHealthCheck()
    }, 60000)
    
    // 错误计数重置 - 每分钟
    setInterval(() => {
        this.errorCount = 0
    }, this.errorWindow)
}

// 执行心跳检查
private performHeartbeat(): void {
    const now = Date.now()
    this.lastHeartbeat = now
    
    // 更新状态
    this.updateState({
        lastHeartbeat: now,
    })
    
    // 检查连接状态
    if (this.connectionState !== ConnectionState.CONNECTED) {
        console.warn(LOG_PRIFIX, '心跳检查:连接状态异常', this.connectionState)
    }
    
    // 触发心跳事件
    this.emitter.emit(IMEventType.HEARTBEAT, {
        timestamp: now,
        connectionState: this.connectionState,
        networkState: this.networkState,
    })
}

// 执行健康检查
private performHealthCheck(): void {
    const healthStatus = {
        connectionState: this.connectionState,
        networkState: this.networkState,
        isReady: this.ready,
        isLoggedIn: this.isUserLoggedIn,
        errorCount: this.errorCount,
        reconnectAttempts: this.reconnectAttempts,
        lastHeartbeat: this.lastHeartbeat,
        timestamp: Date.now(),
    }
    
    console.log(LOG_PRIFIX, '健康检查状态:', healthStatus)
    
    // 触发健康检查事件
    this.emitter.emit(IMEventType.HEALTH_CHECK, healthStatus)
    
    // 如果状态异常,触发警告
    if (this.connectionState === ConnectionState.ERROR || this.errorCount > this.maxErrorCount / 2) {
        this.emitter.emit(IMEventType.HEALTH_WARNING, healthStatus)
    }
}

网络状态监控

private handleNetworkStateChanged(event: any): void {
    const { state } = event.data
    const previousState = this.networkState
    const previousConnectionState = this.connectionState
    
    // 使用腾讯云IM的网络状态常量
    switch (state) {
        case TencentCloudChat.TYPES.NET_STATE_CONNECTED:
            this.networkState = NetworkState.WIFI
            this.connectionState = ConnectionState.CONNECTED
            this.reconnectAttempts = 0
            this.errorCount = 0
            console.log(LOG_PRIFIX, 'IM 网络已连接')
            break
        case TencentCloudChat.TYPES.NET_STATE_CONNECTING:
            this.networkState = NetworkState.MOBILE
            this.connectionState = ConnectionState.CONNECTING
            console.log(LOG_PRIFIX, 'IM 网络正在连接...')
            break
        case TencentCloudChat.TYPES.NET_STATE_DISCONNECTED:
            this.networkState = NetworkState.NONE
            this.connectionState = ConnectionState.DISCONNECTED
            console.warn(LOG_PRIFIX, 'IM 网络已断开')
            this.handleDisconnection()
            break
        default:
            this.networkState = NetworkState.UNKNOWN
            this.connectionState = ConnectionState.ERROR
            console.error(LOG_PRIFIX, 'IM 网络状态异常:', state)
    }
    
    // 更新状态
    this.updateState({
        networkState: this.networkState,
        connectionState: this.connectionState,
        reconnectAttempts: this.reconnectAttempts,
        errorCount: this.errorCount,
    })
    
    // 触发网络状态变化事件
    this.emitter.emit(IMEventType.NETWORK_STATE_CHANGED, {
        previousState,
        currentState: this.networkState,
    })
    
    // 触发连接状态变化事件
    this.emitter.emit(IMEventType.CONNECTION_STATE_CHANGED, {
        previousState: previousConnectionState,
        currentState: this.connectionState,
        reconnectAttempts: this.reconnectAttempts,
    })
}

7. 群组权限管理

踢出群组成员 - 权限控制

public async kickGroupMember(
    groupID: string,
    memberIDList: string[],
    operatorID?: string,
): Promise<KickMemberResult> {
    try {
        this.validateGroupID(groupID)
        this.confirmReady()
        
        // 如果没有指定操作者,使用当前登录用户
        const currentOperatorID = operatorID || this.getCurrentUserID()
        if (!currentOperatorID) {
            return {
                success: false,
                message: '未找到当前用户ID',
                errorCode: 10001,
            }
        }
        
        // 检查权限
        const hasPermission = await this.checkAdminPermission(groupID, currentOperatorID)
        if (!hasPermission) {
            return {
                success: false,
                message: '权限不足,只有群主和管理员可以踢人',
                errorCode: 10002,
                details: {
                    operatorID: currentOperatorID,
                    groupID,
                    requiredRoles: [GroupRole.OWNER, GroupRole.ADMIN],
                },
            }
        }
        
        // 检查是否尝试踢出群主
        const groupInfo = await this.getGroupInfo(groupID)
        const isKickingOwner = memberIDList.includes(groupInfo.ownerID)
        if (isKickingOwner) {
            return {
                success: false,
                message: '不能踢出群主',
                errorCode: 10003,
                details: { ownerID: groupInfo.ownerID },
            }
        }
        
        // 检查是否尝试踢出管理员(只有群主可以踢出管理员)
        const operatorRole = await this.getGroupMemberRole(groupID, currentOperatorID)
        if (operatorRole !== GroupRole.OWNER) {
            // 检查要踢出的用户中是否有管理员
            const membersToKick = await Promise.all(memberIDList.map(userID => this.getGroupMemberRole(groupID, userID)))
            const hasAdminInList = membersToKick.some(role => role === GroupRole.ADMIN)
            if (hasAdminInList) {
                return {
                    success: false,
                    message: '只有群主可以踢出管理员',
                    errorCode: 10004,
                    details: { operatorRole, targetRoles: membersToKick },
                }
            }
        }
        
        // 执行踢人操作
        const result = await this.chat.deleteGroupMember({
            groupID,
            userIDList: memberIDList,
            reason: '被管理员踢出群组',
        })
        
        console.log(LOG_PRIFIX, `踢出群组成员成功: ${groupID}`, {
            operatorID: currentOperatorID,
            memberIDList,
            result,
        })
        
        return {
            success: true,
            message: `成功踢出 ${memberIDList.length} 名成员`,
            details: result,
        }
    } catch (error) {
        // 错误处理...
        return {
            success: false,
            message: '踢出群组成员失败',
            errorCode: 10000,
            details: error,
        }
    }
}

8. Vue Composable 集成

useIMManager - Vue集成

export function useIMManager(userID?: string, userSig?: string) {
    const imManager = IMManager.getInstance()
    const state = ref<IMState>(imManager.getState())
    
    try {
        // 清理旧的事件监听器
        imManager.offAllListeners()
        
        // 设置状态变化监听
        imManager.onStateChange((newState: IMState) => {
            state.value = newState
        })
        
        // 组件挂载时初始化
        onMounted(async () => {
            try {
                // 先登出
                if (imManager.getLoginState()) {
                    await imManager.logout()
                }
                
                // 如果有 userID 和 userSig,则登录
                if (userID && userSig) {
                    await imManager.login(userID, userSig)
                }
            } catch (error) {
                console.error(LOG_PRIFIX, 'IM 初始化失败:', error)
                throw new IMError('IM 初始化失败', 'INIT_FAILED', error)
            }
        })
        
        // 组件卸载时清理资源
        onUnmounted(() => {
            imManager.offAllListeners()
        })
    } catch (error) {
        console.error(LOG_PRIFIX, 'useIMManager 初始化失败:', error)
        throw error
    }
    
    return {
        imManager,
        state: readonly(state),
        // 便捷的状态访问方法
        isReady: computed(() => state.value.isReady),
        isLoggedIn: computed(() => state.value.isLoggedIn),
        connectionState: computed(() => state.value.connectionState),
        networkState: computed(() => state.value.networkState),
        errorCount: computed(() => state.value.errorCount),
        reconnectAttempts: computed(() => state.value.reconnectAttempts),
    }
}

🎯 核心设计模式

1. 单例模式

  • 确保全局只有一个IM实例
  • 避免重复初始化和资源浪费

2. 观察者模式

  • 使用EventEmitter进行事件通信
  • 状态变化自动通知所有监听者

3. 策略模式

  • 不同错误码采用不同处理策略
  • 网络状态变化采用不同重连策略

4. 代理模式

  • 封装腾讯云IM SDK的复杂API
  • 提供更简单易用的接口

🚀 使用示例

// 在Vue组件中使用
const { imManager, isReady, isLoggedIn, connectionState } = useIMManager(userID, userSig)

// 监听事件
imManager.on(IMEventType.CHAT_MSG, (message) => {
    console.log('收到消息:', message)
})

// 发送消息
await imManager.sendGroupTextMessage('group123', 'Hello World!')

// 加入群组
await imManager.joinGroup('group123')

这个IM管理器提供了完整的即时通讯功能,包括用户认证、群组管理、消息收发、状态监控等,是一个功能强大且易于使用的IM解决方案!🎉


文章作者: 高红翔
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 高红翔 !
  目录