<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat Interface</title>
<script src="https://cdn.jsdelivr.net/npm/emoji-picker-element@^1/index.js" type="module"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
align-items: center;
background-color: transparent;
}
.phone-container {
width: min(100%, 360px);
border: 12px solid #1c1c1c;
border-radius: 36px;
background-color: #ededed;
position: relative;
margin: 15px;
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
overflow: hidden;
}
.chat-container {
width: 100%;
height: 600px;
background-color: #ededed;
border-radius: 24px;
overflow: hidden;
font-family: -apple-system, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", sans-serif;
margin: 0 auto;
}
.chat-header {
height: 56px;
background: rgba(247, 247, 247, 0.95);
padding: 0 16px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid rgba(0,0,0,0.08);
backdrop-filter: blur(10px);
position: relative;
}
.back-arrow-btn {
width: 24px;
height: 24px;
color: #333;
cursor: pointer;
flex-shrink: 0;
z-index: 2;
}
.chat-header h2 {
position: absolute;
top: 50%;
left: 50px;
right: 50px;
transform: translateY(-50%);
margin: 0 auto;
text-align: center;
font-size: 17px;
color: #000;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
z-index: 1;
}
.header-icons {
display: flex;
gap: 10px;
flex-shrink: 0;
z-index: 2;
}
.header-icon {
width: 24px;
height: 24px;
color: #333;
}
.header-icon:hover {
color: #000;
transform: scale(1.1);
}
/* 手机刘海 */
.phone-notch {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 160px;
height: 13px;
background: #1c1c1c;
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
z-index: 999;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
.notch-camera, .notch-speaker, .notch-sensor {
background: #444;
border-radius: 50%;
}
.notch-camera { width: 8px; height: 8px; border: 2px solid #333; }
.notch-speaker { width: 40px; height: 4px; border-radius: 2px; }
.notch-sensor { width: 6px; height: 6px; border: 1px solid #333; }
.chat-messages {
height: 492px;
padding: 16px;
overflow-y: auto;
}
.chat-messages::-webkit-scrollbar { width: 6px; }
.chat-messages::-webkit-scrollbar-track { background: transparent; }
.chat-messages::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.1); border-radius: 3px; }
.message {
display: flex;
margin-bottom: 8px;
align-items: flex-start;
position: relative;
animation: message-pop 0.3s ease-out;
}
.message.message-is-card { margin-bottom: 16px; }
@keyframes message-pop {
from { transform: translateY(10px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.message.sent { flex-direction: row-reverse; }
.avatar {
width: 36px;
height: 36px;
border-radius: 8px;
margin-top: 4px;
border: 1px solid rgba(0,0,0,0.05);
transition: transform 0.2s;
}
.avatar:hover { transform: scale(1.1); }
.message-wrapper { max-width: 70%; display: flex; flex-direction: column; margin: 0 10px; }
.message-content {
padding: 10px 14px;
border-radius: 8px;
font-size: 14px;
line-height: 1.4;
word-wrap: break-word;
position: relative;
transition: transform 0.2s;
}
.message-content:hover { transform: translateY(-2px); }
.received .message-content { background: #fff; color: #000; box-shadow: 0 1px 2px rgba(0,0,0,0.05); border-bottom-left-radius: 2px; }
.sent .message-content { background: #a9e97a; color: #000; box-shadow: 0 1px 2px rgba(0,0,0,0.05); border-bottom-right-radius: 2px; }
.message-time { font-size: 11px; color: #b2b2b2; margin-top: 4px; margin-left: 4px; margin-right: 4px; }
.sent .message-time { text-align: right; }
.typing-indicator { display: inline-flex; gap: 2px; padding: 2px 4px; margin-top: 2px; animation: none; }
.typing-dot { width: 3px; height: 3px; background: #999; border-radius: 50%; animation: subtle-typing 1.5s infinite; }
@keyframes subtle-typing {
0%, 100% { transform: translateY(0); opacity: 0.5; }
50% { transform: translateY(-1px); opacity: 0.8; }
}
.chat-input { height: 52px; background: #f7f7f7; padding: 8px 12px; display: flex; align-items: center; gap: 12px; border-top: 1px solid #e7e7e7; position: relative; }
.emoji-btn { background: transparent; border: none; color: #333; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: color 0.2s; flex-shrink: 0; padding: 0; width: 28px; height: 28px; }
.emoji-btn svg { width: 28px; height: 28px; }
.emoji-btn:hover { color: #000; }
/* 表情选择器容器样式 */
.emoji-picker-container {
position: absolute;
bottom: 60px;
left: 12px;
right: 12px;
width: auto;
display: none; /* 【【【修改点 2: 默认隐藏】】】 */
background: #f7f7f7;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000;
}
/* 表情选择器内部组件样式 */
emoji-picker {
width: 100%;
height: 300px;
/* 【【【修改点 1: 调整列数和尺寸以适应容器】】】 */
--num-columns: 8;
--emoji-size: 1.2rem;
--category-font-size: 0.9rem;
/* 统一为灰白主题 */
--background: #f7f7f7;
--border-color: #e7e7e7;
--input-background: #ffffff;
--input-text-color: #333;
--category-icon-color: #555;
--category-icon-hover-color: #000;
--search-input-focus-background: #fff;
}
.input-field { flex: 1; height: 38px; border: none; border-radius: 8px; background: #fff; padding: 0 12px; outline: none; font-size: 14px; color: #333; transition: all 0.2s; margin-right: 0; max-width: calc(100% - 80px); }
.input-field:focus { border: 1px solid #a9e97a; box-shadow: none; padding: 0 11px; }
.input-field::placeholder { color: #d1d1d1; }
.send-btn { background: transparent; border: none; color: #333; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: color 0.2s; flex-shrink: 0; padding: 0; width: 28px; height: 28px; }
.send-btn svg { width: 24px; height: 24px; }
.send-btn:hover { color: #000; }
/* TRANSFER MODAL STYLES */
.transfer-modal { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 320px; background: #ffffff; border-radius: 24px; padding: 28px 24px; text-align: center; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); z-index: 1000; animation: modalSlideIn 0.4s cubic-bezier(0.16, 1, 0.3, 1); }
.transfer-content { display: flex; flex-direction: column; align-items: center; gap: 20px; }
.transfer-modal .transfer-avatar { width: 96px; height: 96px; border-radius: 12px; border: 2px solid #e0e0e0; padding: 3px; margin-bottom: 4px; object-fit: cover; }
.transfer-modal .transfer-title { font-size: 16px; font-weight: 500; color: #333; margin: 0; }
.transfer-form { width: 100%; display: flex; flex-direction: column; gap: 16px; }
.amount-input-group { display: flex; align-items: center; border: 1px solid #e0e0e0; border-radius: 25px; padding: 0 15px; background: #fff; transition: border-color 0.2s; height: 44px; }
.amount-input-group:focus-within { border-color: #888; }
.amount-input { flex: 1; border: none; background: transparent; outline: none; text-align: left; font-size: 16px; color: #333; padding-left: 16px; -moz-appearance: textfield; }
.amount-input::-webkit-inner-spin-button, .amount-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.amount-input::placeholder { color: #ccc; }
.amount-symbol { font-size: 18px; color: #555; font-weight: 500; }
.note-input { width: 100%; padding: 10px 16px; height: 44px; border: 1px solid #e0e0e0; border-radius: 25px; background: #fff; font-size: 14px; color: #333; text-align: center; outline: none; transition: border-color 0.2s; }
.note-input:focus { border-color: #888; }
.note-input::placeholder { color: #ccc; }
.transfer-buttons { display: flex; gap: 16px; margin-top: 8px; justify-content: center; }
.transfer-button { padding: 10px 32px; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; transition: all 0.2s; }
.confirm-transfer { background: #98d6d0; color: white; }
.confirm-transfer:hover { background: #89c7c0; transform: translateY(-2px); }
.cancel-transfer { background: #f0f0f0; color: #555; }
.cancel-transfer:hover { background: #e0e0e0; transform: translateY(-2px); }
/* TRANSFER MESSAGE CARD STYLES */
.transfer-card { background: #fff; border: 1px solid #e9e4f0; border-radius: 10px; padding: 12px 16px; width: fit-content; max-width: 200px; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); display: flex; flex-direction: column; gap: 4px; }
.transfer-card .transfer-header { display: flex; align-items: center; gap: 6px; }
.transfer-card .transfer-icon { width: 18px; height: 18px; color: #555; }
.transfer-card .transfer-title { font-size: 14px; font-weight: 500; color: #333; }
.transfer-card .transfer-amount { font-size: 24px; font-weight: 700; color: #000; text-align: left; line-height: 1.2; margin-left: 24px; }
.transfer-card .transfer-status-text { font-size: 13px; color: #2E8B57; text-align: left; margin-left: 24px; }
@keyframes modalSlideIn {
from { opacity: 0; transform: translate(-50%, -45%); }
to { opacity: 1; transform: translate(-50%, -50%); }
}
@media screen and (max-width: 400px) {
.phone-container { margin: 10px; border-width: 8px; }
.chat-container { height: 580px; }
.chat-messages { height: 472px; }
.message-content { font-size: 13px; }
.input-field { max-width: calc(100% - 80px); }
}
</style>
</head>
<body>
<div class="phone-container">
<div class="phone-notch">
<div class="notch-sensor"></div>
<div class="notch-camera"></div>
<div class="notch-speaker"></div>
<div class="notch-sensor"></div>
</div>
<div class="chat-container">
<div class="transfer-modal" style="display: none;">
<div class="transfer-content">
<img src="https://files.catbox.moe/t511iu.png" class="transfer-avatar" alt="Transfer Avatar">
<div class="transfer-title">转账给 $1</div>
<div class="transfer-form">
<div class="amount-input-group">
<input type="number" class="amount-input" placeholder="输入金额...">
<span class="amount-symbol">¥</span>
</div>
<input type="text" class="note-input" placeholder="输入备注...">
<div class="transfer-buttons">
<button class="transfer-button confirm-transfer">确认</button>
<button class="transfer-button cancel-transfer">取消</button>
</div>
</div>
</div>
</div>
<div class="chat-header">
<svg class="back-arrow-btn" viewBox="0 0 24 24">
<path fill="currentColor" d="M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41z"/>
</svg>
<h2>$1</h2>
<div class="header-icons">
<svg class="header-icon transfer-btn" viewBox="0 0 24 24">
<path fill="currentColor" d="M20,8H4V6H20M20,18H4V12H20M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4Z"/>
</svg>
</div>
</div>
<div class="chat-messages">
$2
</div>
<div class="chat-input">
<button class="emoji-btn">
<svg viewBox="0 0 24 24" width="20" height="20">
<path fill="currentColor" d="M12,2C6.477,2,2,6.477,2,12c0,5.523,4.477,10,10,10s10-4.477,10-10C22,6.477,17.523,2,12,2z M12,20c-4.418,0-8-3.582-8-8 s3.582-8,8-8s8,3.582,8,8S16.418,20,12,20z M15.5,11c0.828,0,1.5-0.672,1.5-1.5S16.328,8,15.5,8S14,8.672,14,9.5S14.672,11,15.5,11z M8.5,11c0.828,0,1.5-0.672,1.5-1.5S9.328,8,8.5,8S7,8.672,7,9.5S7.672,11,8.5,11z M12,17.5c2.33,0,4.31-1.46,5.11-3.5H6.89 C7.69,16.04,9.67,17.5,12,17.5z"/>
</svg>
</button>
<div class="emoji-picker-container">
<emoji-picker></emoji-picker>
</div>
<input type="text" class="input-field" placeholder="输入聊天内容...">
<button class="send-btn">
<svg viewBox="0 0 24 24" width="20" height="20">
<path fill="currentColor" d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const emojiBtn = document.querySelector('.emoji-btn');
const emojiPickerContainer = document.querySelector('.emoji-picker-container');
const emojiPicker = document.querySelector('emoji-picker');
const textInput = document.querySelector('.input-field');
const sendBtn = document.querySelector('.send-btn');
const chatMessages = document.querySelector('.chat-messages');
// 转账弹窗相关
const transferBtn = document.querySelector('.transfer-btn');
const transferModal = document.querySelector('.transfer-modal');
const cancelTransferBtn = document.querySelector('.cancel-transfer');
const confirmTransferBtn = document.querySelector('.confirm-transfer');
const amountInput = document.querySelector('.amount-input');
const noteInput = document.querySelector('.note-input');
// 转账按钮点击事件
transferBtn.addEventListener('click', () => {
playSound('click');
transferModal.style.display = 'block';
setTimeout(() => amountInput.focus(), 300);
});
cancelTransferBtn.addEventListener('click', () => {
playSound('click');
transferModal.style.display = 'none';
});
confirmTransferBtn.addEventListener('click', () => {
const amount = amountInput.value.trim();
const note = noteInput.value.trim();
if (!amount || isNaN(amount) || Number(amount) <= 0) {
amountInput.parentNode.classList.add('shake');
setTimeout(() => amountInput.parentNode.classList.remove('shake'), 500);
return;
}
playSound('send');
transferModal.style.display = 'none';
const chatName = document.querySelector('.chat-header h2').textContent;
let transferCommand = note ? `[user转账给${chatName}:${amount}|${note}]` : `[user转账给${chatName}:${amount}]`;
if (typeof triggerSlash === 'function') {
triggerSlash(`/send ${transferCommand}|/trigger`);
} else {
// 本地测试预览逻辑
console.log("将发送指令: ", transferCommand);
const lastMessage = chatMessages.querySelector('.message:last-child');
let lastTimeStr = '11:47';
if (lastMessage) { lastTimeStr = lastMessage.querySelector('.message-time').textContent; }
const [hours, minutes] = lastTimeStr.split(':').map(Number);
let newMinutes = minutes + 1;
let newHours = hours;
if (newMinutes >= 60) {
newMinutes = 0;
newHours = (newHours + 1) % 24;
}
const timeString = `${newHours.toString().padStart(2, '0')}:${newMinutes.toString().padStart(2, '0')}`;
const bankIconSvg = `<svg class="transfer-icon" viewBox="0 0 24 24"><path fill="currentColor" d="M4,10V7H6V10H4M10,10V7H12V10H10M16,10V7H18V10H16M20,20H2V12L11,3.05L20,12V20M4,18H8V14H4V18M10,18H14V14H10V18M16,18H20V14H16V18Z"/></svg>`;
const messageHtml = `
<div class="message sent message-is-card">
<div class="user_avatar avatar" style="background-size: cover; background-position: center;"></div>
<div class="message-wrapper">
<div class="transfer-card">
<div class="transfer-header">
${bankIconSvg}
<div class="transfer-title">转账给${chatName}</div>
</div>
<div class="transfer-amount">¥${amount}</div>
<div class="transfer-status-text">转账成功</div>
${note ? `<div class="transfer-note" style="font-size:12px;color:#888;margin-top:4px;">备注: ${note}</div>` : ''}
</div>
<div class="message-time">${timeString}</div>
</div>
</div>`;
chatMessages.insertAdjacentHTML('beforeend', messageHtml);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
});
amountInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') noteInput.focus(); });
noteInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') confirmTransferBtn.click(); });
const shakeStyle = document.createElement('style');
shakeStyle.textContent = `
@keyframes shake {
0%, 100% { transform: translateX(0); }
20%, 60% { transform: translateX(-5px); }
40%, 80% { transform: translateX(5px); }
}
.shake { animation: shake 0.5s ease-in-out; border-color: #ff4d4f !important; }
`;
document.head.appendChild(shakeStyle);
const sounds = {
click: new Audio('https://assets.mixkit.co/active_storage/sfx/2568/2568-preview.mp3'),
send: new Audio('https://assets.mixkit.co/active_storage/sfx/2570/2570-preview.mp3'),
emoji: new Audio('https://files.catbox.moe/gb0so5.mp3'),
bubble: new Audio('https://assets.mixkit.co/active_storage/sfx/2568/2568-preview.mp3'),
avatar: new Audio('https://assets.mixkit.co/active_storage/sfx/2575/2575-preview.mp3')
};
function playSound(soundType) {
if (sounds[soundType]) {
sounds[soundType].currentTime = 0;
sounds[soundType].play().catch(e => console.error("音频播放失败:", e));
}
}
// 表情按钮点击事件 - 【【【核心修改点 2: 实现开关逻辑】】】
emojiBtn.addEventListener('click', () => {
playSound('emoji');
const isVisible = emojiPickerContainer.style.display === 'block';
emojiPickerContainer.style.display = isVisible ? 'none' : 'block';
});
// 选择表情事件
emojiPicker.addEventListener('emoji-click', (event) => {
playSound('click');
const emoji = event.detail.unicode;
const start = textInput.selectionStart;
const end = textInput.selectionEnd;
textInput.value = textInput.value.slice(0, start) + emoji + textInput.value.slice(end);
const newCursorPosition = start + emoji.length;
textInput.setSelectionRange(newCursorPosition, newCursorPosition);
emojiPickerContainer.style.display = 'none';
textInput.focus();
});
// 发送按钮点击事件
sendBtn.addEventListener('click', () => {
const messageText = textInput.value.trim();
if (messageText) {
playSound('send');
if (typeof triggerSlash === 'function') {
triggerSlash(`/send 回复和$1的聊天:${messageText}|/trigger`);
}
// 本地预览逻辑... (保持不变)
const lastMessage = chatMessages.querySelector('.message:last-child');
let lastTimeStr = '11:47';
if (lastMessage) { lastTimeStr = lastMessage.querySelector('.message-time').textContent; }
const [hours, minutes] = lastTimeStr.split(':').map(Number);
let newMinutes = minutes + 1;
let newHours = hours;
if (newMinutes >= 60) { newMinutes = 0; newHours = (newHours + 1) % 24; }
const timeString = `${newHours.toString().padStart(2, '0')}:${newMinutes.toString().padStart(2, '0')}`;
const messageHtml = `
<div class="message sent">
<div class="user_avatar avatar" style="background-size: cover; background-position: center;"></div>
<div class="message-wrapper">
<div class="message-content">${messageText}</div>
<div class="message-time">${timeString}</div>
</div>
</div>`;
chatMessages.insertAdjacentHTML('beforeend', messageHtml);
const newAvatar = chatMessages.lastElementChild.querySelector('.avatar');
const newContent = chatMessages.lastElementChild.querySelector('.message-content');
newAvatar.addEventListener('click', () => playSound('avatar'));
newContent.addEventListener('click', () => playSound('bubble'));
chatMessages.scrollTop = chatMessages.scrollHeight;
textInput.value = '';
}
});
textInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') sendBtn.click(); });
// 点击外部关闭表情选择器
document.addEventListener('click', (e) => {
if (!emojiBtn.contains(e.target) && !emojiPickerContainer.contains(e.target)) {
emojiPickerContainer.style.display = 'none';
}
});
Object.values(sounds).forEach(sound => sound.load());
// 动态监听新消息的MutationObserver
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) {
const newAvatars = node.querySelectorAll('.avatar');
const newContents = node.querySelectorAll('.message-content, .transfer-card');
newAvatars.forEach(avatar => avatar.addEventListener('click', () => playSound('avatar')));
newContents.forEach(content => content.addEventListener('click', () => playSound('bubble')));
}
});
}
});
});
observer.observe(chatMessages, { childList: true, subtree: true });
});
(function showMessages() {
const receivedMessages = Array.from(document.querySelectorAll('.message.received'));
const messageSound = new Audio('https://files.catbox.moe/b4amzm.mp3');
function isLatestInterface() {
const currentTimestamp = Date.now();
const lastTimestamp = parseInt(localStorage.getItem('lastInterfaceTimestamp') || '0');
localStorage.setItem('lastInterfaceTimestamp', currentTimestamp.toString());
return (currentTimestamp - lastTimestamp) > 100;
}
if (!isLatestInterface() || document.querySelectorAll('.message.sent').length === 0) {
receivedMessages.forEach(msg => {
msg.style.display = 'flex';
messageSound.currentTime = 0;
messageSound.play();
});
return;
}
receivedMessages.forEach(msg => msg.style.display = 'none');
function getRandomTime(min, max) { return Math.floor(Math.random() * (max - min + 1) + min); }
function typeMessage(messageContent, originalContent, onComplete) {
if (originalContent.includes('<div') || originalContent.includes('<img')) {
messageContent.innerHTML = originalContent;
messageSound.currentTime = 0;
messageSound.play();
onComplete();
return;
}
const typingIndicator = document.createElement('div');
typingIndicator.className = 'typing-indicator';
typingIndicator.innerHTML = '<span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span>';
messageContent.textContent = '';
messageContent.appendChild(typingIndicator);
setTimeout(() => {
typingIndicator.style.transition = 'opacity 0.5s';
typingIndicator.style.opacity = '0';
setTimeout(() => {
typingIndicator.remove();
messageContent.textContent = '';
let charIndex = 0;
const typeChar = () => {
if (charIndex === 0) { messageSound.currentTime = 0; messageSound.play(); }
if (charIndex < originalContent.length) {
messageContent.textContent += originalContent.charAt(charIndex++);
setTimeout(typeChar, 50);
} else {
onComplete();
}
};
typeChar();
}, 500);
}, getRandomTime(1000, 3000));
}
function showMessagesSequentially(messages, index = 0) {
if (index >= messages.length) return;
const message = messages[index];
const messageContent = message.querySelector('.message-content, .transfer-card');
if (!messageContent) return;
const originalContent = messageContent.innerHTML;
message.style.display = 'flex';
typeMessage(messageContent, originalContent, () => {
setTimeout(() => showMessagesSequentially(messages, index + 1), getRandomTime(500, 1500));
});
}
setTimeout(() => showMessagesSequentially(receivedMessages), 1000);
})();
</script>
</body>
</html>
index.html
style.css
index.js
index.html