修复:将 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() 单击用户名切换聊天目标
2. openUserCard() 双击用户名打开名片弹窗
3. 用户名片弹窗 Alpine.js 组件(资料查看 + 管理操作)
3. 用户名片弹窗 Alpine.js 组件(资料查看 + 送花 + 管理操作)
scripts.blade.php frame.blade.php 中抽取,保持代码职责清晰。
@@ -63,194 +63,233 @@
{{-- ═══════════ 用户名片弹窗 (Alpine.js) ═══════════ --}}
@php $gifts = \App\Models\Gift::activeList(); @endphp
<script>
// 礼物数据注入(避免 JSON 破坏 x-data 属性解析)
/**
* 礼物数据注入(避免 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="{
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: {
'X-CSRF-TOKEN': document.querySelector('meta[name=&quot;csrf-token&quot;]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
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: {
'X-CSRF-TOKEN': document.querySelector('meta[name=&quot;csrf-token&quot;]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
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: {
'X-CSRF-TOKEN': document.querySelector('meta[name=&quot;csrf-token&quot;]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
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: {
'X-CSRF-TOKEN': document.querySelector('meta[name=&quot;csrf-token&quot;]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
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: {
'X-CSRF-TOKEN': document.querySelector('meta[name=&quot;csrf-token&quot;]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
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: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'), 'Content-Type'
: 'application/json' , 'Accept' : 'application/json' }, 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; } }">
<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>
{{-- 弹窗头部 --}}