// 用户名片弹窗 Alpine 组件,负责资料查看、好友、礼物、管理和职务快捷操作。 /** * 读取用户弹窗容器上的礼物初始数据。 * * @returns {{gifts: Array>, defaultGiftId: number}} */ function userCardGiftDefaults() { const container = document.getElementById("user-modal-container"); let gifts = []; try { gifts = JSON.parse(atob(container?.dataset.giftsBase64 || "")); } catch (error) { gifts = []; } return { gifts, defaultGiftId: Number(container?.dataset.defaultGiftId || 0), }; } /** * 创建用户名片弹窗 Alpine 组件。 * * @returns {Record} */ export function userCardComponent() { const giftDefaults = userCardGiftDefaults(); return { showUserModal: false, showOriginalLightbox: false, userInfo: { position_history: [] }, isMuting: false, muteDuration: 5, showWhispers: false, whisperList: [], showAnnounce: false, announceText: '', is_friend: false, // 当前用户是否已将对方加为好友 friendLoading: false, // 好友操作加载状态 gifts: giftDefaults.gifts, selectedGiftId: giftDefaults.defaultGiftId, giftCount: 1, sendingGift: false, showGiftPanel: false, showGiftGoldPanel: false, giftGoldAmount: "", giftGoldSending: false, // 职务奖励金币 rewardAmount: 0, sendingReward: false, showRewardPanel: false, // 任命相关 showAppointPanel: false, appointPositions: [], selectedPositionId: null, appointRemark: '', appointLoading: false, // 折叠状态 showAdminView: false, // 管理员视野 showPositionHistory: false, // 职务履历 showAdminPanel: false, // 管理操作(管理操作+职务操作合并) // 婚姻状态 targetMarriage: null, // 对方婚姻状态 { status, partner_name, marriage_id } marriageLoading: false, mySex: window.chatContext?.userSex ?? '', // 当前用户性别(用于求婚异性判断) // 自定义弹窗:直接代理到全局 window.chatDialog $alert: (...args) => window.chatDialog.alert(...args), $confirm: (...args) => window.chatDialog.confirm(...args), $prompt: (...args) => window.chatDialog.prompt(...args), /** * 判断当前操作者是否拥有指定的职务权限码。 */ hasPositionPermission(permissionCode) { return Boolean(window.chatContext?.positionPermissionMap?.[permissionCode]); }, /** * 判断目标用户职务是否严格低于当前操作者。 * * 规则: * 1. 先比较部门位阶 rank * 2. 部门相同再比较职务位阶 rank * 3. 对方没有在职职务时,视为可处理 * 4. id=1 站长始终可处理 */ canManageTargetByDuty() { if (window.chatContext?.isSiteOwner) { return true; } const targetDepartmentRank = Number(this.userInfo.department_rank || 0); const targetPositionRank = Number(this.userInfo.position_rank || 0); if (targetDepartmentRank <= 0 && targetPositionRank <= 0) { return true; } const operatorDepartmentRank = Number(window.chatContext?.operatorDepartmentRank || 0); const operatorPositionRank = Number(window.chatContext?.operatorPositionRank || 0); if (operatorDepartmentRank > targetDepartmentRank) { return true; } if (operatorDepartmentRank < targetDepartmentRank) { return false; } return operatorPositionRank >= targetPositionRank; }, /** 切换好友关系(加好友 / 删好友) */ async toggleFriend() { if (this.friendLoading) return; this.friendLoading = true; const username = this.userInfo.username; const roomId = window.chatContext.roomId; const removing = this.is_friend; try { let res; if (removing) { // 删除好友 res = await fetch(`/friend/${encodeURIComponent(username)}/remove`, { method: 'DELETE', headers: this._headers(), body: JSON.stringify({ room_id: roomId }), }); } else { // 添加好友 res = await fetch(`/friend/${encodeURIComponent(username)}/add`, { method: 'POST', headers: this._headers(), body: JSON.stringify({ room_id: roomId }), }); } const data = await res.json(); const ok = data.status === 'success'; this.$alert( data.message, ok ? (removing ? '已删除好友' : '添加成功 🎉') : '操作失败', ok ? (removing ? '#6b7280' : '#16a34a') : '#cc4444' ); if (ok) { this.is_friend = !this.is_friend; } } catch (e) { this.$alert('网络异常', '错误', '#cc4444'); } this.friendLoading = false; }, async handleConfirmDivorce(marriageId) { // 等待后端接口实现,当前先略 return; }, /** 发起协议离婚(先拉惩罚配置,再弹专属全屏确认弹窗) */ async doDivorce(marriageId) { if (!marriageId) return; this.showUserModal = false; // 从后台实时拉取最新惩罚配置 let divorceConfig = { mutual_charm_penalty: 0, forced_charm_penalty: 0, mutual_cooldown_days: 0, forced_cooldown_days: 0 }; try { const cfgRes = await fetch(window.chatContext.marriage.divorceConfigUrl, { headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }); if (cfgRes.ok) divorceConfig = await cfgRes.json(); } catch (e) { /* 网络异常则使用默认值 */ } // 打开专属离婚确认弹窗 const modal = document.getElementById('divorce-confirm-modal'); if (modal && window.Alpine) { window.Alpine.$data(modal).open(marriageId, divorceConfig); } }, /** 获取用户资料 */ async fetchUser(username) { try { const res = await fetch('/user/' + encodeURIComponent(username), { headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }); if (!res.ok) { const errorData = await res.json().catch(() => ({})); console.error('Failed to fetch user:', errorData.message || res.statusText); return; } const data = await res.json(); if (data.status === 'success') { this.userInfo = data.data; this.showPositionHistory = false; // 加载好友状态(仅对非自己的用户查询) if (data.data.username !== window.chatContext.username) { fetch(`/friend/${encodeURIComponent(data.data.username)}/status`, { headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }).then(r => r.json()).then(s => { this.is_friend = s.is_friend ?? false; }); // 加载对方婚姻状态 this.targetMarriage = null; this.marriageLoading = true; fetch(`/marriage/target?username=${encodeURIComponent(data.data.username)}`, { headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }).then(r => r.json()).then(m => { this.targetMarriage = m.marriage ?? null; }).catch(() => { }).finally(() => { this.marriageLoading = false; }); } this.showUserModal = true; this.isMuting = false; this.showWhispers = false; this.whisperList = []; this.showAppointPanel = false; this.selectedPositionId = null; this.appointRemark = ''; // 有职务的操作人预加载可用职务列表 if (window.chatContext?.hasPosition) { this._loadPositions(); } } } catch (e) { console.error('Error fetching user:', e); } }, /** 加载可任命职务列表 */ async _loadPositions() { if (!window.chatContext?.appointPositionsUrl) return; try { const res = await fetch(window.chatContext.appointPositionsUrl, { headers: { 'Accept': 'application/json' } }); const data = await res.json(); if (data.status === 'success') { this.appointPositions = data.positions; if (this.appointPositions.length > 0) { this.selectedPositionId = this.appointPositions[0].id; } } } catch (e) { /* 静默失败 */ } }, /** 快速任命 */ async doAppoint() { if (this.appointLoading || !this.selectedPositionId) return; this.appointLoading = true; try { const res = await fetch(window.chatContext.appointUrl, { method: 'POST', headers: this._headers(), body: JSON.stringify({ username: this.userInfo.username, position_id: this.selectedPositionId, remark: this.appointRemark.trim() || null, room_id: window.chatContext.roomId ?? null, }) }); const data = await res.json(); const ok = data.status === 'success'; if (ok) { this.showUserModal = false; } else { this.$alert(data.message, '操作失败', '#cc4444'); } } catch (e) { this.$alert('网络异常,请稍后重试', '错误', '#cc4444'); } this.appointLoading = false; }, /** 快速撤销 */ async doRevoke() { const ok = await this.$confirm( '确定要撤销 「' + this.userInfo.username + '」 的职务吗?撤销后将不再拥有相关权限。', '撤销职务' ); if (!ok) return; this.appointLoading = true; try { const res = await fetch(window.chatContext.revokeUrl, { method: 'POST', headers: this._headers(), body: JSON.stringify({ username: this.userInfo.username, remark: '聊天室快速撤销', room_id: window.chatContext.roomId ?? null, }) }); const data = await res.json(); const revOk = data.status === 'success'; if (revOk) { this.showUserModal = false; } else { this.$alert(data.message, '操作失败', '#cc4444'); } } catch (e) { this.$alert('网络异常', '错误', '#cc4444'); } this.appointLoading = false; }, /** 踢出用户 */ async kickUser() { const reason = await this.$prompt('踢出原因(可留空):', '违反聊天室规则', '踢出用户', '#cc4444'); 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 { this.$alert('操作失败:' + data.message, '操作失败', '#cc4444'); } } catch (e) { this.$alert('网络异常', '错误', '#cc4444'); } }, /** 禁言用户 */ 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 { this.$alert('操作失败:' + data.message, '操作失败', '#cc4444'); } } catch (e) { this.$alert('网络异常', '错误', '#cc4444'); } }, /** 警告用户 */ async warnUser() { const reason = await this.$prompt('警告原因:', '请注意言行', '警告用户', '#f59e0b'); 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 { this.$alert('操作失败:' + data.message, '操作失败', '#cc4444'); } } catch (e) { this.$alert('网络异常', '错误', '#cc4444'); } }, /** 冻结用户 */ async freezeUser() { const confirmed = await this.$confirm( '确定要冻结 ' + this.userInfo.username + ' 的账号吗?冻结后将无法登录!', '冻结账号', '#cc4444' ); if (!confirmed) return; const reason = await this.$prompt('冻结原因:', '严重违规', '填写原因', '#cc4444'); 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 { this.$alert('操作失败:' + data.message, '操作失败', '#cc4444'); } } catch (e) { this.$alert('网络异常', '错误', '#cc4444'); } }, /** 查看私信记录 */ 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 { this.$alert(data.message, '操作失败', '#cc4444'); } } catch (e) { this.$alert('网络异常', '错误', '#cc4444'); } }, /** 发送全服公告 */ 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 { this.$alert(data.message, '操作失败', '#cc4444'); } } catch (e) { this.$alert('网络异常', '错误', '#cc4444'); } }, /** 送礼物 */ 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(); this.$alert(data.message, data.status === 'success' ? '送礼成功 🎁' : '操作失败', data.status === 'success' ? '#e11d48' : '#cc4444'); if (data.status === 'success') { this.showUserModal = false; this.giftCount = 1; } } catch (e) { this.$alert('网络异常', '错误', '#cc4444'); } this.sendingGift = false; }, /** 切换礼物面板,和赠送金币面板互斥显示 */ toggleGiftPanel() { this.showGiftPanel = !this.showGiftPanel; this.showGiftGoldPanel = false; }, /** 切换赠送金币面板,和礼物面板互斥显示 */ toggleGiftGoldPanel() { this.showGiftGoldPanel = !this.showGiftGoldPanel; this.showGiftPanel = false; }, /** 给用户赠送自己的金币 */ async sendGiftGold() { const amount = Number.parseInt(this.giftGoldAmount, 10); if (this.giftGoldSending || !amount || amount <= 0) { return; } this.giftGoldSending = true; try { const response = await fetch("/gift/gold", { method: "POST", headers: this._headers(), body: JSON.stringify({ to_user: this.userInfo.username, room_id: window.chatContext.roomId, amount, }), }); const data = await response.json(); if (data.status === "success") { window.chatContext.myGold = data.data?.my_jjb ?? window.chatContext.myGold; this.showGiftGoldPanel = false; this.giftGoldAmount = ""; this.$alert(data.message, "赠送成功 💝", "#d97706"); return; } this.$alert(data.message, "赠送失败", "#cc4444"); } catch (error) { this.$alert("网络异常", "错误", "#cc4444"); } this.giftGoldSending = false; }, /** 职务奖励:向用户发放金币(凭空产生,记入履职记录) */ async sendReward() { if (this.sendingReward) return; const maxOnce = window.chatContext?.myMaxReward ?? 0; const amount = parseInt(this.rewardAmount, 10); if (!amount || amount <= 0) { this.$alert('请输入有效的奖励金额', '提示', '#f59e0b'); return; } // 0 = 禁止(前端按钮不显示,此处二次保护) if (maxOnce === 0) { this.$alert('你的职务没有奖励发放权限', '无权限', '#cc4444'); return; } // -1 = 不限,跳过上限校验;正整数 = 有具体上限 if (maxOnce > 0 && amount > maxOnce) { this.$alert(`单次奖励上限为 ${maxOnce} 金币`, '超出上限', '#cc4444'); return; } this.sendingReward = true; try { const res = await fetch(window.chatContext.rewardUrl, { method: 'POST', headers: this._headers(), body: JSON.stringify({ username: this.userInfo.username, room_id: window.chatContext.roomId, amount, }) }); const data = await res.json(); const ok = data.status === 'success'; this.$alert(data.message, ok ? '奖励发放成功 🎉' : '操作失败', ok ? '#d97706' : '#cc4444'); if (ok) { this.showRewardPanel = false; this.rewardAmount = 0; } } catch (e) { this.$alert('网络异常,请稍后重试', '错误', '#cc4444'); } this.sendingReward = false; }, /** 通用请求头 */ _headers() { return { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'), 'Content-Type': 'application/json', 'Accept': 'application/json' }; } }; } /** * 挂载用户名片组件全局名称,兼容 Blade 的 x-data。 * * @returns {void} */ export function bindUserCardControls() { if (typeof window === "undefined") { return; } window.userCardComponent = userCardComponent; }