Files
chatroom/resources/views/chat/partials/user-actions.blade.php
T
lkddi a1ccaae4c2 修复:将 Alpine.js 名片弹窗组件提取为命名函数
- 将整个组件逻辑从 x-data 属性提取到 userCardComponent() 函数
- x-data 改为引用函数名,彻底解决 HTML 属性引号冲突问题
- 提取 _headers() 通用方法减少代码重复
- 礼物数据仍通过 window.__gifts 全局变量注入
2026-02-27 01:06:29 +08:00

433 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{--
文件功能:用户交互全局函数 + 名片弹窗组件
包含:
1. switchTarget() 单击用户名切换聊天目标
2. openUserCard() 双击用户名打开名片弹窗
3. 用户名片弹窗 Alpine.js 组件(资料查看 + 送花 + 管理操作)
scripts.blade.php frame.blade.php 中抽取,保持代码职责清晰。
@author ChatRoom Laravel
@version 1.0.0
--}}
{{-- ═══════════ 全局交互函数 ═══════════ --}}
<script>
/**
* 全局函数:单击用户名 → 切换聊天目标
*
* 将「对...说」下拉选中为该用户,方便直接发悄悄话。
* 若用户已离线不在下拉列表中,临时添加一个 option。
*/
function switchTarget(username) {
const toUserSelect = document.getElementById('to_user');
if (!toUserSelect) return;
const options = toUserSelect.options;
let found = false;
for (let i = 0; i < options.length; i++) {
if (options[i].value === username) {
toUserSelect.value = username;
found = true;
break;
}
}
// 如果不在列表中(可能已离线),临时添加
if (!found && username !== '大家') {
const opt = document.createElement('option');
opt.value = username;
opt.textContent = username;
toUserSelect.appendChild(opt);
toUserSelect.value = username;
}
// 切换目标后自动聚焦输入框,方便直接输入
document.getElementById('content').focus();
}
/**
* 全局函数:双击用户名 → 打开名片弹窗
*
* 聊天消息区和右侧用户列表统一调用此函数。
* 通过 Alpine.js 的 fetchUser 方法加载用户资料并显示弹窗。
*/
function openUserCard(username) {
if (username === window.chatContext.username) return;
const el = document.getElementById('user-modal-container');
if (el) {
const data = Alpine.$data(el);
if (data) data.fetchUser(username);
}
}
</script>
{{-- ═══════════ 用户名片弹窗 (Alpine.js) ═══════════ --}}
@php $gifts = \App\Models\Gift::activeList(); @endphp
<script>
/**
* 礼物数据注入(避免 JSON 破坏 x-data 属性解析)
*/
window.__gifts = {!! Js::from($gifts) !!};
window.__defaultGiftId = {{ $gifts->first()?->id ?? 0 }};
/**
* 用户名片弹窗 Alpine.js 组件定义
* 提取到 script 标签避免 HTML 属性中的引号冲突
*/
function userCardComponent() {
return {
showUserModal: false,
userInfo: {},
isMuting: false,
muteDuration: 5,
showWhispers: false,
whisperList: [],
showAnnounce: false,
announceText: '',
gifts: window.__gifts || [],
selectedGiftId: window.__defaultGiftId || 0,
giftCount: 1,
sendingGift: false,
/** 获取用户资料 */
async fetchUser(username) {
try {
const res = await fetch('/user/' + encodeURIComponent(username));
const data = await res.json();
if (data.status === 'success') {
this.userInfo = data.data;
this.showUserModal = true;
this.isMuting = false;
this.showWhispers = false;
this.whisperList = [];
}
} catch (e) {
console.error(e);
}
},
/** 踢出用户 */
async kickUser() {
const reason = prompt('踢出原因(可留空):', '违反聊天室规则');
if (reason === null) return;
try {
const res = await fetch('/command/kick', {
method: 'POST',
headers: this._headers(),
body: JSON.stringify({
username: this.userInfo.username,
room_id: window.chatContext.roomId,
reason: reason || '违反聊天室规则'
})
});
const data = await res.json();
if (data.status === 'success') {
this.showUserModal = false;
} else {
alert('操作失败:' + data.message);
}
} catch (e) {
alert('网络异常');
}
},
/** 禁言用户 */
async muteUser() {
try {
const res = await fetch('/command/mute', {
method: 'POST',
headers: this._headers(),
body: JSON.stringify({
username: this.userInfo.username,
room_id: window.chatContext.roomId,
duration: this.muteDuration
})
});
const data = await res.json();
if (data.status === 'success') {
this.showUserModal = false;
} else {
alert('操作失败:' + data.message);
}
} catch (e) {
alert('网络异常');
}
},
/** 警告用户 */
async warnUser() {
const reason = prompt('警告原因:', '请注意言行');
if (reason === null) return;
try {
const res = await fetch('/command/warn', {
method: 'POST',
headers: this._headers(),
body: JSON.stringify({
username: this.userInfo.username,
room_id: window.chatContext.roomId,
reason: reason || '请注意言行'
})
});
const data = await res.json();
if (data.status === 'success') {
this.showUserModal = false;
} else {
alert('操作失败:' + data.message);
}
} catch (e) {
alert('网络异常');
}
},
/** 冻结用户 */
async freezeUser() {
if (!confirm('确定要冻结 ' + this.userInfo.username + ' 的账号吗?冻结后将无法登录!')) return;
const reason = prompt('冻结原因:', '严重违规');
if (reason === null) return;
try {
const res = await fetch('/command/freeze', {
method: 'POST',
headers: this._headers(),
body: JSON.stringify({
username: this.userInfo.username,
room_id: window.chatContext.roomId,
reason: reason || '严重违规'
})
});
const data = await res.json();
if (data.status === 'success') {
this.showUserModal = false;
} else {
alert('操作失败:' + data.message);
}
} catch (e) {
alert('网络异常');
}
},
/** 查看私信记录 */
async loadWhispers() {
try {
const res = await fetch('/command/whispers/' + encodeURIComponent(this.userInfo.username), {
headers: {
'Accept': 'application/json'
}
});
const data = await res.json();
if (data.status === 'success') {
this.whisperList = data.messages;
this.showWhispers = true;
} else {
alert(data.message);
}
} catch (e) {
alert('网络异常');
}
},
/** 发送全服公告 */
async sendAnnounce() {
if (!this.announceText.trim()) return;
try {
const res = await fetch('/command/announce', {
method: 'POST',
headers: this._headers(),
body: JSON.stringify({
content: this.announceText,
room_id: window.chatContext.roomId,
})
});
const data = await res.json();
if (data.status === 'success') {
this.announceText = '';
this.showAnnounce = false;
} else {
alert(data.message);
}
} catch (e) {
alert('网络异常');
}
},
/** 送礼物 */
async sendGift() {
if (this.sendingGift || !this.selectedGiftId) return;
this.sendingGift = true;
try {
const res = await fetch('/gift/flower', {
method: 'POST',
headers: this._headers(),
body: JSON.stringify({
to_user: this.userInfo.username,
room_id: window.chatContext.roomId,
gift_id: this.selectedGiftId,
count: this.giftCount
})
});
const data = await res.json();
alert(data.message);
if (data.status === 'success') {
this.showUserModal = false;
this.giftCount = 1;
}
} catch (e) {
alert('网络异常');
}
this.sendingGift = false;
},
/** 通用请求头 */
_headers() {
return {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
};
}
};
}
</script>
<div id="user-modal-container" x-data="userCardComponent()">
<div x-show="showUserModal" style="display: none;" class="modal-overlay" x-on:click.self="showUserModal = false">
<div class="modal-card" x-transition>
{{-- 弹窗头部 --}}
<div class="modal-header">
<h3 x-text="'用户名片 · ' + userInfo.username"></h3>
<button class="modal-close" x-on:click="showUserModal = false">&times;</button>
</div>
{{-- 弹窗内容 --}}
<div class="modal-body">
<div class="profile-row">
<img class="profile-avatar" x-show="userInfo.headface"
:src="'/images/headface/' + (userInfo.headface || '1.gif').toLowerCase()"
x-on:error="$el.style.display='none'">
<div class="profile-info">
<h4>
<span x-text="userInfo.username"></span>
<span class="level-badge" x-text="'LV.' + userInfo.user_level"></span>
<span class="sex-badge"
x-text="userInfo.sex === '男' ? '♂' : (userInfo.sex === '女' ? '♀' : '')"
:style="userInfo.sex === '男' ? 'color: blue' : (userInfo.sex === '女' ?
'color: deeppink' : '')"></span>
</h4>
<div style="font-size: 11px; color: #999; margin-top: 2px;">
加入: <span x-text="userInfo.created_at"></span>
</div>
</div>
</div>
<div class="profile-detail" x-text="userInfo.sign || '这家伙很懒,什么也没留下'"></div>
</div>
{{-- 普通操作按钮 --}}
<div class="modal-actions" x-show="userInfo.username !== window.chatContext.username">
<button class="btn-whisper"
x-on:click="document.getElementById('to_user').value = userInfo.username; document.getElementById('content').focus(); showUserModal = false;">
悄悄话
</button>
<a class="btn-mail"
:href="'{{ route('guestbook.index', ['tab' => 'outbox']) }}&to=' + encodeURIComponent(userInfo
.username)"
target="_blank">
写私信
</a>
</div>
{{-- 送花/礼物互动区 --}}
<div style="padding: 0 16px 12px;" x-show="userInfo.username !== window.chatContext.username">
<div style="font-size: 11px; color: #e91e8f; margin-bottom: 6px; font-weight: bold;">🎁 送礼物</div>
{{-- 礼物选择列表 --}}
<div style="display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 8px;">
<template x-for="g in gifts" :key="g.id">
<div x-on:click="selectedGiftId = g.id"
:style="selectedGiftId === g.id ? 'border: 2px solid #e91e63; background: #fce4ec;' :
'border: 2px solid #eee; background: #fafafa;'"
style="width: 68px; padding: 4px 2px; border-radius: 6px; text-align: center; cursor: pointer; transition: all 0.15s;">
<img :src="'/images/gifts/' + g.image"
style="width: 36px; height: 36px; object-fit: contain;" :alt="g.name">
<div style="font-size: 10px; color: #333; margin-top: 2px;" x-text="g.name"></div>
<div style="font-size: 9px; color: #e91e63;" x-text="g.cost + '💰 +' + g.charm + '✨'"></div>
</div>
</template>
</div>
{{-- 数量 + 送出按钮 --}}
<div style="display: flex; gap: 6px; align-items: center;">
<select x-model.number="giftCount"
style="width: 60px; padding: 3px; border: 1px solid #f0a0c0; border-radius: 4px; font-size: 12px;">
<option value="1">×1</option>
<option value="5">×5</option>
<option value="10">×10</option>
<option value="99">×99</option>
</select>
<button x-on:click="sendGift()" :disabled="sendingGift"
style="flex: 1; padding: 6px 10px; background: linear-gradient(135deg, #ff6b9d, #e91e63); color: #fff; border: none; border-radius: 6px; font-size: 12px; font-weight: bold; cursor: pointer; transition: opacity 0.15s;"
:style="sendingGift ? 'opacity: 0.5; cursor: not-allowed;' : ''"
x-text="sendingGift ? '送出中...' : '送出 💝'"></button>
</div>
</div>
{{-- 特权操作(各按钮按等级独立显示) --}}
@if ($myLevel >= $levelWarn || $room->master == Auth::user()->username)
<div style="padding: 0 16px 12px;"
x-show="userInfo.username !== window.chatContext.username && userInfo.user_level < {{ $myLevel }}">
<div style="font-size: 11px; color: #c00; margin-bottom: 6px; font-weight: bold;">管理操作</div>
<div style="display: flex; gap: 6px; flex-wrap: wrap;">
@if ($myLevel >= $levelWarn)
<button
style="flex:1; padding: 5px; border-radius: 4px; font-size: 11px; background: #fef3c7; border: 1px solid #f59e0b; cursor: pointer;"
x-on:click="warnUser()">⚠️ 警告</button>
@endif
@if ($myLevel >= $levelKick)
<button
style="flex:1; padding: 5px; border-radius: 4px; font-size: 11px; background: #fee2e2; border: 1px solid #ef4444; cursor: pointer;"
x-on:click="kickUser()">🚫 踢出</button>
@endif
@if ($myLevel >= $levelMute)
<button
style="flex:1; padding: 5px; border-radius: 4px; font-size: 11px; background: #e0e7ff; border: 1px solid #6366f1; cursor: pointer;"
x-on:click="isMuting = !isMuting">🔇 禁言</button>
@endif
@if ($myLevel >= $levelFreeze)
<button
style="flex:1; padding: 5px; border-radius: 4px; font-size: 11px; background: #dbeafe; border: 1px solid #3b82f6; cursor: pointer;"
x-on:click="freezeUser()">🧊 冻结</button>
@endif
@if ($myLevel >= $superLevel)
<button
style="flex:1; padding: 5px; border-radius: 4px; font-size: 11px; background: #f3e8ff; border: 1px solid #a855f7; cursor: pointer;"
x-on:click="loadWhispers()">🔍 私信</button>
@endif
</div>
</div>
{{-- 禁言表单 --}}
<div x-show="isMuting" style="display: none; padding: 0 16px 12px;">
<div style="display: flex; gap: 6px; align-items: center;">
<input type="number" x-model="muteDuration" min="1" max="1440" placeholder="分钟"
style="width: 60px; padding: 4px; border: 1px solid #ccc; border-radius: 3px; font-size: 11px;">
<span style="font-size: 11px; color: #b86e00;">分钟</span>
<button x-on:click="muteUser()"
style="padding: 4px 12px; background: #6366f1; color: #fff; border: none; border-radius: 3px; font-size: 11px; cursor: pointer;">执行</button>
</div>
</div>
@endif
{{-- 私信记录展示区(管理员查看) --}}
<div x-show="showWhispers"
style="display: none; padding: 0 16px 12px; max-height: 200px; overflow-y: auto;">
<div style="font-size: 11px; color: #666; margin-bottom: 4px;"
x-text="'最近 ' + whisperList.length + ' 条悄悄话:'"></div>
<template x-for="w in whisperList" :key="w.id">
<div style="font-size: 11px; padding: 3px 0; border-bottom: 1px solid #f0f0f0;">
<span style="color: #6366f1;" x-text="w.from_user"></span>
<span style="color: #059669;" x-text="w.to_user"></span>
<span x-text="w.content"></span>
<span style="color: #aaa; font-size: 10px;" x-text="'(' + w.sent_at + ')'"></span>
</div>
</template>
<div x-show="whisperList.length === 0" style="font-size: 11px; color: #aaa;">暂无悄悄话记录</div>
</div>
</div>
</div>
</div>