import {
    HttpTransportType,
    HubConnectionState,
    type HubConnection,
    HubConnectionBuilder,
    type IHttpConnectionOptions,
    LogLevel,
} from '@microsoft/signalr';
import { DialogType, ToastType, uiFeedbackService } from '../UiFeedbackService.ts';
import { logger } from '../ConsoleLogger.ts';
import { canvasChat } from './CanvasChat.ts';
import { util } from '../util.ts';
import { pushPageHistory } from '../pageHistory.ts';

interface GroupChatUser {
    id: string;
    name: string;
}

interface GroupChatCurrentUser {
    id: string;
}

interface GroupChatMessage {
    userId: string;
    userName: string;
    avatar: string;
    message: string;
    sendTime: string;
    fontColor?: string | null;
}

declare global {
    interface Window {
        openGroupChat?: () => void;
    }
}

/**
 * 群组聊天模块
 * 彩蛋功能:通过控制台命令开启。
 */
export class GroupChat {
    private readonly _chatHash: string = '#group-chat';
    private readonly _storageKey: string = 'group-chat-opened';
    private readonly _maxHistoryCount: number = 1000;
    private readonly _fontColorStorageKeyPrefix: string = 'group-chat-font-color:';

    private _connection: HubConnection | null = null;
    private _isOpen: boolean = false;
    private _currentUserId: string | null = null;
    private _loadedPageIndex: number = 0;
    private _hasMoreHistory: boolean = true;
    private _isLoadingHistory: boolean = false;
    private _loadedHistoryCount: number = 0;
    private _scrollPosition: number | undefined;

    private _triggerButton: HTMLButtonElement | null = null;
    private _chatPanel: HTMLElement | null = null;
    private _messagesContainer: HTMLElement | null = null;
    private _loadMoreButton: HTMLElement | null = null;
    private _input: HTMLTextAreaElement | null = null;
    private _sendButton: HTMLButtonElement | null = null;
    private _commandsPanel: HTMLElement | null = null;
    private _selectedCommandIndex: number = 0;
    private _localFontColor: string | null = null;

    public constructor() {
        this.init();
        this.initMobileViewport();
        this.initHashListener();
    }

    /**
     * 初始化群聊模块基础能力。
     */
    private init(): void {
        this.createTriggerButton();
        this.checkOpenedHistory();
        this.setupConsoleCommand();
    }

    /**
     * 监听 hash 变化,移动端支持返回键关闭。
     */
    private initHashListener(): void {
        window.addEventListener('hashchange', async () => {
            if (!util.isMobile()) {
                return;
            }

            if (window.location.hash === this._chatHash) {
                if (!this._isOpen) {
                    await this.open();
                }
                return;
            }

            if (this._isOpen) {
                await this.close();
            }
        });
    }

    /**
     * 判断群聊连接当前是否可发送请求。
     */
    private isConnectionReady(): boolean {
        return !!(this._connection && this._connection.state === HubConnectionState.Connected);
    }

    /**
     * 当前用户本地颜色缓存 key。
     */
    private getLocalFontColorStorageKey(): string | null {
        if (!this._currentUserId) {
            return null;
        }

        return `${this._fontColorStorageKeyPrefix}${this._currentUserId}`;
    }

    /**
     * 读取本地字体颜色。
     */
    private loadLocalFontColor(): string | null {
        const key = this.getLocalFontColorStorageKey();
        if (!key) {
            return null;
        }

        const rawValue = localStorage.getItem(key);
        if (!rawValue) {
            return null;
        }

        return this.normalizeColor(rawValue);
    }

    /**
     * 保存本地字体颜色。
     */
    private saveLocalFontColor(color: string): void {
        const key = this.getLocalFontColorStorageKey();
        if (!key) {
            return;
        }

        localStorage.setItem(key, color);
        this._localFontColor = color;
        this.applyInputFontColor();
    }

    /**
     * 将当前字体颜色应用到聊天输入框。
     */
    private applyInputFontColor(): void {
        if (!this._input) {
            return;
        }

        if (this._localFontColor) {
            this._input.style.color = this._localFontColor;
            return;
        }

        this._input.style.color = '';
    }

    /**
     * 将本地字体颜色同步到服务端缓存。
     */
    private async syncServerFontColor(localColor: string): Promise<void> {
        if (!this.isConnectionReady() || !this._connection) {
            return;
        }

        try {
            await this._connection.invoke('SetFontColor', localColor);
        } catch (error) {
            logger.outPutError('同步字体颜色到服务端失败', error);
        }
    }

    /**
     * 连接成功后同步本地与服务端字体颜色。
     */
    private async syncFontColorOnConnect(): Promise<void> {
        if (!this.isConnectionReady() || !this._connection) {
            return;
        }

        const localColor = this.loadLocalFontColor();

        let serverColor: string | null = null;
        try {
            const serverValue = await this._connection.invoke<string | null>('GetFontColor');
            serverColor = serverValue ? this.normalizeColor(serverValue) : null;
        } catch (error) {
            logger.outPutError('获取服务端字体颜色失败', error);
        }

        if (localColor && serverColor && localColor !== serverColor) {
            await this.syncServerFontColor(localColor);
            this._localFontColor = localColor;
            this.applyInputFontColor();
            return;
        }

        if (localColor && !serverColor) {
            await this.syncServerFontColor(localColor);
            this._localFontColor = localColor;
            this.applyInputFontColor();
            return;
        }

        if (!localColor && serverColor) {
            this.saveLocalFontColor(serverColor);
            return;
        }

        this._localFontColor = localColor;
        this.applyInputFontColor();
    }

    /**
     * 移动端打开群聊时,锁定页面滚动。
     */
    private disableBodyScroll(): void {
        if (!util.isMobile()) {
            return;
        }

        document.body.style.overflow = 'hidden';
        document.body.style.position = 'fixed';
        document.body.style.width = '100%';
        document.body.style.top = `-${window.scrollY}px`;
        this._scrollPosition = window.scrollY;
    }

    /**
     * 关闭群聊后恢复页面滚动位置。
     */
    private enableBodyScroll(): void {
        if (!util.isMobile()) {
            return;
        }

        document.body.style.overflow = '';
        document.body.style.position = '';
        document.body.style.width = '';
        document.body.style.top = '';

        if (this._scrollPosition !== undefined) {
            window.scrollTo(0, this._scrollPosition);
            this._scrollPosition = undefined;
        }
    }

    /**
     * 初始化移动端视口高度变量,提升键盘和地址栏场景兼容性。
     */
    private initMobileViewport(): void {
        const setViewportHeight = (): void => {
            const vh = window.innerHeight * 0.01;
            document.documentElement.style.setProperty('--vh', `${vh}px`);
        };

        setViewportHeight();

        window.addEventListener('resize', setViewportHeight);
        window.addEventListener('orientationchange', () => {
            setTimeout(setViewportHeight, 100);
        });

        if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
            let lastScrollTop = 0;
            window.addEventListener(
                'scroll',
                () => {
                    const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
                    if (scrollTop !== lastScrollTop) {
                        setViewportHeight();
                        lastScrollTop = scrollTop;
                    }
                },
                { passive: true }
            );
        }

        if (window.visualViewport) {
            window.visualViewport.addEventListener('resize', setViewportHeight);
            window.visualViewport.addEventListener('scroll', setViewportHeight);
        }
    }

    /**
     * 创建群聊触发按钮。
     */
    private createTriggerButton(): void {
        const trigger = document.createElement('button');
        trigger.className = 'group-chat__trigger';
        trigger.innerHTML = '<i class="fa fa-comments" aria-hidden="true"></i>';
        trigger.setAttribute('aria-label', '打开群组聊天');
        trigger.style.display = 'none';
        trigger.addEventListener('click', () => {
            void this.open();
        });

        document.body.appendChild(trigger);
        this._triggerButton = trigger;
    }

    /**
     * 读取历史状态,决定是否显示触发按钮。
     */
    private checkOpenedHistory(): void {
        try {
            const hasOpened = localStorage.getItem(this._storageKey);
            if (hasOpened === 'true' && this._triggerButton) {
                this._triggerButton.style.display = 'flex';
            }
        } catch (error) {
            logger.outPutError('检查群聊历史失败', error);
        }
    }

    /**
     * 标记用户已开启过群聊。
     */
    private markAsOpened(): void {
        try {
            localStorage.setItem(this._storageKey, 'true');
        } catch (error) {
            logger.outPutError('保存群聊状态失败', error);
        }
    }

    /**
     * 注入控制台命令入口。
     */
    private setupConsoleCommand(): void {
        window.openGroupChat = () => {
            if (!this._isOpen) {
                this.open().then(() => {
                    logger.outPutSuccess('群组聊天已打开');
                });
                return;
            }

            logger.outPutInfo('群组聊天已经打开');
        };

        logger.outPutInfo('💬 群组聊天');
        logger.outPutInfo('输入 window.openGroupChat() 来开启群组聊天');
    }

    /**
     * 打开群聊面板并建立连接。
     */
    public async open(): Promise<void> {
        if (this._isOpen) {
            return;
        }

        this._isOpen = true;
        this.markAsOpened();

        if (util.isMobile()) {
            if (window.location.hash !== this._chatHash) {
                pushPageHistory(this._chatHash);
            }
            this.disableBodyScroll();
        }

        this.createUI();
        await this.connect();
    }

    /**
     * 创建群聊面板 DOM。
     */
    private createUI(): void {
        const chatPanel = document.createElement('div');
        chatPanel.className = 'group-chat';
        chatPanel.innerHTML = `
            <div class="group-chat__header">
                <h3 class="group-chat__title">群组聊天</h3>
                <button class="group-chat__close" aria-label="关闭">
                    <i class="fa fa-times"></i>
                </button>
            </div>
            <div class="group-chat__messages" id="group-chat-messages">
                <div
                    class="group-chat__load-more"
                    id="group-chat-load-more"
                    style="display: none;"
                >
                    <span>加载更多...</span>
                </div>
            </div>
            <div class="group-chat__input-area">
                <div class="group-chat__commands" id="group-chat-commands">
                    <div class="group-chat__command-item" data-command="/canvas">
                        <span class="group-chat__command-name">/canvas</span>
                        <span class="group-chat__command-desc">
                            开启/加入 你画我猜
                        </span>
                    </div>
                    <div class="group-chat__command-item" data-command="/font-color">
                        <span class="group-chat__command-name">/font-color</span>
                        <span class="group-chat__command-desc">
                            设置聊天文字颜色
                        </span>
                    </div>
                </div>
                <textarea
                    class="group-chat__input"
                    id="group-chat-input"
                    placeholder="输入消息... (/ 显示命令)"
                    rows="1"
                ></textarea>
                <button class="group-chat__send-btn" id="group-chat-send">发送</button>
            </div>
        `;

        document.body.appendChild(chatPanel);

        this._chatPanel = chatPanel;
        this._messagesContainer = chatPanel.querySelector<HTMLElement>('#group-chat-messages');
        this._loadMoreButton = chatPanel.querySelector<HTMLElement>('#group-chat-load-more');
        this._input = chatPanel.querySelector<HTMLTextAreaElement>('#group-chat-input');
        this._sendButton = chatPanel.querySelector<HTMLButtonElement>('#group-chat-send');
        this._commandsPanel = chatPanel.querySelector<HTMLElement>('#group-chat-commands');
        this.applyInputFontColor();

        this.bindEvents();

        requestAnimationFrame(() => {
            chatPanel.classList.add('group-chat--visible');
            const vh = window.innerHeight * 0.01;
            document.documentElement.style.setProperty('--vh', `${vh}px`);
        });

        if (this._triggerButton) {
            this._triggerButton.style.display = 'none';
        }
    }

    /**
     * 绑定输入、发送、滚动加载等交互事件。
     */
    private bindEvents(): void {
        if (
            !this._chatPanel ||
            !this._sendButton ||
            !this._input ||
            !this._commandsPanel ||
            !this._loadMoreButton ||
            !this._messagesContainer
        ) {
            return;
        }

        const closeButton = this._chatPanel.querySelector<HTMLButtonElement>('.group-chat__close');
        if (closeButton) {
            closeButton.addEventListener('click', async () => {
                await this.close();
            });
        }

        this._sendButton.addEventListener('click', async () => {
            await this.handleSend();
        });

        this._input.addEventListener('keydown', async (event: KeyboardEvent) => {
            const commandsVisible =
                this._commandsPanel?.classList.contains('group-chat__commands--visible') ?? false;

            if (commandsVisible) {
                if (event.key === 'ArrowDown') {
                    event.preventDefault();
                    this.moveCommandSelection(1);
                    return;
                }

                if (event.key === 'ArrowUp') {
                    event.preventDefault();
                    this.moveCommandSelection(-1);
                    return;
                }

                if (event.key === 'Tab') {
                    event.preventDefault();
                    this.moveCommandSelection(event.shiftKey ? -1 : 1);
                    return;
                }

                if (event.key === 'Enter' && !event.shiftKey) {
                    event.preventDefault();
                    await this.executeSelectedCommand();
                    return;
                }
            }

            if (event.key === 'Enter' && !event.shiftKey) {
                event.preventDefault();
                await this.handleSend();
            }
        });

        this._input.addEventListener('input', () => {
            if (!this._input || !this._commandsPanel) {
                return;
            }

            this._input.style.height = 'auto';
            this._input.style.height = `${Math.min(this._input.scrollHeight, 120)}px`;

            const value = this._input.value;
            if (value.startsWith('/')) {
                this._commandsPanel.classList.add('group-chat__commands--visible');
                this._selectedCommandIndex = 0;
                this.filterCommandItems(value);
                this.updateCommandSelection();
            } else {
                this._commandsPanel.classList.remove('group-chat__commands--visible');
                this.filterCommandItems('');
            }
        });

        const commandItems = this._commandsPanel.querySelectorAll<HTMLElement>(
            '.group-chat__command-item'
        );
        commandItems.forEach((item, index) => {
            item.addEventListener('click', () => {
                if (!this._input || !this._commandsPanel) {
                    return;
                }

                this._input.value = item.dataset.command || '';
                this._selectedCommandIndex = index;
                this.updateCommandSelection();
                this._commandsPanel.classList.remove('group-chat__commands--visible');
                this._input.focus();
            });
        });

        this.updateCommandSelection();

        this._loadMoreButton.addEventListener('click', async () => {
            await this.loadMoreHistory();
        });

        this._messagesContainer.addEventListener('scroll', async () => {
            if (
                this._messagesContainer &&
                this._messagesContainer.scrollTop === 0 &&
                this._hasMoreHistory &&
                !this._isLoadingHistory &&
                this._loadedHistoryCount < this._maxHistoryCount
            ) {
                await this.loadMoreHistory();
            }
        });
    }

    /**
     * 发送入口,自动识别命令或普通消息。
     */
    private async handleSend(): Promise<void> {
        if (!this._input) {
            return;
        }

        const content = this._input.value.trim();
        if (!content) {
            return;
        }

        if (content.startsWith('/')) {
            await this.handleCommand(content);
            return;
        }

        await this.sendMessage();
    }

    /**
     * 处理斜杠命令。
     */
    private async handleCommand(command: string): Promise<void> {
        if (!this._input || !this._commandsPanel) {
            return;
        }

        const [commandName, ...args] = command.split(/\s+/);
        const commandArg = args.join(' ').trim();

        if (commandName === '/canvas') {
            this._input.value = '';
            this._input.style.height = 'auto';
            this._commandsPanel.classList.remove('group-chat__commands--visible');

            canvasChat.open();
            if (!canvasChat.drawingUser) {
                await canvasChat.requestAccess();
            }
            return;
        }

        if (commandName === '/font-color') {
            await this.handleFontColorCommand(commandArg);
            return;
        }

        await this.sendMessage();
    }

    /**
     * 处理字体颜色命令。
     */
    private async handleFontColorCommand(colorArg: string): Promise<void> {
        if (!this._input || !this._commandsPanel) {
            return;
        }

        let color = colorArg;
        if (!color) {
            const pickedColor = await this.pickFontColor();
            if (!pickedColor) {
                return;
            }
            color = pickedColor;
        }

        const normalizedColor = this.normalizeColor(color);
        if (!normalizedColor) {
            await uiFeedbackService.alert('颜色格式无效,请使用 #RRGGBB、#RGB 或 rgb(r,g,b)');
            return;
        }

        if (!this.isConnectionReady() || !this._connection) {
            uiFeedbackService.toast('当前未连接群聊,无法设置颜色', ToastType.Warning);
            return;
        }

        try {
            await this._connection.invoke('SetFontColor', normalizedColor);
            this.saveLocalFontColor(normalizedColor);
            this.applyInputFontColor();
            uiFeedbackService.toast(`文字颜色已设置为 ${normalizedColor}`, ToastType.Success);
            this._input.value = '';
            this._input.style.height = 'auto';
            this._commandsPanel.classList.remove('group-chat__commands--visible');
        } catch (error) {
            logger.outPutError('设置字体颜色失败', error);
            uiFeedbackService.toast('设置字体颜色失败', ToastType.Error);
        }
    }

    /**
     * 弹出颜色选择器并返回选中值。
     */
    private async pickFontColor(): Promise<string | null> {
        return await new Promise((resolve) => {
            const content = document.createElement('div');
            content.className = 'group-chat__font-color-dialog';

            const title = document.createElement('p');
            title.className = 'group-chat__font-color-title';
            title.textContent = '请选择聊天字体颜色';

            const picker = document.createElement('input');
            picker.type = 'color';
            picker.className = 'group-chat__font-color-picker';
            picker.value = this._localFontColor ?? '#000000';

            const actions = document.createElement('div');
            actions.className = 'group-chat__font-color-actions';

            const cancelButton = document.createElement('button');
            cancelButton.type = 'button';
            cancelButton.className = 'dialog__btn dialog__btn--default';
            cancelButton.textContent = '取消';

            const confirmButton = document.createElement('button');
            confirmButton.type = 'button';
            confirmButton.className = 'dialog__btn dialog__btn--primary';
            confirmButton.textContent = '确定';

            actions.appendChild(cancelButton);
            actions.appendChild(confirmButton);

            content.appendChild(title);
            content.appendChild(picker);
            content.appendChild(actions);

            const { overlay } = uiFeedbackService.createDialog(
                '字体颜色',
                content,
                DialogType.Custom,
                {
                    showHeader: false,
                    showFooter: false,
                }
            );

            let resolved = false;
            const finish = (value: string | null): void => {
                if (resolved) {
                    return;
                }

                resolved = true;
                uiFeedbackService.closeDialog(overlay);
                resolve(value);
            };

            cancelButton.addEventListener('click', () => {
                finish(null);
            });

            confirmButton.addEventListener('click', () => {
                finish(picker.value || null);
            });

            overlay.addEventListener('click', (event: MouseEvent) => {
                if (event.target === overlay) {
                    finish(null);
                }
            });

            setTimeout(() => {
                picker.focus();
            }, 0);
        });
    }

    /**
     * 标准化颜色值,仅允许 Hex 与 rgb。
     */
    private normalizeColor(inputColor: string): string | null {
        const value = inputColor.trim();

        const hexPattern = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
        if (hexPattern.test(value)) {
            return value;
        }

        const rgbPattern = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i;
        const rgbMatch = value.match(rgbPattern);
        if (!rgbMatch) {
            return null;
        }

        const red = Number(rgbMatch[1]);
        const green = Number(rgbMatch[2]);
        const blue = Number(rgbMatch[3]);
        if ([red, green, blue].some((num) => num < 0 || num > 255)) {
            return null;
        }

        return `rgb(${red},${green},${blue})`;
    }

    /**
     * 命令列表选择移动。
     */
    private moveCommandSelection(delta: number): void {
        const commandItems = this.getCommandItems(true);
        if (commandItems.length === 0) {
            return;
        }

        const total = commandItems.length;
        this._selectedCommandIndex = (this._selectedCommandIndex + delta + total) % total;
        this.updateCommandSelection();
    }

    /**
     * 执行当前选中的命令。
     */
    private async executeSelectedCommand(): Promise<void> {
        if (!this._input) {
            return;
        }

        const commandItems = this.getCommandItems(true);
        if (commandItems.length === 0) {
            await this.handleSend();
            return;
        }

        const selected = commandItems[this._selectedCommandIndex];
        const selectedCommand = selected?.dataset.command;
        if (!selectedCommand) {
            await this.handleSend();
            return;
        }

        const currentValue = this._input.value.trim();
        const commandValue = this.replaceCommandPrefix(currentValue, selectedCommand);
        this._input.value = commandValue;
        await this.handleCommand(commandValue);
    }

    /**
     * 用选中命令替换当前输入前缀。
     */
    private replaceCommandPrefix(currentValue: string, selectedCommand: string): string {
        const trimmed = currentValue.trim();
        if (trimmed === '/' || trimmed === '') {
            return selectedCommand;
        }

        const parts = trimmed.split(/\s+/);
        const first = parts[0];
        const rest = parts.slice(1).join(' ');
        if (!first.startsWith('/')) {
            return selectedCommand;
        }

        if (rest) {
            return `${selectedCommand} ${rest}`;
        }

        return selectedCommand;
    }

    /**
     * 根据输入前缀筛选命令列表。
     */
    private filterCommandItems(inputValue: string): void {
        const commandItems = this.getCommandItems(false);
        if (commandItems.length === 0) {
            return;
        }

        const firstPart = inputValue.trim().split(/\s+/)[0] || '';
        const prefix = firstPart.startsWith('/') ? firstPart.toLowerCase() : '';

        commandItems.forEach((item) => {
            const command = (item.dataset.command || '').toLowerCase();
            const shouldShow = !prefix || command.startsWith(prefix);
            if (shouldShow) {
                item.classList.remove('group-chat__command-item--hidden');
            } else {
                item.classList.add('group-chat__command-item--hidden');
            }
        });

        const visibleItems = this.getCommandItems(true);
        if (visibleItems.length === 0) {
            this._selectedCommandIndex = 0;
            return;
        }

        if (this._selectedCommandIndex >= visibleItems.length) {
            this._selectedCommandIndex = 0;
        }
    }

    /**
     * 获取命令项列表。
     */
    private getCommandItems(visibleOnly: boolean): HTMLElement[] {
        if (!this._commandsPanel) {
            return [];
        }

        const items = Array.from(
            this._commandsPanel.querySelectorAll<HTMLElement>('.group-chat__command-item')
        );

        if (!visibleOnly) {
            return items;
        }

        return items.filter((item) => {
            return !item.classList.contains('group-chat__command-item--hidden');
        });
    }

    /**
     * 更新命令项选中样式。
     */
    private updateCommandSelection(): void {
        const allItems = this.getCommandItems(false);
        allItems.forEach((item) => {
            item.classList.remove('group-chat__command-item--active');
        });

        const commandItems = this.getCommandItems(true);
        if (commandItems.length === 0) {
            return;
        }

        const selectedItem = commandItems[this._selectedCommandIndex];
        if (selectedItem) {
            selectedItem.classList.add('group-chat__command-item--active');
        }
    }

    /**
     * 建立 SignalR 连接并注册客户端回调。
     */
    private async connect(): Promise<void> {
        try {
            const options: IHttpConnectionOptions = {
                skipNegotiation: true,
                transport: HttpTransportType.WebSockets,
            };

            this._connection = new HubConnectionBuilder()
                .withUrl('/groupchat', options)
                .configureLogging(LogLevel.None)
                .withAutomaticReconnect()
                .build();

            this._connection.on('OnMessageReceived', (message: GroupChatMessage) => {
                this.addMessage(message, message.userId === this._currentUserId);
            });

            this._connection.on('OnUserJoined', (user: GroupChatUser) => {
                this.addSystemMessage(`${user.name} 加入了群组`);
            });

            this._connection.on('OnUserLeft', (user: GroupChatUser) => {
                this.addSystemMessage(`${user.name} 离开了群组`);
            });

            this._connection.on(
                'OnHistoryLoaded',
                (messages: GroupChatMessage[], hasMore: boolean) => {
                    this.handleHistoryLoaded(messages, hasMore);
                }
            );

            this._connection.on('OnError', (error: string) => {
                uiFeedbackService.toast(error, ToastType.Error);
            });

            await this._connection.start();

            const currentUser = await this._connection.invoke<GroupChatCurrentUser>(
                'JoinGroup',
                null
            );

            if (currentUser) {
                this._currentUserId = currentUser.id;
                canvasChat.setUserId(this._currentUserId);
            }

            await this.syncFontColorOnConnect();

            canvasChat.init(this._connection);
            await this.loadInitialHistory();

            logger.outPutSuccess('已连接到群组聊天');
            uiFeedbackService.toast('已连接到群组聊天', ToastType.Success);
        } catch (error) {
            logger.outPutError('连接群组聊天失败', error);
            uiFeedbackService.toast('连接失败,请刷新页面重试', ToastType.Error);
        }
    }

    /**
     * 首次加载历史记录。
     */
    private async loadInitialHistory(): Promise<void> {
        if (!this.isConnectionReady() || !this._connection) {
            return;
        }

        this._loadedPageIndex = 1;
        this._loadedHistoryCount = 0;
        this._hasMoreHistory = true;

        await this._connection.invoke('GetHistory', 1, 50);
    }

    /**
     * 加载更多历史记录并保持滚动位置。
     */
    private async loadMoreHistory(): Promise<void> {
        if (
            !this.isConnectionReady() ||
            !this._connection ||
            !this._loadMoreButton ||
            !this._messagesContainer ||
            this._isLoadingHistory ||
            !this._hasMoreHistory ||
            this._loadedHistoryCount >= this._maxHistoryCount
        ) {
            return;
        }

        this._isLoadingHistory = true;
        this._loadMoreButton.style.display = 'block';
        this._loadMoreButton.classList.add('group-chat__load-more--loading');

        const textSpan = this._loadMoreButton.querySelector<HTMLElement>('span');
        if (textSpan) {
            textSpan.textContent = '加载中...';
        }

        try {
            const nextPage = this._loadedPageIndex + 1;
            const scrollHeight = this._messagesContainer.scrollHeight;
            const scrollTop = this._messagesContainer.scrollTop;

            await this._connection.invoke('GetHistory', nextPage, 50);

            requestAnimationFrame(() => {
                if (!this._messagesContainer) {
                    return;
                }

                const newScrollHeight = this._messagesContainer.scrollHeight;
                this._messagesContainer.scrollTop = newScrollHeight - scrollHeight + scrollTop;
            });
        } catch (error) {
            logger.outPutError('加载历史记录失败', error);
            uiFeedbackService.toast('加载历史记录失败', ToastType.Error);
        } finally {
            this._isLoadingHistory = false;
            this._loadMoreButton.classList.remove('group-chat__load-more--loading');
        }
    }

    /**
     * 处理历史记录数据并更新界面。
     */
    private handleHistoryLoaded(messages: GroupChatMessage[], hasMore: boolean): void {
        if (!this._messagesContainer || !this._loadMoreButton) {
            return;
        }

        this._hasMoreHistory = hasMore;
        this._loadedPageIndex += 1;
        this._loadedHistoryCount += messages.length;

        if (this._loadedHistoryCount >= this._maxHistoryCount) {
            this._hasMoreHistory = false;
            this._loadMoreButton.style.display = 'none';
        } else if (!hasMore) {
            this._loadMoreButton.style.display = 'none';
        } else {
            this._loadMoreButton.style.display = 'block';
            const textSpan = this._loadMoreButton.querySelector<HTMLElement>('span');
            if (textSpan) {
                textSpan.textContent = '加载更多...';
            }
        }

        const fragment = document.createDocumentFragment();
        messages.forEach((message) => {
            const isOwn = message.userId === this._currentUserId;
            const messageElement = this.createMessageElement(message, isOwn);
            fragment.insertBefore(messageElement, fragment.firstChild);
        });

        this._messagesContainer.insertBefore(fragment, this._loadMoreButton.nextSibling);

        if (this._loadedPageIndex === 2) {
            this.scrollToBottom();
        }
    }

    /**
     * 发送普通文本消息。
     */
    private async sendMessage(): Promise<void> {
        if (!this._input || !this._sendButton || !this._connection || !this.isConnectionReady()) {
            return;
        }

        const message = this._input.value.trim();
        if (!message) {
            return;
        }

        this._sendButton.disabled = true;
        this._input.disabled = true;

        try {
            await this._connection.invoke('SendMessage', message);
            this._input.value = '';
            this._input.style.height = 'auto';
        } catch (error) {
            logger.outPutError('发送消息失败', error);
            uiFeedbackService.toast('发送失败,请重试', ToastType.Error);
        } finally {
            this._sendButton.disabled = false;
            this._input.disabled = false;
            this._input.focus();
        }
    }

    /**
     * 追加一条聊天消息。
     */
    private addMessage(message: GroupChatMessage, isOwn: boolean = false): void {
        if (!this._messagesContainer) {
            return;
        }

        if (isOwn && !this._currentUserId) {
            this._currentUserId = message.userId;
        }

        const messageElement = this.createMessageElement(message, isOwn);
        this._messagesContainer.appendChild(messageElement);
        this.scrollToBottom();
    }

    /**
     * 创建消息 DOM。
     */
    private createMessageElement(message: GroupChatMessage, isOwn: boolean = false): HTMLElement {
        const messageDiv = document.createElement('div');
        const ownClass = isOwn ? ' group-chat__message--own' : '';
        messageDiv.className = `group-chat__message${ownClass}`;

        const avatar = document.createElement('img');
        avatar.className = 'group-chat__avatar';
        avatar.src = message.avatar;
        avatar.alt = message.userName;

        const contentDiv = document.createElement('div');
        contentDiv.className = 'group-chat__message-content';

        const username = document.createElement('div');
        username.className = 'group-chat__username';
        username.textContent = message.userName;

        const bubble = document.createElement('div');
        bubble.className = 'group-chat__bubble';
        bubble.textContent = message.message;

        const serverColor = message.fontColor ? this.normalizeColor(message.fontColor) : null;
        if (isOwn) {
            if (this._localFontColor) {
                bubble.style.color = this._localFontColor;
            } else if (serverColor) {
                bubble.style.color = serverColor;
                this.saveLocalFontColor(serverColor);
            }

            if (this._localFontColor && serverColor && this._localFontColor !== serverColor) {
                void this.syncServerFontColor(this._localFontColor);
            }
        } else if (serverColor) {
            bubble.style.color = serverColor;
        }

        const timestamp = document.createElement('div');
        timestamp.className = 'group-chat__timestamp';
        timestamp.textContent = this.formatTime(message.sendTime);

        contentDiv.appendChild(username);
        contentDiv.appendChild(bubble);
        contentDiv.appendChild(timestamp);

        messageDiv.appendChild(avatar);
        messageDiv.appendChild(contentDiv);

        return messageDiv;
    }

    /**
     * 追加一条系统消息。
     */
    private addSystemMessage(text: string): void {
        if (!this._messagesContainer) {
            return;
        }

        const systemDiv = document.createElement('div');
        systemDiv.className = 'group-chat__system-message';
        systemDiv.textContent = text;

        this._messagesContainer.appendChild(systemDiv);
        this.scrollToBottom();
    }

    /**
     * 统一时间展示格式。
     */
    private formatTime(dateTime: string): string {
        const date = new Date(dateTime);
        const year = date.getFullYear();
        const month = String(date.getMonth() + 1).padStart(2, '0');
        const day = String(date.getDate()).padStart(2, '0');
        const hours = String(date.getHours()).padStart(2, '0');
        const minutes = String(date.getMinutes()).padStart(2, '0');
        const seconds = String(date.getSeconds()).padStart(2, '0');

        return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
    }

    /**
     * 滚动消息区到底部。
     */
    private scrollToBottom(): void {
        requestAnimationFrame(() => {
            if (!this._messagesContainer) {
                return;
            }

            this._messagesContainer.scrollTop = this._messagesContainer.scrollHeight;
        });
    }

    /**
     * 关闭群聊并释放资源。
     */
    public async close(): Promise<void> {
        if (!this._isOpen) {
            return;
        }

        if (this._connection) {
            try {
                await this._connection.stop();
            } catch (error) {
                logger.outPutError('断开连接失败', error);
            }
        }

        if (this._chatPanel) {
            this._chatPanel.classList.remove('group-chat--visible');
            setTimeout(() => {
                if (this._chatPanel && this._chatPanel.parentNode) {
                    this._chatPanel.parentNode.removeChild(this._chatPanel);
                }
            }, 300);
        }

        this._isOpen = false;
        this._connection = null;
        this._currentUserId = null;
        this._loadedPageIndex = 0;
        this._hasMoreHistory = true;
        this._isLoadingHistory = false;
        this._loadedHistoryCount = 0;

        this._chatPanel = null;
        this._messagesContainer = null;
        this._loadMoreButton = null;
        this._input = null;
        this._sendButton = null;
        this._commandsPanel = null;

        if (util.isMobile()) {
            this.enableBodyScroll();
            if (window.location.hash === this._chatHash) {
                pushPageHistory(window.location.pathname + window.location.search);
            }
        }

        if (this._triggerButton) {
            this._triggerButton.style.display = 'flex';
        }
    }
}

export const groupChat = new GroupChat();
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这是一个功能完整的群组聊天模块,基于SignalR实现实时通信。让我详细解释一下代码的主要功能和结构:

主要功能

1. 群组聊天核心功能

  • 实时消息收发:使用SignalR WebSocket连接实现实时通信
  • 历史记录加载:支持分页加载历史聊天记录,最多1000条
  • 用户管理:显示用户进入/离开群组的系统消息

2. 特殊命令系统

  • /canvas:开启"你画我猜"游戏功能
  • /font-color:设置聊天文字颜色,支持颜色选择器

3. 移动端优化

  • 响应式设计:适配移动端界面
  • 键盘适配:处理移动端键盘弹出时的视口变化
  • 返回键支持:移动端可通过返回键关闭聊天面板
  • 滚动锁定:打开聊天时锁定页面滚动

核心类结构

私有属性

private _connection: HubConnection | null = null;  // SignalR连接
private _isOpen: boolean = false;                  // 聊天面板状态
private _currentUserId: string | null = null;     // 当前用户ID
private _loadedPageIndex: number = 0;             // 已加载页面索引
private _hasMoreHistory: boolean = true;          // 是否还有更多历史记录

主要方法

初始化相关

  • init():初始化基础功能
  • createTriggerButton():创建聊天触发按钮
  • setupConsoleCommand():注册控制台命令 window.openGroupChat()

连接管理

  • connect():建立SignalR连接并注册事件回调
  • close():关闭连接并清理资源

界面交互

  • createUI():动态创建聊天面板DOM
  • bindEvents():绑定各种交互事件
  • handleSend():处理消息发送(自动识别命令/普通消息)

消息处理

  • addMessage():添加聊天消息
  • addSystemMessage():添加系统消息
  • loadMoreHistory():加载更多历史记录

命令系统

  • handleCommand():处理斜杠命令
  • handleFontColorCommand():处理字体颜色设置
  • moveCommandSelection():命令列表导航

特色功能

1. 字体颜色同步

  • 本地存储用户颜色偏好
  • 与服务端同步颜色设置
  • 支持可视化颜色选择器

2. 命令自动补全

  • 输入 / 显示可用命令列表
  • 支持键盘导航(上下箭头、Tab键)
  • 智能过滤匹配命令

3. 历史记录管理

  • 滚动到顶部自动加载更多历史
  • 保持滚动位置不跳跃
  • 限制最大加载数量防止内存溢出

4. 移动端体验优化

  • 动态视口高度计算(处理iOS Safari地址栏)
  • 键盘弹出时的界面适配
  • Hash路由支持返回键关闭

使用方式

开启群聊

// 控制台命令
window.openGroupChat()

// 或通过触发按钮(首次使用后显示)

可用命令

/canvas          # 开启你画我猜游戏
/font-color      # 设置字体颜色
/font-color #ff0000  # 直接设置红色

技术亮点

  1. 状态管理:完善的连接状态检查和错误处理
  2. 性能优化:使用requestAnimationFrame优化DOM操作
  3. 用户体验:丰富的交互反馈和加载状态
  4. 可扩展性:模块化设计,易于添加新命令
  5. 兼容性:全面的移动端适配和浏览器兼容

这个模块实现了一个功能丰富、用户体验良好的现代化群聊系统,特别注重移动端体验和实时交互性能。

评论加载中...