diff --git a/resources/js/chat-room.js b/resources/js/chat-room.js index 5e799d1..a43765d 100644 --- a/resources/js/chat-room.js +++ b/resources/js/chat-room.js @@ -20,6 +20,7 @@ * - mobile-drawer.js:处理移动端抽屉、房间列表和在线名单。 * - marriage-status.js:处理婚姻状态弹窗、已婚列表、接受拒绝和离婚申请。 * - toolbar.js:处理工具栏按钮和功能快捷入口。 + * - user-card.js:提供用户名片弹窗 Alpine 组件和管理/礼物操作。 * - user-target-actions.js:处理点击用户名切换私聊目标和打开名片。 * - welcome-menu.js:处理欢迎菜单交互。 * - admin-menu.js:处理聊天室管理菜单交互。 @@ -93,6 +94,7 @@ export { tryDivorce, } from "./chat-room/marriage-status.js"; export { bindToolbarControls, runFeatureShortcut, runToolbarAction } from "./chat-room/toolbar.js"; +export { bindUserCardControls, userCardComponent } from "./chat-room/user-card.js"; export { bindUserTargetActions, openUserCard, switchTarget } from "./chat-room/user-target-actions.js"; export { bindWelcomeMenuControls } from "./chat-room/welcome-menu.js"; export { bindAdminMenuControls } from "./chat-room/admin-menu.js"; @@ -263,6 +265,7 @@ import { tryDivorce, } from "./chat-room/marriage-status.js"; import { bindToolbarControls, runFeatureShortcut, runToolbarAction } from "./chat-room/toolbar.js"; +import { bindUserCardControls, userCardComponent } from "./chat-room/user-card.js"; import { bindUserTargetActions, openUserCard, switchTarget } from "./chat-room/user-target-actions.js"; import { bindWelcomeMenuControls } from "./chat-room/welcome-menu.js"; import { bindAdminMenuControls } from "./chat-room/admin-menu.js"; @@ -441,6 +444,8 @@ if (typeof window !== "undefined") { bindToolbarControls, runFeatureShortcut, runToolbarAction, + bindUserCardControls, + userCardComponent, bindUserTargetActions, openUserCard, switchTarget, @@ -626,6 +631,7 @@ if (typeof window !== "undefined") { window.slotPanel = slotPanel; window.runFeatureShortcut = runFeatureShortcut; window.runToolbarAction = runToolbarAction; + window.userCardComponent = userCardComponent; window.buildHolidayClaimActionButton = buildHolidayClaimActionButton; window.buildHolidaySystemMessage = buildHolidaySystemMessage; window.holidayEventModal = holidayEventModal; @@ -731,6 +737,7 @@ if (typeof window !== "undefined") { bindFriendPanelControls(); bindFriendNotificationControls(); bindToolbarControls(); + bindUserCardControls(); bindUserTargetActions(); bindAdminMenuControls(); bindBaccaratPanelControls(); diff --git a/resources/js/chat-room/user-card.js b/resources/js/chat-room/user-card.js new file mode 100644 index 0000000..68f370f --- /dev/null +++ b/resources/js/chat-room/user-card.js @@ -0,0 +1,587 @@ +// 用户名片弹窗 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, + + // 职务奖励金币 + 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; + }, + + /** 职务奖励:向用户发放金币(凭空产生,记入履职记录) */ + 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; +} diff --git a/resources/views/chat/partials/user-actions.blade.php b/resources/views/chat/partials/user-actions.blade.php index 3f1e0d2..54a619e 100644 --- a/resources/views/chat/partials/user-actions.blade.php +++ b/resources/views/chat/partials/user-actions.blade.php @@ -17,564 +17,13 @@ {{-- ═══════════ 用户名片弹窗 (Alpine.js) ═══════════ --}} @php $gifts = \App\Models\Gift::activeList(); @endphp - - -
+