From 3bbde9b4ddea541a77951c792e1c1600042339c3 Mon Sep 17 00:00:00 2001 From: lkddi Date: Sat, 25 Apr 2026 15:00:04 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=81=E7=A7=BB=E5=A9=9A=E5=A7=BB=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/js/chat-room.js | 48 +- resources/js/chat-room/marriage-status.js | 604 ++++++++++++++++-- .../chat/partials/layout/toolbar.blade.php | 334 +--------- 3 files changed, 613 insertions(+), 373 deletions(-) diff --git a/resources/js/chat-room.js b/resources/js/chat-room.js index 11105ba..ca939d8 100644 --- a/resources/js/chat-room.js +++ b/resources/js/chat-room.js @@ -18,7 +18,7 @@ * - lightbox.js:处理聊天图片预览灯箱。 * - lottery-panel.js:提供双色球彩票 lotteryPanel Alpine 组件和全局开关入口。 * - mobile-drawer.js:处理移动端抽屉、房间列表和在线名单。 - * - marriage-status.js:处理婚姻状态展示与用户名片联动。 + * - marriage-status.js:处理婚姻状态弹窗、已婚列表、接受拒绝和离婚申请。 * - toolbar.js:处理工具栏按钮和功能快捷入口。 * - user-target-actions.js:处理点击用户名切换私聊目标和打开名片。 * - welcome-menu.js:处理欢迎菜单交互。 @@ -70,7 +70,18 @@ export { scheduleRenderMobileUserList, switchMobileTab, } from "./chat-room/mobile-drawer.js"; -export { bindMarriageStatusControls } from "./chat-room/marriage-status.js"; +export { + bindMarriageStatusControls, + closeMarriageStatusModal, + fetchMarriedList, + fetchMyMarriageStatus, + marriageAction, + openMarriageStatusModal, + renderMarriedList, + renderMarriageStatus, + switchMarriageTab, + tryDivorce, +} from "./chat-room/marriage-status.js"; export { bindToolbarControls, runFeatureShortcut, runToolbarAction } from "./chat-room/toolbar.js"; export { bindUserTargetActions, openUserCard, switchTarget } from "./chat-room/user-target-actions.js"; export { bindWelcomeMenuControls } from "./chat-room/welcome-menu.js"; @@ -202,7 +213,18 @@ import { scheduleRenderMobileUserList, switchMobileTab, } from "./chat-room/mobile-drawer.js"; -import { bindMarriageStatusControls } from "./chat-room/marriage-status.js"; +import { + bindMarriageStatusControls, + closeMarriageStatusModal, + fetchMarriedList, + fetchMyMarriageStatus, + marriageAction, + openMarriageStatusModal, + renderMarriedList, + renderMarriageStatus, + switchMarriageTab, + tryDivorce, +} from "./chat-room/marriage-status.js"; import { bindToolbarControls, runFeatureShortcut, runToolbarAction } from "./chat-room/toolbar.js"; import { bindUserTargetActions, openUserCard, switchTarget } from "./chat-room/user-target-actions.js"; import { bindWelcomeMenuControls } from "./chat-room/welcome-menu.js"; @@ -402,7 +424,6 @@ if (typeof window !== "undefined") { stopAutoFishing, bindFortunePanelControls, fortunePanel, - bindMarriageStatusControls, bindProfileControls, closeAvatarPicker, closeSettingsModal, @@ -419,6 +440,16 @@ if (typeof window !== "undefined") { sendEmailCode, showInlineMsg, unbindWechat, + bindMarriageStatusControls, + closeMarriageStatusModal, + fetchMarriedList, + fetchMyMarriageStatus, + marriageAction, + openMarriageStatusModal, + renderMarriedList, + renderMarriageStatus, + switchMarriageTab, + tryDivorce, bindShopControls, buyItem, closeGiftDialog, @@ -555,6 +586,15 @@ if (typeof window !== "undefined") { window.sendEmailCode = sendEmailCode; window.showInlineMsg = showInlineMsg; window.unbindWechat = unbindWechat; + window.closeMarriageStatusModal = closeMarriageStatusModal; + window.fetchMarriedList = fetchMarriedList; + window.fetchMyMarriageStatus = fetchMyMarriageStatus; + window.marriageAction = marriageAction; + window.openMarriageStatusModal = openMarriageStatusModal; + window.renderMarriedList = renderMarriedList; + window.renderMarriageStatus = renderMarriageStatus; + window.switchMarriageTab = switchMarriageTab; + window.tryDivorce = tryDivorce; window.buyItem = buyItem; window.closeGiftDialog = closeGiftDialog; window.closeRenameModal = closeRenameModal; diff --git a/resources/js/chat-room/marriage-status.js b/resources/js/chat-room/marriage-status.js index 7fa83f6..ac97e26 100644 --- a/resources/js/chat-room/marriage-status.js +++ b/resources/js/chat-room/marriage-status.js @@ -1,19 +1,46 @@ -// 婚姻状态弹窗基础事件绑定,替代 toolbar 婚姻区域内联 onclick。 +// 婚姻状态弹窗模块,负责我的婚姻、已婚列表、接受/拒绝和离婚申请。 + +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} functionName 全局函数名 - * @param {...unknown} args 参数 - * @returns {void} + * @param {string} id 节点 ID + * @returns {HTMLElement|null} */ -function callMarriageGlobal(functionName, ...args) { - // 婚姻状态业务仍在 Blade 内维护,当前模块只负责按钮事件和旧函数桥接。 - if (typeof window[functionName] === "function") { - window[functionName](...args); +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; } /** @@ -22,7 +49,7 @@ function callMarriageGlobal(functionName, ...args) { * @returns {number} */ function resolveCurrentMarriedPage() { - const pageInfo = document.getElementById("married-page-info")?.textContent || "1 / 1"; + 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; @@ -36,23 +63,534 @@ function resolveCurrentMarriedPage() { */ function openWeddingEnvelopeClaim(ceremonyId) { const detail = window._weddingEnvelopes?.[ceremonyId]; - const modal = document.getElementById("wedding-envelope-modal"); + const modal = byId("wedding-envelope-modal"); if (!detail || !modal || typeof window.Alpine?.$data !== "function") { return; } - // 红包详情仍由 Blade 旧脚本写入 window._weddingEnvelopes,这里只按 ID 打开现有 Alpine 弹窗。 + // 红包详情仍由婚礼弹窗模块维护,这里只按 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 (marriageStatusEventsBound || typeof document === "undefined") { + if (typeof window === "undefined" || typeof document === "undefined") { + return; + } + + exposeMarriageGlobals(); + + if (marriageStatusEventsBound) { return; } @@ -62,26 +600,29 @@ export function bindMarriageStatusControls() { return; } - // 弹窗 tab 与分页内容由存量脚本渲染,事件统一通过 data-* 分发。 + 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(); - callMarriageGlobal("switchMarriageTab", tabButton.getAttribute("data-marriage-tab") || ""); + switchMarriageTab(tabButton.getAttribute("data-marriage-tab") || ""); return; } if (event.target.closest("[data-marriage-modal-close]")) { event.preventDefault(); - callMarriageGlobal("closeMarriageStatusModal"); + closeMarriageStatusModal(); return; } if (event.target.closest("[data-marriage-open-shop]")) { event.preventDefault(); - // 求婚戒指缺失时只转发到商店弹窗,不触碰婚姻弹窗业务状态。 - if (typeof window.openShopModal === "function") { - window.openShopModal(); - } + window.openShopModal?.(); return; } @@ -95,43 +636,34 @@ export function bindMarriageStatusControls() { 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); - callMarriageGlobal("fetchMarriedList", resolveCurrentMarriedPage() + delta); + fetchMarriedList(resolveCurrentMarriedPage() + delta); return; } const userCard = event.target.closest("[data-marriage-user-card]"); if (userCard) { event.preventDefault(); - callMarriageGlobal("openUserCard", userCard.getAttribute("data-marriage-user-card") || ""); + 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") || ""); - // 接受/拒绝沿用原 marriageAction,按钮可声明完成后关闭弹窗。 - const marriageId = actionButton.getAttribute("data-marriage-id") || ""; - const action = actionButton.getAttribute("data-marriage-action") || ""; - callMarriageGlobal("marriageAction", marriageId, action); - - // 部分操作按钮声明完成后立即关闭弹窗,这是按钮级行为约定。 if (actionButton.getAttribute("data-marriage-close-after-action") === "1") { - callMarriageGlobal("closeMarriageStatusModal"); + closeMarriageStatusModal(); } return; } const divorceButton = event.target.closest("[data-marriage-divorce]"); - if (!divorceButton) { - return; + if (divorceButton) { + event.preventDefault(); + tryDivorce(divorceButton.getAttribute("data-marriage-divorce") || ""); } - - event.preventDefault(); - callMarriageGlobal("tryDivorce", divorceButton.getAttribute("data-marriage-divorce") || ""); }); } diff --git a/resources/views/chat/partials/layout/toolbar.blade.php b/resources/views/chat/partials/layout/toolbar.blade.php index 4cf2f82..3f869b4 100644 --- a/resources/views/chat/partials/layout/toolbar.blade.php +++ b/resources/views/chat/partials/layout/toolbar.blade.php @@ -959,339 +959,7 @@ - +{{-- 婚姻状态弹窗业务脚本已迁移到 resources/js/chat-room/marriage-status.js --}} {{-- ═══════════ 银行弹窗 ═══════════ --}}