修复:将 Alpine.js 名片弹窗组件提取为命名函数

- 将整个组件逻辑从 x-data 属性提取到 userCardComponent() 函数
- x-data 改为引用函数名,彻底解决 HTML 属性引号冲突问题
- 提取 _headers() 通用方法减少代码重复
- 礼物数据仍通过 window.__gifts 全局变量注入
This commit is contained in:
2026-02-27 01:06:29 +08:00
parent 2e184832cb
commit a1ccaae4c2
@@ -4,7 +4,7 @@
包含: 包含:
1. switchTarget() 单击用户名切换聊天目标 1. switchTarget() 单击用户名切换聊天目标
2. openUserCard() 双击用户名打开名片弹窗 2. openUserCard() 双击用户名打开名片弹窗
3. 用户名片弹窗 Alpine.js 组件(资料查看 + 管理操作) 3. 用户名片弹窗 Alpine.js 组件(资料查看 + 送花 + 管理操作)
scripts.blade.php frame.blade.php 中抽取,保持代码职责清晰。 scripts.blade.php frame.blade.php 中抽取,保持代码职责清晰。
@@ -63,12 +63,20 @@
{{-- ═══════════ 用户名片弹窗 (Alpine.js) ═══════════ --}} {{-- ═══════════ 用户名片弹窗 (Alpine.js) ═══════════ --}}
@php $gifts = \App\Models\Gift::activeList(); @endphp @php $gifts = \App\Models\Gift::activeList(); @endphp
<script> <script>
// 礼物数据注入(避免 JSON 破坏 x-data 属性解析) /**
* 礼物数据注入(避免 JSON 破坏 x-data 属性解析)
*/
window.__gifts = {!! Js::from($gifts) !!}; window.__gifts = {!! Js::from($gifts) !!};
window.__defaultGiftId = {{ $gifts->first()?->id ?? 0 }}; window.__defaultGiftId = {{ $gifts->first()?->id ?? 0 }};
</script>
<div id="user-modal-container" x-data="{ /**
* 用户名片弹窗 Alpine.js 组件定义
* 提取到 script 标签避免 HTML 属性中的引号冲突
*/
function userCardComponent() {
return {
showUserModal: false, showUserModal: false,
userInfo: {}, userInfo: {},
isMuting: false, isMuting: false,
@@ -82,6 +90,7 @@
giftCount: 1, giftCount: 1,
sendingGift: false, sendingGift: false,
/** 获取用户资料 */
async fetchUser(username) { async fetchUser(username) {
try { try {
const res = await fetch('/user/' + encodeURIComponent(username)); const res = await fetch('/user/' + encodeURIComponent(username));
@@ -93,20 +102,19 @@
this.showWhispers = false; this.showWhispers = false;
this.whisperList = []; this.whisperList = [];
} }
} catch (e) { console.error(e); } } catch (e) {
console.error(e);
}
}, },
/** 踢出用户 */
async kickUser() { async kickUser() {
const reason = prompt('踢出原因(可留空):', '违反聊天室规则'); const reason = prompt('踢出原因(可留空):', '违反聊天室规则');
if (reason === null) return; if (reason === null) return;
try { try {
const res = await fetch('/command/kick', { const res = await fetch('/command/kick', {
method: 'POST', method: 'POST',
headers: { headers: this._headers(),
'X-CSRF-TOKEN': document.querySelector('meta[name=&quot;csrf-token&quot;]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ body: JSON.stringify({
username: this.userInfo.username, username: this.userInfo.username,
room_id: window.chatContext.roomId, room_id: window.chatContext.roomId,
@@ -119,18 +127,17 @@
} else { } else {
alert('操作失败:' + data.message); alert('操作失败:' + data.message);
} }
} catch (e) { alert('网络异常'); } } catch (e) {
alert('网络异常');
}
}, },
/** 禁言用户 */
async muteUser() { async muteUser() {
try { try {
const res = await fetch('/command/mute', { const res = await fetch('/command/mute', {
method: 'POST', method: 'POST',
headers: { headers: this._headers(),
'X-CSRF-TOKEN': document.querySelector('meta[name=&quot;csrf-token&quot;]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ body: JSON.stringify({
username: this.userInfo.username, username: this.userInfo.username,
room_id: window.chatContext.roomId, room_id: window.chatContext.roomId,
@@ -143,20 +150,19 @@
} else { } else {
alert('操作失败:' + data.message); alert('操作失败:' + data.message);
} }
} catch (e) { alert('网络异常'); } } catch (e) {
alert('网络异常');
}
}, },
/** 警告用户 */
async warnUser() { async warnUser() {
const reason = prompt('警告原因:', '请注意言行'); const reason = prompt('警告原因:', '请注意言行');
if (reason === null) return; if (reason === null) return;
try { try {
const res = await fetch('/command/warn', { const res = await fetch('/command/warn', {
method: 'POST', method: 'POST',
headers: { headers: this._headers(),
'X-CSRF-TOKEN': document.querySelector('meta[name=&quot;csrf-token&quot;]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ body: JSON.stringify({
username: this.userInfo.username, username: this.userInfo.username,
room_id: window.chatContext.roomId, room_id: window.chatContext.roomId,
@@ -169,9 +175,12 @@
} else { } else {
alert('操作失败:' + data.message); alert('操作失败:' + data.message);
} }
} catch (e) { alert('网络异常'); } } catch (e) {
alert('网络异常');
}
}, },
/** 冻结用户 */
async freezeUser() { async freezeUser() {
if (!confirm('确定要冻结 ' + this.userInfo.username + ' 的账号吗?冻结后将无法登录!')) return; if (!confirm('确定要冻结 ' + this.userInfo.username + ' 的账号吗?冻结后将无法登录!')) return;
const reason = prompt('冻结原因:', '严重违规'); const reason = prompt('冻结原因:', '严重违规');
@@ -179,11 +188,7 @@
try { try {
const res = await fetch('/command/freeze', { const res = await fetch('/command/freeze', {
method: 'POST', method: 'POST',
headers: { headers: this._headers(),
'X-CSRF-TOKEN': document.querySelector('meta[name=&quot;csrf-token&quot;]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ body: JSON.stringify({
username: this.userInfo.username, username: this.userInfo.username,
room_id: window.chatContext.roomId, room_id: window.chatContext.roomId,
@@ -196,13 +201,18 @@
} else { } else {
alert('操作失败:' + data.message); alert('操作失败:' + data.message);
} }
} catch (e) { alert('网络异常'); } } catch (e) {
alert('网络异常');
}
}, },
/** 查看私信记录 */
async loadWhispers() { async loadWhispers() {
try { try {
const res = await fetch('/command/whispers/' + encodeURIComponent(this.userInfo.username), { const res = await fetch('/command/whispers/' + encodeURIComponent(this.userInfo.username), {
headers: { 'Accept': 'application/json' } headers: {
'Accept': 'application/json'
}
}); });
const data = await res.json(); const data = await res.json();
if (data.status === 'success') { if (data.status === 'success') {
@@ -211,19 +221,18 @@
} else { } else {
alert(data.message); alert(data.message);
} }
} catch (e) { alert('网络异常'); } } catch (e) {
alert('网络异常');
}
}, },
/** 发送全服公告 */
async sendAnnounce() { async sendAnnounce() {
if (!this.announceText.trim()) return; if (!this.announceText.trim()) return;
try { try {
const res = await fetch('/command/announce', { const res = await fetch('/command/announce', {
method: 'POST', method: 'POST',
headers: { headers: this._headers(),
'X-CSRF-TOKEN': document.querySelector('meta[name=&quot;csrf-token&quot;]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ body: JSON.stringify({
content: this.announceText, content: this.announceText,
room_id: window.chatContext.roomId, room_id: window.chatContext.roomId,
@@ -236,21 +245,51 @@
} else { } else {
alert(data.message); alert(data.message);
} }
} catch (e) { alert('网络异常'); } } catch (e) {
alert('网络异常');
}
}, },
/** 送礼物 */
async sendGift() { async sendGift() {
if (this.sendingGift || !this.selectedGiftId) return; if (this.sendingGift || !this.selectedGiftId) return;
this.sendingGift = true; this.sendingGift = true;
try { try {
const res = await fetch('/gift/flower', { const res = await fetch('/gift/flower', {
method: 'POST', method: 'POST',
headers: { headers: this._headers(),
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'), 'Content-Type' body: JSON.stringify({
: 'application/json' , 'Accept' : 'application/json' }, body: JSON.stringify({ to_user: this.userInfo.username, to_user: this.userInfo.username,
room_id: window.chatContext.roomId, gift_id: this.selectedGiftId, count: this.giftCount }) }); const data=await room_id: window.chatContext.roomId,
res.json(); alert(data.message); if (data.status === 'success') { this.showUserModal=false; this.giftCount=1; } } gift_id: this.selectedGiftId,
catch (e) { alert('网络异常'); } this.sendingGift=false; } }"> 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 x-show="showUserModal" style="display: none;" class="modal-overlay" x-on:click.self="showUserModal = false">
<div class="modal-card" x-transition> <div class="modal-card" x-transition>
{{-- 弹窗头部 --}} {{-- 弹窗头部 --}}