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:
2
ai.js
2
ai.js
@@ -566,7 +566,7 @@ export function buildSystemPrompt(contact, options = {}) {
|
||||
- 回复要符合角色性格
|
||||
- 不要使用任何格式标记,直接输出对话内容
|
||||
- 【禁止】不要使用小括号描述动作/表情/语气!如"(笑)"、"(害羞地说)"等,这是文字聊天不是小说!
|
||||
- 如果想发送语音消息,使用格式:[语音:语音内容]
|
||||
- 如果想发送语音消息,使用格式:[语音:实际说的话](注意:是你说的具体话语,不是声音描述!错误示例:[语音:声音低沉带笑] 正确示例:[语音:宝贝早上好呀])
|
||||
- 如果想发送照片,使用格式:[照片:照片描述]
|
||||
- 【绝对禁止】语音/照片消息必须单独一条!格式前后不能有任何其他文字!
|
||||
错误示例:叫得这么甜 [照片:xxx] ← 错误!
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/**
|
||||
* 聊天背景功能模块
|
||||
* 支持每个联系人独立设置背景,含图片裁剪功能
|
||||
* 支持每个联系人/群聊独立设置背景,含图片裁剪功能
|
||||
*/
|
||||
|
||||
import { requestSave } from './save-manager.js';
|
||||
import { getSettings } from './config.js';
|
||||
import { showToast } from './toast.js';
|
||||
import { currentChatIndex } from './chat.js';
|
||||
import { isInGroupChat, getCurrentGroupIndex } from './group-chat.js';
|
||||
|
||||
// 裁剪器状态
|
||||
let cropperState = {
|
||||
@@ -76,12 +77,19 @@ export function showChatBgPanel() {
|
||||
|
||||
if (!panel || !preview) return;
|
||||
|
||||
// 获取当前联系人的背景
|
||||
// 获取当前聊天对象的背景(支持群聊和私聊)
|
||||
const settings = getSettings();
|
||||
const contact = settings.contacts[currentChatIndex];
|
||||
let chatTarget = null;
|
||||
|
||||
if (contact?.chatBackground) {
|
||||
preview.innerHTML = `<img src="${contact.chatBackground}" alt="背景预览">`;
|
||||
if (isInGroupChat()) {
|
||||
const groupIndex = getCurrentGroupIndex();
|
||||
chatTarget = settings.groupChats?.[groupIndex];
|
||||
} else {
|
||||
chatTarget = settings.contacts[currentChatIndex];
|
||||
}
|
||||
|
||||
if (chatTarget?.chatBackground) {
|
||||
preview.innerHTML = `<img src="${chatTarget.chatBackground}" alt="背景预览">`;
|
||||
} else {
|
||||
preview.innerHTML = '<span class="wechat-chat-bg-placeholder">暂无背景</span>';
|
||||
}
|
||||
@@ -367,14 +375,21 @@ function confirmCrop() {
|
||||
// 保存聊天背景
|
||||
function saveChatBackground(imageData) {
|
||||
const settings = getSettings();
|
||||
const contact = settings.contacts[currentChatIndex];
|
||||
let chatTarget = null;
|
||||
|
||||
if (!contact) {
|
||||
if (isInGroupChat()) {
|
||||
const groupIndex = getCurrentGroupIndex();
|
||||
chatTarget = settings.groupChats?.[groupIndex];
|
||||
} else {
|
||||
chatTarget = settings.contacts[currentChatIndex];
|
||||
}
|
||||
|
||||
if (!chatTarget) {
|
||||
showToast('保存失败', '⚠️');
|
||||
return;
|
||||
}
|
||||
|
||||
contact.chatBackground = imageData;
|
||||
chatTarget.chatBackground = imageData;
|
||||
requestSave();
|
||||
|
||||
// 立即应用背景
|
||||
@@ -385,11 +400,18 @@ function saveChatBackground(imageData) {
|
||||
// 清除聊天背景
|
||||
function clearChatBackground() {
|
||||
const settings = getSettings();
|
||||
const contact = settings.contacts[currentChatIndex];
|
||||
let chatTarget = null;
|
||||
|
||||
if (!contact) return;
|
||||
if (isInGroupChat()) {
|
||||
const groupIndex = getCurrentGroupIndex();
|
||||
chatTarget = settings.groupChats?.[groupIndex];
|
||||
} else {
|
||||
chatTarget = settings.contacts[currentChatIndex];
|
||||
}
|
||||
|
||||
delete contact.chatBackground;
|
||||
if (!chatTarget) return;
|
||||
|
||||
delete chatTarget.chatBackground;
|
||||
requestSave();
|
||||
|
||||
// 清除背景
|
||||
@@ -428,3 +450,15 @@ export function loadContactBackground(contactIndex) {
|
||||
applyChatBackground(null);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载群聊背景(openGroupChat时调用)
|
||||
export function loadGroupBackground(groupIndex) {
|
||||
const settings = getSettings();
|
||||
const groupChat = settings.groupChats?.[groupIndex];
|
||||
|
||||
if (groupChat?.chatBackground) {
|
||||
applyChatBackground(groupChat.chatBackground);
|
||||
} else {
|
||||
applyChatBackground(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@ export const defaultSettings = {
|
||||
|
||||
【回复要求】
|
||||
- 根据聊天内容自然判断哪些角色会回复,不需要所有人都说话
|
||||
- 语音消息格式:[角色名]: [语音:内容]
|
||||
- 语音消息格式:[角色名]: [语音:实际说的话](是具体话语,不是声音描述)
|
||||
- 语音消息必须独立发送
|
||||
|
||||
示例:
|
||||
@@ -302,7 +302,7 @@ export const authorNoteTemplate = `【可乐不加冰 消息格式指南】每
|
||||
|
||||
【消息类型格式】
|
||||
- 普通消息:直接写内容
|
||||
- 语音消息:[语音:语音内容文字]
|
||||
- 语音消息:[语音:实际说的话](是具体话语,不是声音描述!)
|
||||
- 照片/图片/视频/自拍:[照片:媒体描述]
|
||||
- 表情包回复:[表情:序号或名称]
|
||||
- 音乐分享:[音乐:歌名]
|
||||
|
||||
104
emoji-panel.js
104
emoji-panel.js
@@ -171,12 +171,30 @@ export function refreshEmojiGrid() {
|
||||
const content = document.getElementById('wechat-emoji-content');
|
||||
if (!content) return;
|
||||
|
||||
const settings = getSettings();
|
||||
const userStickers = Array.isArray(settings.stickers) ? settings.stickers : [];
|
||||
|
||||
let html = '';
|
||||
|
||||
// 我的表情区域(用户添加的表情)
|
||||
html += '<div class="wechat-emoji-section-title">我的表情</div>';
|
||||
html += '<div class="wechat-emoji-grid" id="wechat-emoji-user-grid">';
|
||||
html += `<button class="wechat-emoji-add" id="wechat-emoji-add-btn">+</button>`;
|
||||
userStickers.forEach((sticker, index) => {
|
||||
html += `
|
||||
<div class="wechat-emoji-swipe-container" data-user-index="${index}">
|
||||
<div class="wechat-emoji-item wechat-emoji-user-item" title="${sticker.name || '自定义表情'}">
|
||||
<img src="${sticker.url}" alt="${sticker.name || ''}" loading="lazy">
|
||||
</div>
|
||||
<div class="wechat-emoji-delete-bg">删除</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
// 默认表情区域
|
||||
html += '<div class="wechat-emoji-section-title">默认表情</div>';
|
||||
html += '<div class="wechat-emoji-grid" id="wechat-emoji-default-grid">';
|
||||
html += `<button class="wechat-emoji-add" id="wechat-emoji-add-btn">+</button>`;
|
||||
DEFAULT_STICKERS.forEach((sticker, index) => {
|
||||
const url = getCatboxUrl(sticker.id, sticker.ext);
|
||||
html += `
|
||||
@@ -192,6 +210,11 @@ export function refreshEmojiGrid() {
|
||||
// 绑定添加按钮事件
|
||||
document.getElementById('wechat-emoji-add-btn')?.addEventListener('click', showAddStickerDialog);
|
||||
|
||||
// 绑定用户表情左滑删除
|
||||
content.querySelectorAll('.wechat-emoji-swipe-container').forEach(container => {
|
||||
setupSwipeToDelete(container);
|
||||
});
|
||||
|
||||
// 绑定默认表情点击事件
|
||||
content.querySelectorAll('.wechat-emoji-default-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
@@ -201,6 +224,85 @@ export function refreshEmojiGrid() {
|
||||
});
|
||||
}
|
||||
|
||||
// 设置左滑删除
|
||||
function setupSwipeToDelete(container) {
|
||||
const item = container.querySelector('.wechat-emoji-user-item');
|
||||
const index = parseInt(container.dataset.userIndex);
|
||||
let startX = 0;
|
||||
let currentX = 0;
|
||||
let isDragging = false;
|
||||
let isOpen = false;
|
||||
|
||||
const deleteThreshold = 50; // 滑动超过此距离触发删除状态
|
||||
|
||||
function onStart(e) {
|
||||
// 如果已经打开,点击任意位置关闭
|
||||
if (isOpen) {
|
||||
resetPosition();
|
||||
return;
|
||||
}
|
||||
isDragging = true;
|
||||
startX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
|
||||
item.style.transition = 'none';
|
||||
}
|
||||
|
||||
function onMove(e) {
|
||||
if (!isDragging) return;
|
||||
currentX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
|
||||
let diff = currentX - startX;
|
||||
// 只允许左滑
|
||||
if (diff > 0) diff = 0;
|
||||
// 限制最大滑动距离
|
||||
if (diff < -deleteThreshold - 20) diff = -deleteThreshold - 20;
|
||||
item.style.transform = `translateX(${diff}px)`;
|
||||
}
|
||||
|
||||
function onEnd() {
|
||||
if (!isDragging) return;
|
||||
isDragging = false;
|
||||
item.style.transition = 'transform 0.2s';
|
||||
|
||||
const diff = currentX - startX;
|
||||
if (diff < -deleteThreshold) {
|
||||
// 显示删除按钮
|
||||
item.style.transform = `translateX(-${deleteThreshold}px)`;
|
||||
isOpen = true;
|
||||
} else if (Math.abs(diff) < 5 && !isOpen) {
|
||||
// 点击发送
|
||||
sendUserSticker(index);
|
||||
} else {
|
||||
resetPosition();
|
||||
}
|
||||
}
|
||||
|
||||
function resetPosition() {
|
||||
item.style.transition = 'transform 0.2s';
|
||||
item.style.transform = 'translateX(0)';
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
// 触摸事件
|
||||
container.addEventListener('touchstart', onStart, { passive: true });
|
||||
container.addEventListener('touchmove', onMove, { passive: true });
|
||||
container.addEventListener('touchend', onEnd);
|
||||
|
||||
// 鼠标事件
|
||||
container.addEventListener('mousedown', onStart);
|
||||
container.addEventListener('mousemove', onMove);
|
||||
container.addEventListener('mouseup', onEnd);
|
||||
container.addEventListener('mouseleave', () => {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
resetPosition();
|
||||
}
|
||||
});
|
||||
|
||||
// 删除按钮点击
|
||||
container.querySelector('.wechat-emoji-delete-bg').addEventListener('click', () => {
|
||||
deleteSticker(index);
|
||||
});
|
||||
}
|
||||
|
||||
// 显示添加表情对话框
|
||||
function showAddStickerDialog() {
|
||||
const choice = prompt(
|
||||
|
||||
@@ -18,46 +18,27 @@ let floatingBallState = {
|
||||
hasMoved: false
|
||||
};
|
||||
|
||||
// SVG 图标 - 渐变圆圈和猫咪
|
||||
// SVG 图标 - 简约猫咪(只有耳朵和胡须)
|
||||
const FLOATING_BALL_SVG = `
|
||||
<svg viewBox="0 0 100 100" width="60" height="60" class="floating-ball-svg">
|
||||
<svg viewBox="0 0 100 100" width="30" height="30" 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 id="bg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#FFFFFF;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#FFE4EC;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"/>
|
||||
<!-- 圆形背景 -->
|
||||
<circle cx="50" cy="50" r="48" fill="url(#bg-gradient)"/>
|
||||
<!-- 左耳 -->
|
||||
<path d="M18,45 L28,12 L45,38" fill="#FFB6C1" stroke="#333" stroke-width="2" stroke-linejoin="round"/>
|
||||
<!-- 右耳 -->
|
||||
<path d="M82,45 L72,12 L55,38" fill="#FFB6C1" stroke="#333" stroke-width="2" stroke-linejoin="round"/>
|
||||
<!-- 胡须 -->
|
||||
<g stroke="#333" stroke-width="2" stroke-linecap="round">
|
||||
<path d="M8,52 L35,56"/>
|
||||
<path d="M8,64 L35,62"/>
|
||||
<path d="M92,52 L65,56"/>
|
||||
<path d="M92,64 L65,62"/>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
@@ -95,14 +76,14 @@ function restorePosition(ball) {
|
||||
|
||||
if (savedPos && savedPos.x !== undefined && savedPos.y !== undefined) {
|
||||
// 确保位置在视口内
|
||||
const maxX = window.innerWidth - 60;
|
||||
const maxY = window.innerHeight - 60;
|
||||
const maxX = window.innerWidth - 30;
|
||||
const maxY = window.innerHeight - 30;
|
||||
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;
|
||||
floatingBallState.currentX = window.innerWidth - 40;
|
||||
floatingBallState.currentY = (window.innerHeight - 30) / 2;
|
||||
}
|
||||
|
||||
ball.style.left = floatingBallState.currentX + 'px';
|
||||
@@ -133,8 +114,8 @@ function bindFloatingBallEvents(ball) {
|
||||
|
||||
// 窗口大小变化时调整位置
|
||||
window.addEventListener('resize', () => {
|
||||
const maxX = window.innerWidth - 60;
|
||||
const maxY = window.innerHeight - 60;
|
||||
const maxX = window.innerWidth - 30;
|
||||
const maxY = window.innerHeight - 30;
|
||||
if (floatingBallState.currentX > maxX) {
|
||||
floatingBallState.currentX = maxX;
|
||||
ball.style.left = floatingBallState.currentX + 'px';
|
||||
@@ -200,8 +181,8 @@ function onDragMove(e) {
|
||||
let newY = floatingBallState.initialY + deltaY;
|
||||
|
||||
// 限制在视口内
|
||||
const maxX = window.innerWidth - 60;
|
||||
const maxY = window.innerHeight - 60;
|
||||
const maxX = window.innerWidth - 30;
|
||||
const maxY = window.innerHeight - 30;
|
||||
newX = Math.min(Math.max(0, newX), maxX);
|
||||
newY = Math.min(Math.max(0, newY), maxY);
|
||||
|
||||
|
||||
354
gift.js
354
gift.js
@@ -58,11 +58,17 @@ let currentCategory = 'normal';
|
||||
let selectedGift = null;
|
||||
let selectedTarget = 'character'; // 'character' 送角色 | 'user' 送用户
|
||||
|
||||
// 多选模式状态
|
||||
let multiSelectMode = false;
|
||||
let selectedGifts = []; // 多选时存储多个礼物
|
||||
|
||||
// 显示礼物页面
|
||||
export function showGiftPage() {
|
||||
currentCategory = 'normal';
|
||||
selectedGift = null;
|
||||
selectedTarget = 'character';
|
||||
multiSelectMode = false;
|
||||
selectedGifts = [];
|
||||
|
||||
const page = document.getElementById('wechat-gift-page');
|
||||
if (page) {
|
||||
@@ -85,9 +91,38 @@ function renderGiftContent() {
|
||||
const gridContainer = document.getElementById('wechat-gift-grid');
|
||||
const sendBtn = document.getElementById('wechat-gift-send');
|
||||
const targetContainer = document.getElementById('wechat-gift-target');
|
||||
const headerEl = document.querySelector('.wechat-gift-header');
|
||||
|
||||
if (!tabsContainer || !gridContainer) return;
|
||||
|
||||
// 渲染多选按钮(仅情趣玩具分类显示)
|
||||
let multiSelectBtn = document.getElementById('wechat-gift-multi-select-btn');
|
||||
if (currentCategory === 'toy') {
|
||||
if (!multiSelectBtn && headerEl) {
|
||||
multiSelectBtn = document.createElement('button');
|
||||
multiSelectBtn.id = 'wechat-gift-multi-select-btn';
|
||||
multiSelectBtn.className = 'wechat-gift-multi-select-btn';
|
||||
headerEl.appendChild(multiSelectBtn);
|
||||
}
|
||||
if (multiSelectBtn) {
|
||||
if (multiSelectMode) {
|
||||
multiSelectBtn.textContent = selectedGifts.length > 0 ? `完成(${selectedGifts.length})` : '取消';
|
||||
multiSelectBtn.classList.add('active');
|
||||
} else {
|
||||
multiSelectBtn.textContent = '多选';
|
||||
multiSelectBtn.classList.remove('active');
|
||||
}
|
||||
multiSelectBtn.onclick = toggleMultiSelectMode;
|
||||
}
|
||||
} else {
|
||||
// 非情趣玩具分类,移除多选按钮并重置状态
|
||||
if (multiSelectBtn) {
|
||||
multiSelectBtn.remove();
|
||||
}
|
||||
multiSelectMode = false;
|
||||
selectedGifts = [];
|
||||
}
|
||||
|
||||
// 渲染送礼目标选择(仅情趣玩具显示)
|
||||
if (targetContainer) {
|
||||
if (currentCategory === 'toy') {
|
||||
@@ -132,6 +167,11 @@ function renderGiftContent() {
|
||||
tab.addEventListener('click', () => {
|
||||
currentCategory = tab.dataset.category;
|
||||
selectedGift = null;
|
||||
// 切换分类时不重置多选,只在非toy分类时重置
|
||||
if (tab.dataset.category !== 'toy') {
|
||||
multiSelectMode = false;
|
||||
selectedGifts = [];
|
||||
}
|
||||
renderGiftContent();
|
||||
});
|
||||
});
|
||||
@@ -140,13 +180,21 @@ function renderGiftContent() {
|
||||
const category = GIFT_CATEGORIES[currentCategory];
|
||||
let gridHtml = '';
|
||||
category.items.forEach(item => {
|
||||
const selectedClass = selectedGift?.id === item.id ? 'selected' : '';
|
||||
// 多选模式下检查是否在selectedGifts中
|
||||
const isSelectedInMulti = multiSelectMode && selectedGifts.some(g => g.id === item.id);
|
||||
// 单选模式下检查是否是selectedGift
|
||||
const isSelectedSingle = !multiSelectMode && selectedGift?.id === item.id;
|
||||
const selectedClass = (isSelectedInMulti || isSelectedSingle) ? 'selected' : '';
|
||||
const controlBadge = item.hasControl ? '<span class="wechat-gift-control-badge">可控</span>' : '';
|
||||
// 多选模式下显示勾选标记
|
||||
const checkMark = isSelectedInMulti ? '<span class="wechat-gift-check-mark">✓</span>' : '';
|
||||
|
||||
gridHtml += `
|
||||
<div class="wechat-gift-item ${selectedClass}" data-gift-id="${item.id}">
|
||||
<span class="wechat-gift-emoji">${item.emoji}</span>
|
||||
<span class="wechat-gift-name">${item.name}</span>
|
||||
${controlBadge}
|
||||
${checkMark}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
@@ -156,14 +204,39 @@ function renderGiftContent() {
|
||||
gridContainer.querySelectorAll('.wechat-gift-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const giftId = item.dataset.giftId;
|
||||
selectedGift = category.items.find(g => g.id === giftId);
|
||||
const gift = category.items.find(g => g.id === giftId);
|
||||
|
||||
if (multiSelectMode && currentCategory === 'toy') {
|
||||
// 多选模式:只能选择有控制功能的玩具
|
||||
if (!gift.hasControl) {
|
||||
showToast('该玩具不支持多选控制');
|
||||
return;
|
||||
}
|
||||
// 切换选中状态
|
||||
const existingIndex = selectedGifts.findIndex(g => g.id === giftId);
|
||||
if (existingIndex >= 0) {
|
||||
selectedGifts.splice(existingIndex, 1);
|
||||
} else {
|
||||
if (selectedGifts.length >= 5) {
|
||||
showToast('最多选择5个玩具');
|
||||
return;
|
||||
}
|
||||
selectedGifts.push(gift);
|
||||
}
|
||||
} else {
|
||||
// 单选模式
|
||||
selectedGift = gift;
|
||||
}
|
||||
renderGiftContent();
|
||||
});
|
||||
});
|
||||
|
||||
// 更新发送按钮状态
|
||||
if (sendBtn) {
|
||||
if (selectedGift) {
|
||||
if (multiSelectMode && selectedGifts.length > 0) {
|
||||
sendBtn.disabled = false;
|
||||
sendBtn.textContent = `送出 ${selectedGifts.length} 件玩具`;
|
||||
} else if (!multiSelectMode && selectedGift) {
|
||||
sendBtn.disabled = false;
|
||||
sendBtn.textContent = `送出 ${selectedGift.name}`;
|
||||
} else {
|
||||
@@ -173,9 +246,27 @@ function renderGiftContent() {
|
||||
}
|
||||
}
|
||||
|
||||
// 切换多选模式
|
||||
function toggleMultiSelectMode() {
|
||||
if (multiSelectMode && selectedGifts.length > 0) {
|
||||
// 如果已有选择,点击"完成"按钮触发发送
|
||||
sendGift();
|
||||
} else {
|
||||
// 切换模式
|
||||
multiSelectMode = !multiSelectMode;
|
||||
if (!multiSelectMode) {
|
||||
selectedGifts = [];
|
||||
}
|
||||
selectedGift = null;
|
||||
renderGiftContent();
|
||||
}
|
||||
}
|
||||
|
||||
// 发送礼物
|
||||
export async function sendGift() {
|
||||
if (!selectedGift) {
|
||||
// 检查是否有选择
|
||||
const isMulti = multiSelectMode && selectedGifts.length > 0;
|
||||
if (!isMulti && !selectedGift) {
|
||||
showToast('请选择礼物');
|
||||
return;
|
||||
}
|
||||
@@ -189,7 +280,6 @@ export async function sendGift() {
|
||||
const contact = settings.contacts[currentChatIndex];
|
||||
if (!contact) return;
|
||||
|
||||
const gift = selectedGift;
|
||||
const isToy = currentCategory === 'toy';
|
||||
const target = isToy ? selectedTarget : null;
|
||||
|
||||
@@ -201,6 +291,150 @@ export async function sendGift() {
|
||||
const customDesc = descInput?.value?.trim() || '';
|
||||
if (descInput) descInput.value = '';
|
||||
|
||||
// 保存到聊天历史
|
||||
const now = new Date();
|
||||
const timeStr = `${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2,'0')}-${now.getDate().toString().padStart(2,'0')} ${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}`;
|
||||
|
||||
if (!contact.chatHistory) {
|
||||
contact.chatHistory = [];
|
||||
}
|
||||
|
||||
// 多选模式处理
|
||||
if (isMulti) {
|
||||
const giftsToSend = [...selectedGifts];
|
||||
const giftNames = giftsToSend.map(g => g.name).join('、');
|
||||
const giftEmojis = giftsToSend.map(g => g.emoji).join(' ');
|
||||
const targetText = target === 'character' ? '送TA' : '送自己';
|
||||
const giftMessage = `[情趣礼物套装] ${giftEmojis} ${giftNames}(${targetText})${customDesc ? ` - ${customDesc}` : ''}`;
|
||||
|
||||
const giftRecord = {
|
||||
role: 'user',
|
||||
content: giftMessage,
|
||||
time: timeStr,
|
||||
timestamp: Date.now(),
|
||||
isGift: true,
|
||||
isMultiGift: true,
|
||||
giftInfo: {
|
||||
gifts: giftsToSend.map(g => ({
|
||||
id: g.id,
|
||||
name: g.name,
|
||||
emoji: g.emoji,
|
||||
desc: g.desc,
|
||||
hasControl: g.hasControl,
|
||||
hasShock: g.hasShock
|
||||
})),
|
||||
isToy: true,
|
||||
target: target,
|
||||
customDesc: customDesc
|
||||
}
|
||||
};
|
||||
|
||||
contact.chatHistory.push(giftRecord);
|
||||
|
||||
// 显示礼物消息(多选版本)
|
||||
appendMultiGiftMessage('user', giftsToSend, customDesc, contact, target);
|
||||
|
||||
contact.lastMessage = giftMessage;
|
||||
|
||||
// 添加到待配送列表(作为一个多玩具组合)
|
||||
if (!contact.pendingGifts) {
|
||||
contact.pendingGifts = [];
|
||||
}
|
||||
|
||||
const multiPendingGift = {
|
||||
isMulti: true,
|
||||
toys: giftsToSend.map(g => ({
|
||||
giftId: g.id,
|
||||
giftName: g.name,
|
||||
giftEmoji: g.emoji,
|
||||
giftDesc: g.desc,
|
||||
hasControl: g.hasControl,
|
||||
hasShock: g.hasShock || false
|
||||
})),
|
||||
target: target,
|
||||
startMessageCount: contact.chatHistory.length,
|
||||
deliveredAt: null,
|
||||
isDelivered: false,
|
||||
isUsing: false,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
contact.pendingGifts.push(multiPendingGift);
|
||||
|
||||
// 显示配送中弹窗
|
||||
setTimeout(() => {
|
||||
showNotificationBanner('快递', `您选择的${giftsToSend.length}件商品正在配送中~`, 4000);
|
||||
}, 500);
|
||||
|
||||
// 2秒后弹出加急配送弹窗
|
||||
setTimeout(() => {
|
||||
showExpressDeliveryModal(multiPendingGift, contact);
|
||||
}, 2000);
|
||||
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
|
||||
// 显示打字指示器
|
||||
showTypingIndicator(contact);
|
||||
|
||||
// 构建给AI的提示
|
||||
const targetTextAI = target === 'character' ? '你' : '用户';
|
||||
const aiPrompt = `[系统提示:用户刚刚购买了一套情趣玩具套装,包括:${giftNames},准备送给${targetTextAI}使用。商品正在配送中,预计很快就会送达。${customDesc ? `用户附言:${customDesc}` : ''}
|
||||
|
||||
请根据你的角色性格,对这套即将到来的礼物做出反应:
|
||||
- 如果是送给你的:可以表现出期待、害羞、紧张、好奇等情绪,可以问用户打算怎么用这些
|
||||
- 如果是送给用户的:可以表现出好奇、调侃、期待看到用户反应等
|
||||
- 根据你的人设和与用户的关系,反应可以是含蓄的、热情的、或者假装矜持的
|
||||
- 回复不要太短,请展现角色的内心活动和情绪变化
|
||||
|
||||
【重要】只能输出纯文字消息,禁止输出任何特殊格式标签]`;
|
||||
|
||||
try {
|
||||
const aiResponse = await callAI(contact, aiPrompt);
|
||||
hideTypingIndicator();
|
||||
|
||||
if (aiResponse) {
|
||||
const aiMessages = splitAIMessages(aiResponse);
|
||||
|
||||
for (const msg of aiMessages) {
|
||||
let reply = msg.trim();
|
||||
reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim();
|
||||
reply = reply.replace(/\[.*?\]/g, '').trim();
|
||||
reply = reply.replace(/([^)]*)/g, '').trim();
|
||||
reply = reply.replace(/\([^)]*\)/g, '').trim();
|
||||
|
||||
if (reply) {
|
||||
contact.chatHistory.push({
|
||||
role: 'assistant',
|
||||
content: reply,
|
||||
time: timeStr,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
appendMessage('assistant', reply, contact);
|
||||
}
|
||||
}
|
||||
|
||||
const lastMsg = aiMessages[aiMessages.length - 1]?.trim()?.replace(/\[.*?\]/g, '').trim();
|
||||
if (lastMsg) {
|
||||
contact.lastMessage = lastMsg.length > 20 ? lastMsg.substring(0, 20) + '...' : lastMsg;
|
||||
}
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
}
|
||||
} catch (err) {
|
||||
hideTypingIndicator();
|
||||
console.error('[可乐] 礼物AI回复失败:', err);
|
||||
}
|
||||
|
||||
// 重置多选状态
|
||||
multiSelectMode = false;
|
||||
selectedGifts = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// 单选模式处理(原有逻辑)
|
||||
const gift = selectedGift;
|
||||
|
||||
// 构建礼物消息
|
||||
let giftMessage;
|
||||
if (isToy) {
|
||||
@@ -210,14 +444,6 @@ export async function sendGift() {
|
||||
giftMessage = `[礼物] ${gift.emoji} ${gift.name}${customDesc ? ` - ${customDesc}` : ''}`;
|
||||
}
|
||||
|
||||
// 保存到聊天历史
|
||||
const now = new Date();
|
||||
const timeStr = `${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2,'0')}-${now.getDate().toString().padStart(2,'0')} ${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}`;
|
||||
|
||||
if (!contact.chatHistory) {
|
||||
contact.chatHistory = [];
|
||||
}
|
||||
|
||||
const giftRecord = {
|
||||
role: 'user',
|
||||
content: giftMessage,
|
||||
@@ -271,6 +497,11 @@ export async function sendGift() {
|
||||
setTimeout(() => {
|
||||
showNotificationBanner('快递', '您选择的商品正在配送中~', 4000);
|
||||
}, 500);
|
||||
|
||||
// 2秒后弹出加急配送弹窗
|
||||
setTimeout(() => {
|
||||
showExpressDeliveryModal(pendingGift, contact);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
requestSave();
|
||||
@@ -391,6 +622,46 @@ export function checkGiftDelivery(contact) {
|
||||
}
|
||||
}
|
||||
|
||||
// 显示加急配送弹窗
|
||||
export function showExpressDeliveryModal(gift, contact) {
|
||||
const modal = document.getElementById('wechat-express-delivery-modal');
|
||||
if (!modal) return;
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
const yesBtn = document.getElementById('wechat-express-yes');
|
||||
const noBtn = document.getElementById('wechat-express-no');
|
||||
|
||||
const handleYes = () => {
|
||||
modal.classList.add('hidden');
|
||||
yesBtn.removeEventListener('click', handleYes);
|
||||
noBtn.removeEventListener('click', handleNo);
|
||||
|
||||
// 标记为已送达
|
||||
gift.isDelivered = true;
|
||||
gift.deliveredAt = Date.now();
|
||||
requestSave();
|
||||
|
||||
// 显示送达通知
|
||||
showNotificationBanner('快递', '您的商品已送达~', 3000);
|
||||
|
||||
// 2秒后弹出"是否开始玩"弹窗
|
||||
setTimeout(() => {
|
||||
showGiftArrivalModal(gift, contact);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const handleNo = () => {
|
||||
modal.classList.add('hidden');
|
||||
yesBtn.removeEventListener('click', handleYes);
|
||||
noBtn.removeEventListener('click', handleNo);
|
||||
// 什么都不做,走原有的25条消息检测逻辑
|
||||
};
|
||||
|
||||
yesBtn.addEventListener('click', handleYes);
|
||||
noBtn.addEventListener('click', handleNo);
|
||||
}
|
||||
|
||||
// 显示礼物送达询问弹窗
|
||||
export function showGiftArrivalModal(gift, contact) {
|
||||
const modal = document.getElementById('wechat-gift-arrival-modal');
|
||||
@@ -398,10 +669,16 @@ export function showGiftArrivalModal(gift, contact) {
|
||||
|
||||
if (!modal || !bodyEl) return;
|
||||
|
||||
bodyEl.innerHTML = `您的 <strong>${gift.giftName}</strong> 已送达,您要现在开始玩吗?`;
|
||||
// 支持多选礼物
|
||||
if (gift.isMulti) {
|
||||
const toyNames = gift.toys.map(t => t.giftName).join('、');
|
||||
bodyEl.innerHTML = `您的 <strong>${toyNames}</strong> 已送达,您要现在开始玩吗?`;
|
||||
} else {
|
||||
bodyEl.innerHTML = `您的 <strong>${gift.giftName}</strong> 已送达,您要现在开始玩吗?`;
|
||||
}
|
||||
|
||||
// 存储当前礼物信息
|
||||
modal.dataset.giftId = gift.giftId;
|
||||
modal.dataset.giftId = gift.giftId || (gift.isMulti ? 'multi' : '');
|
||||
modal.dataset.giftTimestamp = gift.timestamp;
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
@@ -494,6 +771,53 @@ export function appendGiftMessage(role, gift, isToy, customDesc, contact, target
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// 添加多选礼物消息到界面
|
||||
export function appendMultiGiftMessage(role, gifts, customDesc, contact, target = null) {
|
||||
const messagesContainer = document.getElementById('wechat-chat-messages');
|
||||
if (!messagesContainer) return;
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `wechat-message ${role === 'user' ? 'self' : ''}`;
|
||||
|
||||
const firstChar = contact?.name ? contact.name.charAt(0) : '?';
|
||||
|
||||
// 获取用户头像
|
||||
let avatarContent;
|
||||
if (role === 'user') {
|
||||
const settings = getSettings();
|
||||
if (settings.userAvatar) {
|
||||
avatarContent = `<img src="${settings.userAvatar}" alt="" onerror="this.style.display='none';this.parentElement.textContent='我'">`;
|
||||
} else {
|
||||
avatarContent = '我';
|
||||
}
|
||||
} else {
|
||||
avatarContent = contact?.avatar
|
||||
? `<img src="${contact.avatar}" alt="" onerror="this.style.display='none';this.parentElement.innerHTML='${firstChar}'">`
|
||||
: firstChar;
|
||||
}
|
||||
|
||||
const giftEmojis = gifts.map(g => g.emoji).join(' ');
|
||||
const giftNames = gifts.map(g => escapeHtml(g.name)).join('、');
|
||||
const giftTypeLabel = target === 'character' ? '情趣套装·送TA' : '情趣套装·送自己';
|
||||
|
||||
messageDiv.innerHTML = `
|
||||
<div class="wechat-message-avatar">${avatarContent}</div>
|
||||
<div class="wechat-message-content">
|
||||
<div class="wechat-gift-bubble wechat-gift-bubble-toy wechat-gift-bubble-multi">
|
||||
<div class="wechat-gift-bubble-emoji">${giftEmojis}</div>
|
||||
<div class="wechat-gift-bubble-info">
|
||||
<div class="wechat-gift-bubble-name">${giftNames}</div>
|
||||
${customDesc ? `<div class="wechat-gift-bubble-desc">${escapeHtml(customDesc)}</div>` : ''}
|
||||
</div>
|
||||
<div class="wechat-gift-bubble-label">${giftTypeLabel}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesContainer.appendChild(messageDiv);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// 获取礼物分类数据(供其他模块使用)
|
||||
export function getGiftCategories() {
|
||||
return GIFT_CATEGORIES;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getSTChatContext, HAKIMI_HEADER } from './ai.js';
|
||||
import { playMusic as kugouPlayMusic } from './music.js';
|
||||
import { showMessageMenu } from './message-menu.js';
|
||||
import { showGroupRedPacketDetail } from './group-red-packet.js';
|
||||
import { loadGroupBackground } from './chat-background.js';
|
||||
|
||||
// 当前群聊的索引
|
||||
export let currentGroupChatIndex = -1;
|
||||
@@ -631,6 +632,9 @@ export function openGroupChat(groupIndex) {
|
||||
messagesContainer.dataset.isGroup = 'true';
|
||||
messagesContainer.dataset.groupIndex = groupIndex;
|
||||
console.log('[可乐] 群聊标记已设置:', { isGroup: messagesContainer.dataset.isGroup, groupIndex: messagesContainer.dataset.groupIndex });
|
||||
|
||||
// 加载群聊背景
|
||||
loadGroupBackground(groupIndex);
|
||||
}
|
||||
|
||||
// 渲染群聊历史
|
||||
@@ -1757,7 +1761,7 @@ function buildSingleCharacterPrompt(member, groupChat, members, silentCharacters
|
||||
4. 保持角色性格特点,回复要符合你的人设
|
||||
5. 可以使用表情符号
|
||||
6. 必须回复至少一条消息,哪怕只是"嗯"、"哦"、表情符号等简短回应
|
||||
7. 语音消息格式:[语音:内容]
|
||||
7. 语音消息格式:[语音:实际说的话](是你说的具体话语,不是声音描述!)
|
||||
8. 语音消息必须独立发送,不能和其他消息混在一起
|
||||
|
||||
【交错显示机制】
|
||||
|
||||
17
main.js
17
main.js
@@ -538,8 +538,21 @@ function onMinimizedDragMove(e) {
|
||||
minimizeState.hasMoved = true;
|
||||
}
|
||||
|
||||
const newLeft = minimizeState.initialLeft + deltaX;
|
||||
const newTop = minimizeState.initialTop + deltaY;
|
||||
// 直接使用当前触摸/鼠标位置减去元素尺寸的一半,让元素中心跟随手指
|
||||
const scale = 0.25;
|
||||
const rect = phone.getBoundingClientRect();
|
||||
const scaledWidth = rect.width;
|
||||
const scaledHeight = rect.height;
|
||||
|
||||
// 计算新位置:让缩小后的手机中心跟随手指
|
||||
let newLeft = clientX - scaledWidth / 2;
|
||||
let newTop = clientY - scaledHeight / 2;
|
||||
|
||||
// 限制在视口内
|
||||
const maxX = window.innerWidth - scaledWidth;
|
||||
const maxY = window.innerHeight - scaledHeight;
|
||||
newLeft = Math.min(Math.max(0, newLeft), maxX);
|
||||
newTop = Math.min(Math.max(0, newTop), maxY);
|
||||
|
||||
phone.style.left = newLeft + 'px';
|
||||
phone.style.top = newTop + 'px';
|
||||
|
||||
@@ -1312,6 +1312,17 @@ function generateGiftPageHTML() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加急配送弹窗 -->
|
||||
<div id="wechat-express-delivery-modal" class="wechat-modal hidden">
|
||||
<div class="wechat-express-modal-content">
|
||||
<div class="wechat-express-text">您要加急配送吗(100元)?</div>
|
||||
<div class="wechat-express-actions">
|
||||
<button class="wechat-express-btn confirm" id="wechat-express-yes">确认</button>
|
||||
<button class="wechat-express-btn cancel" id="wechat-express-no">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 礼物送达询问弹窗 -->
|
||||
<div id="wechat-gift-arrival-modal" class="wechat-modal hidden">
|
||||
<div class="wechat-modal-content wechat-gift-arrival-content">
|
||||
|
||||
368
style.css
368
style.css
@@ -4295,6 +4295,49 @@
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* 左滑删除容器 */
|
||||
.wechat-emoji-swipe-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.wechat-emoji-swipe-container .wechat-emoji-user-item {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.wechat-emoji-swipe-container .wechat-emoji-user-item:active {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* 删除背景 */
|
||||
.wechat-emoji-delete-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 50px;
|
||||
height: 100%;
|
||||
background: #ff3b30;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.wechat-emoji-delete-bg:active {
|
||||
background: #d32f2f;
|
||||
}
|
||||
|
||||
.wechat-emoji-item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -10561,6 +10604,64 @@
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
/* 多选按钮 */
|
||||
.wechat-gift-multi-select-btn {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 6px 14px;
|
||||
background: linear-gradient(135deg, #ff6b8a, #ff4d6d);
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 2px 8px rgba(255, 77, 109, 0.3);
|
||||
}
|
||||
|
||||
.wechat-gift-multi-select-btn:hover {
|
||||
background: linear-gradient(135deg, #ff7a96, #ff5c7a);
|
||||
box-shadow: 0 3px 12px rgba(255, 77, 109, 0.4);
|
||||
}
|
||||
|
||||
.wechat-gift-multi-select-btn.active {
|
||||
background: linear-gradient(135deg, #ff8fab, #ff6b8a);
|
||||
}
|
||||
|
||||
/* 多选勾选标记 */
|
||||
.wechat-gift-check-mark {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: linear-gradient(135deg, #ff6b8a, #ff4d6d);
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 6px rgba(255, 77, 109, 0.4);
|
||||
animation: checkMarkPop 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes checkMarkPop {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
70% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 礼物描述输入 */
|
||||
.wechat-gift-desc-row {
|
||||
margin-top: 15px;
|
||||
@@ -11127,6 +11228,52 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ========== 加急配送弹窗样式 ========== */
|
||||
.wechat-express-modal-content {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 24px 20px;
|
||||
width: 280px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wechat-express-text {
|
||||
color: #000000;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.wechat-express-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.wechat-express-btn {
|
||||
flex: 1;
|
||||
padding: 10px 0;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.wechat-express-btn.confirm {
|
||||
background: #FFC107;
|
||||
color: #000000;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.wechat-express-btn.cancel {
|
||||
background: #E5E5E5;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.wechat-express-btn:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* ========== 礼物送达询问弹窗样式 ========== */
|
||||
.wechat-gift-arrival-content {
|
||||
background: #fff !important;
|
||||
@@ -11298,6 +11445,33 @@
|
||||
background: linear-gradient(135deg, #ffc107, #ff9800);
|
||||
}
|
||||
|
||||
/* 媒体按钮(麦克风/摄像头) */
|
||||
.wechat-toy-btn-media {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: linear-gradient(135deg, #ff6b8a, #ff4d6d);
|
||||
border-color: rgba(255, 107, 138, 0.3);
|
||||
box-shadow: 0 4px 12px rgba(255, 77, 109, 0.3);
|
||||
}
|
||||
|
||||
.wechat-toy-btn-media svg {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.wechat-toy-btn-media:hover {
|
||||
background: linear-gradient(135deg, #ff7a96, #ff5c7a);
|
||||
}
|
||||
|
||||
.wechat-toy-btn-media.active {
|
||||
background: linear-gradient(135deg, #ff8fab, #ff6b8a);
|
||||
border-color: #ff8fab;
|
||||
box-shadow: 0 0 20px rgba(255, 143, 171, 0.5);
|
||||
}
|
||||
|
||||
.wechat-toy-btn-media.active svg {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.wechat-toy-shock-row {
|
||||
padding-top: 5px;
|
||||
border-top: 1px dashed rgba(255, 107, 138, 0.3);
|
||||
@@ -11365,6 +11539,186 @@
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* 系统消息(如切换玩具提示) */
|
||||
.wechat-toy-control-msg.system {
|
||||
align-self: center;
|
||||
background: rgba(255, 107, 138, 0.15);
|
||||
color: #ff6b8a;
|
||||
font-size: 12px;
|
||||
padding: 6px 14px;
|
||||
border-radius: 16px;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
/* 多玩具轮盘选择器 */
|
||||
.wechat-toy-wheel-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 180px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.wechat-toy-wheel-heart {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: linear-gradient(135deg, #ff6b8a, #ff4d6d);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 15px rgba(255, 77, 109, 0.4);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 10;
|
||||
animation: heartPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes heartPulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.wechat-toy-wheel-heart:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 6px 20px rgba(255, 77, 109, 0.5);
|
||||
}
|
||||
|
||||
.wechat-toy-wheel-heart:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.wechat-toy-wheel-heart svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.wechat-toy-wheel-options {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wechat-toy-wheel-options.open {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.wechat-toy-wheel-option {
|
||||
position: absolute;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: 2px solid rgba(255, 107, 138, 0.3);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.15);
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.wechat-toy-wheel-options.open .wechat-toy-wheel-option {
|
||||
opacity: 1;
|
||||
transform: translate(calc(-50% + var(--x)), calc(-50% + var(--y))) scale(1);
|
||||
}
|
||||
|
||||
.wechat-toy-wheel-option:hover {
|
||||
background: #fff;
|
||||
border-color: #ff6b8a;
|
||||
transform: translate(calc(-50% + var(--x)), calc(-50% + var(--y))) scale(1.15);
|
||||
box-shadow: 0 5px 20px rgba(255, 107, 138, 0.3);
|
||||
}
|
||||
|
||||
.wechat-toy-wheel-option.active {
|
||||
background: linear-gradient(135deg, #fff0f3, #ffe0e6);
|
||||
border-color: #ff6b8a;
|
||||
box-shadow: 0 0 0 3px rgba(255, 107, 138, 0.2);
|
||||
}
|
||||
|
||||
.wechat-toy-wheel-option .emoji {
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.wechat-toy-wheel-option .name {
|
||||
font-size: 9px;
|
||||
color: #666;
|
||||
max-width: 50px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wechat-toy-wheel-option.active .name {
|
||||
color: #ff6b8a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 轮盘选择器移动端适配 */
|
||||
@media (max-width: 420px) {
|
||||
.wechat-toy-wheel-container {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.wechat-toy-wheel-heart {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.wechat-toy-wheel-heart svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.wechat-toy-wheel-option {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.wechat-toy-wheel-option .emoji {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.wechat-toy-wheel-option .name {
|
||||
font-size: 8px;
|
||||
max-width: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 轮盘背景遮罩(点击关闭) */
|
||||
.wechat-toy-wheel-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: transparent;
|
||||
z-index: 5;
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wechat-toy-wheel-overlay.active {
|
||||
display: block;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
@@ -11664,8 +12018,8 @@
|
||||
/* ===== 悬浮球样式 ===== */
|
||||
.wechat-floating-ball {
|
||||
position: fixed;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
z-index: 99999;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@@ -11679,24 +12033,24 @@
|
||||
}
|
||||
|
||||
.wechat-floating-ball:hover {
|
||||
transform: scale(1.08);
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
.wechat-floating-ball:active,
|
||||
.wechat-floating-ball.dragging {
|
||||
transform: scale(0.95);
|
||||
transform: scale(0.92);
|
||||
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));
|
||||
filter: drop-shadow(0 1px 4px rgba(255, 182, 193, 0.5));
|
||||
transition: filter 0.2s ease;
|
||||
}
|
||||
|
||||
.wechat-floating-ball:hover .floating-ball-svg {
|
||||
filter: drop-shadow(0 4px 12px rgba(255, 182, 193, 0.6));
|
||||
filter: drop-shadow(0 2px 6px rgba(255, 182, 193, 0.7));
|
||||
}
|
||||
|
||||
/* 悬浮球呼吸动画(可选,可删除) */
|
||||
@@ -11705,7 +12059,7 @@
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.03);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -323,11 +323,11 @@ export function generateSummaryPrompt(allChats, cupNumber) {
|
||||
- 不要使用markdown代码块
|
||||
- 直接以 { 开头,以 } 结尾
|
||||
- keys: 3-5个能代表本次聊天核心内容的关键词(人名、地点、事件等)
|
||||
- content: 原样复制的对话记录,每条一行,格式为"发言者: 内容"
|
||||
- content: 以"以下是线上聊天内容:"开头,然后原样复制对话记录,每条一行,格式为"发言者: 内容"
|
||||
- comment: "${getCupName(cupNumber)}"
|
||||
|
||||
【JSON示例】
|
||||
{"keys":["公园","约会","周末"],"content":"{{user}}: 今天去哪玩?\\n{{char}}: 去公园吧\\n{{user}}: 好呀\\n{{char}}: 那我们下午2点见","comment":"${getCupName(cupNumber)}"}
|
||||
{"keys":["公园","约会","周末"],"content":"以下是线上聊天内容:\\n{{user}}: 今天去哪玩?\\n{{char}}: 去公园吧\\n{{user}}: 好呀\\n{{char}}: 那我们下午2点见","comment":"${getCupName(cupNumber)}"}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
451
toy-control.js
451
toy-control.js
@@ -18,7 +18,13 @@ const TOY_ICONS = {
|
||||
wave: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M2 12c2-3 4-6 6-6s4 6 6 6 4-6 6-6" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 18c2-3 4-6 6-6s4 6 6 6 4-6 6-6" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
||||
pause: `<svg viewBox="0 0 24 24" width="28" height="28"><rect x="6" y="4" width="4" height="16" stroke="currentColor" stroke-width="1.5" fill="none" rx="1"/><rect x="14" y="4" width="4" height="16" stroke="currentColor" stroke-width="1.5" fill="none" rx="1"/></svg>`,
|
||||
shock: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" stroke="currentColor" stroke-width="1.5" fill="currentColor" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
||||
back: `<svg viewBox="0 0 24 24" width="24" height="24"><path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>`
|
||||
back: `<svg viewBox="0 0 24 24" width="24" height="24"><path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
||||
micOn: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M19 10v2a7 7 0 01-14 0v-2" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/><line x1="12" y1="19" x2="12" y2="23" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><line x1="8" y1="23" x2="16" y2="23" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
|
||||
micOff: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M19 10v2a7 7 0 01-14 0v-2" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/><line x1="12" y1="19" x2="12" y2="23" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><line x1="8" y1="23" x2="16" y2="23" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
|
||||
cameraOn: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M23 7l-7 5 7 5V7z" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>`,
|
||||
cameraOff: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M23 7l-7 5 7 5V7z" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>`,
|
||||
// 像素爱心图标(用于多玩具切换)
|
||||
pixelHeart: `<svg viewBox="0 0 16 16" width="24" height="24"><rect x="2" y="4" width="2" height="2" fill="currentColor"/><rect x="4" y="2" width="2" height="2" fill="currentColor"/><rect x="6" y="2" width="2" height="2" fill="currentColor"/><rect x="8" y="2" width="2" height="2" fill="currentColor"/><rect x="10" y="2" width="2" height="2" fill="currentColor"/><rect x="12" y="4" width="2" height="2" fill="currentColor"/><rect x="2" y="6" width="2" height="2" fill="currentColor"/><rect x="4" y="4" width="2" height="2" fill="currentColor"/><rect x="6" y="4" width="2" height="2" fill="currentColor"/><rect x="8" y="4" width="2" height="2" fill="currentColor"/><rect x="10" y="4" width="2" height="2" fill="currentColor"/><rect x="12" y="6" width="2" height="2" fill="currentColor"/><rect x="2" y="8" width="2" height="2" fill="currentColor"/><rect x="4" y="6" width="2" height="2" fill="currentColor"/><rect x="6" y="6" width="2" height="2" fill="currentColor"/><rect x="8" y="6" width="2" height="2" fill="currentColor"/><rect x="10" y="6" width="2" height="2" fill="currentColor"/><rect x="12" y="8" width="2" height="2" fill="currentColor"/><rect x="4" y="8" width="2" height="2" fill="currentColor"/><rect x="6" y="8" width="2" height="2" fill="currentColor"/><rect x="8" y="8" width="2" height="2" fill="currentColor"/><rect x="10" y="8" width="2" height="2" fill="currentColor"/><rect x="6" y="10" width="2" height="2" fill="currentColor"/><rect x="8" y="10" width="2" height="2" fill="currentColor"/><rect x="4" y="10" width="2" height="2" fill="currentColor"/><rect x="10" y="10" width="2" height="2" fill="currentColor"/><rect x="6" y="12" width="4" height="2" fill="currentColor"/></svg>`
|
||||
};
|
||||
|
||||
// 控制模式定义
|
||||
@@ -73,11 +79,21 @@ let toyControlState = {
|
||||
currentMode: null,
|
||||
messages: [],
|
||||
activeModes: new Set(),
|
||||
sessionStartTime: null
|
||||
sessionStartTime: null,
|
||||
micEnabled: false, // 麦克风状态
|
||||
cameraEnabled: false, // 摄像头状态
|
||||
// 多玩具支持
|
||||
isMulti: false, // 是否多玩具模式
|
||||
toys: [], // 多玩具列表
|
||||
currentToyIndex: 0, // 当前控制的玩具索引
|
||||
wheelOpen: false // 轮盘是否展开
|
||||
};
|
||||
|
||||
// 显示控制界面
|
||||
export function showToyControlPage(gift, contact, contactIndex) {
|
||||
// 判断是否多玩具模式
|
||||
const isMulti = gift.isMulti === true;
|
||||
|
||||
toyControlState = {
|
||||
isActive: true,
|
||||
gift: gift,
|
||||
@@ -87,9 +103,30 @@ export function showToyControlPage(gift, contact, contactIndex) {
|
||||
currentMode: null,
|
||||
messages: [],
|
||||
activeModes: new Set(),
|
||||
sessionStartTime: Date.now()
|
||||
sessionStartTime: Date.now(),
|
||||
micEnabled: false,
|
||||
cameraEnabled: false,
|
||||
// 多玩具支持
|
||||
isMulti: isMulti,
|
||||
toys: isMulti ? gift.toys : [],
|
||||
currentToyIndex: 0,
|
||||
wheelOpen: false
|
||||
};
|
||||
|
||||
// 如果是多玩具模式,设置当前玩具为第一个
|
||||
if (isMulti && gift.toys && gift.toys.length > 0) {
|
||||
const firstToy = gift.toys[0];
|
||||
toyControlState.gift = {
|
||||
...gift,
|
||||
giftId: firstToy.giftId,
|
||||
giftName: firstToy.giftName,
|
||||
giftEmoji: firstToy.giftEmoji,
|
||||
giftDesc: firstToy.giftDesc,
|
||||
hasControl: firstToy.hasControl,
|
||||
hasShock: firstToy.hasShock
|
||||
};
|
||||
}
|
||||
|
||||
// 标记正在使用
|
||||
if (contact.pendingGifts) {
|
||||
const pendingGift = contact.pendingGifts.find(g => g.timestamp === gift.timestamp);
|
||||
@@ -159,6 +196,9 @@ function renderToyControlPage() {
|
||||
</button>
|
||||
</div>
|
||||
<div class="wechat-toy-btn-row">
|
||||
<button class="wechat-toy-btn wechat-toy-btn-media" data-media="mic" title="麦克风">
|
||||
${toyControlState.micEnabled ? TOY_ICONS.micOn : TOY_ICONS.micOff}
|
||||
</button>
|
||||
<button class="wechat-toy-btn" data-mode="wave">
|
||||
${TOY_CONTROL_MODES.wave.icon}
|
||||
<span class="wechat-toy-btn-label">${TOY_CONTROL_MODES.wave.name}</span>
|
||||
@@ -167,6 +207,9 @@ function renderToyControlPage() {
|
||||
${TOY_CONTROL_MODES.pause.icon}
|
||||
<span class="wechat-toy-btn-label">${TOY_CONTROL_MODES.pause.name}</span>
|
||||
</button>
|
||||
<button class="wechat-toy-btn wechat-toy-btn-media" data-media="camera" title="摄像头">
|
||||
${toyControlState.cameraEnabled ? TOY_ICONS.cameraOn : TOY_ICONS.cameraOff}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
buttonsEl.innerHTML = buttonsHtml;
|
||||
@@ -187,12 +230,246 @@ function renderToyControlPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 清空消息
|
||||
if (messagesEl) {
|
||||
// 多玩具轮盘选择器
|
||||
renderToyWheelSelector();
|
||||
|
||||
// 不清空消息(保留聊天内容)
|
||||
// 只在首次进入时清空
|
||||
if (messagesEl && messagesEl.children.length === 0 && toyControlState.messages.length === 0) {
|
||||
messagesEl.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染多玩具轮盘选择器
|
||||
function renderToyWheelSelector() {
|
||||
// 移除旧的轮盘和遮罩
|
||||
const existingWheel = document.getElementById('wechat-toy-wheel-container');
|
||||
if (existingWheel) {
|
||||
existingWheel.remove();
|
||||
}
|
||||
const existingOverlay = document.getElementById('wechat-toy-wheel-overlay');
|
||||
if (existingOverlay) {
|
||||
existingOverlay.remove();
|
||||
}
|
||||
|
||||
// 只在多玩具模式下显示
|
||||
if (!toyControlState.isMulti || toyControlState.toys.length <= 1) return;
|
||||
|
||||
const controlPage = document.getElementById('wechat-toy-control-page');
|
||||
if (!controlPage) return;
|
||||
|
||||
// 创建背景遮罩(点击关闭轮盘)
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'wechat-toy-wheel-overlay';
|
||||
overlay.className = `wechat-toy-wheel-overlay ${toyControlState.wheelOpen ? 'active' : ''}`;
|
||||
|
||||
// 创建轮盘容器
|
||||
const wheelContainer = document.createElement('div');
|
||||
wheelContainer.id = 'wechat-toy-wheel-container';
|
||||
wheelContainer.className = 'wechat-toy-wheel-container';
|
||||
|
||||
// 创建中心爱心按钮
|
||||
const heartBtn = document.createElement('button');
|
||||
heartBtn.className = 'wechat-toy-wheel-heart';
|
||||
heartBtn.innerHTML = TOY_ICONS.pixelHeart;
|
||||
heartBtn.title = '切换玩具';
|
||||
|
||||
// 创建轮盘选项
|
||||
const wheelOptions = document.createElement('div');
|
||||
wheelOptions.className = `wechat-toy-wheel-options ${toyControlState.wheelOpen ? 'open' : ''}`;
|
||||
|
||||
const toys = toyControlState.toys;
|
||||
const angleStep = 360 / toys.length;
|
||||
// 移动端使用更小的半径
|
||||
const isMobile = window.innerWidth <= 420;
|
||||
const radius = isMobile ? 55 : 70;
|
||||
|
||||
toys.forEach((toy, index) => {
|
||||
const option = document.createElement('button');
|
||||
option.className = `wechat-toy-wheel-option ${index === toyControlState.currentToyIndex ? 'active' : ''}`;
|
||||
option.dataset.toyIndex = index;
|
||||
|
||||
// 计算位置(从顶部开始,顺时针排列)
|
||||
const angle = -90 + (angleStep * index); // -90 从顶部开始
|
||||
const x = Math.cos(angle * Math.PI / 180) * radius;
|
||||
const y = Math.sin(angle * Math.PI / 180) * radius;
|
||||
|
||||
option.style.setProperty('--x', `${x}px`);
|
||||
option.style.setProperty('--y', `${y}px`);
|
||||
option.innerHTML = `<span class="emoji">${toy.giftEmoji}</span><span class="name">${toy.giftName}</span>`;
|
||||
|
||||
wheelOptions.appendChild(option);
|
||||
});
|
||||
|
||||
wheelContainer.appendChild(wheelOptions);
|
||||
wheelContainer.appendChild(heartBtn);
|
||||
|
||||
// 插入遮罩和轮盘到控制区域上方
|
||||
const chatArea = controlPage.querySelector('.wechat-toy-control-chat');
|
||||
if (chatArea) {
|
||||
chatArea.parentNode.insertBefore(overlay, chatArea);
|
||||
chatArea.parentNode.insertBefore(wheelContainer, chatArea);
|
||||
} else {
|
||||
controlPage.appendChild(overlay);
|
||||
controlPage.appendChild(wheelContainer);
|
||||
}
|
||||
|
||||
// 绑定爱心按钮事件(支持触摸)
|
||||
heartBtn.addEventListener('click', toggleToyWheel);
|
||||
heartBtn.addEventListener('touchend', (e) => {
|
||||
e.preventDefault();
|
||||
toggleToyWheel();
|
||||
});
|
||||
|
||||
// 绑定遮罩点击事件(关闭轮盘)
|
||||
overlay.addEventListener('click', closeToyWheel);
|
||||
overlay.addEventListener('touchend', (e) => {
|
||||
e.preventDefault();
|
||||
closeToyWheel();
|
||||
});
|
||||
|
||||
// 绑定轮盘选项事件(支持触摸)
|
||||
wheelOptions.querySelectorAll('.wechat-toy-wheel-option').forEach(opt => {
|
||||
opt.addEventListener('click', (e) => {
|
||||
const index = parseInt(opt.dataset.toyIndex);
|
||||
switchToToy(index);
|
||||
});
|
||||
opt.addEventListener('touchend', (e) => {
|
||||
e.preventDefault();
|
||||
const index = parseInt(opt.dataset.toyIndex);
|
||||
switchToToy(index);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 切换轮盘展开/收起
|
||||
function toggleToyWheel() {
|
||||
toyControlState.wheelOpen = !toyControlState.wheelOpen;
|
||||
|
||||
const options = document.querySelector('.wechat-toy-wheel-options');
|
||||
const overlay = document.getElementById('wechat-toy-wheel-overlay');
|
||||
if (options) {
|
||||
options.classList.toggle('open', toyControlState.wheelOpen);
|
||||
}
|
||||
if (overlay) {
|
||||
overlay.classList.toggle('active', toyControlState.wheelOpen);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭轮盘
|
||||
function closeToyWheel() {
|
||||
toyControlState.wheelOpen = false;
|
||||
|
||||
const options = document.querySelector('.wechat-toy-wheel-options');
|
||||
const overlay = document.getElementById('wechat-toy-wheel-overlay');
|
||||
if (options) {
|
||||
options.classList.remove('open');
|
||||
}
|
||||
if (overlay) {
|
||||
overlay.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 切换到指定玩具
|
||||
async function switchToToy(index) {
|
||||
if (index < 0 || index >= toyControlState.toys.length) return;
|
||||
if (index === toyControlState.currentToyIndex) {
|
||||
// 同一个玩具,只关闭轮盘
|
||||
closeToyWheel();
|
||||
return;
|
||||
}
|
||||
|
||||
const previousToy = toyControlState.toys[toyControlState.currentToyIndex];
|
||||
const newToy = toyControlState.toys[index];
|
||||
|
||||
// 更新当前玩具索引
|
||||
toyControlState.currentToyIndex = index;
|
||||
|
||||
// 更新当前gift信息
|
||||
toyControlState.gift = {
|
||||
...toyControlState.gift,
|
||||
giftId: newToy.giftId,
|
||||
giftName: newToy.giftName,
|
||||
giftEmoji: newToy.giftEmoji,
|
||||
giftDesc: newToy.giftDesc,
|
||||
hasControl: newToy.hasControl,
|
||||
hasShock: newToy.hasShock
|
||||
};
|
||||
|
||||
// 重置模式状态(切换玩具后从暂停开始)
|
||||
toyControlState.currentMode = null;
|
||||
toyControlState.activeModes.clear();
|
||||
|
||||
// 关闭轮盘状态(渲染时会应用)
|
||||
toyControlState.wheelOpen = false;
|
||||
|
||||
// 重新渲染界面(保留消息)
|
||||
renderToyControlPage();
|
||||
|
||||
// 更新按钮状态
|
||||
document.querySelectorAll('.wechat-toy-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
// 添加切换提示消息
|
||||
addToyMessage('system', `已切换到 ${newToy.giftEmoji} ${newToy.giftName}`);
|
||||
|
||||
// AI对切换做出反应
|
||||
showToyTypingIndicator();
|
||||
|
||||
const isCharacterUsing = toyControlState.target === 'character';
|
||||
let prompt;
|
||||
|
||||
if (isCharacterUsing) {
|
||||
prompt = `[玩具切换]
|
||||
用户刚把玩具从"${previousToy.giftName}"切换到"${newToy.giftName}"了。
|
||||
${newToy.giftName}的特点:${newToy.giftDesc}
|
||||
|
||||
请对这个切换做出反应:
|
||||
- 可以表现出对新玩具的期待、紧张或好奇
|
||||
- 如果是更刺激的玩具可以表现出紧张
|
||||
- 如果是比较温和的可以表现出失落或放松
|
||||
|
||||
【重要规则】
|
||||
1. 只能输出纯文字,禁止使用任何特殊格式标签
|
||||
2. 禁止使用小括号描述动作如(xxx)
|
||||
3. 回复简短,1-2句话即可`;
|
||||
} else {
|
||||
prompt = `[玩具切换]
|
||||
你把用户正在用的玩具从"${previousToy.giftName}"切换到"${newToy.giftName}"了。
|
||||
${newToy.giftName}的特点:${newToy.giftDesc}
|
||||
|
||||
请对这个切换做出反应:
|
||||
- 可以调侃用户接下来要体验的感觉
|
||||
- 或者表达你为什么要给用户换这个
|
||||
|
||||
【重要规则】
|
||||
1. 只能输出纯文字,禁止使用任何特殊格式标签
|
||||
2. 禁止使用小括号描述动作如(xxx)
|
||||
3. 回复简短,1-2句话即可`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await callToyAI(prompt);
|
||||
hideToyTypingIndicator();
|
||||
|
||||
if (response) {
|
||||
let reply = response.trim();
|
||||
reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim();
|
||||
reply = reply.replace(/\[.*?\]/g, '').trim();
|
||||
reply = reply.replace(/([^)]*)/g, '').trim();
|
||||
reply = reply.replace(/\([^)]*\)/g, '').trim();
|
||||
|
||||
if (reply) {
|
||||
addToyMessage('ai', reply);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
hideToyTypingIndicator();
|
||||
console.error('[可乐] 玩具切换AI回复失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
let toyEventsBound = false;
|
||||
function bindToyControlEvents() {
|
||||
@@ -216,6 +493,12 @@ function bindToyControlEvents() {
|
||||
document.getElementById('wechat-toy-control-buttons')?.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.wechat-toy-btn');
|
||||
if (btn) {
|
||||
// 检查是否是媒体按钮(麦克风/摄像头)
|
||||
const media = btn.dataset.media;
|
||||
if (media) {
|
||||
onMediaToggle(media);
|
||||
return;
|
||||
}
|
||||
const mode = btn.dataset.mode;
|
||||
if (mode) {
|
||||
onButtonPress(mode, 'user');
|
||||
@@ -234,6 +517,99 @@ function bindToyControlEvents() {
|
||||
});
|
||||
}
|
||||
|
||||
// 麦克风/摄像头切换处理
|
||||
async function onMediaToggle(mediaType) {
|
||||
if (!toyControlState.isActive) return;
|
||||
|
||||
const isMic = mediaType === 'mic';
|
||||
const wasEnabled = isMic ? toyControlState.micEnabled : toyControlState.cameraEnabled;
|
||||
const nowEnabled = !wasEnabled;
|
||||
|
||||
// 更新状态
|
||||
if (isMic) {
|
||||
toyControlState.micEnabled = nowEnabled;
|
||||
} else {
|
||||
toyControlState.cameraEnabled = nowEnabled;
|
||||
}
|
||||
|
||||
// 更新按钮图标
|
||||
updateMediaButtonUI();
|
||||
|
||||
// 显示typing
|
||||
showToyTypingIndicator();
|
||||
|
||||
// 构建提示词
|
||||
const prompt = buildMediaTogglePrompt(mediaType, nowEnabled);
|
||||
|
||||
try {
|
||||
const response = await callToyAI(prompt);
|
||||
hideToyTypingIndicator();
|
||||
|
||||
if (response) {
|
||||
await processAIResponse(response);
|
||||
}
|
||||
} catch (err) {
|
||||
hideToyTypingIndicator();
|
||||
console.error('[可乐] 玩具控制AI回复失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新媒体按钮UI
|
||||
function updateMediaButtonUI() {
|
||||
const micBtn = document.querySelector('.wechat-toy-btn-media[data-media="mic"]');
|
||||
const cameraBtn = document.querySelector('.wechat-toy-btn-media[data-media="camera"]');
|
||||
|
||||
if (micBtn) {
|
||||
micBtn.innerHTML = toyControlState.micEnabled ? TOY_ICONS.micOn : TOY_ICONS.micOff;
|
||||
micBtn.classList.toggle('active', toyControlState.micEnabled);
|
||||
}
|
||||
if (cameraBtn) {
|
||||
cameraBtn.innerHTML = toyControlState.cameraEnabled ? TOY_ICONS.cameraOn : TOY_ICONS.cameraOff;
|
||||
cameraBtn.classList.toggle('active', toyControlState.cameraEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建媒体切换提示词
|
||||
function buildMediaTogglePrompt(mediaType, isEnabled) {
|
||||
const isCharacterUsing = toyControlState.target === 'character';
|
||||
const isMic = mediaType === 'mic';
|
||||
const mediaName = isMic ? '麦克风' : '摄像头';
|
||||
const action = isEnabled ? '打开' : '关闭';
|
||||
|
||||
let prompt = `【${mediaName}${action}】\n`;
|
||||
|
||||
if (isCharacterUsing) {
|
||||
// 角色在用玩具
|
||||
if (isEnabled) {
|
||||
prompt += isMic
|
||||
? `用户打开了麦克风,现在可以听到用户的声音了。你正在使用玩具,听到用户的声音会让你更有感觉。请做出反应。`
|
||||
: `用户打开了摄像头,现在可以看到用户了。你正在使用玩具,看到用户会让你更害羞/更有感觉。请做出反应。`;
|
||||
} else {
|
||||
prompt += isMic
|
||||
? `用户关闭了麦克风,现在听不到用户的声音了。你可能会有点失落或者松一口气。请做出反应。`
|
||||
: `用户关闭了摄像头,现在看不到用户了。你可能会有点失落或者松一口气。请做出反应。`;
|
||||
}
|
||||
} else {
|
||||
// 用户在用玩具
|
||||
if (isEnabled) {
|
||||
prompt += isMic
|
||||
? `用户打开了麦克风,你现在可以听到用户的声音/喘息/呻吟了。请做出反应,可以调侃、撩拨或关心用户。`
|
||||
: `用户打开了摄像头,你现在可以看到用户使用玩具的样子了。请做出反应,可以调侃、撩拨或关心用户。`;
|
||||
} else {
|
||||
prompt += isMic
|
||||
? `用户关闭了麦克风,你现在听不到用户的声音了。请做出反应。`
|
||||
: `用户关闭了摄像头,你现在看不到用户了。请做出反应。`;
|
||||
}
|
||||
}
|
||||
|
||||
prompt += `\n\n【当前状态】
|
||||
- 麦克风:${toyControlState.micEnabled ? '已打开(可以听到声音)' : '已关闭'}
|
||||
- 摄像头:${toyControlState.cameraEnabled ? '已打开(可以看到画面)' : '已关闭'}
|
||||
- 当前模式:${toyControlState.currentMode ? (TOY_CONTROL_MODES[toyControlState.currentMode]?.name || '已暂停') : '未开始'}`;
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
// 按钮点击处理
|
||||
async function onButtonPress(buttonId, pressedBy = 'user') {
|
||||
if (!toyControlState.isActive) return;
|
||||
@@ -345,6 +721,18 @@ function buildButtonPressPrompt(buttonId, buttonName, pressedBy) {
|
||||
3. 禁止使用[语音:xxx]、[照片:xxx]、[表情:xxx]等格式
|
||||
4. 直接输出角色说的话和感受`;
|
||||
|
||||
// 添加当前媒体状态信息
|
||||
const mediaStatus = [];
|
||||
if (toyControlState.micEnabled) {
|
||||
mediaStatus.push('麦克风已开启(可以听到对方的声音/喘息)');
|
||||
}
|
||||
if (toyControlState.cameraEnabled) {
|
||||
mediaStatus.push('摄像头已开启(可以看到对方)');
|
||||
}
|
||||
if (mediaStatus.length > 0) {
|
||||
prompt += `\n\n【当前连接状态】${mediaStatus.join(',')}`;
|
||||
}
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
@@ -644,21 +1032,44 @@ function saveToySession() {
|
||||
const seconds = (durationSec % 60).toString().padStart(2, '0');
|
||||
const durationStr = `${minutes}:${seconds}`;
|
||||
|
||||
const session = {
|
||||
gift: {
|
||||
id: toyControlState.gift.giftId,
|
||||
name: toyControlState.gift.giftName,
|
||||
emoji: toyControlState.gift.giftEmoji
|
||||
},
|
||||
target: toyControlState.target,
|
||||
time: timeStr,
|
||||
timestamp: toyControlState.sessionStartTime || Date.now(),
|
||||
duration: durationStr,
|
||||
messages: toyControlState.messages.map(m => ({
|
||||
role: m.role,
|
||||
content: m.content
|
||||
}))
|
||||
};
|
||||
// 构建session记录
|
||||
let session;
|
||||
if (toyControlState.isMulti) {
|
||||
// 多玩具模式
|
||||
session = {
|
||||
isMulti: true,
|
||||
toys: toyControlState.toys.map(t => ({
|
||||
id: t.giftId,
|
||||
name: t.giftName,
|
||||
emoji: t.giftEmoji
|
||||
})),
|
||||
target: toyControlState.target,
|
||||
time: timeStr,
|
||||
timestamp: toyControlState.sessionStartTime || Date.now(),
|
||||
duration: durationStr,
|
||||
messages: toyControlState.messages.map(m => ({
|
||||
role: m.role,
|
||||
content: m.content
|
||||
}))
|
||||
};
|
||||
} else {
|
||||
// 单玩具模式
|
||||
session = {
|
||||
gift: {
|
||||
id: toyControlState.gift.giftId,
|
||||
name: toyControlState.gift.giftName,
|
||||
emoji: toyControlState.gift.giftEmoji
|
||||
},
|
||||
target: toyControlState.target,
|
||||
time: timeStr,
|
||||
timestamp: toyControlState.sessionStartTime || Date.now(),
|
||||
duration: durationStr,
|
||||
messages: toyControlState.messages.map(m => ({
|
||||
role: m.role,
|
||||
content: m.content
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
contact.toyHistory.push(session);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user