mirror of
https://github.com/Cola-Echo/Cola.git
synced 2026-06-06 03:35:50 +00:00
Add files via upload
This commit is contained in:
58
chat.js
58
chat.js
@@ -155,35 +155,45 @@ async function handleBlockedExclamationClick(contact, exclamationEl) {
|
|||||||
await triggerAIAfterUnblock(contact);
|
await triggerAIAfterUnblock(contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示"已添加好友"的手机弹窗
|
// 显示"已添加好友"的仿手机弹窗
|
||||||
function showFriendAddedPopup(name) {
|
function showFriendAddedPopup(name) {
|
||||||
// 创建弹窗遮罩
|
// 获取手机容器
|
||||||
const overlay = document.createElement('div');
|
const phoneContainer = document.querySelector('.wechat-phone');
|
||||||
overlay.className = 'wechat-phone-popup-overlay';
|
if (!phoneContainer) return;
|
||||||
overlay.innerHTML = `
|
|
||||||
<div class="wechat-phone-popup">
|
// 创建仿手机弹窗(使用与其他弹窗一致的 wechat-modal 样式)
|
||||||
<div class="wechat-phone-popup-icon">
|
const modal = document.createElement('div');
|
||||||
<svg viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="#07c160" stroke-width="2">
|
modal.className = 'wechat-modal';
|
||||||
<circle cx="12" cy="12" r="10"/>
|
modal.id = 'wechat-friend-added-modal';
|
||||||
<path d="M8 12l2.5 2.5L16 9"/>
|
modal.innerHTML = `
|
||||||
</svg>
|
<div class="wechat-modal-content">
|
||||||
|
<div class="wechat-modal-title">添加好友成功</div>
|
||||||
|
<div class="wechat-modal-body">
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="#07c160" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="10"/>
|
||||||
|
<path d="M8 12l2.5 2.5L16 9"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
${escapeHtml(name)}已添加您为好友,现在可以开始聊天了。
|
||||||
|
</div>
|
||||||
|
<div class="wechat-modal-actions">
|
||||||
|
<button class="wechat-btn wechat-btn-primary" id="wechat-friend-added-confirm">确定</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="wechat-phone-popup-text">${escapeHtml(name)}已添加您为好友,现在可以开始聊天了。</div>
|
|
||||||
<div class="wechat-phone-popup-btn">确定</div>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document.body.appendChild(overlay);
|
phoneContainer.appendChild(modal);
|
||||||
|
|
||||||
// 点击确定关闭
|
// 点击确定关闭
|
||||||
overlay.querySelector('.wechat-phone-popup-btn').addEventListener('click', () => {
|
modal.querySelector('#wechat-friend-added-confirm').addEventListener('click', () => {
|
||||||
overlay.remove();
|
modal.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 点击遮罩也关闭
|
// 点击遮罩也关闭
|
||||||
overlay.addEventListener('click', (e) => {
|
modal.addEventListener('click', (e) => {
|
||||||
if (e.target === overlay) {
|
if (e.target === modal) {
|
||||||
overlay.remove();
|
modal.remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -862,7 +872,7 @@ export function renderChatHistory(contact, chatHistory, indexOffset = 0) {
|
|||||||
const isTimeout = callInfo === '对方已取消';
|
const isTimeout = callInfo === '对方已取消';
|
||||||
|
|
||||||
// 线条电话图标
|
// 线条电话图标
|
||||||
const phoneIconSVG = `<svg class="wechat-call-record-icon" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
const phoneIconSVG = `<svg class="wechat-call-record-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
||||||
</svg>`;
|
</svg>`;
|
||||||
|
|
||||||
@@ -871,16 +881,16 @@ export function renderChatHistory(contact, chatHistory, indexOffset = 0) {
|
|||||||
// 已接通:显示通话时长
|
// 已接通:显示通话时长
|
||||||
callRecordHTML = `
|
callRecordHTML = `
|
||||||
<div class="wechat-call-record">
|
<div class="wechat-call-record">
|
||||||
<span class="wechat-call-record-text">通话时长 ${callInfo}</span>
|
|
||||||
${phoneIconSVG}
|
${phoneIconSVG}
|
||||||
|
<span class="wechat-call-record-text">通话时长 ${callInfo}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (isCancelled) {
|
} else if (isCancelled) {
|
||||||
// 用户发起未接通:已取消
|
// 用户发起未接通:已取消
|
||||||
callRecordHTML = `
|
callRecordHTML = `
|
||||||
<div class="wechat-call-record">
|
<div class="wechat-call-record">
|
||||||
<span class="wechat-call-record-text">已取消</span>
|
|
||||||
${phoneIconSVG}
|
${phoneIconSVG}
|
||||||
|
<span class="wechat-call-record-text">已取消</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (isRejected) {
|
} else if (isRejected) {
|
||||||
@@ -903,8 +913,8 @@ export function renderChatHistory(contact, chatHistory, indexOffset = 0) {
|
|||||||
// 兜底:显示原始内容
|
// 兜底:显示原始内容
|
||||||
callRecordHTML = `
|
callRecordHTML = `
|
||||||
<div class="wechat-call-record">
|
<div class="wechat-call-record">
|
||||||
<span class="wechat-call-record-text">${escapeHtml(callInfo)}</span>
|
|
||||||
${phoneIconSVG}
|
${phoneIconSVG}
|
||||||
|
<span class="wechat-call-record-text">${escapeHtml(callInfo)}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -928,7 +938,7 @@ export function renderChatHistory(contact, chatHistory, indexOffset = 0) {
|
|||||||
const isTimeout = callInfo === '对方已取消';
|
const isTimeout = callInfo === '对方已取消';
|
||||||
|
|
||||||
// 摄像机图标
|
// 摄像机图标
|
||||||
const cameraIconSVG = `<svg class="wechat-call-record-icon wechat-video-call-icon" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
const cameraIconSVG = `<svg class="wechat-call-record-icon wechat-video-call-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<rect x="2" y="6" width="13" height="12" rx="2"/>
|
<rect x="2" y="6" width="13" height="12" rx="2"/>
|
||||||
<path d="M22 8l-7 4 7 4V8z"/>
|
<path d="M22 8l-7 4 7 4V8z"/>
|
||||||
</svg>`;
|
</svg>`;
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export const LISTEN_TOGETHER_PROMPT_TEMPLATE = `##【一起听歌场景】
|
|||||||
【核心要求 - 必须遵守】
|
【核心要求 - 必须遵守】
|
||||||
1. 只能发送纯文字消息,像朋友之间真实聊天一样
|
1. 只能发送纯文字消息,像朋友之间真实聊天一样
|
||||||
2. 保持你的性格特点,用符合你角色设定的方式说话
|
2. 保持你的性格特点,用符合你角色设定的方式说话
|
||||||
3. 每次回复请发送2-4条消息,用换行分隔,让对话更有层次感
|
3. 每次回复1-3条消息即可,用换行分隔,不要刻意凑数量
|
||||||
4. 可以聊歌曲、聊心情、聊任何话题,自然就好
|
4. 可以聊歌曲、聊心情、聊任何话题,自然就好
|
||||||
5. 发表对歌曲的看法时,要结合你的角色性格和经历
|
5. 发表对歌曲的看法时,要结合你的角色性格和经历
|
||||||
|
|
||||||
|
|||||||
11
contacts.js
11
contacts.js
@@ -14,6 +14,9 @@ let pendingAvatarContactIndex = -1;
|
|||||||
// 当前编辑的联系人索引
|
// 当前编辑的联系人索引
|
||||||
let currentEditingContactIndex = -1;
|
let currentEditingContactIndex = -1;
|
||||||
|
|
||||||
|
// 弹窗打开时间(用于防止点击穿透)
|
||||||
|
let contactSettingsOpenTime = 0;
|
||||||
|
|
||||||
// 添加联系人
|
// 添加联系人
|
||||||
export function addContact(characterData) {
|
export function addContact(characterData) {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
@@ -183,6 +186,9 @@ export function openContactSettings(contactIndex) {
|
|||||||
|
|
||||||
currentEditingContactIndex = contactIndex;
|
currentEditingContactIndex = contactIndex;
|
||||||
|
|
||||||
|
// 记录打开时间,用于防止点击穿透
|
||||||
|
contactSettingsOpenTime = Date.now();
|
||||||
|
|
||||||
// 填充头像和名称
|
// 填充头像和名称
|
||||||
const avatarPreview = document.getElementById('wechat-contact-avatar-preview');
|
const avatarPreview = document.getElementById('wechat-contact-avatar-preview');
|
||||||
const nameEl = document.getElementById('wechat-contact-settings-name');
|
const nameEl = document.getElementById('wechat-contact-settings-name');
|
||||||
@@ -258,6 +264,11 @@ export function openContactSettings(contactIndex) {
|
|||||||
export function saveContactSettings() {
|
export function saveContactSettings() {
|
||||||
if (currentEditingContactIndex < 0) return;
|
if (currentEditingContactIndex < 0) return;
|
||||||
|
|
||||||
|
// 防止点击穿透:如果弹窗刚打开(300ms内),忽略保存操作
|
||||||
|
if (Date.now() - contactSettingsOpenTime < 300) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
const contact = settings.contacts[currentEditingContactIndex];
|
const contact = settings.contacts[currentEditingContactIndex];
|
||||||
if (!contact) return;
|
if (!contact) return;
|
||||||
|
|||||||
@@ -119,6 +119,26 @@ function getCatboxUrl(id, ext) {
|
|||||||
return `https://files.catbox.moe/${id}.${ext}`;
|
return `https://files.catbox.moe/${id}.${ext}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 生成唯一的表情名称(如果已存在同名则添加数字后缀)
|
||||||
|
function getUniqueStickerName(baseName, stickers) {
|
||||||
|
if (!stickers || stickers.length === 0) return baseName;
|
||||||
|
|
||||||
|
// 检查是否已存在同名
|
||||||
|
const existingNames = stickers.map(s => s.name);
|
||||||
|
if (!existingNames.includes(baseName)) {
|
||||||
|
return baseName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加数字后缀直到找到唯一名称
|
||||||
|
let counter = 1;
|
||||||
|
let newName = `${baseName}${counter}`;
|
||||||
|
while (existingNames.includes(newName)) {
|
||||||
|
counter++;
|
||||||
|
newName = `${baseName}${counter}`;
|
||||||
|
}
|
||||||
|
return newName;
|
||||||
|
}
|
||||||
|
|
||||||
// 切换表情面板显示/隐藏
|
// 切换表情面板显示/隐藏
|
||||||
export function toggleEmojiPanel() {
|
export function toggleEmojiPanel() {
|
||||||
const panel = document.getElementById('wechat-emoji-panel');
|
const panel = document.getElementById('wechat-emoji-panel');
|
||||||
@@ -241,19 +261,22 @@ function addStickersFromInput(inputs) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否已存在
|
// 检查是否已存在(按URL判断)
|
||||||
const exists = settings.stickers.some(s => s.url === url);
|
const exists = settings.stickers.some(s => s.url === url);
|
||||||
if (exists) {
|
if (exists) {
|
||||||
showToast(`已存在: ${name}`, 'info');
|
showToast(`已存在: ${name}`, 'info');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 生成唯一名称(如果同名则自动添加数字后缀)
|
||||||
|
const uniqueName = getUniqueStickerName(name, settings.stickers);
|
||||||
|
|
||||||
// 调试:显示添加的表情信息
|
// 调试:显示添加的表情信息
|
||||||
console.log('[可乐] 添加表情:', { name, url });
|
console.log('[可乐] 添加表情:', { name: uniqueName, url });
|
||||||
|
|
||||||
settings.stickers.push({
|
settings.stickers.push({
|
||||||
url,
|
url,
|
||||||
name,
|
name: uniqueName,
|
||||||
addedTime: new Date().toISOString()
|
addedTime: new Date().toISOString()
|
||||||
});
|
});
|
||||||
addedCount++;
|
addedCount++;
|
||||||
@@ -287,9 +310,11 @@ function addStickerFromFile() {
|
|||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
try {
|
try {
|
||||||
const dataUrl = await readFileAsDataURL(file);
|
const dataUrl = await readFileAsDataURL(file);
|
||||||
|
// 生成唯一名称(如果同名则自动添加数字后缀)
|
||||||
|
const uniqueName = getUniqueStickerName(file.name, settings.stickers);
|
||||||
settings.stickers.push({
|
settings.stickers.push({
|
||||||
url: dataUrl,
|
url: dataUrl,
|
||||||
name: file.name,
|
name: uniqueName,
|
||||||
addedTime: new Date().toISOString()
|
addedTime: new Date().toISOString()
|
||||||
});
|
});
|
||||||
addedCount++;
|
addedCount++;
|
||||||
|
|||||||
290
floating-ball.js
Normal file
290
floating-ball.js
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
/**
|
||||||
|
* 悬浮球组件
|
||||||
|
* 可爱猫咪悬浮窗,支持拖拽,点击打开主界面
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getSettings } from './config.js';
|
||||||
|
import { requestSave } from './save-manager.js';
|
||||||
|
|
||||||
|
// 悬浮球状态
|
||||||
|
let floatingBallState = {
|
||||||
|
isDragging: false,
|
||||||
|
startX: 0,
|
||||||
|
startY: 0,
|
||||||
|
initialX: 0,
|
||||||
|
initialY: 0,
|
||||||
|
currentX: 0,
|
||||||
|
currentY: 0,
|
||||||
|
hasMoved: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// SVG 图标 - 渐变圆圈和猫咪
|
||||||
|
const FLOATING_BALL_SVG = `
|
||||||
|
<svg viewBox="0 0 100 100" width="60" height="60" class="floating-ball-svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="ring-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#FFB6C1;stop-opacity:1" />
|
||||||
|
<stop offset="50%" style="stop-color:#FFC0CB;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#FFEFD5;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<!-- 渐变圆圈 -->
|
||||||
|
<circle cx="50" cy="50" r="44" fill="none" stroke="url(#ring-gradient)" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
<!-- 猫咪头部轮廓 -->
|
||||||
|
<g transform="translate(50, 52)" stroke="#333" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- 头部 -->
|
||||||
|
<ellipse cx="0" cy="0" rx="22" ry="18"/>
|
||||||
|
<!-- 左耳 -->
|
||||||
|
<path d="M-18,-12 L-22,-24 L-12,-16"/>
|
||||||
|
<!-- 右耳 -->
|
||||||
|
<path d="M18,-12 L22,-24 L12,-16"/>
|
||||||
|
<!-- 内耳(粉色填充) -->
|
||||||
|
<path d="M-17,-14 L-19,-21 L-13,-16" fill="#FFB6C1" stroke="none"/>
|
||||||
|
<path d="M17,-14 L19,-21 L13,-16" fill="#FFB6C1" stroke="none"/>
|
||||||
|
<!-- 左眼 -->
|
||||||
|
<circle cx="-8" cy="-2" r="3" fill="#333"/>
|
||||||
|
<!-- 右眼 -->
|
||||||
|
<circle cx="8" cy="-2" r="3" fill="#333"/>
|
||||||
|
<!-- 鼻子 -->
|
||||||
|
<ellipse cx="0" cy="6" rx="2" ry="1.5" fill="#FFB6C1"/>
|
||||||
|
<!-- 嘴巴 -->
|
||||||
|
<path d="M0,7 Q-4,12 -8,9" fill="none"/>
|
||||||
|
<path d="M0,7 Q4,12 8,9" fill="none"/>
|
||||||
|
<!-- 腮红 -->
|
||||||
|
<ellipse cx="-14" cy="4" rx="4" ry="3" fill="#FFB6C1" opacity="0.5" stroke="none"/>
|
||||||
|
<ellipse cx="14" cy="4" rx="4" ry="3" fill="#FFB6C1" opacity="0.5" stroke="none"/>
|
||||||
|
<!-- 胡须 -->
|
||||||
|
<path d="M-24,0 L-12,2"/>
|
||||||
|
<path d="M-24,6 L-12,5"/>
|
||||||
|
<path d="M24,0 L12,2"/>
|
||||||
|
<path d="M24,6 L12,5"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 创建悬浮球
|
||||||
|
export function createFloatingBall() {
|
||||||
|
// 检查是否已存在
|
||||||
|
if (document.getElementById('wechat-floating-ball')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ball = document.createElement('div');
|
||||||
|
ball.id = 'wechat-floating-ball';
|
||||||
|
ball.className = 'wechat-floating-ball';
|
||||||
|
ball.innerHTML = FLOATING_BALL_SVG;
|
||||||
|
|
||||||
|
document.body.appendChild(ball);
|
||||||
|
|
||||||
|
// 恢复位置
|
||||||
|
restorePosition(ball);
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
bindFloatingBallEvents(ball);
|
||||||
|
|
||||||
|
// 根据主界面状态设置悬浮球可见性
|
||||||
|
updateFloatingBallVisibility();
|
||||||
|
|
||||||
|
return ball;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复保存的位置
|
||||||
|
function restorePosition(ball) {
|
||||||
|
const settings = getSettings();
|
||||||
|
const savedPos = settings.floatingBallPosition;
|
||||||
|
|
||||||
|
if (savedPos && savedPos.x !== undefined && savedPos.y !== undefined) {
|
||||||
|
// 确保位置在视口内
|
||||||
|
const maxX = window.innerWidth - 60;
|
||||||
|
const maxY = window.innerHeight - 60;
|
||||||
|
floatingBallState.currentX = Math.min(Math.max(0, savedPos.x), maxX);
|
||||||
|
floatingBallState.currentY = Math.min(Math.max(0, savedPos.y), maxY);
|
||||||
|
} else {
|
||||||
|
// 默认位置:右侧中间
|
||||||
|
floatingBallState.currentX = window.innerWidth - 80;
|
||||||
|
floatingBallState.currentY = (window.innerHeight - 60) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
ball.style.left = floatingBallState.currentX + 'px';
|
||||||
|
ball.style.top = floatingBallState.currentY + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存位置
|
||||||
|
function savePosition() {
|
||||||
|
const settings = getSettings();
|
||||||
|
settings.floatingBallPosition = {
|
||||||
|
x: floatingBallState.currentX,
|
||||||
|
y: floatingBallState.currentY
|
||||||
|
};
|
||||||
|
requestSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
function bindFloatingBallEvents(ball) {
|
||||||
|
// 鼠标事件
|
||||||
|
ball.addEventListener('mousedown', onDragStart);
|
||||||
|
document.addEventListener('mousemove', onDragMove);
|
||||||
|
document.addEventListener('mouseup', onDragEnd);
|
||||||
|
|
||||||
|
// 触摸事件
|
||||||
|
ball.addEventListener('touchstart', onDragStart, { passive: false });
|
||||||
|
document.addEventListener('touchmove', onDragMove, { passive: false });
|
||||||
|
document.addEventListener('touchend', onDragEnd);
|
||||||
|
|
||||||
|
// 窗口大小变化时调整位置
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
const maxX = window.innerWidth - 60;
|
||||||
|
const maxY = window.innerHeight - 60;
|
||||||
|
if (floatingBallState.currentX > maxX) {
|
||||||
|
floatingBallState.currentX = maxX;
|
||||||
|
ball.style.left = floatingBallState.currentX + 'px';
|
||||||
|
}
|
||||||
|
if (floatingBallState.currentY > maxY) {
|
||||||
|
floatingBallState.currentY = maxY;
|
||||||
|
ball.style.top = floatingBallState.currentY + 'px';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始拖拽
|
||||||
|
function onDragStart(e) {
|
||||||
|
const ball = document.getElementById('wechat-floating-ball');
|
||||||
|
if (!ball) return;
|
||||||
|
|
||||||
|
floatingBallState.isDragging = true;
|
||||||
|
floatingBallState.hasMoved = false;
|
||||||
|
|
||||||
|
// 获取起始位置
|
||||||
|
if (e.type === 'touchstart') {
|
||||||
|
floatingBallState.startX = e.touches[0].clientX;
|
||||||
|
floatingBallState.startY = e.touches[0].clientY;
|
||||||
|
e.preventDefault();
|
||||||
|
} else {
|
||||||
|
floatingBallState.startX = e.clientX;
|
||||||
|
floatingBallState.startY = e.clientY;
|
||||||
|
}
|
||||||
|
|
||||||
|
floatingBallState.initialX = floatingBallState.currentX;
|
||||||
|
floatingBallState.initialY = floatingBallState.currentY;
|
||||||
|
|
||||||
|
ball.classList.add('dragging');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拖拽移动
|
||||||
|
function onDragMove(e) {
|
||||||
|
if (!floatingBallState.isDragging) return;
|
||||||
|
|
||||||
|
const ball = document.getElementById('wechat-floating-ball');
|
||||||
|
if (!ball) return;
|
||||||
|
|
||||||
|
let clientX, clientY;
|
||||||
|
if (e.type === 'touchmove') {
|
||||||
|
clientX = e.touches[0].clientX;
|
||||||
|
clientY = e.touches[0].clientY;
|
||||||
|
e.preventDefault();
|
||||||
|
} else {
|
||||||
|
clientX = e.clientX;
|
||||||
|
clientY = e.clientY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltaX = clientX - floatingBallState.startX;
|
||||||
|
const deltaY = clientY - floatingBallState.startY;
|
||||||
|
|
||||||
|
// 如果移动距离超过5px,认为是拖拽而非点击
|
||||||
|
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
|
||||||
|
floatingBallState.hasMoved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算新位置
|
||||||
|
let newX = floatingBallState.initialX + deltaX;
|
||||||
|
let newY = floatingBallState.initialY + deltaY;
|
||||||
|
|
||||||
|
// 限制在视口内
|
||||||
|
const maxX = window.innerWidth - 60;
|
||||||
|
const maxY = window.innerHeight - 60;
|
||||||
|
newX = Math.min(Math.max(0, newX), maxX);
|
||||||
|
newY = Math.min(Math.max(0, newY), maxY);
|
||||||
|
|
||||||
|
floatingBallState.currentX = newX;
|
||||||
|
floatingBallState.currentY = newY;
|
||||||
|
|
||||||
|
ball.style.left = newX + 'px';
|
||||||
|
ball.style.top = newY + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 结束拖拽
|
||||||
|
function onDragEnd(e) {
|
||||||
|
if (!floatingBallState.isDragging) return;
|
||||||
|
|
||||||
|
const ball = document.getElementById('wechat-floating-ball');
|
||||||
|
if (ball) {
|
||||||
|
ball.classList.remove('dragging');
|
||||||
|
}
|
||||||
|
|
||||||
|
floatingBallState.isDragging = false;
|
||||||
|
|
||||||
|
// 如果没有移动,视为点击
|
||||||
|
if (!floatingBallState.hasMoved) {
|
||||||
|
toggleMainInterface();
|
||||||
|
} else {
|
||||||
|
// 保存位置
|
||||||
|
savePosition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换主界面显示
|
||||||
|
function toggleMainInterface() {
|
||||||
|
const phone = document.getElementById('wechat-phone');
|
||||||
|
if (!phone) return;
|
||||||
|
|
||||||
|
const isHidden = phone.classList.contains('hidden');
|
||||||
|
|
||||||
|
if (isHidden) {
|
||||||
|
phone.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
phone.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新设置
|
||||||
|
const settings = getSettings();
|
||||||
|
settings.phoneVisible = isHidden;
|
||||||
|
requestSave();
|
||||||
|
|
||||||
|
// 更新悬浮球状态
|
||||||
|
updateFloatingBallVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新悬浮球可见性(主界面显示时隐藏悬浮球,反之显示)
|
||||||
|
export function updateFloatingBallVisibility() {
|
||||||
|
const ball = document.getElementById('wechat-floating-ball');
|
||||||
|
const phone = document.getElementById('wechat-phone');
|
||||||
|
|
||||||
|
if (!ball) return;
|
||||||
|
|
||||||
|
// 主界面隐藏时显示悬浮球,主界面显示时也显示悬浮球(方便用户随时关闭)
|
||||||
|
ball.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示悬浮球
|
||||||
|
export function showFloatingBall() {
|
||||||
|
const ball = document.getElementById('wechat-floating-ball');
|
||||||
|
if (ball) {
|
||||||
|
ball.style.display = 'flex';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏悬浮球
|
||||||
|
export function hideFloatingBall() {
|
||||||
|
const ball = document.getElementById('wechat-floating-ball');
|
||||||
|
if (ball) {
|
||||||
|
ball.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 销毁悬浮球
|
||||||
|
export function destroyFloatingBall() {
|
||||||
|
const ball = document.getElementById('wechat-floating-ball');
|
||||||
|
if (ball) {
|
||||||
|
ball.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
8
gift.js
8
gift.js
@@ -349,8 +349,8 @@ export function checkGiftDelivery(contact) {
|
|||||||
const currentCount = contact.chatHistory?.length || 0;
|
const currentCount = contact.chatHistory?.length || 0;
|
||||||
|
|
||||||
for (const gift of contact.pendingGifts) {
|
for (const gift of contact.pendingGifts) {
|
||||||
// 如果正在使用中,跳过
|
// 如果正在使用中或已完成,跳过
|
||||||
if (gift.isUsing) continue;
|
if (gift.isUsing || gift.completed) continue;
|
||||||
|
|
||||||
// 首次送达检测
|
// 首次送达检测
|
||||||
if (!gift.isDelivered && currentCount >= gift.startMessageCount + 25) {
|
if (!gift.isDelivered && currentCount >= gift.startMessageCount + 25) {
|
||||||
@@ -415,6 +415,10 @@ export function showGiftArrivalModal(gift, contact) {
|
|||||||
yesBtn.removeEventListener('click', handleYes);
|
yesBtn.removeEventListener('click', handleYes);
|
||||||
noBtn.removeEventListener('click', handleNo);
|
noBtn.removeEventListener('click', handleNo);
|
||||||
|
|
||||||
|
// 标记礼物为已完成,防止重复触发弹窗
|
||||||
|
gift.completed = true;
|
||||||
|
requestSave();
|
||||||
|
|
||||||
// 打开玩具控制界面
|
// 打开玩具控制界面
|
||||||
const { showToyControlPage } = await import('./toy-control.js');
|
const { showToyControlPage } = await import('./toy-control.js');
|
||||||
showToyControlPage(gift, contact, currentChatIndex);
|
showToyControlPage(gift, contact, currentChatIndex);
|
||||||
|
|||||||
@@ -150,8 +150,8 @@ export function refreshHistoryList(filter = 'all') {
|
|||||||
<div class="wechat-history-item" data-index="${lb.originalIndex}" style="padding: 12px; border-bottom: 1px solid var(--wechat-border); cursor: pointer;">
|
<div class="wechat-history-item" data-index="${lb.originalIndex}" style="padding: 12px; border-bottom: 1px solid var(--wechat-border); cursor: pointer;">
|
||||||
<div style="display: flex; align-items: center; gap: 10px;">
|
<div style="display: flex; align-items: center; gap: 10px;">
|
||||||
<div style="flex: 1; min-width: 0;">
|
<div style="flex: 1; min-width: 0;">
|
||||||
<div style="font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #000;">${escapeHtml(displayName)}</div>
|
<div style="font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--wechat-text-primary);">${escapeHtml(displayName)}</div>
|
||||||
<div style="font-size: 12px; color: #000;">${entriesCount} 杯总结 · ${lb.lastUpdated || lb.addedTime || '未知时间'}</div>
|
<div style="font-size: 12px; color: var(--wechat-text-secondary);">${entriesCount} 杯总结 · ${lb.lastUpdated || lb.addedTime || '未知时间'}</div>
|
||||||
</div>
|
</div>
|
||||||
<label class="wechat-toggle wechat-toggle-small" onclick="event.stopPropagation()">
|
<label class="wechat-toggle wechat-toggle-small" onclick="event.stopPropagation()">
|
||||||
<input type="checkbox" class="wechat-history-toggle" data-index="${lb.originalIndex}" ${lb.enabled !== false ? 'checked' : ''}>
|
<input type="checkbox" class="wechat-history-toggle" data-index="${lb.originalIndex}" ${lb.enabled !== false ? 'checked' : ''}>
|
||||||
@@ -339,7 +339,6 @@ export function renderToyHistory(contact) {
|
|||||||
contentEl.innerHTML = sortedHistory.map((session, sortedIdx) => {
|
contentEl.innerHTML = sortedHistory.map((session, sortedIdx) => {
|
||||||
const targetText = session.target === 'character' ? 'TA在用' : '你在用';
|
const targetText = session.target === 'character' ? 'TA在用' : '你在用';
|
||||||
const messages = session.messages || [];
|
const messages = session.messages || [];
|
||||||
const previewMessages = messages.slice(0, 5); // 只显示前5条消息预览
|
|
||||||
const originalIndex = toyHistory.indexOf(session);
|
const originalIndex = toyHistory.indexOf(session);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -350,7 +349,7 @@ export function renderToyHistory(contact) {
|
|||||||
<span class="wechat-toy-history-card-gift-name">${escapeHtml(session.gift?.name || '未知玩具')}</span>
|
<span class="wechat-toy-history-card-gift-name">${escapeHtml(session.gift?.name || '未知玩具')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="wechat-toy-history-card-actions">
|
<div class="wechat-toy-history-card-actions">
|
||||||
<span class="wechat-toy-history-card-target">${targetText}</span>
|
<span class="wechat-toy-history-card-target">${targetText}<button class="wechat-toy-target-close-btn" data-tab="toy" data-index="${originalIndex}" title="删除">×</button></span>
|
||||||
<button class="wechat-history-delete-btn" data-tab="toy" data-index="${originalIndex}" title="删除">×</button>
|
<button class="wechat-history-delete-btn" data-tab="toy" data-index="${originalIndex}" title="删除">×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -358,16 +357,15 @@ export function renderToyHistory(contact) {
|
|||||||
<span>${escapeHtml(session.time || '未知时间')}</span>
|
<span>${escapeHtml(session.time || '未知时间')}</span>
|
||||||
<span>时长 ${escapeHtml(session.duration || '00:00')}</span>
|
<span>时长 ${escapeHtml(session.duration || '00:00')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="wechat-toy-history-card-messages">
|
<div class="wechat-toy-history-card-messages wechat-toy-history-scrollable">
|
||||||
${previewMessages.length === 0 ? '<div style="color: #999; text-align: center;">暂无对话记录</div>' :
|
${messages.length === 0 ? '<div style="color: #999; text-align: center;">暂无对话记录</div>' :
|
||||||
previewMessages.map(msg => `
|
messages.map(msg => `
|
||||||
<div class="wechat-toy-history-msg">
|
<div class="wechat-toy-history-msg">
|
||||||
<span class="wechat-toy-history-msg-sender ${msg.role === 'user' ? 'user' : 'ai'}">${msg.role === 'user' ? '你' : 'TA'}:</span>
|
<span class="wechat-toy-history-msg-sender ${msg.role === 'user' ? 'user' : 'ai'}">${msg.role === 'user' ? '你' : 'TA'}:</span>
|
||||||
<span class="wechat-toy-history-msg-content">${escapeHtml((msg.content || '').substring(0, 50))}${(msg.content?.length || 0) > 50 ? '...' : ''}</span>
|
<span class="wechat-toy-history-msg-content">${escapeHtml(msg.content || '')}</span>
|
||||||
</div>
|
</div>
|
||||||
`).join('')
|
`).join('')
|
||||||
}
|
}
|
||||||
${messages.length > 5 ? `<div style="color: #ff6b8a; font-size: 12px; text-align: center; margin-top: 8px;">还有 ${messages.length - 5} 条消息...</div>` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -143,6 +143,13 @@ function showListenTogetherPage() {
|
|||||||
const page = document.getElementById('wechat-listen-together-page');
|
const page = document.getElementById('wechat-listen-together-page');
|
||||||
if (!page) return;
|
if (!page) return;
|
||||||
|
|
||||||
|
// 清空上次的聊天消息 DOM
|
||||||
|
const messagesEl = document.getElementById('wechat-listen-messages');
|
||||||
|
if (messagesEl) {
|
||||||
|
messagesEl.innerHTML = '';
|
||||||
|
messagesEl.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
const contact = listenState.contact;
|
const contact = listenState.contact;
|
||||||
const song = listenState.currentSong;
|
const song = listenState.currentSong;
|
||||||
|
|||||||
262
main.js
262
main.js
@@ -15,7 +15,7 @@ import { ICON_SUCCESS, ICON_INFO } from './icons.js';
|
|||||||
import { addContact, refreshContactsList, openContactSettings, saveContactSettings, closeContactSettings, changeContactAvatar, getCurrentEditingContactIndex } from './contacts.js';
|
import { addContact, refreshContactsList, openContactSettings, saveContactSettings, closeContactSettings, changeContactAvatar, getCurrentEditingContactIndex } from './contacts.js';
|
||||||
import { openChatByContactId, setCurrentChatIndex, sendMessage, showRecalledMessages, currentChatIndex, openChat, updateBlockMenuText, startBlockedAIMessages, stopBlockedAIMessages, showBlockedMessages } from './chat.js';
|
import { openChatByContactId, setCurrentChatIndex, sendMessage, showRecalledMessages, currentChatIndex, openChat, updateBlockMenuText, startBlockedAIMessages, stopBlockedAIMessages, showBlockedMessages } from './chat.js';
|
||||||
import { refreshFavoritesList, showLorebookModal, syncCharacterBookToTavern, showAddLorebookPanel, showAddPersonaPanel } from './favorites.js';
|
import { refreshFavoritesList, showLorebookModal, syncCharacterBookToTavern, showAddLorebookPanel, showAddPersonaPanel } from './favorites.js';
|
||||||
import { executeSummary, rollbackSummary, refreshSummaryChatList, selectAllSummaryChats } from './summary.js';
|
import { executeSummary, rollbackSummary, refreshSummaryChatList, selectAllSummaryChats, recoverFromTavernWorldbook } from './summary.js';
|
||||||
import { fetchModelListFromApi } from './ai.js';
|
import { fetchModelListFromApi } from './ai.js';
|
||||||
|
|
||||||
import { extractCharacterFromPNG, extractCharacterFromJSON, importCharacterToST } from './character-import.js';
|
import { extractCharacterFromPNG, extractCharacterFromJSON, importCharacterToST } from './character-import.js';
|
||||||
@@ -36,6 +36,7 @@ import { initTransferEvents } from './transfer.js';
|
|||||||
import { initGroupRedPacket } from './group-red-packet.js';
|
import { initGroupRedPacket } from './group-red-packet.js';
|
||||||
import { initGiftEvents } from './gift.js';
|
import { initGiftEvents } from './gift.js';
|
||||||
import { initCropper } from './cropper.js';
|
import { initCropper } from './cropper.js';
|
||||||
|
import { createFloatingBall, showFloatingBall, hideFloatingBall } from './floating-ball.js';
|
||||||
|
|
||||||
// ========== 历史记录功能 ==========
|
// ========== 历史记录功能 ==========
|
||||||
let currentHistoryTab = 'listen';
|
let currentHistoryTab = 'listen';
|
||||||
@@ -125,6 +126,14 @@ function renderHistoryContent(contact, tabType) {
|
|||||||
deleteHistoryRecord('toy', index);
|
deleteHistoryRecord('toy', index);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
// 绑定标签内的叉叉按钮事件
|
||||||
|
contentEl.querySelectorAll('.wechat-toy-target-close-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const index = parseInt(btn.dataset.index);
|
||||||
|
deleteHistoryRecord('toy', index);
|
||||||
|
});
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,7 +340,241 @@ function updateWalletAmountDisplay() {
|
|||||||
amountEl.textContent = amount.startsWith('¥') ? amount : `¥${amount}`;
|
amountEl.textContent = amount.startsWith('¥') ? amount : `¥${amount}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 缩小/恢复手机功能 =====
|
||||||
|
let minimizeState = {
|
||||||
|
isDragging: false,
|
||||||
|
startX: 0,
|
||||||
|
startY: 0,
|
||||||
|
initialLeft: 0,
|
||||||
|
initialTop: 0,
|
||||||
|
hasMoved: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// 悬浮窗开关
|
||||||
|
function toggleFloatingBallEnabled() {
|
||||||
|
const settings = getSettings();
|
||||||
|
const isEnabled = settings.floatingBallEnabled !== false;
|
||||||
|
|
||||||
|
if (isEnabled) {
|
||||||
|
// 关闭悬浮窗
|
||||||
|
settings.floatingBallEnabled = false;
|
||||||
|
hideFloatingBall();
|
||||||
|
updateFloatingBallMenuText(false);
|
||||||
|
} else {
|
||||||
|
// 开启悬浮窗
|
||||||
|
settings.floatingBallEnabled = true;
|
||||||
|
// 只有非缩小状态才显示
|
||||||
|
const phone = document.getElementById('wechat-phone');
|
||||||
|
if (!phone?.classList.contains('minimized')) {
|
||||||
|
showFloatingBall();
|
||||||
|
}
|
||||||
|
updateFloatingBallMenuText(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFloatingBallMenuText(enabled) {
|
||||||
|
const textEl = document.getElementById('wechat-floating-ball-text');
|
||||||
|
if (textEl) {
|
||||||
|
textEl.textContent = enabled ? '关闭悬浮窗' : '开启悬浮窗';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupPhoneMinimize() {
|
||||||
|
const phone = document.getElementById('wechat-phone');
|
||||||
|
const minimizeBtn = document.getElementById('wechat-minimize-btn');
|
||||||
|
|
||||||
|
if (!phone || !minimizeBtn) return;
|
||||||
|
|
||||||
|
// 点击右上角图标 - 缩小 (PC)
|
||||||
|
minimizeBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
minimizePhone();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移动端触摸支持
|
||||||
|
let minimizeBtnTouchMoved = false;
|
||||||
|
minimizeBtn.addEventListener('touchstart', (e) => {
|
||||||
|
minimizeBtnTouchMoved = false;
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
minimizeBtn.addEventListener('touchmove', (e) => {
|
||||||
|
minimizeBtnTouchMoved = true;
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
minimizeBtn.addEventListener('touchend', (e) => {
|
||||||
|
if (!minimizeBtnTouchMoved) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
minimizePhone();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 缩小后点击恢复 + 拖动支持
|
||||||
|
phone.addEventListener('mousedown', onMinimizedDragStart);
|
||||||
|
document.addEventListener('mousemove', onMinimizedDragMove);
|
||||||
|
document.addEventListener('mouseup', onMinimizedDragEnd);
|
||||||
|
|
||||||
|
// 触摸支持
|
||||||
|
phone.addEventListener('touchstart', onMinimizedDragStart, { passive: false });
|
||||||
|
document.addEventListener('touchmove', onMinimizedDragMove, { passive: false });
|
||||||
|
document.addEventListener('touchend', onMinimizedDragEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
function minimizePhone() {
|
||||||
|
const phone = document.getElementById('wechat-phone');
|
||||||
|
if (!phone) return;
|
||||||
|
|
||||||
|
// 获取当前位置
|
||||||
|
const rect = phone.getBoundingClientRect();
|
||||||
|
const settings = getSettings();
|
||||||
|
|
||||||
|
// 保存原始位置
|
||||||
|
if (!settings.phoneOriginalPosition) {
|
||||||
|
settings.phoneOriginalPosition = {
|
||||||
|
left: phone.style.left || rect.left + 'px',
|
||||||
|
top: phone.style.top || rect.top + 'px'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缩小后移到右下角
|
||||||
|
const scale = 0.25;
|
||||||
|
const phoneWidth = rect.width * scale;
|
||||||
|
const phoneHeight = rect.height * scale;
|
||||||
|
|
||||||
|
// 使用保存的缩小位置或默认右下角
|
||||||
|
const savedMinPos = settings.phoneMinimizedPosition;
|
||||||
|
let targetLeft, targetTop;
|
||||||
|
|
||||||
|
if (savedMinPos) {
|
||||||
|
targetLeft = savedMinPos.left;
|
||||||
|
targetTop = savedMinPos.top;
|
||||||
|
} else {
|
||||||
|
targetLeft = window.innerWidth - phoneWidth - 20;
|
||||||
|
targetTop = window.innerHeight - phoneHeight - 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
phone.style.left = targetLeft + 'px';
|
||||||
|
phone.style.top = targetTop + 'px';
|
||||||
|
phone.style.right = 'auto';
|
||||||
|
phone.style.bottom = 'auto';
|
||||||
|
|
||||||
|
phone.classList.add('minimized');
|
||||||
|
|
||||||
|
// 缩小时隐藏悬浮球
|
||||||
|
hideFloatingBall();
|
||||||
|
|
||||||
|
requestSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
function restorePhone() {
|
||||||
|
const phone = document.getElementById('wechat-phone');
|
||||||
|
if (!phone) return;
|
||||||
|
|
||||||
|
const settings = getSettings();
|
||||||
|
|
||||||
|
phone.classList.remove('minimized');
|
||||||
|
|
||||||
|
// 恢复原始位置或居中
|
||||||
|
if (settings.phoneOriginalPosition) {
|
||||||
|
phone.style.left = settings.phoneOriginalPosition.left;
|
||||||
|
phone.style.top = settings.phoneOriginalPosition.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复时根据设置显示悬浮球
|
||||||
|
if (settings.floatingBallEnabled !== false) {
|
||||||
|
showFloatingBall();
|
||||||
|
}
|
||||||
|
|
||||||
|
requestSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMinimizedDragStart(e) {
|
||||||
|
const phone = document.getElementById('wechat-phone');
|
||||||
|
if (!phone || !phone.classList.contains('minimized')) return;
|
||||||
|
|
||||||
|
minimizeState.isDragging = true;
|
||||||
|
minimizeState.hasMoved = false;
|
||||||
|
|
||||||
|
const rect = phone.getBoundingClientRect();
|
||||||
|
// 缩小状态下需要考虑缩放后的实际位置
|
||||||
|
minimizeState.initialLeft = parseFloat(phone.style.left) || rect.left;
|
||||||
|
minimizeState.initialTop = parseFloat(phone.style.top) || rect.top;
|
||||||
|
|
||||||
|
if (e.type === 'touchstart') {
|
||||||
|
minimizeState.startX = e.touches[0].clientX;
|
||||||
|
minimizeState.startY = e.touches[0].clientY;
|
||||||
|
e.preventDefault();
|
||||||
|
} else {
|
||||||
|
minimizeState.startX = e.clientX;
|
||||||
|
minimizeState.startY = e.clientY;
|
||||||
|
}
|
||||||
|
|
||||||
|
phone.style.transition = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMinimizedDragMove(e) {
|
||||||
|
if (!minimizeState.isDragging) return;
|
||||||
|
|
||||||
|
const phone = document.getElementById('wechat-phone');
|
||||||
|
if (!phone || !phone.classList.contains('minimized')) return;
|
||||||
|
|
||||||
|
let clientX, clientY;
|
||||||
|
if (e.type === 'touchmove') {
|
||||||
|
clientX = e.touches[0].clientX;
|
||||||
|
clientY = e.touches[0].clientY;
|
||||||
|
e.preventDefault();
|
||||||
|
} else {
|
||||||
|
clientX = e.clientX;
|
||||||
|
clientY = e.clientY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltaX = clientX - minimizeState.startX;
|
||||||
|
const deltaY = clientY - minimizeState.startY;
|
||||||
|
|
||||||
|
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
|
||||||
|
minimizeState.hasMoved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLeft = minimizeState.initialLeft + deltaX;
|
||||||
|
const newTop = minimizeState.initialTop + deltaY;
|
||||||
|
|
||||||
|
phone.style.left = newLeft + 'px';
|
||||||
|
phone.style.top = newTop + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMinimizedDragEnd(e) {
|
||||||
|
if (!minimizeState.isDragging) return;
|
||||||
|
|
||||||
|
const phone = document.getElementById('wechat-phone');
|
||||||
|
minimizeState.isDragging = false;
|
||||||
|
|
||||||
|
if (phone) {
|
||||||
|
phone.style.transition = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!minimizeState.hasMoved) {
|
||||||
|
// 没有移动,视为点击 - 恢复
|
||||||
|
restorePhone();
|
||||||
|
} else {
|
||||||
|
// 移动了,保存位置
|
||||||
|
if (phone && phone.classList.contains('minimized')) {
|
||||||
|
const settings = getSettings();
|
||||||
|
settings.phoneMinimizedPosition = {
|
||||||
|
left: parseFloat(phone.style.left),
|
||||||
|
top: parseFloat(phone.style.top)
|
||||||
|
};
|
||||||
|
requestSave();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function bindEvents() {
|
function bindEvents() {
|
||||||
|
// ===== 缩小/恢复手机功能 =====
|
||||||
|
setupPhoneMinimize();
|
||||||
|
|
||||||
// 添加按钮 - 显示下拉菜单
|
// 添加按钮 - 显示下拉菜单
|
||||||
document.getElementById('wechat-add-btn')?.addEventListener('click', (e) => {
|
document.getElementById('wechat-add-btn')?.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -369,6 +612,12 @@ function bindEvents() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 下拉菜单 - 悬浮窗开关
|
||||||
|
document.getElementById('wechat-menu-floating-ball')?.addEventListener('click', () => {
|
||||||
|
document.getElementById('wechat-dropdown-menu')?.classList.add('hidden');
|
||||||
|
toggleFloatingBallEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
// ===== 群聊创建弹窗事件 =====
|
// ===== 群聊创建弹窗事件 =====
|
||||||
document.getElementById('wechat-group-create-close')?.addEventListener('click', closeGroupCreateModal);
|
document.getElementById('wechat-group-create-close')?.addEventListener('click', closeGroupCreateModal);
|
||||||
document.getElementById('wechat-group-create-confirm')?.addEventListener('click', createGroupChat);
|
document.getElementById('wechat-group-create-confirm')?.addEventListener('click', createGroupChat);
|
||||||
@@ -1330,6 +1579,9 @@ function bindEvents() {
|
|||||||
rollbackSummary();
|
rollbackSummary();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 暴露恢复函数到全局,可在控制台调用: window.keleRecoverSummary()
|
||||||
|
window.keleRecoverSummary = recoverFromTavernWorldbook;
|
||||||
|
|
||||||
document.getElementById('wechat-summary-close')?.addEventListener('click', () => {
|
document.getElementById('wechat-summary-close')?.addEventListener('click', () => {
|
||||||
document.getElementById('wechat-summary-panel')?.classList.add('hidden');
|
document.getElementById('wechat-summary-panel')?.classList.add('hidden');
|
||||||
});
|
});
|
||||||
@@ -1933,6 +2185,14 @@ function init() {
|
|||||||
// 首次可见时居中
|
// 首次可见时居中
|
||||||
centerPhoneInViewport({ force: true });
|
centerPhoneInViewport({ force: true });
|
||||||
|
|
||||||
|
// 初始化悬浮球
|
||||||
|
createFloatingBall();
|
||||||
|
// 根据设置决定是否显示
|
||||||
|
if (settings.floatingBallEnabled === false) {
|
||||||
|
hideFloatingBall();
|
||||||
|
}
|
||||||
|
updateFloatingBallMenuText(settings.floatingBallEnabled !== false);
|
||||||
|
|
||||||
console.log('✅ 可乐不加冰 已加载');
|
console.log('✅ 可乐不加冰 已加载');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function generatePhoneHTML() {
|
|||||||
<!-- 状态栏 -->
|
<!-- 状态栏 -->
|
||||||
<div class="wechat-statusbar">
|
<div class="wechat-statusbar">
|
||||||
<span class="wechat-statusbar-time">${getCurrentTime()}</span>
|
<span class="wechat-statusbar-time">${getCurrentTime()}</span>
|
||||||
<div class="wechat-statusbar-icons">
|
<div class="wechat-statusbar-icons" id="wechat-minimize-btn" title="缩小窗口">
|
||||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z" fill="currentColor"/></svg>
|
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z" fill="currentColor"/></svg>
|
||||||
<svg viewBox="0 0 24 24" width="22" height="22"><rect x="2" y="6" width="18" height="12" rx="2" ry="2" stroke="currentColor" stroke-width="1.5" fill="none"/><rect x="20" y="10" width="2" height="4" fill="currentColor"/><rect x="4" y="8" width="12" height="8" rx="1" fill="currentColor"/></svg>
|
<svg viewBox="0 0 24 24" width="22" height="22"><rect x="2" y="6" width="18" height="12" rx="2" ry="2" stroke="currentColor" stroke-width="1.5" fill="none"/><rect x="20" y="10" width="2" height="4" fill="currentColor"/><rect x="4" y="8" width="12" height="8" rx="1" fill="currentColor"/></svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,6 +114,10 @@ export function generatePhoneHTML() {
|
|||||||
<svg viewBox="0 0 24 24"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/></svg>
|
<svg viewBox="0 0 24 24"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/></svg>
|
||||||
<span>收付款</span>
|
<span>收付款</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="wechat-dropdown-item" id="wechat-menu-floating-ball">
|
||||||
|
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="12" cy="10" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M7 16.5c0-2 2.2-3.5 5-3.5s5 1.5 5 3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/></svg>
|
||||||
|
<span id="wechat-floating-ball-text">悬浮窗</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 添加朋友页面 -->
|
<!-- 添加朋友页面 -->
|
||||||
|
|||||||
215
style.css
215
style.css
@@ -4391,9 +4391,18 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 确保照片气泡不被其他样式折叠 */
|
/* 确保照片气泡不被其他样式折叠,强制垂直排列 */
|
||||||
.wechat-message-content .wechat-photo-bubble {
|
.wechat-message-content .wechat-photo-bubble {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 180px !important;
|
||||||
|
float: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保消息内容区域的子元素垂直排列 */
|
||||||
|
.wechat-message-content {
|
||||||
|
flex-wrap: nowrap !important;
|
||||||
|
flex-direction: column !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wechat-photo-blur {
|
.wechat-photo-blur {
|
||||||
@@ -4737,7 +4746,7 @@
|
|||||||
|
|
||||||
/* 通话中对话框 */
|
/* 通话中对话框 */
|
||||||
.wechat-voice-call-chat {
|
.wechat-voice-call-chat {
|
||||||
margin: 0 16px;
|
margin: 0 16px 15px 16px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -4836,6 +4845,7 @@
|
|||||||
background: rgba(50, 50, 50, 0.8);
|
background: rgba(50, 50, 50, 0.8);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: 4px 4px 4px 16px;
|
padding: 4px 4px 4px 16px;
|
||||||
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wechat-voice-call-input {
|
.wechat-voice-call-input {
|
||||||
@@ -6888,7 +6898,7 @@
|
|||||||
|
|
||||||
.wechat-moment-images.grid-2 {
|
.wechat-moment-images.grid-2 {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
max-width: 280px;
|
max-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wechat-moment-images.grid-3,
|
.wechat-moment-images.grid-3,
|
||||||
@@ -6974,6 +6984,19 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 当朋友圈图片区域只包含照片描述卡片时,使用垂直布局 */
|
||||||
|
.wechat-moment-images:has(.wechat-moment-photo-card) {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
gap: 8px;
|
||||||
|
max-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-moment-images:has(.wechat-moment-photo-card) .wechat-moment-photo-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
.wechat-moment-photo-card .wechat-photo-blur {
|
.wechat-moment-photo-card .wechat-photo-blur {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
@@ -11284,6 +11307,7 @@
|
|||||||
/* 聊天区域 */
|
/* 聊天区域 */
|
||||||
.wechat-toy-control-chat {
|
.wechat-toy-control-chat {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: rgba(255, 255, 255, 0.6);
|
background: rgba(255, 255, 255, 0.6);
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
@@ -11291,6 +11315,25 @@
|
|||||||
box-shadow: inset 0 2px 8px rgba(255, 107, 138, 0.1);
|
box-shadow: inset 0 2px 8px rgba(255, 107, 138, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 聊天区域滚动条样式 */
|
||||||
|
.wechat-toy-control-chat::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-toy-control-chat::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 107, 138, 0.1);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-toy-control-chat::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 107, 138, 0.4);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-toy-control-chat::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 107, 138, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
.wechat-toy-control-messages {
|
.wechat-toy-control-messages {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -11416,6 +11459,18 @@
|
|||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wechat-dark .wechat-toy-control-chat::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 107, 138, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-dark .wechat-toy-control-chat::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 107, 138, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-dark .wechat-toy-control-chat::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 107, 138, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
.wechat-dark .wechat-toy-control-msg.ai {
|
.wechat-dark .wechat-toy-control-msg.ai {
|
||||||
background: #2a2a2a;
|
background: #2a2a2a;
|
||||||
color: #e9e9e9;
|
color: #e9e9e9;
|
||||||
@@ -11500,6 +11555,25 @@
|
|||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-toy-target-close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
color: inherit;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-toy-target-close-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wechat-toy-history-card-meta {
|
.wechat-toy-history-card-meta {
|
||||||
@@ -11514,11 +11588,30 @@
|
|||||||
|
|
||||||
.wechat-toy-history-card-messages {
|
.wechat-toy-history-card-messages {
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
max-height: 200px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: rgba(255, 255, 255, 0.5);
|
background: rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 心动瞬间消息滚动条样式 */
|
||||||
|
.wechat-toy-history-scrollable::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-toy-history-scrollable::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 107, 138, 0.1);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-toy-history-scrollable::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 107, 138, 0.4);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-toy-history-scrollable::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 107, 138, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
.wechat-toy-history-msg {
|
.wechat-toy-history-msg {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -11567,3 +11660,117 @@
|
|||||||
.wechat-dark .wechat-toy-history-msg-content {
|
.wechat-dark .wechat-toy-history-msg-content {
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 悬浮球样式 ===== */
|
||||||
|
.wechat-floating-ball {
|
||||||
|
position: fixed;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
z-index: 99999;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
touch-action: none;
|
||||||
|
transition: transform 0.15s ease, opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-floating-ball:hover {
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-floating-ball:active,
|
||||||
|
.wechat-floating-ball.dragging {
|
||||||
|
transform: scale(0.95);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-floating-ball .floating-ball-svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
filter: drop-shadow(0 2px 8px rgba(255, 182, 193, 0.4));
|
||||||
|
transition: filter 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-floating-ball:hover .floating-ball-svg {
|
||||||
|
filter: drop-shadow(0 4px 12px rgba(255, 182, 193, 0.6));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 悬浮球呼吸动画(可选,可删除) */
|
||||||
|
@keyframes floating-ball-breathe {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-floating-ball:not(:hover):not(.dragging) {
|
||||||
|
animation: floating-ball-breathe 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 缩小手机样式 ===== */
|
||||||
|
#wechat-minimize-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin: -4px -8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#wechat-minimize-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-dark #wechat-minimize-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 缩小状态 */
|
||||||
|
.wechat-phone.minimized {
|
||||||
|
transform: scale(0.25) !important;
|
||||||
|
transform-origin: center center;
|
||||||
|
position: fixed !important;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
box-shadow 0.3s ease,
|
||||||
|
top 0.3s ease,
|
||||||
|
left 0.3s ease,
|
||||||
|
right 0.3s ease;
|
||||||
|
z-index: 99998;
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-phone.minimized:hover {
|
||||||
|
transform: scale(0.27) !important;
|
||||||
|
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 缩小时禁用内部交互,但允许事件冒泡到父元素 */
|
||||||
|
.wechat-phone.minimized::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 99999;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-phone.minimized * {
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 缩小时的过渡动画 */
|
||||||
|
.wechat-phone {
|
||||||
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|||||||
103
summary.js
103
summary.js
@@ -852,3 +852,106 @@ export async function rollbackSummary() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从酒馆世界书恢复总结数据
|
||||||
|
* 当插件的 selectedLorebooks 条目丢失但酒馆世界书还在时使用
|
||||||
|
*/
|
||||||
|
export async function recoverFromTavernWorldbook() {
|
||||||
|
const settings = getSettings();
|
||||||
|
const selectedLorebooks = settings.selectedLorebooks || [];
|
||||||
|
|
||||||
|
// 找到所有总结生成的世界书(条目为空的)
|
||||||
|
const emptyBooks = selectedLorebooks.filter(lb =>
|
||||||
|
(lb.fromSummary === true || (lb.name && lb.name.startsWith(LOREBOOK_NAME_PREFIX))) &&
|
||||||
|
(!lb.entries || lb.entries.length === 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (emptyBooks.length === 0) {
|
||||||
|
alert('没有需要恢复的世界书(所有世界书都有条目,或没有总结类世界书)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = emptyBooks.map((lb, idx) => `${idx + 1}. ${lb.name}`).join('\n');
|
||||||
|
const choice = prompt(`以下世界书条目为空,可尝试从酒馆恢复:\n\n${options}\n\n输入序号(或输入 all 恢复全部):`);
|
||||||
|
|
||||||
|
if (!choice) return;
|
||||||
|
|
||||||
|
const booksToRecover = choice.toLowerCase() === 'all'
|
||||||
|
? emptyBooks
|
||||||
|
: [emptyBooks[parseInt(choice) - 1]].filter(Boolean);
|
||||||
|
|
||||||
|
if (booksToRecover.length === 0) {
|
||||||
|
alert('无效的选择');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let recoveredCount = 0;
|
||||||
|
let totalEntries = 0;
|
||||||
|
|
||||||
|
for (const book of booksToRecover) {
|
||||||
|
try {
|
||||||
|
const name = book.name;
|
||||||
|
|
||||||
|
// 检查酒馆世界书是否存在
|
||||||
|
const worldExists = typeof world_names !== 'undefined' &&
|
||||||
|
Array.isArray(world_names) &&
|
||||||
|
world_names.includes(name);
|
||||||
|
|
||||||
|
if (!worldExists) {
|
||||||
|
console.log(`[可乐] 酒馆中不存在世界书: ${name}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载酒馆世界书
|
||||||
|
if (typeof loadWorldInfo !== 'function') {
|
||||||
|
console.error('[可乐] loadWorldInfo 函数不可用');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const worldInfo = await loadWorldInfo(name);
|
||||||
|
if (!worldInfo?.entries || Object.keys(worldInfo.entries).length === 0) {
|
||||||
|
console.log(`[可乐] 酒馆世界书 ${name} 没有条目`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将酒馆条目转换为插件格式
|
||||||
|
const entries = [];
|
||||||
|
const sortedKeys = Object.keys(worldInfo.entries).sort((a, b) => parseInt(a) - parseInt(b));
|
||||||
|
|
||||||
|
for (const key of sortedKeys) {
|
||||||
|
const tavernEntry = worldInfo.entries[key];
|
||||||
|
if (!tavernEntry) continue;
|
||||||
|
|
||||||
|
entries.push({
|
||||||
|
content: tavernEntry.content || '',
|
||||||
|
comment: tavernEntry.comment || getCupName(entries.length + 1),
|
||||||
|
keys: tavernEntry.key || [],
|
||||||
|
enabled: !tavernEntry.disable,
|
||||||
|
addedTime: new Date().toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries.length > 0) {
|
||||||
|
// 更新插件的 selectedLorebooks
|
||||||
|
const bookIndex = selectedLorebooks.findIndex(lb => lb.name === name);
|
||||||
|
if (bookIndex >= 0) {
|
||||||
|
selectedLorebooks[bookIndex].entries = entries;
|
||||||
|
selectedLorebooks[bookIndex].lastUpdated = new Date().toISOString();
|
||||||
|
recoveredCount++;
|
||||||
|
totalEntries += entries.length;
|
||||||
|
console.log(`[可乐] 已恢复 ${name}: ${entries.length} 条`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[可乐] 恢复 ${book.name} 失败:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recoveredCount > 0) {
|
||||||
|
requestSave();
|
||||||
|
alert(`恢复完成!\n\n已恢复 ${recoveredCount} 个世界书,共 ${totalEntries} 条总结。\n\n请刷新页面查看。`);
|
||||||
|
} else {
|
||||||
|
alert('恢复失败:酒馆中没有找到对应的世界书数据。\n\n数据可能已彻底丢失。');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ async function onButtonPress(buttonId, pressedBy = 'user') {
|
|||||||
hideToyTypingIndicator();
|
hideToyTypingIndicator();
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
processAIResponse(response);
|
await processAIResponse(response);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
hideToyTypingIndicator();
|
hideToyTypingIndicator();
|
||||||
@@ -290,7 +290,7 @@ function updateButtonState(buttonId) {
|
|||||||
// 构建按钮按下提示词
|
// 构建按钮按下提示词
|
||||||
function buildButtonPressPrompt(buttonId, buttonName, pressedBy) {
|
function buildButtonPressPrompt(buttonId, buttonName, pressedBy) {
|
||||||
const isCharacterUsing = toyControlState.target === 'character';
|
const isCharacterUsing = toyControlState.target === 'character';
|
||||||
const presserText = pressedBy === 'user' ? '用户' : '你自己';
|
const isAIPress = pressedBy === 'ai';
|
||||||
|
|
||||||
const modeEffects = {
|
const modeEffects = {
|
||||||
classic: '稳定持续的震动开始了',
|
classic: '稳定持续的震动开始了',
|
||||||
@@ -301,18 +301,40 @@ function buildButtonPressPrompt(buttonId, buttonName, pressedBy) {
|
|||||||
shock: '一阵微电流刺激瞬间传来,让人猛地一颤'
|
shock: '一阵微电流刺激瞬间传来,让人猛地一颤'
|
||||||
};
|
};
|
||||||
|
|
||||||
let prompt = `[${presserText}按下了"${buttonName}"按钮]
|
let prompt;
|
||||||
|
|
||||||
|
if (isAIPress) {
|
||||||
|
// AI主动按按钮的情况
|
||||||
|
prompt = `[你主动按下了"${buttonName}"按钮]
|
||||||
|
|
||||||
效果:${modeEffects[buttonId]}
|
效果:${modeEffects[buttonId]}
|
||||||
|
|
||||||
`;
|
`;
|
||||||
|
if (isCharacterUsing) {
|
||||||
if (isCharacterUsing) {
|
prompt += `你主动切换了${toyControlState.gift.giftName}的模式,请描述你主动调整后的反应:
|
||||||
prompt += `你正在使用${toyControlState.gift.giftName},请根据这个刺激变化做出反应。
|
- 为什么要主动切换这个模式(想要更多刺激/受不了想暂停/想换个感觉等)
|
||||||
描述你的身体感受、情绪变化。回复要有情感细节,符合你的角色性格。`;
|
- 切换后的身体感受和情绪变化
|
||||||
|
- 回复要有情感细节,符合你的角色性格`;
|
||||||
|
} else {
|
||||||
|
prompt += `你主动控制了用户正在使用的${toyControlState.gift.giftName},请描述你主动操作后的感受:
|
||||||
|
- 为什么要主动给用户切换这个模式(想折磨对方/想看对方的反应/调侃等)
|
||||||
|
- 可以调侃、挑逗用户
|
||||||
|
- 回复要有趣,符合你的角色性格`;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
prompt += `用户正在使用${toyControlState.gift.giftName},你在观察。
|
// 用户按按钮的情况
|
||||||
|
prompt = `[用户按下了"${buttonName}"按钮]
|
||||||
|
|
||||||
|
效果:${modeEffects[buttonId]}
|
||||||
|
|
||||||
|
`;
|
||||||
|
if (isCharacterUsing) {
|
||||||
|
prompt += `你正在使用${toyControlState.gift.giftName},请根据这个刺激变化做出反应。
|
||||||
|
描述你的身体感受、情绪变化。回复要有情感细节,符合你的角色性格。`;
|
||||||
|
} else {
|
||||||
|
prompt += `用户正在使用${toyControlState.gift.giftName},你在观察。
|
||||||
请描述你观察到的用户可能的反应,可以调侃、鼓励或挑逗。回复要有趣,符合你的角色性格。`;
|
请描述你观察到的用户可能的反应,可以调侃、鼓励或挑逗。回复要有趣,符合你的角色性格。`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt += `
|
prompt += `
|
||||||
@@ -351,7 +373,7 @@ async function sendToyMessage() {
|
|||||||
hideToyTypingIndicator();
|
hideToyTypingIndicator();
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
processAIResponse(response);
|
await processAIResponse(response);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
hideToyTypingIndicator();
|
hideToyTypingIndicator();
|
||||||
@@ -416,15 +438,20 @@ async function callToyAI(prompt) {
|
|||||||
return await callAI(toyControlState.contact, prompt, historyMessages);
|
return await callAI(toyControlState.contact, prompt, historyMessages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 辅助函数:延迟
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
// 处理AI回复(检测是否有按按钮指令)
|
// 处理AI回复(检测是否有按按钮指令)
|
||||||
function processAIResponse(response) {
|
async function processAIResponse(response) {
|
||||||
if (!response) return;
|
if (!response) return;
|
||||||
|
|
||||||
// 分割多条消息
|
// 分割多条消息
|
||||||
const parts = splitAIMessages(response);
|
const parts = splitAIMessages(response);
|
||||||
|
|
||||||
for (const part of parts) {
|
for (let i = 0; i < parts.length; i++) {
|
||||||
let reply = part.trim();
|
let reply = parts[i].trim();
|
||||||
|
|
||||||
// 过滤特殊标签
|
// 过滤特殊标签
|
||||||
reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim();
|
reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim();
|
||||||
@@ -441,6 +468,13 @@ function processAIResponse(response) {
|
|||||||
reply = reply.replace(/([^)]*)/g, '').trim();
|
reply = reply.replace(/([^)]*)/g, '').trim();
|
||||||
reply = reply.replace(/\([^)]*\)/g, '').trim();
|
reply = reply.replace(/\([^)]*\)/g, '').trim();
|
||||||
|
|
||||||
|
// 如果不是第一条消息,显示typing并延迟
|
||||||
|
if (i > 0 && reply) {
|
||||||
|
showToyTypingIndicator();
|
||||||
|
await sleep(1500 + Math.random() * 1000); // 1.5-2.5秒延迟
|
||||||
|
hideToyTypingIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
// 添加AI消息
|
// 添加AI消息
|
||||||
if (reply) {
|
if (reply) {
|
||||||
addToyMessage('ai', reply);
|
addToyMessage('ai', reply);
|
||||||
@@ -459,6 +493,13 @@ function processAIResponse(response) {
|
|||||||
reply = reply.replace(/([^)]*)/g, '').trim();
|
reply = reply.replace(/([^)]*)/g, '').trim();
|
||||||
reply = reply.replace(/\([^)]*\)/g, '').trim();
|
reply = reply.replace(/\([^)]*\)/g, '').trim();
|
||||||
|
|
||||||
|
// 如果不是第一条消息,显示typing并延迟
|
||||||
|
if (i > 0 && reply) {
|
||||||
|
showToyTypingIndicator();
|
||||||
|
await sleep(1500 + Math.random() * 1000); // 1.5-2.5秒延迟
|
||||||
|
hideToyTypingIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
if (reply) {
|
if (reply) {
|
||||||
addToyMessage('ai', reply);
|
addToyMessage('ai', reply);
|
||||||
}
|
}
|
||||||
@@ -527,7 +568,7 @@ async function triggerToyAIGreeting() {
|
|||||||
hideToyTypingIndicator();
|
hideToyTypingIndicator();
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
processAIResponse(response);
|
await processAIResponse(response);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
hideToyTypingIndicator();
|
hideToyTypingIndicator();
|
||||||
|
|||||||
@@ -351,6 +351,26 @@ export function checkVideoAIHangupAfterReply() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检测AI是否有挂断意图
|
||||||
|
function detectVideoHangupIntent(text) {
|
||||||
|
if (!text) return false;
|
||||||
|
const hangupPatterns = [
|
||||||
|
/我(先)?挂了/,
|
||||||
|
/那我挂了/,
|
||||||
|
/先挂(了)?啊?/,
|
||||||
|
/挂了(啊|哈|呀|哦)?$/,
|
||||||
|
/我(要)?挂(电话|断)了/,
|
||||||
|
/拜拜.*挂/,
|
||||||
|
/挂.*拜拜/,
|
||||||
|
/再见.*挂/,
|
||||||
|
/不聊了.*挂/,
|
||||||
|
/不说了.*挂/,
|
||||||
|
/那就这样.*挂/,
|
||||||
|
/就这样吧.*挂/
|
||||||
|
];
|
||||||
|
return hangupPatterns.some(pattern => pattern.test(text));
|
||||||
|
}
|
||||||
|
|
||||||
// AI主动挂断视频电话
|
// AI主动挂断视频电话
|
||||||
function videoAIHangup() {
|
function videoAIHangup() {
|
||||||
if (!videoCallState.isConnected) return;
|
if (!videoCallState.isConnected) return;
|
||||||
@@ -505,7 +525,7 @@ function appendVideoCallRecordMessage(role, status, duration, contact) {
|
|||||||
: firstChar);
|
: firstChar);
|
||||||
|
|
||||||
// 摄像机图标
|
// 摄像机图标
|
||||||
const cameraIconSVG = `<svg class="wechat-call-record-icon wechat-video-call-icon" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
const cameraIconSVG = `<svg class="wechat-call-record-icon wechat-video-call-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<rect x="2" y="6" width="13" height="12" rx="2"/>
|
<rect x="2" y="6" width="13" height="12" rx="2"/>
|
||||||
<path d="M22 8l-7 4 7 4V8z"/>
|
<path d="M22 8l-7 4 7 4V8z"/>
|
||||||
</svg>`;
|
</svg>`;
|
||||||
@@ -1053,7 +1073,19 @@ async function sendVideoCallMessage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AI回复完成后,检查是否要主动挂断(5%概率,通话30秒后生效)
|
// AI回复完成后,检查是否要主动挂断
|
||||||
|
// 1. 检测AI的挂断意图(如"我挂了"、"先挂了"等)
|
||||||
|
const fullReply = parts.join(' ');
|
||||||
|
if (detectVideoHangupIntent(fullReply)) {
|
||||||
|
console.log('[可乐] 检测到视频通话AI挂断意图:', fullReply);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (videoCallState.isConnected) {
|
||||||
|
videoAIHangup();
|
||||||
|
}
|
||||||
|
}, 1500 + Math.random() * 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 2. 随机5%概率挂断(通话30秒后生效)
|
||||||
checkVideoAIHangupAfterReply();
|
checkVideoAIHangupAfterReply();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
hideVideoCallTypingIndicator();
|
hideVideoCallTypingIndicator();
|
||||||
|
|||||||
@@ -310,6 +310,27 @@ export function checkAIHangupAfterReply() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检测AI是否有挂断意图
|
||||||
|
function detectHangupIntent(text) {
|
||||||
|
if (!text) return false;
|
||||||
|
// 常见的挂断表达
|
||||||
|
const hangupPatterns = [
|
||||||
|
/我(先)?挂了/,
|
||||||
|
/那我挂了/,
|
||||||
|
/先挂(了)?啊?/,
|
||||||
|
/挂了(啊|哈|呀|哦)?$/,
|
||||||
|
/我(要)?挂(电话|断)了/,
|
||||||
|
/拜拜.*挂/,
|
||||||
|
/挂.*拜拜/,
|
||||||
|
/再见.*挂/,
|
||||||
|
/不聊了.*挂/,
|
||||||
|
/不说了.*挂/,
|
||||||
|
/那就这样.*挂/,
|
||||||
|
/就这样吧.*挂/
|
||||||
|
];
|
||||||
|
return hangupPatterns.some(pattern => pattern.test(text));
|
||||||
|
}
|
||||||
|
|
||||||
// AI主动挂断电话
|
// AI主动挂断电话
|
||||||
function aiHangup() {
|
function aiHangup() {
|
||||||
if (!callState.isConnected) return;
|
if (!callState.isConnected) return;
|
||||||
@@ -489,7 +510,7 @@ function appendCallRecordMessage(role, status, duration, contact) {
|
|||||||
|
|
||||||
// 通话记录卡片内容
|
// 通话记录卡片内容
|
||||||
// 线条电话图标
|
// 线条电话图标
|
||||||
const phoneIconSVG = `<svg class="wechat-call-record-icon" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
const phoneIconSVG = `<svg class="wechat-call-record-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
||||||
</svg>`;
|
</svg>`;
|
||||||
|
|
||||||
@@ -956,7 +977,19 @@ async function sendCallMessage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AI回复完成后,检查是否要主动挂断(5%概率,通话30秒后生效)
|
// AI回复完成后,检查是否要主动挂断
|
||||||
|
// 1. 检测AI的挂断意图(如"我挂了"、"先挂了"等)
|
||||||
|
const fullReply = parts.join(' ');
|
||||||
|
if (detectHangupIntent(fullReply)) {
|
||||||
|
console.log('[可乐] 检测到AI挂断意图:', fullReply);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (callState.isConnected) {
|
||||||
|
aiHangup();
|
||||||
|
}
|
||||||
|
}, 1500 + Math.random() * 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 2. 随机5%概率挂断(通话30秒后生效)
|
||||||
checkAIHangupAfterReply();
|
checkAIHangupAfterReply();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
hideCallTypingIndicator();
|
hideCallTypingIndicator();
|
||||||
|
|||||||
Reference in New Issue
Block a user