Files
chatroom/resources/js/chat-room/user-card.js
T
2026-04-26 11:31:46 +08:00

825 lines
33 KiB
JavaScript

// 用户名片弹窗 Alpine 组件,负责资料查看、好友、礼物、管理和职务快捷操作。
/**
* 读取用户弹窗容器上的礼物初始数据。
*
* @returns {{gifts: Array<Record<string, any>>, 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<string, any>}
*/
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;
},
/** 返回名片资产字段的中文名称。 */
assetValueLabel(asset) {
return {
exp_num: "经验",
jjb: "金币",
meili: "魅力",
}[asset] || "资产";
},
/** 判断名片资产字段是否处于可付费查看的隐藏状态。 */
canRevealAssetValue(asset) {
return Boolean(
this.userInfo.asset_numbers_can_reveal
&& this.userInfo.asset_numbers_masked
&& this.userInfo[asset] === "******"
&& this.userInfo.asset_reveal_user_id
);
},
/** 格式化名片里的经验、金币、魅力显示。 */
displayAssetValue(asset) {
if (this.canRevealAssetValue(asset)) {
return "****** 👁️";
}
return Number(this.userInfo[asset] || 0).toLocaleString();
},
/** 返回名片资产字段的悬停提示。 */
assetValueTitle(asset) {
if (this.canRevealAssetValue(asset)) {
const cost = Number(this.userInfo.asset_numbers_reveal_cost || 1000).toLocaleString();
return `点击查看${this.assetValueLabel(asset)},需扣除 ${cost} 金币`;
}
return this.assetValueLabel(asset);
},
/** 返回名片资产字段的点击态样式。 */
assetValueStyle(asset, color) {
const clickable = this.canRevealAssetValue(asset);
return [
"font-weight: 700",
`color: ${color}`,
"font-size: 14px",
clickable ? "cursor: pointer" : "cursor: default",
"text-decoration: none",
].join("; ");
},
/** 付费查看当前名片里的经验、金币或魅力。 */
async revealAssetValue(asset) {
if (!this.canRevealAssetValue(asset)) {
return;
}
const cost = Number(this.userInfo.asset_numbers_reveal_cost || 1000);
const label = this.assetValueLabel(asset);
const confirmed = await this.$confirm(
`查看 TA 的${label}需扣除 ${cost.toLocaleString()} 金币,是否继续?`,
"信息查看付费",
"#d97706",
"扣费查看",
"取消"
);
if (!confirmed) {
return;
}
try {
const response = await fetch("/user/reveal-info", {
method: "POST",
headers: this._headers(),
body: JSON.stringify({
user_id: this.userInfo.asset_reveal_user_id,
asset,
}),
});
const data = await response.json();
if (data.status !== "success") {
this.$alert(data.message || "查看失败,请稍后重试。", "查看失败", "#cc4444");
return;
}
// 只解锁当前点击的资产字段,其他隐藏字段仍保持星号并可单独付费查看。
this.userInfo[asset] = data.value;
if (window.chatContext && data.jjb !== undefined) {
window.chatContext.myGold = data.jjb;
}
this.$alert(data.message || `${label}已显示。`, "查看成功", "#16a34a");
} catch (e) {
this.$alert("网络异常,请稍后重试。", "查看失败", "#cc4444");
}
},
/** 格式化名片里的银行存款显示。 */
displayBankBalance() {
if (this.userInfo.bank_jjb_can_reveal && this.userInfo.bank_jjb_masked) {
return "****** 👁️";
}
return Number(this.userInfo.bank_jjb || 0).toLocaleString();
},
/** 返回银行存款字段的悬停提示。 */
bankBalanceTitle() {
if (this.userInfo.bank_jjb_can_reveal && this.userInfo.bank_jjb_masked) {
const cost = Number(this.userInfo.bank_jjb_reveal_cost || 1000).toLocaleString();
return `点击查看存款,需扣除 ${cost} 金币`;
}
return "银行存款";
},
/** 返回银行存款字段的点击态样式。 */
bankBalanceStyle() {
const clickable = this.userInfo.bank_jjb_can_reveal && this.userInfo.bank_jjb_masked;
return [
"font-weight: 700",
"color: #059669",
"font-size: 14px",
clickable ? "cursor: pointer" : "cursor: default",
"text-decoration: none",
].join("; ");
},
/** 付费查看当前名片用户的银行存款。 */
async revealBankBalance() {
if (!this.userInfo.bank_jjb_can_reveal || !this.userInfo.bank_jjb_masked || !this.userInfo.bank_reveal_user_id) {
return;
}
const cost = Number(this.userInfo.bank_jjb_reveal_cost || 1000);
const confirmed = await this.$confirm(
`查看 TA 的银行存款需扣除 ${cost.toLocaleString()} 金币,是否继续?`,
"信息查看付费",
"#d97706",
"扣费查看",
"取消"
);
if (!confirmed) {
return;
}
try {
const response = await fetch("/user/reveal-info", {
method: "POST",
headers: this._headers(),
body: JSON.stringify({
user_id: this.userInfo.bank_reveal_user_id,
asset: "bank_jjb",
}),
});
const data = await response.json();
if (data.status !== "success") {
this.$alert(data.message || "查看失败,请稍后重试。", "查看失败", "#cc4444");
return;
}
// 仅解锁当前名片这一处显示,重新打开名片时仍以后端返回的遮罩状态为准。
this.userInfo.bank_jjb = data.value;
this.userInfo.bank_jjb_masked = false;
this.userInfo.bank_jjb_can_reveal = false;
if (window.chatContext && data.jjb !== undefined) {
window.chatContext.myGold = data.jjb;
}
this.$alert(data.message || "存款金额已显示。", "查看成功", "#16a34a");
} catch (e) {
this.$alert("网络异常,请稍后重试。", "查看失败", "#cc4444");
}
},
/** 切换好友关系(加好友 / 删好友) */
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;
}