如何实现支持@触发及自定义图文模板的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
相关产品推荐
相关产品推荐

