优化商店个性装扮体验

This commit is contained in:
2026-04-27 11:12:51 +08:00
parent 32af6abeb2
commit ffccfa26e9
14 changed files with 1027 additions and 20 deletions
+210 -6
View File
@@ -47,9 +47,34 @@ const SHOP_GROUPS = [
},
];
const DECORATION_GROUPS = [
{
label: "💬 消息气泡",
desc: "同类型只保留最新购买",
type: "msg_bubble",
},
{
label: "🎨 昵称颜色",
desc: "同类型只保留最新购买",
type: "msg_name_color",
},
{
label: "🖼️ 头像框",
desc: "同类型只保留最新购买",
type: "avatar_frame",
},
];
const DECORATION_TYPE_TO_SLOT = {
msg_bubble: "bubble",
msg_name_color: "name_color",
avatar_frame: "avatar_frame",
};
let shopControlEventsBound = false;
let shopLoaded = false;
let giftItem = null;
let activeDecorations = {};
/**
* 读取商店根节点上由 Blade 注入的接口地址。
@@ -106,6 +131,7 @@ export function openShopModal() {
}
modal.style.display = "flex";
bindShopTabs();
if (!shopLoaded) {
shopLoaded = true;
fetchShopData();
@@ -124,6 +150,45 @@ export function closeShopModal() {
}
}
/**
* 绑定商店 Tab 切换逻辑。
*
* @returns {void}
*/
function bindShopTabs() {
const tabsContainer = document.getElementById("shop-tabs");
if (!tabsContainer || tabsContainer.dataset.shopTabsBound) {
return;
}
tabsContainer.dataset.shopTabsBound = "1";
tabsContainer.addEventListener("click", (event) => {
const tab = event.target.closest("[data-shop-tab]");
if (!tab) {
return;
}
const tabName = tab.dataset.shopTab;
const itemsList = document.getElementById("shop-items-list");
const decorationsList = document.getElementById("shop-decorations-list");
// 切换当前 Tab 的选中状态。
tabsContainer.querySelectorAll(".shop-tab").forEach((btn) => {
btn.classList.toggle("active", btn.dataset.shopTab === tabName);
});
// 切换对应商品列表,装扮列表需要显式恢复 grid 布局。
if (tabName === "items") {
if (itemsList) itemsList.style.display = "";
if (decorationsList) decorationsList.style.display = "none";
} else {
if (itemsList) itemsList.style.display = "none";
// 装扮列表在 CSS 中为 display:none,需显式设置为 grid 才能覆盖
if (decorationsList) decorationsList.style.display = "grid";
}
});
}
/**
* 拉取商品数据并渲染列表。
*
@@ -136,6 +201,7 @@ export async function fetchShopData() {
});
const data = await response.json();
renderShop(data);
renderDecorations(data);
} catch (error) {
showShopToast("⚠ 加载失败,请重试", false);
}
@@ -245,6 +311,90 @@ export function renderShop(data) {
});
}
/**
* 渲染个人装扮商品列表。
*
* @param {Record<string, any>} data 商店接口数据
* @returns {void}
*/
export function renderDecorations(data) {
const list = document.getElementById("shop-decorations-list");
if (!list) {
return;
}
// 记录当前激活装扮,渲染时只给当前款式显示"已激活"。
activeDecorations = data.active_decorations || {};
const items = Array.isArray(data.items) ? data.items : [];
list.innerHTML = "";
// 购买说明明确同类型替换规则,避免用户误以为可以叠加多款气泡或头像框。
const note = document.createElement("div");
note.className = "decoration-note";
note.innerHTML = "📌 购买说明:每个类型只生效一个,购买同类型新装扮后,旧装扮自动作废且不退款。";
list.appendChild(note);
DECORATION_GROUPS.forEach((group) => {
const groupItems = items.filter((item) => item.type === group.type);
if (!groupItems.length) {
return;
}
// 分组标题(独占一整行),描述文字内嵌到标题中避免单独占一格
const header = document.createElement("div");
header.className = "shop-group-header";
header.innerHTML = `${escapeHtml(group.label)}${group.desc ? ` <span>${escapeHtml(group.desc)}</span>` : ''}`;
list.appendChild(header);
groupItems.forEach((item) => {
const card = document.createElement("div");
card.className = "shop-card";
// active_decorations 由后端按槽位名索引,先把商品 type 映射到对应槽位。
const slot = DECORATION_TYPE_TO_SLOT[item.type] || null;
const activeDeco = slot ? (activeDecorations[slot] || null) : null;
const isActiveStyle = activeDeco && activeDeco.style === item.slug;
const expiresAt = activeDeco ? activeDeco.expires_at : null;
let daysLeft = "";
if (isActiveStyle && expiresAt) {
const remaining = Math.max(0, Math.ceil((new Date(expiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24)));
daysLeft = remaining > 0 ? `剩余 ${remaining}` : "即将过期";
}
const button = document.createElement("button");
if (isActiveStyle) {
button.className = "shop-btn";
button.textContent = "续费 💰 " + Number(item.price || 0).toLocaleString();
} else {
button.className = "shop-btn";
button.textContent = "购买 💰 " + Number(item.price || 0).toLocaleString();
}
button.addEventListener("click", () => confirmAndBuyItem(item));
// 仅当前已激活的款式显示状态标签,同槽位其他款式保持普通购买状态。
const statusHtml = isActiveStyle
? `<span class="decoration-status active">已激活${daysLeft ? ' · ' + daysLeft : ''}</span>`
: "";
const validityHtml = buildValidityHtml(item);
card.innerHTML = `
<div class="shop-card-top">
<span class="shop-card-icon">${escapeHtml(item.icon)}</span>
<span class="shop-card-name">${escapeHtml(item.name)}</span>
</div>
${statusHtml ? `<div class="decoration-status-line">${statusHtml}</div>` : ""}
<div class="shop-card-desc">${escapeHtml(item.description ?? "")}</div>
${validityHtml}
${isActiveStyle && expiresAt ? `<div class="decoration-expiry">⏳ 到期:${escapeHtml(expiresAt.replace('T', ' ').slice(0, 16))}</div>` : ""}
`;
card.appendChild(button);
list.appendChild(card);
});
});
}
/**
* 创建单个商品卡片,并挂载购买或使用按钮事件。
*
@@ -302,9 +452,7 @@ function buildShopCardHtml(item, options) {
<span style="position:absolute;top:-4px;right:-6px;background:#f43f5e;color:#fff;font-size:9px;font-weight:800;min-width:15px;height:15px;border-radius:8px;text-align:center;line-height:15px;padding:0 2px;">${options.ownedQty}</span>
</span>`
: `<span class="shop-card-icon">${escapeHtml(item.icon)}</span>`;
const durationLabel = options.isAutoFishing && Number(item.duration_minutes || 0) > 0
? `<div style="font-size:9px;margin-top:3px;color:#7c3aed;">⏱ 有效期 ${escapeHtml(formatMinutes(item.duration_minutes))}</div>`
: "";
const validityHtml = buildValidityHtml(item);
const ringBonus = options.isRing && (Number(item.intimacy_bonus || 0) > 0 || Number(item.charm_bonus || 0) > 0)
? `<div style="font-size:9px;margin-top:3px;display:flex;gap:8px;">
${Number(item.intimacy_bonus || 0) > 0 ? `<span style="color:#f43f5e;">💞 亲密 +${Number(item.intimacy_bonus || 0)}</span>` : ""}
@@ -319,10 +467,52 @@ function buildShopCardHtml(item, options) {
</div>
<div class="shop-card-desc">${escapeHtml(item.description ?? "")}</div>
${ringBonus}
${durationLabel}
${validityHtml}
`;
}
/**
* 生成购买前展示的有效期或生效方式文案。
*
* @param {Record<string, any>} item 商品数据
* @returns {string}
*/
function buildValidityText(item) {
if (Number(item.duration_days || 0) > 0) {
return `有效期:${Number(item.duration_days)}`;
}
if (Number(item.duration_minutes || 0) > 0) {
return `有效期:${formatMinutes(item.duration_minutes)}`;
}
if (item.type === "instant") {
return "购买后立即播放 1 次";
}
if (item.type === "ring") {
return "购买后存入背包,求婚时消耗";
}
if (["one_time", "sign_repair"].includes(item.type)) {
return "购买后存入背包,使用时消耗";
}
return "";
}
/**
* 生成商品卡片里的有效期标签 HTML。
*
* @param {Record<string, any>} item 商品数据
* @returns {string}
*/
function buildValidityHtml(item) {
const text = buildValidityText(item);
return text ? `<div class="shop-validity">⏱ ${escapeHtml(text)}</div>` : "";
}
/**
* 格式化分钟数,供自动钓鱼卡有效期展示。
*
@@ -351,7 +541,11 @@ async function confirmAndBuyItem(item) {
}
}
const confirmMessage = `确认花费 💰 ${Number(Number(item.price || 0) * quantity).toLocaleString()} 金币购买\n${item.name}${quantity > 1 ? ` × ${quantity}` : ""} 吗?`;
const validityText = buildValidityText(item);
const replacementText = DECORATION_TYPE_TO_SLOT[item.type]
? "\n购买说明:同类型只生效最新购买,原有同类型装扮会自动作废且不退款。"
: "";
const confirmMessage = `确认花费 💰 ${Number(Number(item.price || 0) * quantity).toLocaleString()} 金币购买\n${item.name}${quantity > 1 ? ` × ${quantity}` : ""}${validityText ? `\n${validityText}` : ""}${replacementText}\n\n确定购买吗?`;
const confirmed = await confirmShopPurchase(confirmMessage);
if (confirmed) {
@@ -401,7 +595,8 @@ export function openGiftDialog(item) {
}
if (itemName) {
itemName.textContent = `${item.icon} ${item.name}(💰 ${Number(item.price || 0).toLocaleString()}`;
const validityText = buildValidityText(item);
itemName.textContent = `${item.icon} ${item.name}(💰 ${Number(item.price || 0).toLocaleString()}${validityText ? ` · ${validityText}` : ""}`;
}
if (dialog) {
@@ -499,6 +694,14 @@ function handleBuySuccess(data, itemName) {
showShopToast(`${itemName} 购买成功!`, true);
// 装扮购买成功后先更新本地缓存,随后再拉接口刷新完整状态。
if (data.slot && data.style) {
activeDecorations[data.slot] = {
style: data.style,
expires_at: data.expires_at,
};
}
// 购买者本地也要立即看到特效,广播只负责其他在线用户。
if (data.play_effect && window.EffectManager) {
window.EffectManager.play(data.play_effect);
@@ -625,6 +828,7 @@ function exposeShopGlobals() {
window.loadShop = loadShop;
window.fetchShopData = fetchShopData;
window.renderShop = renderShop;
window.renderDecorations = renderDecorations;
window.openGiftDialog = openGiftDialog;
window.closeGiftDialog = closeGiftDialog;
window.confirmGift = confirmGift;
@@ -341,6 +341,40 @@
background: #f6faff;
}
/* 装扮列表区 — 与商品列表同风格 */
#shop-decorations-list {
flex: 1;
overflow-y: auto;
padding: 10px 12px;
display: none;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
align-content: start;
background: #f6faff;
}
/* Tab 导航 */
#shop-tabs {
display: flex;
border-bottom: 1px solid #cde;
flex-shrink: 0;
background: #eef4fb;
}
.shop-tab {
flex: 1;
padding: 8px 6px;
background: transparent;
border: none;
color: #6b7280;
font-size: 12px;
cursor: pointer;
border-bottom: 2px solid transparent;
font-weight: bold;
transition: all .2s;
}
.shop-tab.active { color: #336699 !important; border-bottom-color: #336699 !important; }
.shop-tab:hover { color: #5a8fc0; }
/* 分组标题 — 独占一整行 */
.shop-group-header {
grid-column: 1 / -1;
@@ -404,6 +438,37 @@
flex: 1;
}
/* 装扮Tab购买说明 */
.decoration-note {
font-size: 11px;
color: #b45309;
background: #fef3c7;
border: 1px solid #fde68a;
border-radius: 6px;
padding: 6px 10px;
margin-bottom: 10px;
grid-column: 1 / -1;
}
/* 装扮卡片状态行(独立一行,显示在商品名下方) */
.decoration-status-line {
margin-top: 4px;
}
/* 装扮卡片状态标签 */
.decoration-status {
font-size: 9px;
padding: 1px 6px;
border-radius: 8px;
}
.decoration-status.active { background: #065f46; color: #6ee7b7; }
.decoration-status.expired { background: #7f1d1d; color: #fca5a5; }
/* 装扮有效期提示 */
.decoration-duration {
font-size: 10px;
color: #6366f1;
margin-top: 2px;
}
.decoration-expiry { font-size: 10px; color: #9ca3af; margin-top: 2px; }
.shop-btn {
display: flex;
align-items: center;
@@ -598,11 +663,22 @@
{{-- Toast --}}
<div id="shop-toast"></div>
{{-- Tab 导航 --}}
<div id="shop-tabs">
<button class="shop-tab active" data-shop-tab="items">特效道具</button>
<button class="shop-tab" data-shop-tab="decorations">个人装扮</button>
</div>
{{-- 商品网格 --}}
<div id="shop-items-list">
<div style="grid-column:1/-1; text-align:center; color:#6366f1; padding:30px 0; font-size:13px;">加载中…</div>
</div>
{{-- 装扮网格 --}}
<div id="shop-decorations-list">
<div style="grid-column:1/-1; text-align:center; color:#6366f1; padding:30px 0; font-size:13px;">加载中…</div>
</div>
{{-- 改名内嵌遮罩 --}}
<div id="shop-rename-overlay">
<div id="shop-rename-box">
+270 -10
View File
@@ -24,6 +24,238 @@
@version 2.0.0
--}}
{{-- 个人装扮样式(消息气泡 / 昵称颜色 / 头像框) --}}
<style>
/* ========== 消息气泡装扮:在原版逐行消息基础上增加纹理、角标和轻量动效 ========== */
.msg-line[class*="msg-bubble--"] {
position: relative;
isolation: isolate;
min-height: 24px;
margin: 4px 0;
padding: 5px 12px 5px 14px;
border-radius: 8px;
overflow: hidden;
border: 1px solid rgba(51, 102, 153, .16);
background: rgba(255, 255, 255, .72);
box-shadow: 0 1px 4px rgba(51, 102, 153, .12);
}
.msg-line[class*="msg-bubble--"]::before,
.msg-line[class*="msg-bubble--"]::after {
content: "";
position: absolute;
pointer-events: none;
z-index: 0;
}
.msg-line[class*="msg-bubble--"] > * {
position: relative;
z-index: 1;
}
.msg-bubble--golden {
border-color: rgba(217, 119, 6, .32) !important;
background:
linear-gradient(90deg, rgba(245, 158, 11, .32) 0 4px, transparent 4px),
radial-gradient(circle at 28px 8px, rgba(255, 255, 255, .85), transparent 10px),
linear-gradient(135deg, #fff8df 0%, #fffdf5 56%, #fff1c2 100%) !important;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .78), 0 2px 8px rgba(217, 119, 6, .18);
}
.msg-bubble--golden::after {
top: 0;
bottom: 0;
left: -36px;
width: 36px;
background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .72), transparent);
animation: msg-bubble-shine 3.6s ease-in-out infinite;
}
.msg-bubble--sakura {
border-color: rgba(244, 114, 182, .32) !important;
background:
radial-gradient(circle at 18px 10px, rgba(244, 114, 182, .42) 0 2px, transparent 3px),
radial-gradient(circle at 44px 20px, rgba(251, 207, 232, .86) 0 3px, transparent 4px),
linear-gradient(135deg, #fff7fb 0%, #fff 48%, #ffe4f1 100%) !important;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .78), 0 2px 8px rgba(244, 114, 182, .14);
}
.msg-bubble--star {
border-color: rgba(79, 70, 229, .32) !important;
background:
radial-gradient(circle at 20px 9px, rgba(255, 255, 255, .9) 0 1px, transparent 2px),
radial-gradient(circle at 76px 20px, rgba(99, 102, 241, .36) 0 2px, transparent 3px),
linear-gradient(135deg, #eef2ff 0%, #f8fbff 54%, #dbeafe 100%) !important;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .8), 0 2px 10px rgba(79, 70, 229, .16);
}
.msg-bubble--star::before {
right: 10px;
top: 5px;
width: 42px;
height: 16px;
background: radial-gradient(circle, rgba(67, 56, 202, .42) 0 1px, transparent 2px);
background-size: 11px 8px;
opacity: .72;
}
.msg-bubble--rainbow {
border-color: rgba(59, 130, 246, .22) !important;
background:
linear-gradient(#ffffffd9, #ffffffd9) padding-box,
linear-gradient(120deg, rgba(239, 68, 68, .16), rgba(245, 158, 11, .16), rgba(34, 197, 94, .16), rgba(59, 130, 246, .16), rgba(168, 85, 247, .16)) border-box !important;
box-shadow: 0 2px 10px rgba(59, 130, 246, .14);
}
.msg-bubble--rainbow::before {
left: 0;
right: 0;
top: 0;
height: 3px;
background: linear-gradient(90deg, #ef4444, #f59e0b, #22c55e, #3b82f6, #a855f7, #ef4444);
background-size: 180% 100%;
animation: msg-bubble-rainbow 4.2s linear infinite;
}
.msg-bubble--crown {
border-color: rgba(180, 83, 9, .34) !important;
background:
linear-gradient(90deg, rgba(180, 83, 9, .24) 0 4px, transparent 4px),
radial-gradient(circle at right 12px top 8px, rgba(251, 191, 36, .36), transparent 18px),
linear-gradient(135deg, #fff7d6 0%, #fffdfa 46%, #fde68a 100%) !important;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .82), 0 3px 12px rgba(180, 83, 9, .22);
}
.msg-bubble--crown::after {
content: "";
top: 2px;
right: 8px;
z-index: 0;
color: rgba(180, 83, 9, .26);
font-size: 18px;
line-height: 1;
}
@keyframes msg-bubble-shine {
0%, 62% { transform: translateX(0); opacity: 0; }
72% { opacity: .82; }
100% { transform: translateX(280px); opacity: 0; }
}
@keyframes msg-bubble-rainbow {
from { background-position: 0% 50%; }
to { background-position: 180% 50%; }
}
/* ========== 昵称颜色 ========== */
.msg-name--golden { color: #fbbf24 !important; font-weight: 700; }
.msg-name--rainbow {
background: linear-gradient(90deg, #ef4444, #f59e0b, #22c55e, #3b82f6, #a855f7);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: 700;
}
.msg-name--glow {
color: #e2e8f0 !important;
text-shadow: 0 0 6px #818cf8, 0 0 14px #6366f1;
}
.msg-name--flame {
color: #f97316 !important;
font-weight: 700;
animation: name-flame 1.5s ease-in-out infinite;
}
@keyframes name-flame {
0%, 100% { text-shadow: 0 0 4px #ef4444; }
50% { text-shadow: 0 0 10px #fbbf24, 0 0 16px #ef4444; }
}
/* ========== 头像框 ========== */
.avatar-frame-wrapper {
position: relative;
display: inline-grid;
place-items: center;
width: 44px;
height: 44px;
flex: 0 0 44px;
line-height: 0;
}
.avatar-frame-wrapper .user-head {
position: relative;
z-index: 2;
width: 36px;
height: 36px;
border-radius: 50%;
}
.avatar-frame {
position: absolute;
inset: 0;
border-radius: 50%;
pointer-events: none;
z-index: 1;
}
.avatar-frame::before,
.avatar-frame::after {
content: "";
position: absolute;
pointer-events: none;
}
.avatar-frame--silver {
background: conic-gradient(from 25deg, #ffffff, #94a3b8, #e2e8f0, #64748b, #ffffff);
box-shadow: 0 0 0 1px rgba(148, 163, 184, .38), 0 2px 8px rgba(100, 116, 139, .24);
}
.avatar-frame--silver::before,
.avatar-frame--gold::before,
.avatar-frame--star::before,
.avatar-frame--dragon::before {
inset: 4px;
border-radius: 50%;
background: #eaf3ff;
}
.avatar-frame--gold {
background: conic-gradient(from -20deg, #fff7ad, #f59e0b, #fff1a6, #b45309, #fff7ad);
box-shadow: 0 0 0 1px rgba(217, 119, 6, .34), 0 0 12px rgba(245, 158, 11, .38);
}
.avatar-frame--star {
background:
radial-gradient(circle at 50% 0%, #ffffff 0 2px, transparent 3px),
conic-gradient(from 0deg, #fef08a, #818cf8, #ffffff, #fbbf24, #818cf8, #fef08a);
box-shadow: 0 0 14px rgba(129, 140, 248, .48);
animation: frame-rotate 4s linear infinite;
}
.avatar-frame--star::after {
inset: -2px;
border-radius: 50%;
background: radial-gradient(circle at 50% 0%, rgba(255, 255, 255, .9) 0 2px, transparent 3px);
transform-origin: 50% 50%;
}
.avatar-frame--dragon {
background:
conic-gradient(from 45deg, #7f1d1d, #f59e0b, #ef4444, #991b1b, #f59e0b, #7f1d1d);
box-shadow: 0 0 14px rgba(239, 68, 68, .42), 0 0 0 1px rgba(127, 29, 29, .38);
}
.avatar-frame--dragon::after {
inset: 5px;
border-radius: 50%;
border: 1px dashed rgba(254, 202, 202, .82);
}
@keyframes frame-rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
<script>
/**
* 聊天室前端交互逻辑
@@ -1821,9 +2053,25 @@
// 女生名字使用玫粉色
const nameColor = (user.sex == 2) ? 'color:#e91e8c;' : '';
// ── 昵称颜色装扮 ──
var userNameExtraClass = '';
if (user.name_color) {
userNameExtraClass = ' msg-name--' + user.name_color.replace(/^msg_name_/, '');
}
// ── 头像框装扮 ──
var avatarHtml = '';
if (user.avatar_frame) {
var frameClass = 'avatar-frame--' + user.avatar_frame.replace(/^avatar_frame_/, '');
avatarHtml = '<span class="avatar-frame-wrapper">' +
'<span class="avatar-frame ' + frameClass + '"></span>' +
'<img class="user-head" src="' + headImgSrc + '" onerror="this.src=\'/images/headface/1.gif\'">' +
'</span>';
} else {
avatarHtml = '<img class="user-head" src="' + headImgSrc + '" onerror="this.src=\'/images/headface/1.gif\'">';
}
item.innerHTML = `
<img class="user-head" src="${headImgSrc}" onerror="this.src='/images/headface/1.gif'">
<span class="user-name" style="${nameColor}">${username}</span>
${avatarHtml}
<span class="user-name${userNameExtraClass}" style="${nameColor}">${username}</span>
<span class="user-badge-slot">${badges}</span>
`;
@@ -1834,6 +2082,7 @@
targetContainer.replaceChildren(fragment);
refreshRenderedUserBadges(targetContainer);
}
window._renderUserListToContainer = _renderUserListToContainer;
function renderUserList() {
if (userListRenderTimer) {
@@ -2122,9 +2371,20 @@
div.dataset.blockKey = blockRuleKey;
}
// ── 消息气泡装扮 ──
if (msg.msg_bubble) {
var bubbleStyle = msg.msg_bubble.replace(/^msg_bubble_/, '');
div.classList.add('msg-bubble--' + bubbleStyle);
}
const timeStr = msg.sent_at || '';
let timeStrOverride = false;
var nameClass = '';
if (msg.msg_name_color) {
nameClass = ' msg-name--' + msg.msg_name_color.replace(/^msg_name_/, '');
}
// 系统用户名列表(不可被选为聊天对象)
const systemUsers = ['钓鱼播报', '星海小博士', '系统传音', '系统公告', '送花播报', '系统', '欢迎', '系统播报', '神秘箱子'];
@@ -2201,15 +2461,15 @@
};
// 用户名(单击切换发言对象,双击查看资料;事件委托已迁至 Vite right-panel.js
const clickableUser = (uName, color) => {
const clickableUser = (uName, color, extraClass = '') => {
const safeName = escapeHtml(uName);
if (uName === 'AI小班长') {
return `<span class="msg-user" data-chat-message-user data-u="${safeName}" style="color: ${color}; cursor: pointer;">${safeName}</span>`;
return `<span class="msg-user${extraClass}" data-chat-message-user data-u="${safeName}" style="color: ${color}; cursor: pointer;">${safeName}</span>`;
}
if (systemUsers.includes(uName) || isGameLabel(uName)) {
return `<span class="msg-user" style="color: ${color};">${safeName}</span>`;
return `<span class="msg-user${extraClass}" style="color: ${color};">${safeName}</span>`;
}
return `<span class="msg-user" data-chat-message-user data-u="${safeName}" style="color: ${color}; cursor: pointer;">${safeName}</span>`;
return `<span class="msg-user${extraClass}" data-chat-message-user data-u="${safeName}" style="color: ${color}; cursor: pointer;">${safeName}</span>`;
};
// 普通用户(包括 AI小班长)用数据库头像,播报类用特殊喇叭图标
@@ -2330,7 +2590,7 @@
});
html =
`${headImg}<span style="font-weight: bold;">${clickableUser(msg.from_user, fontColor)}</span><span class="msg-content" style="color: ${fontColor}">${parsedContent}</span>${giftHtml}`;
`${headImg}<span style="font-weight: bold;">${clickableUser(msg.from_user, fontColor, nameClass)}</span><span class="msg-content" style="color: ${fontColor}">${parsedContent}</span>${giftHtml}`;
}
} else if (msg.is_secret) {
if (msg.from_user === '系统') {
@@ -2341,7 +2601,7 @@
`<span style="color:#16a34a;font-weight:bold;">📢 系统:</span><span style="color:#15803d;">${msg.content}</span>`;
} else {
// 普通悄悄话样式(原版:紫色斜体,使用自然语序动作)
const fromHtml = clickableUser(msg.from_user, '#cc00cc');
const fromHtml = clickableUser(msg.from_user, '#cc00cc', nameClass);
const toHtml = clickableUser(msg.to_user, '#cc00cc');
const verbStr = msg.action ?
buildActionStr(msg.action, fromHtml, toHtml, '悄悄说') :
@@ -2351,7 +2611,7 @@
}
} else if (msg.to_user && msg.to_user !== '大家') {
// 对特定对象说话
const fromHtml = clickableUser(msg.from_user, '#000099');
const fromHtml = clickableUser(msg.from_user, '#000099', nameClass);
const toHtml = clickableUser(msg.to_user, '#000099');
const verbStr = msg.action ?
buildActionStr(msg.action, fromHtml, toHtml) :
@@ -2359,7 +2619,7 @@
html = `${headImg}${verbStr}<span class="msg-content" style="color: ${fontColor}">${messageBodyHtml}</span>`;
} else {
// 对大家说话
const fromHtml = clickableUser(msg.from_user, '#000099');
const fromHtml = clickableUser(msg.from_user, '#000099', nameClass);
const verbStr = msg.action ?
buildActionStr(msg.action, fromHtml, '大家') :
`${fromHtml}对大家说:`;
@@ -9,8 +9,8 @@
#shop-panel {
display: none;
position: absolute;
/* 顶部 tab 栏高度约 26px,底部状态栏约 22px */
top: 26px;
/* 顶部 tab 栏高度约 56px,底部状态栏约 22px */
top: 56px;
left: 0;
right: 0;
bottom: 22px;
@@ -19,6 +19,9 @@
z-index: 10;
}
.shop-tab.active { color: #e2e8f0 !important; border-bottom-color: #818cf8 !important; }
.shop-tab:hover { color: #c7d2fe; }
#shop-balance-bar {
padding: 6px 8px;
background: linear-gradient(135deg, #1e1b4b, #312e81);
@@ -133,6 +136,21 @@
line-height: 1.35;
}
.shop-validity {
display: inline-flex;
align-items: center;
width: fit-content;
margin-top: 4px;
padding: 1px 6px;
border-radius: 999px;
background: rgba(79, 70, 229, .18);
border: 1px solid rgba(129, 140, 248, .42);
color: #c7d2fe;
font-size: 9px;
font-weight: 700;
line-height: 1.5;
}
/* 购买按钮 */
.shop-btn {
display: inline-flex;
@@ -241,6 +259,37 @@
margin-top: 5px;
min-height: 14px;
}
/* ── 装扮卡片状态标签 ────────────── */
.decoration-status {
font-size: 9px;
padding: 1px 6px;
border-radius: 8px;
margin-left: 6px;
}
.decoration-status.active { background: #065f46; color: #6ee7b7; }
.decoration-expiry { font-size: 9px; color: #9ca3af; margin-top: 2px; }
.decoration-note {
grid-column: 1 / -1;
margin: 0 0 6px;
padding: 7px 8px;
border: 1px solid rgba(251, 191, 36, .35);
border-radius: 8px;
background: rgba(120, 53, 15, .28);
color: #fde68a;
font-size: 10px;
line-height: 1.45;
}
#shop-decorations-list {
flex: 1;
overflow-y: auto;
padding: 6px 5px;
scrollbar-width: thin;
scrollbar-color: #4338ca #0f0c29;
display: none;
}
</style>
<div id="shop-panel"
@@ -254,14 +303,25 @@
<span id="shop-week-badge"></span>
</div>
{{-- Tab 导航 --}}
<div id="shop-tabs" style="display:flex; border-bottom: 1px solid #3730a3; flex-shrink: 0;">
<button class="shop-tab active" data-shop-tab="items" style="flex:1; padding: 6px; background: transparent; border: none; color: #a5b4fc; font-size: 11px; cursor: pointer; border-bottom: 2px solid transparent;">特效道具</button>
<button class="shop-tab" data-shop-tab="decorations" style="flex:1; padding: 6px; background: transparent; border: none; color: #6b7280; font-size: 11px; cursor: pointer; border-bottom: 2px solid transparent;">个人装扮</button>
</div>
{{-- Toast --}}
<div id="shop-toast"></div>
{{-- 商品列表 --}}
{{-- 特效道具列表 --}}
<div id="shop-items-list">
<div style="text-align:center;color:#6366f1;padding:20px 0;font-size:11px;">加载中…</div>
</div>
{{-- 个人装扮列表 --}}
<div id="shop-decorations-list">
<div style="text-align:center;color:#6366f1;padding:20px 0;font-size:11px;">加载中…</div>
</div>
{{-- 改名弹框 --}}
<div id="rename-modal">
<div id="rename-modal-inner">