feat: 新增消息文字颜色特效装扮(七彩/流光/霓虹/火焰/冰蓝)
- 新增 msg_text_color 商品类型,扩展 shop_items.type ENUM
- DecorationService 支持 text_color 槽位,自动注入消息广播
- CSS 动画:rainbow(彩虹流动)、shimmer(金属流光)、neon(霓虹脉动)、flame(火焰跃动)、ice(冰蓝流转)
- ShopItemSeeder 新增 5 款文字颜色特效商品
- 商店前端新增「🌈 文字颜色」装扮分组
- 消息渲染 appendMessage/buildChatMessageContent 支持文字特效 class
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
* - bubble : 消息气泡边框样式
|
||||
* - name_color : 昵称颜色效果
|
||||
* - avatar_frame: 头像装饰边框
|
||||
* - text_color : 消息文字颜色特效
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
@@ -37,6 +38,7 @@ class DecorationService
|
||||
'msg_bubble' => 'bubble',
|
||||
'msg_name_color' => 'name_color',
|
||||
'avatar_frame' => 'avatar_frame',
|
||||
'msg_text_color' => 'text_color',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -173,11 +175,11 @@ class DecorationService
|
||||
/**
|
||||
* 获取消息广播 payload 需要携带的装扮字段。
|
||||
*
|
||||
* 消息广播只需要气泡样式(msg_bubble)和昵称颜色(msg_name_color),
|
||||
* 消息广播只需要气泡样式(msg_bubble)、昵称颜色(msg_name_color)和文字颜色特效(msg_text_color),
|
||||
* 头像框不需要在消息中展示。
|
||||
*
|
||||
* @param User $user 消息发送者
|
||||
* @return array{msg_bubble?:string, msg_name_color?:string}
|
||||
* @return array{msg_bubble?:string, msg_name_color?:string, msg_text_color?:string}
|
||||
*/
|
||||
public function getDecorationsForMessage(User $user): array
|
||||
{
|
||||
@@ -190,6 +192,9 @@ class DecorationService
|
||||
if (! empty($decorations['name_color']['style'])) {
|
||||
$result['msg_name_color'] = $decorations['name_color']['style'];
|
||||
}
|
||||
if (! empty($decorations['text_color']['style'])) {
|
||||
$result['msg_text_color'] = $decorations['text_color']['style'];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:为店铺商品类型加入消息文字颜色特效装扮(msg_text_color)。
|
||||
*
|
||||
* 用户在商店购买文字颜色特效后,消息正文呈现动态色彩效果(七彩、流光、霓虹等)。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 扩展 shop_items.type ENUM,加入 msg_text_color 类型。
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (DB::getDriverName() === 'mysql') {
|
||||
DB::statement("ALTER TABLE `shop_items` MODIFY `type` ENUM('instant','duration','one_time','ring','auto_fishing','sign_repair','msg_bubble','msg_name_color','avatar_frame','msg_text_color') NOT NULL COMMENT '道具类型'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚:移除 msg_text_color,将已有该类型商品标记为 one_time。
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (DB::getDriverName() === 'mysql') {
|
||||
DB::statement("UPDATE `shop_items` SET `type` = 'one_time' WHERE `type` = 'msg_text_color'");
|
||||
DB::statement("ALTER TABLE `shop_items` MODIFY `type` ENUM('instant','duration','one_time','ring','auto_fishing','sign_repair','msg_bubble','msg_name_color','avatar_frame') NOT NULL COMMENT '道具类型'");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -93,19 +93,19 @@ class ShopItemSeeder extends Seeder
|
||||
// ── 消息气泡装扮 ──────────────────────────
|
||||
['name' => '鎏金流光气泡', 'slug' => 'msg_bubble_golden', 'icon' => '🟡',
|
||||
'description' => '浅金底纹、左侧金线和流光扫过,发言更醒目但不刺眼。',
|
||||
'price' => 300, 'type' => 'msg_bubble', 'duration_days' => 1, 'sort_order' => 50],
|
||||
'price' => 3000, 'type' => 'msg_bubble', 'duration_days' => 1, 'sort_order' => 50],
|
||||
['name' => '樱语花笺气泡', 'slug' => 'msg_bubble_sakura', 'icon' => '🌸',
|
||||
'description' => '粉白信笺底色配花点纹理,适合温柔、浪漫的发言氛围。',
|
||||
'price' => 500, 'type' => 'msg_bubble', 'duration_days' => 3, 'sort_order' => 51],
|
||||
'price' => 3000, 'type' => 'msg_bubble', 'duration_days' => 1, 'sort_order' => 51],
|
||||
['name' => '星河微光气泡', 'slug' => 'msg_bubble_star', 'icon' => '🌌',
|
||||
'description' => '淡蓝星河底纹和微光星点,保留清爽阅读感。',
|
||||
'price' => 800, 'type' => 'msg_bubble', 'duration_days' => 7, 'sort_order' => 52],
|
||||
'price' => 5000, 'type' => 'msg_bubble', 'duration_days' => 1, 'sort_order' => 52],
|
||||
['name' => '霓虹彩带气泡', 'slug' => 'msg_bubble_rainbow', 'icon' => '🌈',
|
||||
'description' => '顶部流动彩带和浅色底框,发言有动态高光。',
|
||||
'price' => 1500, 'type' => 'msg_bubble', 'duration_days' => 7, 'sort_order' => 53],
|
||||
'price' => 8000, 'type' => 'msg_bubble', 'duration_days' => 1, 'sort_order' => 53],
|
||||
['name' => '皇冠礼赞气泡', 'slug' => 'msg_bubble_crown', 'icon' => '👑',
|
||||
'description' => '金色礼赞底纹、皇冠角标和侧边金线,突出尊贵发言。',
|
||||
'price' => 3000, 'type' => 'msg_bubble', 'duration_days' => 30, 'sort_order' => 54],
|
||||
'price' => 8000, 'type' => 'msg_bubble', 'duration_days' => 1, 'sort_order' => 54],
|
||||
|
||||
// ── 昵称颜色装扮 ──────────────────────────
|
||||
['name' => '金色昵称', 'slug' => 'msg_name_golden', 'icon' => '🥇',
|
||||
@@ -113,27 +113,44 @@ class ShopItemSeeder extends Seeder
|
||||
'price' => 200, 'type' => 'msg_name_color', 'duration_days' => 1, 'sort_order' => 60],
|
||||
['name' => '渐变昵称', 'slug' => 'msg_name_rainbow', 'icon' => '🎨',
|
||||
'description' => '彩虹渐变色昵称,五彩斑斓。',
|
||||
'price' => 500, 'type' => 'msg_name_color', 'duration_days' => 3, 'sort_order' => 61],
|
||||
'price' => 500, 'type' => 'msg_name_color', 'duration_days' => 1, 'sort_order' => 61],
|
||||
['name' => '发光昵称', 'slug' => 'msg_name_glow', 'icon' => '✨',
|
||||
'description' => '昵称带柔和发光效果,暗夜中最亮的星。',
|
||||
'price' => 800, 'type' => 'msg_name_color', 'duration_days' => 7, 'sort_order' => 62],
|
||||
'price' => 800, 'type' => 'msg_name_color', 'duration_days' => 1, 'sort_order' => 62],
|
||||
['name' => '火焰昵称', 'slug' => 'msg_name_flame', 'icon' => '🔥',
|
||||
'description' => '火焰色脉动昵称,热情似火。',
|
||||
'price' => 1500, 'type' => 'msg_name_color', 'duration_days' => 7, 'sort_order' => 63],
|
||||
'price' => 1500, 'type' => 'msg_name_color', 'duration_days' => 1, 'sort_order' => 63],
|
||||
|
||||
// ── 消息文字颜色特效装扮 ─────────────────
|
||||
['name' => '七彩文字', 'slug' => 'msg_text_rainbow', 'icon' => '🌈',
|
||||
'description' => '文字色彩在彩虹七色间平滑流动,发言自带虹光特效。',
|
||||
'price' => 5000, 'type' => 'msg_text_color', 'duration_days' => 1, 'sort_order' => 65],
|
||||
['name' => '流光文字', 'slug' => 'msg_text_shimmer', 'icon' => '💫',
|
||||
'description' => '一道流光从左到右扫过文字表面,如金属般闪亮。',
|
||||
'price' => 5000, 'type' => 'msg_text_color', 'duration_days' => 1, 'sort_order' => 66],
|
||||
['name' => '霓虹文字', 'slug' => 'msg_text_neon', 'icon' => '💜',
|
||||
'description' => '紫蓝霓虹光晕在文字周围脉动呼吸,像灯牌一样醒目。',
|
||||
'price' => 8000, 'type' => 'msg_text_color', 'duration_days' => 1, 'sort_order' => 67],
|
||||
['name' => '火焰文字', 'slug' => 'msg_text_flame', 'icon' => '🔥',
|
||||
'description' => '橙红烈火在文字中上下跃动,热情燃烧般的视觉冲击。',
|
||||
'price' => 8000, 'type' => 'msg_text_color', 'duration_days' => 1, 'sort_order' => 68],
|
||||
['name' => '冰蓝文字', 'slug' => 'msg_text_ice', 'icon' => '❄️',
|
||||
'description' => '冰蓝晶光在文字表面流转,如同冰晶折射出的冷艳光泽。',
|
||||
'price' => 8000, 'type' => 'msg_text_color', 'duration_days' => 1, 'sort_order' => 69],
|
||||
|
||||
// ── 头像框装扮 ────────────────────────────
|
||||
['name' => '月银守护头像框', 'slug' => 'avatar_frame_silver', 'icon' => '🥈',
|
||||
'description' => '银白金属光泽外框,低调但比普通头像更精致。',
|
||||
'price' => 500, 'type' => 'avatar_frame', 'duration_days' => 7, 'sort_order' => 70],
|
||||
'price' => 5000, 'type' => 'avatar_frame', 'duration_days' => 1, 'sort_order' => 70],
|
||||
['name' => '金辉勋章头像框', 'slug' => 'avatar_frame_gold', 'icon' => '🥇',
|
||||
'description' => '金色勋章质感外框,带柔和光晕。',
|
||||
'price' => 1000, 'type' => 'avatar_frame', 'duration_days' => 7, 'sort_order' => 71],
|
||||
'price' => 5000, 'type' => 'avatar_frame', 'duration_days' => 1, 'sort_order' => 71],
|
||||
['name' => '星轨环绕头像框', 'slug' => 'avatar_frame_star', 'icon' => '⭐',
|
||||
'description' => '星轨渐变环绕头像旋转,适合高调展示。',
|
||||
'price' => 2000, 'type' => 'avatar_frame', 'duration_days' => 14, 'sort_order' => 72],
|
||||
'price' => 8000, 'type' => 'avatar_frame', 'duration_days' => 1, 'sort_order' => 72],
|
||||
['name' => '龙焰御守头像框', 'slug' => 'avatar_frame_dragon', 'icon' => '🐉',
|
||||
'description' => '红金御守质感外框,带虚线纹理和强烈光晕。',
|
||||
'price' => 5000, 'type' => 'avatar_frame', 'duration_days' => 30, 'sort_order' => 73],
|
||||
'price' => 8000, 'type' => 'avatar_frame', 'duration_days' => 1, 'sort_order' => 73],
|
||||
];
|
||||
|
||||
foreach ($items as $item) {
|
||||
|
||||
@@ -58,6 +58,11 @@ const DECORATION_GROUPS = [
|
||||
desc: "同类型只保留最新购买",
|
||||
type: "msg_name_color",
|
||||
},
|
||||
{
|
||||
label: "🌈 文字颜色",
|
||||
desc: "同类型只保留最新购买",
|
||||
type: "msg_text_color",
|
||||
},
|
||||
{
|
||||
label: "🖼️ 头像框",
|
||||
desc: "同类型只保留最新购买",
|
||||
@@ -68,6 +73,7 @@ const DECORATION_GROUPS = [
|
||||
const DECORATION_TYPE_TO_SLOT = {
|
||||
msg_bubble: "bubble",
|
||||
msg_name_color: "name_color",
|
||||
msg_text_color: "text_color",
|
||||
avatar_frame: "avatar_frame",
|
||||
};
|
||||
|
||||
|
||||
@@ -170,6 +170,82 @@
|
||||
50% { text-shadow: 0 0 10px #fbbf24, 0 0 16px #ef4444; }
|
||||
}
|
||||
|
||||
/* ========== 消息文字颜色特效 ========== */
|
||||
.msg-text--rainbow {
|
||||
background: linear-gradient(90deg,
|
||||
#ef4444, #f97316, #eab308, #22c55e, #06b6d4, #3b82f6, #a855f7, #ef4444);
|
||||
background-size: 300% 100%;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
font-weight: 600;
|
||||
animation: text-rainbow 3.5s linear infinite;
|
||||
}
|
||||
@keyframes text-rainbow {
|
||||
0% { background-position: 0% 50%; }
|
||||
100% { background-position: 300% 50%; }
|
||||
}
|
||||
|
||||
.msg-text--shimmer {
|
||||
background: linear-gradient(110deg,
|
||||
#6b7280 0%, #9ca3af 18%, #f3f4f6 28%, #d1d5db 36%,
|
||||
#6b7280 52%, #9ca3af 66%, #f9fafb 74%, #6b7280 100%);
|
||||
background-size: 300% 100%;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
font-weight: 600;
|
||||
animation: text-shimmer 4s ease-in-out infinite;
|
||||
}
|
||||
@keyframes text-shimmer {
|
||||
0% { background-position: 100% 50%; }
|
||||
100% { background-position: -200% 50%; }
|
||||
}
|
||||
|
||||
.msg-text--neon {
|
||||
color: #a855f7 !important;
|
||||
font-weight: 600;
|
||||
text-shadow:
|
||||
0 0 7px #a855f7,
|
||||
0 0 14px #7c3aed,
|
||||
0 0 28px #6366f1,
|
||||
0 0 42px #4f46e5;
|
||||
animation: text-neon 2s ease-in-out infinite alternate;
|
||||
}
|
||||
@keyframes text-neon {
|
||||
0% { text-shadow: 0 0 7px #a855f7, 0 0 14px #7c3aed, 0 0 28px #6366f1; }
|
||||
100% { text-shadow: 0 0 14px #c084fc, 0 0 28px #a855f7, 0 0 48px #7c3aed, 0 0 64px #6366f1; }
|
||||
}
|
||||
|
||||
.msg-text--flame {
|
||||
background: linear-gradient(180deg, #fef3c7 0%, #f59e0b 28%, #ea580c 60%, #dc2626 100%);
|
||||
background-size: 100% 200%;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
font-weight: 700;
|
||||
animation: text-flame 1.2s ease-in-out infinite alternate;
|
||||
}
|
||||
@keyframes text-flame {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 0% 100%; }
|
||||
}
|
||||
|
||||
.msg-text--ice {
|
||||
background: linear-gradient(135deg, #e0f2fe 0%, #7dd3fc 25%, #38bdf8 50%, #bae6fd 75%, #e0f2fe 100%);
|
||||
background-size: 200% 200%;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
font-weight: 600;
|
||||
animation: text-ice 3s ease-in-out infinite;
|
||||
filter: drop-shadow(0 0 4px rgba(56, 189, 248, 0.5));
|
||||
}
|
||||
@keyframes text-ice {
|
||||
0%, 100% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
}
|
||||
|
||||
/* ========== 头像框 ========== */
|
||||
.avatar-frame-wrapper {
|
||||
position: relative;
|
||||
@@ -1630,15 +1706,16 @@
|
||||
/**
|
||||
* 构建普通聊天消息的正文区域,支持缩略图与过期占位渲染。
|
||||
*/
|
||||
function buildChatMessageContent(msg, fontColor) {
|
||||
function buildChatMessageContent(msg, fontColor, textColorClass) {
|
||||
const rawContent = msg.content || '';
|
||||
|
||||
if (msg.message_type === 'image' && !isExpiredChatImageMessage(msg)) {
|
||||
const fullUrl = escapeHtml(msg.image_url || '');
|
||||
const thumbUrl = escapeHtml(msg.image_thumb_url || '');
|
||||
const imageName = escapeHtml(msg.image_original_name || '聊天图片');
|
||||
const captionColorStyle = textColorClass ? '' : `color:${fontColor};`;
|
||||
const captionHtml = rawContent ?
|
||||
`<span style="display:inline-block; max-width:220px; color:${fontColor}; line-height:1.55;">${rawContent}</span>` :
|
||||
`<span class="msg-content${textColorClass || ''}" style="display:inline-block; max-width:220px; ${captionColorStyle} line-height:1.55;">${rawContent}</span>` :
|
||||
'';
|
||||
|
||||
return `
|
||||
@@ -1654,8 +1731,9 @@
|
||||
}
|
||||
|
||||
if (msg.message_type === 'expired_image' || isExpiredChatImageMessage(msg)) {
|
||||
const captionColorStyle = textColorClass ? '' : `color:${fontColor};`;
|
||||
const captionHtml = rawContent ?
|
||||
`<span style="display:inline-block; color:${fontColor}; line-height:1.55;">${rawContent}</span>` :
|
||||
`<span class="msg-content${textColorClass || ''}" style="display:inline-block; ${captionColorStyle} line-height:1.55;">${rawContent}</span>` :
|
||||
'';
|
||||
|
||||
return `
|
||||
@@ -2368,6 +2446,11 @@
|
||||
nameClass = ' msg-name--' + msg.msg_name_color.replace(/^msg_name_/, '');
|
||||
}
|
||||
|
||||
var textColorClass = '';
|
||||
if (msg.msg_text_color) {
|
||||
textColorClass = ' msg-text--' + msg.msg_text_color.replace(/^msg_text_/, '');
|
||||
}
|
||||
|
||||
// 系统用户名列表(不可被选为聊天对象)
|
||||
const systemUsers = ['钓鱼播报', '星海小博士', '系统传音', '系统公告', '送花播报', '系统', '欢迎', '系统播报', '神秘箱子'];
|
||||
|
||||
@@ -2464,7 +2547,7 @@
|
||||
}
|
||||
const headImg =
|
||||
`<img src="${headImgSrc}" style="display:inline;width:16px;height:16px;vertical-align:middle;margin-right:2px;mix-blend-mode: multiply;" onerror="this.src='/images/headface/1.gif'">`;
|
||||
const messageBodyHtml = buildChatMessageContent(msg, fontColor);
|
||||
const messageBodyHtml = buildChatMessageContent(msg, fontColor, textColorClass);
|
||||
|
||||
let html = '';
|
||||
|
||||
@@ -2559,7 +2642,7 @@
|
||||
return '【' + clickableUser(uName, '#000099') + '】';
|
||||
});
|
||||
html =
|
||||
`${headImg}<span style="font-weight: bold;">${clickableUser(msg.from_user, fontColor, nameClass)}:</span><span class="msg-content" style="color: ${fontColor}; font-weight: bold;">${parsedContent}</span>`;
|
||||
`${headImg}<span style="font-weight: bold;">${clickableUser(msg.from_user, fontColor, nameClass)}:</span><span class="msg-content${textColorClass}" style="color: ${fontColor}; font-weight: bold;">${parsedContent}</span>`;
|
||||
} else {
|
||||
// 自动升级播报 / 赠礼通知 / 婚恋广播 等非游戏系统传音:金色左边框,轻量提示样式,不喧宾夺主
|
||||
div.style.cssText =
|
||||
@@ -2593,7 +2676,7 @@
|
||||
});
|
||||
|
||||
html =
|
||||
`${headImg}<span style="font-weight: bold;">${clickableUser(msg.from_user, fontColor, nameClass)}:</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${textColorClass}" style="color: ${fontColor}">${parsedContent}</span>${giftHtml}`;
|
||||
}
|
||||
} else if (msg.is_secret) {
|
||||
if (msg.from_user === '系统') {
|
||||
@@ -2610,7 +2693,7 @@
|
||||
buildActionStr(msg.action, fromHtml, toHtml, '悄悄说') :
|
||||
`${fromHtml}对${toHtml}悄悄说:`;
|
||||
html =
|
||||
`${headImg}<span class="msg-secret">${verbStr}</span><span class="msg-content" style="color: ${fontColor}; font-style: italic;">${messageBodyHtml}</span>`;
|
||||
`${headImg}<span class="msg-secret">${verbStr}</span><span class="msg-content${textColorClass}" style="color: ${fontColor}; font-style: italic;">${messageBodyHtml}</span>`;
|
||||
}
|
||||
} else if (msg.to_user && msg.to_user !== '大家') {
|
||||
// 对特定对象说话
|
||||
@@ -2619,14 +2702,14 @@
|
||||
const verbStr = msg.action ?
|
||||
buildActionStr(msg.action, fromHtml, toHtml) :
|
||||
`${fromHtml}对${toHtml}说:`;
|
||||
html = `${headImg}${verbStr}<span class="msg-content" style="color: ${fontColor}">${messageBodyHtml}</span>`;
|
||||
html = `${headImg}${verbStr}<span class="msg-content${textColorClass}" style="color: ${fontColor}">${messageBodyHtml}</span>`;
|
||||
} else {
|
||||
// 对大家说话
|
||||
const fromHtml = clickableUser(msg.from_user, '#000099', nameClass);
|
||||
const verbStr = msg.action ?
|
||||
buildActionStr(msg.action, fromHtml, '大家') :
|
||||
`${fromHtml}对大家说:`;
|
||||
html = `${headImg}${verbStr}<span class="msg-content" style="color: ${fontColor}">${messageBodyHtml}</span>`;
|
||||
html = `${headImg}${verbStr}<span class="msg-content${textColorClass}" style="color: ${fontColor}">${messageBodyHtml}</span>`;
|
||||
}
|
||||
|
||||
if (!timeStrOverride) {
|
||||
|
||||
Reference in New Issue
Block a user