Files
chatroom/resources/js/chat-room/marriage-status.js
T
2026-04-25 15:00:04 +08:00

670 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 婚姻状态弹窗模块,负责我的婚姻、已婚列表、接受/拒绝和离婚申请。
import { escapeHtml } from "./html.js";
const DEFAULT_STATUS_URL = "/marriage/status";
const DEFAULT_LIST_URL = "/marriage/list";
const DEFAULT_HEADFACE_URL = "/images/headface/1.gif";
let marriageStatusEventsBound = false;
/**
* 快速读取 DOM 节点。
*
* @param {string} id 节点 ID
* @returns {HTMLElement|null}
*/
function byId(id) {
return document.getElementById(id);
}
/**
* 读取 CSRF Token,给婚姻操作接口使用。
*
* @returns {string}
*/
function csrfToken() {
return document.querySelector('meta[name="csrf-token"]')?.content ?? "";
}
/**
* 限定头像 URL 来源,避免把异常协议写入 img src。
*
* @param {unknown} url 头像 URL
* @returns {string}
*/
function normalizeHeadfaceUrl(url) {
const value = String(url || "").trim();
if (value.startsWith("/images/") || value.startsWith("http://") || value.startsWith("https://")) {
return value;
}
return DEFAULT_HEADFACE_URL;
}
/**
* 从分页文本解析当前页码。
*
* @returns {number}
*/
function resolveCurrentMarriedPage() {
const pageInfo = byId("married-page-info")?.textContent || "1 / 1";
const currentPage = Number.parseInt(pageInfo.split("/")[0]?.trim() || "1", 10);
return Number.isInteger(currentPage) && currentPage > 0 ? currentPage : 1;
}
/**
* 打开婚礼红包领取弹窗。
*
* @param {string} ceremonyId 婚礼记录 ID
* @returns {void}
*/
function openWeddingEnvelopeClaim(ceremonyId) {
const detail = window._weddingEnvelopes?.[ceremonyId];
const modal = byId("wedding-envelope-modal");
if (!detail || !modal || typeof window.Alpine?.$data !== "function") {
return;
}
// 红包详情仍由婚礼弹窗模块维护,这里只按 ID 打开现有 Alpine 弹窗。
window.Alpine.$data(modal).open(detail);
}
/**
* 打开婚姻状态弹窗并默认进入“我的婚姻”。
*
* @returns {void}
*/
export function openMarriageStatusModal() {
const modal = byId("marriage-status-modal");
if (!modal) {
return;
}
modal.style.display = "flex";
switchMarriageTab("mine");
}
/**
* 切换婚姻状态弹窗的 tab。
*
* @param {string} tabName tab 名称
* @returns {void}
*/
export function switchMarriageTab(tabName) {
byId("marriage-tabbtn-mine")?.classList.toggle("active", tabName === "mine");
byId("marriage-tabbtn-list")?.classList.toggle("active", tabName === "list");
const mineView = byId("marriage-view-mine");
const listView = byId("marriage-view-list");
if (mineView) {
mineView.style.display = tabName === "mine" ? "flex" : "none";
}
if (listView) {
listView.style.display = tabName === "list" ? "flex" : "none";
}
if (tabName === "mine") {
fetchMyMarriageStatus();
return;
}
fetchMarriedList(1);
}
/**
* 拉取当前用户婚姻状态。
*
* @returns {Promise<void>}
*/
export async function fetchMyMarriageStatus() {
const body = byId("marriage-status-body");
const footer = byId("marriage-status-footer");
if (body) {
body.innerHTML = '<div style="text-align:center;color:#aaa;padding:30px 0;font-size:12px;">加载中…</div>';
}
if (footer) {
footer.innerHTML = "";
}
try {
const response = await fetch(DEFAULT_STATUS_URL, {
headers: {
Accept: "application/json",
},
});
const data = await response.json();
renderMarriageStatus(data);
} catch (error) {
if (body) {
body.innerHTML = '<div style="text-align:center;color:#e55;padding:30px 0;font-size:12px;">❌ 加载失败,请稍后重试</div>';
}
}
}
/**
* 拉取已婚列表分页数据。
*
* @param {number} page 页码
* @returns {Promise<void>}
*/
export async function fetchMarriedList(page) {
if (page < 1) {
return;
}
const container = byId("married-list-container");
if (container) {
container.innerHTML = '<div style="text-align:center;color:#aaa;padding:30px 0;font-size:12px;">加载中…</div>';
}
try {
const response = await fetch(`${DEFAULT_LIST_URL}?page=${encodeURIComponent(page)}`, {
headers: {
Accept: "application/json",
},
});
const json = await response.json();
if (json.status === "success") {
window.marriedListPage = json.pagination.current_page;
renderMarriedList(json.data, json.pagination);
}
} catch (error) {
if (container) {
container.innerHTML = '<div style="text-align:center;color:#e55;padding:30px 0;font-size:12px;">❌ 加载失败</div>';
}
}
}
/**
* 渲染已婚列表。
*
* @param {Array<Record<string, any>>} data 已婚记录
* @param {Record<string, any>} pagination 分页信息
* @returns {void}
*/
export function renderMarriedList(data, pagination) {
const container = byId("married-list-container");
const paginationElement = byId("married-list-pagination");
if (!container) {
return;
}
if (!data || data.length === 0) {
container.innerHTML = '<div style="text-align:center;color:#aaa;padding:40px 0;font-size:12px;">💖 暂无婚姻记录,快去寻找你的另一半吧</div>';
if (paginationElement) {
paginationElement.style.display = "none";
}
return;
}
if (paginationElement) {
paginationElement.style.display = "flex";
}
updateMarriedPagination(pagination);
container.innerHTML = data.map((marriage) => buildMarriedListItemHtml(marriage)).join("");
}
/**
* 更新已婚列表分页按钮状态。
*
* @param {Record<string, any>} pagination 分页信息
* @returns {void}
*/
function updateMarriedPagination(pagination) {
const currentPage = Number(pagination.current_page || 1);
const lastPage = Number(pagination.last_page || 1);
const pageInfo = byId("married-page-info");
const prevButton = byId("married-prev-btn");
const nextButton = byId("married-next-btn");
if (pageInfo) {
pageInfo.textContent = `${currentPage} / ${lastPage}`;
}
if (prevButton) {
prevButton.disabled = currentPage <= 1;
prevButton.style.opacity = currentPage <= 1 ? 0.5 : 1;
}
if (nextButton) {
nextButton.disabled = currentPage >= lastPage;
nextButton.style.opacity = currentPage >= lastPage ? 0.5 : 1;
}
}
/**
* 构建已婚列表单条记录 HTML。
*
* @param {Record<string, any>} marriage 婚姻记录
* @returns {string}
*/
function buildMarriedListItemHtml(marriage) {
const user = marriage.user || {};
const partner = marriage.partner || {};
const ring = marriage.ring_item;
const date = marriage.married_at ? String(marriage.married_at).substring(0, 10) : "—";
const userColor = Number(user.sex || 0) === 2 ? "color:#e91e8c;" : "";
const partnerColor = Number(partner.sex || 0) === 2 ? "color:#e91e8c;" : "";
return `
<div class="married-list-item">
<div class="married-couple-info">
${buildMarriedUserHtml(user, userColor)}
<div class="married-heart">💖</div>
${buildMarriedUserHtml(partner, partnerColor)}
</div>
<div class="married-meta-info">
<span>💍 ${escapeHtml(ring ? ring.name : "无戒指")}</span>
<span>💞 <span class="married-intimacy">${Number(marriage.intimacy || 0).toLocaleString()}</span></span>
<span>📅 ${escapeHtml(date)}</span>
</div>
</div>
`;
}
/**
* 构建已婚列表中的用户头像和名称。
*
* @param {Record<string, any>} user 用户信息
* @param {string} colorStyle 性别颜色样式
* @returns {string}
*/
function buildMarriedUserHtml(user, colorStyle) {
const username = user.username || "—";
const headfaceUrl = normalizeHeadfaceUrl(user.headface_url);
return `
<div class="married-user-box" style="cursor:pointer;" data-marriage-user-card="${escapeHtml(username)}">
<img src="${escapeHtml(headfaceUrl)}" class="married-user-avatar" onerror="this.src='${DEFAULT_HEADFACE_URL}'">
<span class="married-user-name" style="${colorStyle}" title="${escapeHtml(username)}">${escapeHtml(username)}</span>
</div>
`;
}
/**
* 关闭婚姻状态弹窗。
*
* @returns {void}
*/
export function closeMarriageStatusModal() {
const modal = byId("marriage-status-modal");
if (modal) {
modal.style.display = "none";
}
}
/**
* 渲染“我的婚姻”状态。
*
* @param {Record<string, any>} data 婚姻状态接口返回
* @returns {void}
*/
export function renderMarriageStatus(data) {
const body = byId("marriage-status-body");
const footer = byId("marriage-status-footer");
if (!body || !footer) {
return;
}
if (!data.status || data.status === "none" || !data.marriage) {
renderSingleStatus(body, footer);
return;
}
if (data.status === "pending") {
renderPendingStatus(body, footer, data.marriage);
return;
}
if (data.status === "married") {
renderMarriedStatus(body, footer, data.marriage);
return;
}
body.innerHTML = '<div style="text-align:center;color:#999;padding:30px 0;font-size:12px;">暂无有效婚姻记录</div>';
footer.innerHTML = closeButtonHtml();
}
/**
* 渲染单身状态。
*
* @param {HTMLElement} body 内容容器
* @param {HTMLElement} footer 底部容器
* @returns {void}
*/
function renderSingleStatus(body, footer) {
body.innerHTML = `
<div style="text-align:center; padding:16px 0;">
<div style="font-size:40px; margin-bottom:10px;">🕊️</div>
<div style="font-size:14px; font-weight:bold; color:#555;">目前单身</div>
<div style="font-size:11px; color:#999; margin-top:6px; line-height:1.7;">
还没有婚姻记录。<br>可在用户名片上点击「求婚」发起求婚。
</div>
</div>`;
footer.innerHTML = closeButtonHtml();
}
/**
* 渲染求婚中状态。
*
* @param {HTMLElement} body 内容容器
* @param {HTMLElement} footer 底部容器
* @param {Record<string, any>} marriage 婚姻记录
* @returns {void}
*/
function renderPendingStatus(body, footer, marriage) {
const me = window.__chatUser;
const other = marriage.user?.id === me?.id ? marriage.partner : marriage.user;
const iProposed = marriage.user?.id === me?.id;
const expireAt = marriage.expires_at
? new Date(marriage.expires_at).toLocaleString("zh-CN", {
hour12: false,
})
: "—";
const ringHtml = marriage.ring ? `<span style="font-size:13px;">${escapeHtml(marriage.ring.icon ?? "💍")} ${escapeHtml(marriage.ring.name)}</span>` : "";
const otherName = other?.username ?? "—";
body.innerHTML = `
<div style="text-align:center; padding:8px 0;">
<div style="font-size:36px; margin-bottom:8px;">💌</div>
<div style="font-size:14px; font-weight:bold; color:#be185d;">
${iProposed ? `你向 ${escapeHtml(otherName)} 发出了求婚` : `${escapeHtml(otherName)} 向你求婚啦!`}
</div>
${ringHtml ? `<div style="margin:8px 0; font-size:12px; color:#666;">戒指:${ringHtml}</div>` : ""}
<div style="font-size:11px; color:#999; margin-top:6px;">
过期时间:${escapeHtml(expireAt)}
</div>
</div>`;
footer.innerHTML = iProposed ? waitingFooterHtml() : pendingActionFooterHtml(marriage.id);
}
/**
* 渲染已婚状态。
*
* @param {HTMLElement} body 内容容器
* @param {HTMLElement} footer 底部容器
* @param {Record<string, any>} marriage 婚姻记录
* @returns {void}
*/
function renderMarriedStatus(body, footer, marriage) {
const me = window.__chatUser;
const other = marriage.user?.id === me?.id ? marriage.partner : marriage.user;
const levelIcon = marriage.level_icon ?? "💑";
const levelName = marriage.level_name ?? "新婚";
const days = Number(marriage.days || 0);
const intimacy = Number(marriage.intimacy || 0);
const marriedAt = marriage.married_at ?? "—";
const ringHtml = marriage.ring ? `${marriage.ring.icon ?? "💍"} ${marriage.ring.name}` : "无";
body.innerHTML = `
<div style="text-align:center; margin-bottom:12px;">
<div style="font-size:36px; margin-bottom:6px;">${escapeHtml(levelIcon)}</div>
<div style="font-size:14px; font-weight:bold; color:#be185d;">
已与 <strong>${escapeHtml(other?.username ?? "—")}</strong> 成婚 🎉
</div>
<div style="font-size:12px; color:#999; margin-top:4px;">婚姻等级:${escapeHtml(levelName)}</div>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:8px; font-size:12px;">
<div style="background:#fdf2f8;border:1px solid #fbcfe8;border-radius:6px;padding:10px;text-align:center;">
<div style="color:#be185d;font-weight:bold;font-size:18px;">${days}</div>
<div style="color:#888;margin-top:2px;">携手天数</div>
</div>
<div style="background:#fdf4ff;border:1px solid #e9d5ff;border-radius:6px;padding:10px;text-align:center;">
<div style="color:#7c3aed;font-weight:bold;font-size:18px;">${intimacy.toLocaleString()}</div>
<div style="color:#888;margin-top:2px;">亲密度</div>
</div>
<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;padding:8px 10px;grid-column:1/-1;">
<span style="color:#666;">💍 戒指:</span><span style="color:#333;">${escapeHtml(ringHtml)}</span>
&nbsp;&nbsp;
<span style="color:#666;">📅 婚期:</span><span style="color:#333;">${escapeHtml(marriedAt)}</span>
</div>
</div>`;
footer.innerHTML = `
${closeButtonHtml()}
<button data-marriage-divorce="${escapeHtml(marriage.id)}"
style="flex:.8;padding:9px;border:1px solid #fca5a5;background:#fff;
color:#dc2626;border-radius:6px;font-size:12px;cursor:pointer;">
💔 申请离婚
</button>`;
}
/**
* 生成关闭按钮 HTML。
*
* @returns {string}
*/
function closeButtonHtml() {
return `
<button data-marriage-modal-close
style="flex:1;padding:9px;background:#f3f4f6;color:#555;
border:1px solid #d1d5db;border-radius:6px;font-size:13px;cursor:pointer;">
关闭
</button>`;
}
/**
* 生成等待对方回应按钮 HTML。
*
* @returns {string}
*/
function waitingFooterHtml() {
return `
<button data-marriage-modal-close
style="flex:1;padding:9px;background:#f3f4f6;color:#555;
border:1px solid #d1d5db;border-radius:6px;font-size:13px;cursor:pointer;">
关闭(等待对方回应)
</button>`;
}
/**
* 生成求婚方操作按钮 HTML。
*
* @param {number|string} marriageId 婚姻记录 ID
* @returns {string}
*/
function pendingActionFooterHtml(marriageId) {
const safeMarriageId = escapeHtml(marriageId);
return `
<button data-marriage-action="reject" data-marriage-id="${safeMarriageId}" data-marriage-close-after-action="1"
style="flex:1;padding:9px;background:#f3f4f6;color:#555;
border:1px solid #d1d5db;border-radius:6px;font-size:13px;cursor:pointer;">
😢 婉拒
</button>
<button data-marriage-action="accept" data-marriage-id="${safeMarriageId}" data-marriage-close-after-action="1"
style="flex:1;padding:9px;background:linear-gradient(135deg,#be185d,#f43f5e);
color:#fff;border:none;border-radius:6px;font-size:13px;
font-weight:bold;cursor:pointer;">
💑 答应啦!
</button>`;
}
/**
* 通用婚姻操作,处理接受或拒绝求婚。
*
* @param {string|number} marriageId 婚姻记录 ID
* @param {string} action 操作类型
* @returns {Promise<void>}
*/
export async function marriageAction(marriageId, action) {
try {
const response = await fetch(`/marriage/${encodeURIComponent(marriageId)}/${encodeURIComponent(action)}`, {
method: "POST",
headers: {
"X-CSRF-TOKEN": csrfToken(),
"Content-Type": "application/json",
Accept: "application/json",
},
});
const data = await response.json();
if (data.ok) {
window.chatDialog?.alert(data.message || (action === "accept" ? "已接受求婚!" : "已婉拒求婚"), action === "accept" ? "💑 恭喜!" : "提示", action === "accept" ? "#be185d" : "#6b7280");
return;
}
window.chatDialog?.alert(data.message || "操作失败", "提示", "#f59e0b");
} catch (error) {
window.chatDialog?.alert("网络异常,请稍后重试", "错误", "#ef4444");
}
}
/**
* 申请协议离婚。
*
* @param {string|number} marriageId 婚姻记录 ID
* @returns {Promise<void>}
*/
export async function tryDivorce(marriageId) {
closeMarriageStatusModal();
const confirmed = await window.chatDialog?.confirm(
"申请协议离婚后,对方有权同意或拒绝(拒绝即转为强制离婚,双方均扣除魅力值)。\n\n确定要申请吗?",
"💔 申请离婚",
"#dc2626",
);
if (!confirmed) {
return;
}
try {
const response = await fetch(`/marriage/${encodeURIComponent(marriageId)}/divorce`, {
method: "POST",
headers: {
"X-CSRF-TOKEN": csrfToken(),
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
type: "mutual",
}),
});
const data = await response.json();
window.chatDialog?.alert(data.message || "申请已发送", "提示", data.ok ? "#10b981" : "#f59e0b");
} catch (error) {
window.chatDialog?.alert("网络异常,请稍后重试", "错误", "#ef4444");
}
}
/**
* 把婚姻状态模块函数暴露给仍在 Blade 内的入口。
*
* @returns {void}
*/
function exposeMarriageGlobals() {
window.marriedListPage = window.marriedListPage || 1;
window.openMarriageStatusModal = openMarriageStatusModal;
window.switchMarriageTab = switchMarriageTab;
window.fetchMyMarriageStatus = fetchMyMarriageStatus;
window.fetchMarriedList = fetchMarriedList;
window.renderMarriedList = renderMarriedList;
window.closeMarriageStatusModal = closeMarriageStatusModal;
window.renderMarriageStatus = renderMarriageStatus;
window.marriageAction = marriageAction;
window.tryDivorce = tryDivorce;
}
/**
* 绑定婚姻弹窗 tab、分页、用户名片、状态操作和婚礼红包领取事件。
*
* @returns {void}
*/
export function bindMarriageStatusControls() {
if (typeof window === "undefined" || typeof document === "undefined") {
return;
}
exposeMarriageGlobals();
if (marriageStatusEventsBound) {
return;
}
marriageStatusEventsBound = true;
document.addEventListener("click", (event) => {
if (!(event.target instanceof Element)) {
return;
}
const modal = byId("marriage-status-modal");
if (modal && event.target === modal) {
closeMarriageStatusModal();
return;
}
const tabButton = event.target.closest("[data-marriage-tab]");
if (tabButton) {
event.preventDefault();
switchMarriageTab(tabButton.getAttribute("data-marriage-tab") || "");
return;
}
if (event.target.closest("[data-marriage-modal-close]")) {
event.preventDefault();
closeMarriageStatusModal();
return;
}
if (event.target.closest("[data-marriage-open-shop]")) {
event.preventDefault();
window.openShopModal?.();
return;
}
const weddingEnvelopeButton = event.target.closest("[data-wedding-envelope-claim]");
if (weddingEnvelopeButton) {
event.preventDefault();
openWeddingEnvelopeClaim(weddingEnvelopeButton.getAttribute("data-wedding-envelope-claim") || "");
return;
}
const pageButton = event.target.closest("[data-marriage-page-delta]");
if (pageButton) {
event.preventDefault();
const delta = Number.parseInt(pageButton.getAttribute("data-marriage-page-delta") || "0", 10);
fetchMarriedList(resolveCurrentMarriedPage() + delta);
return;
}
const userCard = event.target.closest("[data-marriage-user-card]");
if (userCard) {
event.preventDefault();
window.openUserCard?.(userCard.getAttribute("data-marriage-user-card") || "");
return;
}
const actionButton = event.target.closest("[data-marriage-action]");
if (actionButton) {
event.preventDefault();
marriageAction(actionButton.getAttribute("data-marriage-id") || "", actionButton.getAttribute("data-marriage-action") || "");
if (actionButton.getAttribute("data-marriage-close-after-action") === "1") {
closeMarriageStatusModal();
}
return;
}
const divorceButton = event.target.closest("[data-marriage-divorce]");
if (divorceButton) {
event.preventDefault();
tryDivorce(divorceButton.getAttribute("data-marriage-divorce") || "");
}
});
}