You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现支持@触发及自定义图文模板的Blazor Mention组件?

实现Blazor Mention组件步骤

1. 搭建核心组件结构

先创建Mention.razor组件,包含输入框和下拉菜单的基础UI。输入框用textarea(或InputText)绑定值,下拉菜单默认隐藏,通过布尔变量控制显示状态。

<div class="mention-container" style="position: relative;">
    <textarea @bind="@InputValue" 
              @oninput="@HandleInput" 
              @onkeydown="@HandleKeyDown"
              style="width: 100%; min-height: 100px; padding: 8px; border: 1px solid #eee;"></textarea>

    @if (IsDropdownVisible)
    {
        <div id="mention-dropdown" class="mention-dropdown" style="position: absolute; border: 1px solid #ccc; background: white; z-index: 1000; border-radius: 4px;">
            @foreach (var item in FilteredItems)
            {
                <div @onclick="@(() => SelectItem(item))" 
                     style="padding: 8px 12px; cursor: pointer; display: flex; align-items: center;">
                    @if (ItemTemplate != null)
                    {
                        @ItemTemplate(item)
                    }
                    else
                    {
                        <!-- 默认模板 -->
                        <img src="@item.AvatarUrl" style="width: 32px; height: 32px; border-radius: 50%; margin-right: 8px;" />
                        <span>@item.DisplayName</span>
                    }
                </div>
            }
        </div>
    }
</div>

@code {
    [Inject] private IJSRuntime JSRuntime { get; set; }

    [Parameter] public string InputValue { get; set; }
    [Parameter] public EventCallback<string> InputValueChanged { get; set; }
    [Parameter] public List<UserItem> Items { get; set; } = new();
    [Parameter] public RenderFragment<UserItem> ItemTemplate { get; set; }

    private bool IsDropdownVisible { get; set; }
    private List<UserItem> FilteredItems { get; set; } = new();
    private int MentionStartIndex { get; set; } // 记录@符号的起始位置
    private int SelectedItemIndex { get; set; } = 0; // 下拉选项选中索引

    public class UserItem
    {
        public string Id { get; set; }
        public string DisplayName { get; set; }
        public string AvatarUrl { get; set; }
    }

    // 后续逻辑实现
}

2. 输入监听与@符号检测

在HandleInput方法中实时分析输入内容,触发下拉菜单的显示条件:

  • 找到最近的@符号,且@后无空格或已完成的提及内容
  • 根据@后的文本过滤匹配选项
private async Task HandleInput(ChangeEventArgs e)
{
    InputValue = e.Value.ToString();
    await InputValueChanged.InvokeAsync(InputValue);

    // 查找最近的@符号位置
    var lastAtSymbolIndex = InputValue.LastIndexOf('@');
    if (lastAtSymbolIndex == -1)
    {
        IsDropdownVisible = false;
        return;
    }

    // 检查@后是否为有效输入(无空格)
    var textAfterAt = InputValue.Substring(lastAtSymbolIndex + 1);
    if (string.IsNullOrWhiteSpace(textAfterAt) || textAfterAt.Contains(' '))
    {
        IsDropdownVisible = false;
        return;
    }

    // 过滤匹配选项
    FilteredItems = Items.Where(item => item.DisplayName.Contains(textAfterAt, StringComparison.OrdinalIgnoreCase)).ToList();
    IsDropdownVisible = FilteredItems.Any();
    MentionStartIndex = lastAtSymbolIndex;
    SelectedItemIndex = 0;

    // 调用JS定位下拉菜单到@符号位置
    await JSRuntime.InvokeVoidAsync("positionMentionDropdown", "mention-dropdown", lastAtSymbolIndex, InputValue);
}

3. 下拉菜单定位(JS互操作)

在_Host.cshtml中添加JS方法,计算@符号在输入框中的坐标,设置下拉菜单位置:

window.positionMentionDropdown = function (dropdownId, atIndex, inputValue) {
    const textarea = document.querySelector('.mention-container textarea');
    const dropdown = document.getElementById(dropdownId);
    if (!textarea || !dropdown) return;

    // 创建临时元素计算@符号位置
    const tempDiv = document.createElement('div');
    tempDiv.style.position = 'absolute';
    tempDiv.style.visibility = 'hidden';
    tempDiv.style.whiteSpace = 'pre-wrap';
    tempDiv.style.font = window.getComputedStyle(textarea).font;
    tempDiv.style.width = textarea.clientWidth + 'px';
    tempDiv.textContent = inputValue.substring(0, atIndex + 1);
    document.body.appendChild(tempDiv);

    const rect = tempDiv.getBoundingClientRect();
    const textareaRect = textarea.getBoundingClientRect();

    dropdown.style.top = (rect.bottom - textareaRect.top) + 'px';
    dropdown.style.left = (rect.left - textareaRect.left) + 'px';

    document.body.removeChild(tempDiv);
};

4. 支持自定义模板

组件通过RenderFragment<UserItem>参数接收自定义模板,父组件使用时直接传入自定义结构:

<Mention InputValue="@Message" InputValueChanged="@(val => Message = val)" Items="@UserList">
    <ItemTemplate>
        <div style="display: flex; align-items: center; gap: 10px;">
            <img src="@context.AvatarUrl" style="width: 40px; height: 40px; border-radius: 50%;" />
            <div>
                <div style="font-weight: 600;">@context.DisplayName</div>
                <div style="font-size: 12px; color: #666;">@context.Id</div>
            </div>
        </div>
    </ItemTemplate>
</Mention>

@code {
    private string Message { get; set; }
    private List<Mention.UserItem> UserList = new()
    {
        new Mention.UserItem { Id = "user_001", DisplayName = "Alice Smith", AvatarUrl = "/avatars/alice.jpg" },
        new Mention.UserItem { Id = "user_002", DisplayName = "Bob Johnson", AvatarUrl = "/avatars/bob.jpg" }
    };
}

5. 选项选择与光标控制

实现SelectItem方法,将选中内容插入输入框,同时支持键盘上下箭头选择、回车确认:

private async Task SelectItem(UserItem item)
{
    // 替换@开头的文本为选中内容(可自定义格式,比如@[DisplayName](Id))
    var newText = InputValue.Substring(0, MentionStartIndex) + $"@{item.DisplayName} " + InputValue.Substring(MentionStartIndex + 1 + item.DisplayName.Length);
    InputValue = newText;
    await InputValueChanged.InvokeAsync(InputValue);

    IsDropdownVisible = false;

    // 调用JS将光标移动到插入内容末尾
    await JSRuntime.InvokeVoidAsync("setCursorPosition", ".mention-container textarea", MentionStartIndex + item.DisplayName.Length + 2);
}

private async Task HandleKeyDown(KeyboardEventArgs e)
{
    if (!IsDropdownVisible || !FilteredItems.Any()) return;

    if (e.Key == "ArrowDown")
    {
        SelectedItemIndex = (SelectedItemIndex + 1) % FilteredItems.Count;
        e.PreventDefault();
    }
    else if (e.Key == "ArrowUp")
    {
        SelectedItemIndex = SelectedItemIndex == 0 ? FilteredItems.Count - 1 : SelectedItemIndex - 1;
        e.PreventDefault();
    }
    else if (e.Key == "Enter")
    {
        await SelectItem(FilteredItems[SelectedItemIndex]);
        e.PreventDefault();
    }
}

对应的光标定位JS方法:

window.setCursorPosition = function (textareaSelector, position) {
    const textarea = document.querySelector(textareaSelector);
    if (textarea) {
        textarea.focus();
        textarea.setSelectionRange(position, position);
    }
};

6. 样式优化

添加基础样式提升交互体验:

.mention-container {
    position: relative;
}

.mention-dropdown {
    max-height: 220px;
    overflow-y: auto;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

.mention-dropdown > div:hover,
.mention-dropdown > div.selected {
    background-color: #f5f7fa;
}

内容的提问来源于stack exchange,提问作者Lóri Nóda

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 20:22:10