// 婚姻状态弹窗模块,负责我的婚姻、已婚列表、接受/拒绝和离婚申请。 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} */ export async function fetchMyMarriageStatus() { const body = byId("marriage-status-body"); const footer = byId("marriage-status-footer"); if (body) { body.innerHTML = '
加载中…
'; } 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 = '
❌ 加载失败,请稍后重试
'; } } } /** * 拉取已婚列表分页数据。 * * @param {number} page 页码 * @returns {Promise} */ export async function fetchMarriedList(page) { if (page < 1) { return; } const container = byId("married-list-container"); if (container) { container.innerHTML = '
加载中…
'; } 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 = '
❌ 加载失败
'; } } } /** * 渲染已婚列表。 * * @param {Array>} data 已婚记录 * @param {Record} 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 = '
💖 暂无婚姻记录,快去寻找你的另一半吧
'; 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} 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} 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 `
${buildMarriedUserHtml(user, userColor)}
💖
${buildMarriedUserHtml(partner, partnerColor)}
💍 ${escapeHtml(ring ? ring.name : "无戒指")} 💞 ${Number(marriage.intimacy || 0).toLocaleString()} 📅 ${escapeHtml(date)}
`; } /** * 构建已婚列表中的用户头像和名称。 * * @param {Record} user 用户信息 * @param {string} colorStyle 性别颜色样式 * @returns {string} */ function buildMarriedUserHtml(user, colorStyle) { const username = user.username || "—"; const headfaceUrl = normalizeHeadfaceUrl(user.headface_url); return `
${escapeHtml(username)}
`; } /** * 关闭婚姻状态弹窗。 * * @returns {void} */ export function closeMarriageStatusModal() { const modal = byId("marriage-status-modal"); if (modal) { modal.style.display = "none"; } } /** * 渲染“我的婚姻”状态。 * * @param {Record} 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 = '
暂无有效婚姻记录
'; footer.innerHTML = closeButtonHtml(); } /** * 渲染单身状态。 * * @param {HTMLElement} body 内容容器 * @param {HTMLElement} footer 底部容器 * @returns {void} */ function renderSingleStatus(body, footer) { body.innerHTML = `
🕊️
目前单身
还没有婚姻记录。
可在用户名片上点击「求婚」发起求婚。
`; footer.innerHTML = closeButtonHtml(); } /** * 渲染求婚中状态。 * * @param {HTMLElement} body 内容容器 * @param {HTMLElement} footer 底部容器 * @param {Record} 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 ? `${escapeHtml(marriage.ring.icon ?? "💍")} ${escapeHtml(marriage.ring.name)}` : ""; const otherName = other?.username ?? "—"; body.innerHTML = `
💌
${iProposed ? `你向 ${escapeHtml(otherName)} 发出了求婚` : `${escapeHtml(otherName)} 向你求婚啦!`}
${ringHtml ? `
戒指:${ringHtml}
` : ""}
过期时间:${escapeHtml(expireAt)}
`; footer.innerHTML = iProposed ? waitingFooterHtml() : pendingActionFooterHtml(marriage.id); } /** * 渲染已婚状态。 * * @param {HTMLElement} body 内容容器 * @param {HTMLElement} footer 底部容器 * @param {Record} 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 = `
${escapeHtml(levelIcon)}
已与 ${escapeHtml(other?.username ?? "—")} 成婚 🎉
婚姻等级:${escapeHtml(levelName)}
${days}
携手天数
${intimacy.toLocaleString()}
亲密度
💍 戒指:${escapeHtml(ringHtml)}  |  📅 婚期:${escapeHtml(marriedAt)}
`; footer.innerHTML = ` ${closeButtonHtml()} `; } /** * 生成关闭按钮 HTML。 * * @returns {string} */ function closeButtonHtml() { return ` `; } /** * 生成等待对方回应按钮 HTML。 * * @returns {string} */ function waitingFooterHtml() { return ` `; } /** * 生成求婚方操作按钮 HTML。 * * @param {number|string} marriageId 婚姻记录 ID * @returns {string} */ function pendingActionFooterHtml(marriageId) { const safeMarriageId = escapeHtml(marriageId); return ` `; } /** * 通用婚姻操作,处理接受或拒绝求婚。 * * @param {string|number} marriageId 婚姻记录 ID * @param {string} action 操作类型 * @returns {Promise} */ 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} */ 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") || ""); } }); }