<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>jQuery仿新浪微博@功能特效</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
}
body {
background-color: #f6f6f6;
padding: 20px;
color: #333;
}
.container {
max-width: 600px;
margin: 0 auto;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 20px;
}
h1 {
font-size: 22px;
margin-bottom: 20px;
color: #333;
text-align: center;
}
.note {
background-color: #f8f4e5;
padding: 12px 15px;
border-left: 4px solid #e67e22;
margin-bottom: 20px;
border-radius: 0 4px 4px 0;
font-size: 14px;
line-height: 1.5;
}
.note a {
color: #e67e22;
text-decoration: none;
}
.note a:hover {
text-decoration: underline;
}
.weibo-editor {
position: relative;
margin-bottom: 20px;
}
.editor-box {
width: 100%;
min-height: 120px;
padding: 12px;
border: 1px solid #e6e6e6;
border-radius: 4px;
font-size: 14px;
line-height: 1.5;
outline: none;
resize: none;
transition: border-color 0.3s;
}
.editor-box:focus {
border-color: #3498db;
}
.mention-tag {
color: #3498db;
background-color: #e8f4fd;
padding: 0 2px;
border-radius: 2px;
cursor: pointer;
}
.mention-tag:hover {
text-decoration: underline;
}
.suggest-list {
position: absolute;
left: 0;
top: 100%;
width: 100%;
max-height: 200px;
overflow-y: auto;
background-color: #fff;
border: 1px solid #e6e6e6;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
z-index: 100;
display: none;
}
.suggest-item {
padding: 8px 12px;
cursor: pointer;
display: flex;
align-items: center;
transition: background-color 0.2s;
}
.suggest-item:hover {
background-color: #f5f5f5;
}
.suggest-item.active {
background-color: #e8f4fd;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
margin-right: 10px;
object-fit: cover;
}
.user-info {
flex: 1;
}
.user-name {
font-size: 14px;
font-weight: bold;
margin-bottom: 2px;
}
.user-desc {
font-size: 12px;
color: #999;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 15px;
border-top: 1px solid #f0f0f0;
}
.btn {
padding: 6px 16px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
border: none;
outline: none;
transition: all 0.3s;
}
.btn-publish {
background-color: #ff8200;
color: #fff;
}
.btn-publish:hover {
background-color: #f77000;
}
.btn-cancel {
background-color: transparent;
color: #666;
border: 1px solid #d9d9d9;
}
.btn-cancel:hover {
border-color: #999;
}
.counter {
font-size: 12px;
color: #999;
}
.counter.warning {
color: #ff8200;
}
.counter.error {
color: #f44336;
}
</style>
</head>
<body>
<div class="container">
<h1>jQuery仿新浪微博@功能特效</h1>
<div class="weibo-editor">
<div class="editor-box" id="weiboEditor" contenteditable="true" placeholder="分享你的新鲜事..."></div>
<div class="suggest-list" id="suggestList">
<!-- 建议列表将通过JS动态生成 -->
</div>
</div>
<div class="footer">
<div class="counter" id="wordCounter">0/140</div>
<div>
<button class="btn btn-cancel">取消</button>
<button class="btn btn-publish">发布</button>
</div>
</div>
</div>
<!-- 引入jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function() {
const $editor = $('#weiboEditor');
const $suggestList = $('#suggestList');
const $wordCounter = $('#wordCounter');
const MAX_LENGTH = 140;
// 模拟用户数据
const users = [
{ id: 1, name: '张三', avatar: 'https://randomuser.me/api/portraits/men/1.jpg', desc: '前端开发工程师' },
{ id: 2, name: '李四', avatar: 'https://randomuser.me/api/portraits/women/2.jpg', desc: 'UI设计师' },
{ id: 3, name: '王五', avatar: 'https://randomuser.me/api/portraits/men/3.jpg', desc: '产品经理' },
{ id: 4, name: '赵六', avatar: 'https://randomuser.me/api/portraits/women/4.jpg', desc: '后端开发工程师' },
{ id: 5, name: '钱七', avatar: 'https://randomuser.me/api/portraits/men/5.jpg', desc: '测试工程师' },
{ id: 6, name: '孙八', avatar: 'https://randomuser.me/api/portraits/women/6.jpg', desc: '运维工程师' },
{ id: 7, name: '周九', avatar: 'https://randomuser.me/api/portraits/men/7.jpg', desc: '全栈工程师' },
{ id: 8, name: '吴十', avatar: 'https://randomuser.me/api/portraits/women/8.jpg', desc: '数据分析师' }
];
// 当前是否在输入@
let isMentioning = false;
// 当前输入的@关键字
let mentionKeyword = '';
// 当前选中的建议项索引
let selectedIndex = -1;
// 初始化编辑器
function initEditor() {
updateWordCounter();
// 监听输入事件
$editor.on('input', function() {
updateWordCounter();
checkMention();
});
// 监听键盘事件
$editor.on('keydown', function(e) {
// 如果建议列表显示,处理上下键和回车键
if ($suggestList.is(':visible')) {
switch(e.keyCode) {
case 38: // 上箭头
e.preventDefault();
moveSelection(-1);
break;
case 40: // 下箭头
e.preventDefault();
moveSelection(1);
break;
case 13: // 回车
e.preventDefault();
selectUser();
break;
case 27: // ESC
hideSuggestList();
break;
}
}
// 输入@时开始提及
if (e.keyCode === 50 && e.shiftKey) { // @符号
setTimeout(() => {
startMention();
}, 10);
}
});
// 点击编辑器外部时隐藏建议列表
$(document).on('click', function(e) {
if (!$(e.target).closest('.weibo-editor').length) {
hideSuggestList();
}
});
}
// 更新字数统计
function updateWordCounter() {
const text = $editor.text();
const length = text.length;
$wordCounter.text(`${length}/${MAX_LENGTH}`);
if (length > MAX_LENGTH) {
$wordCounter.addClass('error');
} else if (length > MAX_LENGTH - 20) {
$wordCounter.addClass('warning');
} else {
$wordCounter.removeClass('warning error');
}
}
// 检查是否正在输入@
function checkMention() {
if (!isMentioning) return;
const text = $editor.text();
const cursorPos = getCaretPosition($editor[0]);
const textBeforeCursor = text.substring(0, cursorPos);
// 获取@后面的内容
const atIndex = textBeforeCursor.lastIndexOf('@');
if (atIndex === -1) {
isMentioning = false;
hideSuggestList();
return;
}
mentionKeyword = textBeforeCursor.substring(atIndex + 1).trim();
if (mentionKeyword.length > 0) {
showSuggestList(mentionKeyword);
} else {
hideSuggestList();
}
}
// 开始@提及
function startMention() {
isMentioning = true;
mentionKeyword = '';
selectedIndex = -1;
}
// 显示建议列表
function showSuggestList(keyword) {
const filteredUsers = users.filter(user =>
user.name.includes(keyword) ||
(user.desc && user.desc.includes(keyword))
.slice(0, 8);
if (filteredUsers.length === 0) {
hideSuggestList();
return;
}
// 生成建议列表HTML
let html = '';
filteredUsers.forEach((user, index) => {
html += `
<div class="suggest-item ${index === selectedIndex ? 'active' : ''}" data-id="${user.id}">
<img src="${user.avatar}" class="user-avatar" alt="${user.name}">
<div class="user-info">
<div class="user-name">${user.name}</div>
<div class="user-desc">${user.desc}</div>
</div>
</div>
`;
});
$suggestList.html(html).show();
// 绑定点击事件
$suggestList.find('.suggest-item').on('click', function() {
const $item = $(this);
selectUser($item.index());
});
}
// 隐藏建议列表
function hideSuggestList() {
$suggestList.hide();
isMentioning = false;
selectedIndex = -1;
}
// 移动选择项
function moveSelection(step) {
const $items = $suggestList.find('.suggest-item');
if ($items.length === 0) return;
selectedIndex += step;
if (selectedIndex < 0) {
selectedIndex = $items.length - 1;
} else if (selectedIndex >= $items.length) {
selectedIndex = 0;
}
$items.removeClass('active');
$items.eq(selectedIndex).addClass('active');
// 滚动到可见区域
const $selectedItem = $items.eq(selectedIndex);
const containerHeight = $suggestList.height();
const itemHeight = $selectedItem.outerHeight();
const itemTop = $selectedItem.position().top;
if (itemTop < 0) {
$suggestList.scrollTop($suggestList.scrollTop() + itemTop);
} else if (itemTop + itemHeight > containerHeight) {
$suggestList.scrollTop($suggestList.scrollTop() + (itemTop + itemHeight - containerHeight));
}
}
// 选择用户
function selectUser(index) {
if (index !== undefined) {
selectedIndex = index;
}
if (selectedIndex === -1) return;
const $items = $suggestList.find('.suggest-item');
if (selectedIndex >= $items.length) return;
const $selectedItem = $items.eq(selectedIndex);
const userId = $selectedItem.data('id');
const userName = $selectedItem.find('.user-name').text();
// 获取当前文本和光标位置
const text = $editor.text();
const cursorPos = getCaretPosition($editor[0]);
const textBeforeCursor = text.substring(0, cursorPos);
// 找到@位置
const atIndex = textBeforeCursor.lastIndexOf('@');
if (atIndex === -1) return;
// 替换@后面的内容为用户名
const textBeforeAt = text.substring(0, atIndex);
const textAfterAt = text.substring(cursorPos);
// 创建mention标签
const mentionTag = document.createElement('span');
mentionTag.className = 'mention-tag';
mentionTag.textContent = `@${userName}`;
mentionTag.dataset.userId = userId;
// 保存当前选区
const selection = window.getSelection();
const range = selection.getRangeAt(0);
// 替换文本
$editor.empty();
$editor.append(document.createTextNode(textBeforeAt));
$editor.append(mentionTag);
$editor.append(document.createTextNode(textAfterAt));
// 恢复光标位置
const newRange = document.createRange();
newRange.setStartAfter(mentionTag);
newRange.collapse(true);
selection.removeAllRanges();
selection.addRange(newRange);
hideSuggestList();
updateWordCounter();
}
// 获取光标位置
function getCaretPosition(editableDiv) {
let caretPos = 0;
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(editableDiv);
preCaretRange.setEnd(range.endContainer, range.endOffset);
caretPos = preCaretRange.toString().length;
}
return caretPos;
}
// 初始化
initEditor();
});
</script>
</body>
</html>