(function (root, factory) { if (typeof module === 'object' && module.exports) { module.exports = factory(root); return; } root.MemoPages = factory(root); if (root.document) { root.document.addEventListener('DOMContentLoaded', function () { root.MemoPages.init(); }); } })(typeof globalThis !== 'undefined' ? globalThis : window, function (root) { 'use strict'; var documentRef = root.document; var memosById = Object.create(null); var writePending = false; function getApi() { return root.GenealogyApi && root.GenealogyApi.defaultClient; } function query(selector, node) { return documentRef ? (node || documentRef).querySelector(selector) : null; } function queryAll(selector, node) { return documentRef ? Array.prototype.slice.call((node || documentRef).querySelectorAll(selector)) : []; } function trimOrUndefined(value) { var text = String(value === undefined || value === null ? '' : value).trim(); return text || undefined; } function queryParam(search, name) { return new URLSearchParams(String(search || '').replace(/^\?/, '')).get(name) || ''; } function normalizeId(value) { if (value === undefined || value === null || value === '') return ''; if (typeof value === 'number' && !Number.isSafeInteger(value)) return ''; return /^[1-9][0-9]*$/.test(String(value)) ? String(value) : ''; } function getCurrentGenealogyId(search) { var source = search === undefined && root.location ? root.location.search : search; var profileGenealogyId; if (search === undefined && root.ProfileUI && root.ProfileUI.getGenealogyId) { profileGenealogyId = root.ProfileUI.getGenealogyId(); if (profileGenealogyId) return normalizeId(profileGenealogyId); } return normalizeId(queryParam(source, 'genealogyId')); } function getCurrentMemoId(search) { var source = search === undefined && root.location ? root.location.search : search; return normalizeId(queryParam(source, 'memoId')); } function toBackendDateTime(value) { var text = trimOrUndefined(value); if (!text) return undefined; if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(text)) return text.replace('T', ' ') + ':00'; if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(text)) return text.replace('T', ' '); return text; } function toDateTimeInputValue(value) { var text = String(value || ''); if (!/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/.test(text)) return ''; return text.slice(0, 16).replace(' ', 'T'); } function toSafeInteger(value) { var text = trimOrUndefined(value); var number; if (!text) return undefined; number = Number(text); return Number.isSafeInteger(number) ? number : undefined; } function buildMemoBody(values) { var source = values || {}; var body = { memoTitle: String(source.memoTitle || '').trim() }; var memoContent = trimOrUndefined(source.memoContent); var remindTime = toBackendDateTime(source.remindTime); var completed = trimOrUndefined(source.completed); var mediaOssIds = trimOrUndefined(source.mediaOssIds); var sortOrderText = trimOrUndefined(source.sortOrder); var status = trimOrUndefined(source.status); if (memoContent !== undefined) body.memoContent = memoContent; if (remindTime !== undefined) body.remindTime = remindTime; if (completed !== undefined) body.completed = completed; if (mediaOssIds !== undefined) body.mediaOssIds = mediaOssIds; if (sortOrderText !== undefined) { body.sortOrder = toSafeInteger(sortOrderText); if (body.sortOrder === undefined) body.invalidSortOrder = true; } if (status !== undefined) body.status = status; return body; } function isValidBackendDateTime(value) { var match = String(value || '').match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/); var year; var month; var day; var days; if (!match) return false; year = Number(match[1]); month = Number(match[2]); day = Number(match[3]); if (month < 1 || month > 12 || Number(match[4]) > 23 || Number(match[5]) > 59 || Number(match[6]) > 59) return false; days = [31, year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; return day >= 1 && day <= days[month - 1]; } function validateMemoBody(body) { if (!body || !body.memoTitle) return '请填写备忘录标题'; if (body.remindTime !== undefined && !isValidBackendDateTime(body.remindTime)) return '提醒时间格式无效'; if (body.completed !== undefined && body.completed !== '0' && body.completed !== '1') { return '完成状态只能是未完成或已完成'; } if (body.mediaOssIds !== undefined && !/^[1-9][0-9]*(,[1-9][0-9]*)*$/.test(body.mediaOssIds)) { return '附件上传结果无效'; } if (body.invalidSortOrder || (Object.prototype.hasOwnProperty.call(body, 'sortOrder') && !Number.isSafeInteger(body.sortOrder))) { return '排序值必须是安全整数'; } if (body.status === '1') return '当前 PC 无法重新读取停用备忘录,暂不开放停用'; if (body.status !== undefined && body.status !== '0') return '备忘录状态只能是 0'; return ''; } function stringValue(value) { return value === undefined || value === null ? '' : String(value); } function optionalSafeInteger(value) { var number; if (value === undefined || value === null || value === '') return undefined; number = Number(value); return Number.isSafeInteger(number) ? number : undefined; } function normalizeMemo(item) { var memoId = normalizeId(item && item.memoId); var genealogyId = normalizeId(item && item.genealogyId); var appUserId = normalizeId(item && item.appUserId); var memoTitle = String(item && item.memoTitle || '').trim(); var completed = stringValue(item && item.completed); var mediaOssIds = stringValue(item && item.mediaOssIds); var status = stringValue(item && item.status); var sortOrder = optionalSafeInteger(item && item.sortOrder); var memo; if (!memoId || !genealogyId || !memoTitle || (completed !== '0' && completed !== '1') || (status !== '0' && status !== '1')) { return null; } if (item.appUserId !== undefined && item.appUserId !== null && item.appUserId !== '' && !appUserId) return null; if (mediaOssIds && !/^[1-9][0-9]*(,[1-9][0-9]*)*$/.test(mediaOssIds)) return null; if (item.sortOrder !== undefined && item.sortOrder !== null && item.sortOrder !== '' && sortOrder === undefined) return null; if (item.remindTime && !isValidBackendDateTime(String(item.remindTime))) return null; memo = { memoId: memoId, genealogyId: genealogyId, genealogyNo: stringValue(item.genealogyNo), genealogyName: stringValue(item.genealogyName), surname: stringValue(item.surname), appUserNickName: stringValue(item.appUserNickName), appUserPhone: stringValue(item.appUserPhone), memoTitle: memoTitle, memoContent: stringValue(item.memoContent), remindTime: stringValue(item.remindTime), completed: completed, mediaOssIds: mediaOssIds, status: status, remark: stringValue(item.remark) }; if (appUserId) memo.appUserId = appUserId; if (sortOrder !== undefined) memo.sortOrder = sortOrder; return memo; } function matchesSavedMemo(item, expectedMemoId) { var memo = normalizeMemo(item); return Boolean(memo && memo.memoId === normalizeId(expectedMemoId)); } function isEditableMemo(memo) { return Boolean(memo && memo.status === '0'); } function escapeHtml(value) { return stringValue(value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function attachmentCount(mediaOssIds) { return mediaOssIds ? mediaOssIds.split(',').filter(Boolean).length : 0; } function buildListUrl(genealogyId, memoId) { var url = 'profile-memo.html?genealogyId=' + encodeURIComponent(genealogyId); return memoId ? url + '&memoId=' + encodeURIComponent(memoId) : url; } function buildEditUrl(genealogyId, memoId) { var url = 'profile-memo-edit.html?genealogyId=' + encodeURIComponent(genealogyId); return memoId ? url + '&memoId=' + encodeURIComponent(memoId) : url; } function renderMemoRow(memo) { var summary = []; summary.push(memo.completed === '1' ? '已完成' : '未完成'); if (memo.remindTime) summary.push('提醒:' + memo.remindTime); if (memo.memoContent) summary.push(memo.memoContent); return '

' + escapeHtml(memo.memoTitle) + '

' + escapeHtml(summary.join(' · ')) + '

' + '' + '编辑' + '' + '
'; } function detailValue(label, value) { return '

' + escapeHtml(label) + '' + escapeHtml(value || '未提供') + '

'; } function renderMemoDetail(memo) { var count; if (!memo) return '
请选择一条备忘录查看详情
'; count = attachmentCount(memo.mediaOssIds); return '
' + detailValue('备忘标题', memo.memoTitle) + detailValue('备忘内容', memo.memoContent) + detailValue('提醒时间', memo.remindTime) + detailValue('完成状态', memo.completed === '1' ? '已完成' : '未完成') + detailValue('附件', count ? count + ' 个附件' : '未上传') + detailValue('记录人', memo.appUserNickName) + detailValue('备注', memo.remark) + '
编辑备忘录' + '
'; } function showMessage(message) { if (root.layui && root.layui.layer) root.layui.layer.msg(message); else if (root.alert) root.alert(message); } function shouldRedirectToLogin(api, error) { var status = error && (error.status || error.code); return !api || !api.getToken || !api.getToken() || Number(status) === 401; } function isForbidden(error) { return Number(error && (error.status || error.code)) === 403; } function redirectUnauthorized(api, error) { if (!shouldRedirectToLogin(api, error)) return false; if (api && api.clearToken) api.clearToken(); if (!root.location) return true; if (typeof root.location.replace === 'function') root.location.replace('login.html'); else root.location.href = 'login.html'; return true; } function setFormStatus(message) { var target = query('[data-memo-form-status]'); if (target) target.textContent = message || ''; } function setEditorEnabled(enabled, message) { var placeholder = query('[data-memo-editor-placeholder]'); queryAll('[data-memo-editor], [data-memo-editor-action]').forEach(function (element) { element.hidden = !enabled; }); if (placeholder) { placeholder.hidden = Boolean(enabled); if (message) placeholder.textContent = message; } } function setWritePending(value) { writePending = Boolean(value); queryAll('[data-memo-form] button, [data-memo-form] input, [data-memo-form] textarea, [data-memo-form] select, [data-memo-delete-id]').forEach(function (control) { control.disabled = writePending; }); } function syncLinks() { if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) root.ProfileUI.syncGenealogyContextLinks(); } function renderNoContext() { var list = query('[data-memo-list]'); var detail = query('[data-memo-detail]'); var message = '请先选择家谱,再查看或维护备忘录。'; if (list) list.innerHTML = '
' + message + '
'; if (detail) detail.innerHTML = '
当前没有家谱上下文。
'; setEditorEnabled(false, message); setFormStatus(message); } function requireGenealogyId() { var genealogyId = getCurrentGenealogyId(); if (!genealogyId) renderNoContext(); return genealogyId; } function renderMemos(data) { var container = query('[data-memo-list]'); var normalized = Array.isArray(data) ? data.map(normalizeMemo) : []; var invalidCount = normalized.filter(function (memo) { return !memo; }).length; var memos = normalized.filter(Boolean); memosById = Object.create(null); memos.forEach(function (memo) { memosById[memo.memoId] = memo; }); if (!container) return memos; if (!Array.isArray(data) || invalidCount) { container.innerHTML = '
备忘录响应缺少稳定 MemoVo 字段,请联系后端核对。
'; return []; } if (!memos.length) { container.innerHTML = '
暂无备忘录
'; return []; } container.innerHTML = memos.map(renderMemoRow).join(''); return memos; } function renderDetail(memo) { var container = query('[data-memo-detail]'); if (container) container.innerHTML = renderMemoDetail(memo); } async function loadMemoDetail(memoId) { var api = getApi(); var genealogyId = requireGenealogyId(); var memo; if (!genealogyId || !normalizeId(memoId) || redirectUnauthorized(api)) return null; try { memo = normalizeMemo(await api.memoDetail(genealogyId, memoId)); if (!memo) throw new Error('备忘录详情响应缺少稳定 MemoVo 字段'); memosById[memo.memoId] = memo; renderDetail(memo); return memo; } catch (error) { if (redirectUnauthorized(api, error)) return null; renderDetail(null); showMessage(isForbidden(error) ? '当前账号无权查看该备忘录。' : (error.message || '备忘录详情加载失败')); return null; } } async function loadMemos() { var api = getApi(); var genealogyId; var memos; var requestedMemoId; if (redirectUnauthorized(api)) return; genealogyId = requireGenealogyId(); if (!genealogyId) return; syncLinks(); try { memos = renderMemos(await api.memos(genealogyId)); requestedMemoId = getCurrentMemoId(); if (requestedMemoId && memosById[requestedMemoId]) { await loadMemoDetail(requestedMemoId); } else if (memos.length) { await loadMemoDetail(memos[0].memoId); } else { renderDetail(null); } } catch (error) { if (redirectUnauthorized(api, error)) return; if (query('[data-memo-list]')) { query('[data-memo-list]').innerHTML = '
' + (isForbidden(error) ? '当前账号无权查看该家谱的备忘录。' : '备忘录加载失败,请稍后重试。') + '
'; } showMessage(isForbidden(error) ? '当前账号无权查看该家谱的备忘录。' : (error.message || '备忘录加载失败')); } } function getFormValues(form) { var values = {}; queryAll('[name]', form).forEach(function (field) { values[field.name] = field.value; }); return values; } function setFieldValue(name, value) { var field = query('[data-memo-form] [name="' + name + '"]'); if (field) field.value = value === undefined || value === null ? '' : String(value); } function fillMemoForm(memo) { if (!isEditableMemo(memo)) throw new Error('停用备忘录无法通过当前 PC 接口重新读取或编辑'); setFieldValue('memoTitle', memo.memoTitle); setFieldValue('memoContent', memo.memoContent); setFieldValue('remindTime', toDateTimeInputValue(memo.remindTime)); setFieldValue('completed', memo.completed); setFieldValue('mediaOssIds', memo.mediaOssIds); setFieldValue('sortOrder', memo.sortOrder); setFieldValue('status', '0'); if (query('[data-memo-media-status]')) { query('[data-memo-media-status]').textContent = memo.mediaOssIds ? '已保留 ' + attachmentCount(memo.mediaOssIds) + ' 个附件,可继续选择追加' : '未选择文件'; } } async function loadMemoEditor() { var api = getApi(); var genealogyId; var memoId; var memo; if (redirectUnauthorized(api)) return; genealogyId = requireGenealogyId(); if (!genealogyId) return; memoId = getCurrentMemoId(); syncLinks(); setEditorEnabled(false, '正在加载备忘录编辑信息…'); try { if (memoId) { memo = normalizeMemo(await api.memoDetail(genealogyId, memoId)); if (!memo) throw new Error('备忘录详情响应缺少稳定 MemoVo 字段'); fillMemoForm(memo); if (query('[data-memo-editor-title]')) query('[data-memo-editor-title]').textContent = '编辑备忘录'; } setEditorEnabled(true); } catch (error) { if (redirectUnauthorized(api, error)) return; setEditorEnabled(false, isForbidden(error) ? '当前账号无权维护该家谱的备忘录。' : (error.message || '备忘录编辑信息加载失败')); setFormStatus(error.message || '备忘录编辑信息加载失败'); } } async function submitMemo(form) { var api = getApi(); var genealogyId = requireGenealogyId(); var memoId = getCurrentMemoId(); var body; var validation; var result; var saved; var savedId; var verifiedDetail; if (writePending || !genealogyId || redirectUnauthorized(api)) return; body = buildMemoBody(getFormValues(form)); validation = validateMemoBody(body); if (validation) { setFormStatus(validation); return; } delete body.invalidSortOrder; setWritePending(true); setFormStatus('正在保存备忘录…'); try { result = memoId ? await api.updateMemo(genealogyId, memoId, body) : await api.createMemo(genealogyId, body); saved = normalizeMemo(result); savedId = memoId || (saved && saved.memoId); if (!savedId) throw new Error('保存响应缺少 memoId'); verifiedDetail = await api.memoDetail(genealogyId, savedId); if (!matchesSavedMemo(verifiedDetail, savedId)) throw new Error('保存后重读未返回同一条备忘录'); if (root.location) root.location.href = buildListUrl(genealogyId, savedId); } catch (error) { if (redirectUnauthorized(api, error)) return; setFormStatus(isForbidden(error) ? '当前账号无权保存该备忘录。' : (error.message || '备忘录保存失败')); } finally { setWritePending(false); } } async function deleteMemo(memoId) { var api = getApi(); var genealogyId = requireGenealogyId(); var memo = memosById[memoId]; if (writePending || !genealogyId || !normalizeId(memoId) || redirectUnauthorized(api)) return; if (root.confirm && !root.confirm('确认删除“' + (memo ? memo.memoTitle : '该备忘录') + '”吗?删除后不可恢复,并会释放附件引用。')) return; setWritePending(true); try { await api.deleteMemo(genealogyId, memoId); await loadMemos(); showMessage('备忘录已删除'); } catch (error) { if (redirectUnauthorized(api, error)) return; showMessage(isForbidden(error) ? '当前账号无权删除该备忘录。' : (error.message || '备忘录删除失败')); } finally { setWritePending(false); } } function clearMediaSelection() { var target = query('[data-memo-form] [name="mediaOssIds"]'); var status = query('[data-memo-media-status]'); var fileInput = query('[data-memo-media-input]'); if (target) target.value = ''; if (fileInput) fileInput.value = ''; if (status) status.textContent = '未选择文件'; } function bindActions() { if (!documentRef) return; documentRef.addEventListener('click', function (event) { var detailButton = event.target.closest('[data-memo-detail-id]'); var deleteButton = event.target.closest('[data-memo-delete-id]'); var clearMediaButton = event.target.closest('[data-memo-clear-media]'); if (detailButton) { event.preventDefault(); loadMemoDetail(detailButton.getAttribute('data-memo-detail-id')); return; } if (deleteButton) { event.preventDefault(); deleteMemo(deleteButton.getAttribute('data-memo-delete-id')); return; } if (clearMediaButton) { event.preventDefault(); clearMediaSelection(); } }); documentRef.addEventListener('submit', function (event) { var form = event.target.closest('[data-memo-form]'); if (!form) return; event.preventDefault(); submitMemo(form); }); } function init() { if (!documentRef) return; bindActions(); if (query('[data-memo-page]')) loadMemos(); if (query('[data-memo-edit-page]')) loadMemoEditor(); } return { getCurrentGenealogyId: getCurrentGenealogyId, getCurrentMemoId: getCurrentMemoId, buildMemoBody: buildMemoBody, validateMemoBody: validateMemoBody, normalizeMemo: normalizeMemo, matchesSavedMemo: matchesSavedMemo, isEditableMemo: isEditableMemo, renderMemoDetail: renderMemoDetail, shouldRedirectToLogin: shouldRedirectToLogin, isForbidden: isForbidden, init: init }; });