feat(api): 完善家谱系统API客户端契约

- 实现家谱管理相关方法,包括创建、详情、概览、我的家谱和选项查询
- 添加家谱加入申请功能,支持申请、审核、取消和待审核列表操作
- 集成通知详情获取方法和通知ID安全验证机制
- 完善功德记录、谱文、相册、视频、祭祀活动的完整CRUD操作契约
- 实现家谱成员管理功能,包含成员列表、更新、移除和转让所有者操作
- 优化路径ID验证逻辑,拒绝不安全的数值ID并提供明确错误提示
- 更新测试用例以验证所有新增API方法的路径和请求体白名单机制
This commit is contained in:
fizzleaf
2026-07-29 16:57:27 +08:00
parent 59a72fb22b
commit fb1743aa2a
74 changed files with 13166 additions and 1320 deletions
+736
View File
@@ -0,0 +1,736 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.AlbumPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.AlbumPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
var albumsById = Object.create(null);
var photosById = Object.create(null);
var currentAlbum = null;
var canEditContent = false;
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 stringValue(value) {
return value === undefined || value === null ? '' : String(value);
}
function trimOrUndefined(value) {
var text = stringValue(value).trim();
return text || undefined;
}
function queryParam(search, name) {
return new URLSearchParams(stringValue(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 normalizeNullableId(value) {
if (value === undefined || value === null || value === '') return '';
return normalizeId(value);
}
function optionalSafeInteger(value) {
var number;
if (value === undefined || value === null || value === '') return undefined;
number = Number(value);
return Number.isSafeInteger(number) ? number : undefined;
}
function getCurrentGenealogyId(search) {
var source = search === undefined && root.location ? root.location.search : search;
var contextId;
if (search === undefined && root.ProfileUI && root.ProfileUI.getGenealogyId) {
contextId = root.ProfileUI.getGenealogyId();
if (contextId) return normalizeId(contextId);
}
return normalizeId(queryParam(source, 'genealogyId'));
}
function getCurrentAlbumId(search) {
var source = search === undefined && root.location ? root.location.search : search;
return normalizeId(queryParam(source, 'albumId'));
}
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 = stringValue(value);
if (!/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/.test(text)) return '';
return text.slice(0, 16).replace(' ', 'T');
}
function isValidBackendDateTime(value) {
var match = stringValue(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 buildAlbumBody(values) {
var source = values || {};
var body = {
albumName: stringValue(source.albumName).trim(),
status: '0'
};
var albumDesc = trimOrUndefined(source.albumDesc);
var coverOssId = normalizeNullableId(source.coverOssId);
var sortOrder = optionalSafeInteger(source.sortOrder);
if (albumDesc !== undefined) body.albumDesc = albumDesc;
if (source.coverOssId !== undefined && source.coverOssId !== null && source.coverOssId !== '') {
if (coverOssId) body.coverOssId = coverOssId;
else body.invalidCoverOssId = true;
}
if (source.sortOrder !== undefined && source.sortOrder !== null && source.sortOrder !== '') {
if (sortOrder !== undefined) body.sortOrder = sortOrder;
else body.invalidSortOrder = true;
}
return body;
}
function validateAlbumBody(body) {
if (!body || !body.albumName) return '请填写相册名称';
if (body.invalidCoverOssId) return '封面文件编号无效,请重新选择文件';
if (body.invalidSortOrder) return '排序值必须是安全整数';
if (body.status === '1') return '当前 PC 无法重新读取停用相册,暂不开放停用';
if (body.status !== '0') return '相册状态只能是 0';
return '';
}
function buildAlbumPhotoBody(values) {
var source = values || {};
var body = { status: '0' };
var ossId = normalizeNullableId(source.ossId);
var photoTitle = trimOrUndefined(source.photoTitle);
var photoDesc = trimOrUndefined(source.photoDesc);
var photographer = trimOrUndefined(source.photographer);
var shootTime = toBackendDateTime(source.shootTime);
var sortOrder = optionalSafeInteger(source.sortOrder);
if (ossId) body.ossId = ossId;
else if (source.ossId !== undefined && source.ossId !== null && source.ossId !== '') body.invalidOssId = true;
if (photoTitle !== undefined) body.photoTitle = photoTitle;
if (photoDesc !== undefined) body.photoDesc = photoDesc;
if (photographer !== undefined) body.photographer = photographer;
if (shootTime !== undefined) body.shootTime = shootTime;
if (source.sortOrder !== undefined && source.sortOrder !== null && source.sortOrder !== '') {
if (sortOrder !== undefined) body.sortOrder = sortOrder;
else body.invalidSortOrder = true;
}
return body;
}
function validateAlbumPhotoBody(body) {
if (!body || !body.ossId || body.invalidOssId) return '请选择并上传照片';
if (body.shootTime !== undefined && !isValidBackendDateTime(body.shootTime)) return '拍摄时间格式无效';
if (body.invalidSortOrder) return '排序值必须是安全整数';
if (body.status === '1') return '当前 PC 无法重新读取停用照片,暂不开放停用';
if (body.status !== '0') return '照片状态只能是 0';
return '';
}
function normalizeAlbum(item) {
var albumId = normalizeId(item && item.albumId);
var genealogyId = normalizeId(item && item.genealogyId);
var coverOssId = normalizeNullableId(item && item.coverOssId);
var albumName = stringValue(item && item.albumName).trim();
var photoCount = optionalSafeInteger(item && item.photoCount);
var sortOrder = optionalSafeInteger(item && item.sortOrder);
var status = stringValue(item && item.status);
var album;
if (!albumId || !genealogyId || !albumName) return null;
if (item.coverOssId !== undefined && item.coverOssId !== null && item.coverOssId !== '' && !coverOssId) return null;
if (item.photoCount !== undefined && item.photoCount !== null && item.photoCount !== '' && photoCount === undefined) return null;
if (item.sortOrder !== undefined && item.sortOrder !== null && item.sortOrder !== '' && sortOrder === undefined) return null;
if (status !== '0' && status !== '1') return null;
album = {
albumId: albumId,
genealogyId: genealogyId,
genealogyNo: stringValue(item.genealogyNo),
genealogyName: stringValue(item.genealogyName),
surname: stringValue(item.surname),
albumName: albumName,
albumDesc: stringValue(item.albumDesc),
status: status,
remark: stringValue(item.remark)
};
if (coverOssId) album.coverOssId = coverOssId;
if (photoCount !== undefined) album.photoCount = photoCount;
if (sortOrder !== undefined) album.sortOrder = sortOrder;
return album;
}
function normalizeAlbumPhoto(item) {
var photoId = normalizeId(item && item.photoId);
var genealogyId = normalizeId(item && item.genealogyId);
var albumId = normalizeId(item && item.albumId);
var ossId = normalizeId(item && item.ossId);
var sortOrder = optionalSafeInteger(item && item.sortOrder);
var status = stringValue(item && item.status);
var photo;
if (!photoId || !genealogyId || !albumId || !ossId) return null;
if (item.sortOrder !== undefined && item.sortOrder !== null && item.sortOrder !== '' && sortOrder === undefined) return null;
if (status !== '0' && status !== '1') return null;
photo = {
photoId: photoId,
genealogyId: genealogyId,
genealogyNo: stringValue(item.genealogyNo),
genealogyName: stringValue(item.genealogyName),
surname: stringValue(item.surname),
albumId: albumId,
albumName: stringValue(item.albumName),
ossId: ossId,
photoTitle: stringValue(item.photoTitle),
photoDesc: stringValue(item.photoDesc),
photographer: stringValue(item.photographer),
shootTime: stringValue(item.shootTime),
status: status,
remark: stringValue(item.remark)
};
if (sortOrder !== undefined) photo.sortOrder = sortOrder;
return photo;
}
function normalizeAlbums(data) {
var normalized;
if (!Array.isArray(data)) return [];
normalized = data.map(normalizeAlbum);
return normalized.some(function (album) { return !album; }) ? [] : normalized;
}
function normalizeAlbumPhotos(data) {
var normalized;
if (!Array.isArray(data)) return [];
normalized = data.map(normalizeAlbumPhoto);
return normalized.some(function (photo) { return !photo; }) ? [] : normalized;
}
function findAlbumById(data, albumId) {
var albums = normalizeAlbums(data);
var expectedId = normalizeId(albumId);
return albums.find(function (album) { return album.albumId === expectedId; }) || null;
}
function escapeHtml(value) {
return stringValue(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function buildListUrl(genealogyId) {
return 'profile-album.html?genealogyId=' + encodeURIComponent(genealogyId);
}
function buildEditUrl(genealogyId, albumId) {
var url = 'profile-album-edit.html?genealogyId=' + encodeURIComponent(genealogyId);
return albumId ? url + '&albumId=' + encodeURIComponent(albumId) : url;
}
function buildDetailUrl(genealogyId, albumId) {
return 'profile-album-detail.html?genealogyId=' + encodeURIComponent(genealogyId) +
'&albumId=' + encodeURIComponent(albumId);
}
function detailValue(label, value) {
return '<p><span>' + escapeHtml(label) + '</span><b>' + escapeHtml(value || '未提供') + '</b></p>';
}
function renderAlbumDetail(album) {
var actions = '';
if (!album) return '<div class="api-empty">未找到该相册</div>';
if (canEditContent) {
actions = '<div class="bottom-actions"><a class="btn ghost" href="' +
escapeHtml(buildEditUrl(album.genealogyId, album.albumId)) + '">编辑相册</a>' +
'<button class="btn ghost" type="button" data-album-delete-id="' +
escapeHtml(album.albumId) + '">删除相册</button></div>';
}
return '<div class="form-like album-detail">' +
detailValue('相册名称', album.albumName) +
detailValue('相册说明', album.albumDesc) +
detailValue('照片数量', album.photoCount === undefined ? '' : album.photoCount) +
detailValue('备注', album.remark) +
'</div>' + actions;
}
function renderAlbumRow(album) {
var actions = '<a class="pill" href="' + escapeHtml(buildDetailUrl(album.genealogyId, album.albumId)) +
'">查看照片</a>';
if (canEditContent) {
actions += '<a class="pill" href="' + escapeHtml(buildEditUrl(album.genealogyId, album.albumId)) +
'">编辑</a><button class="pill is-danger" type="button" data-album-delete-id="' +
escapeHtml(album.albumId) + '">删除</button>';
}
return '<article class="module-row album-row"><div><h3>' + escapeHtml(album.albumName) +
'</h3><p>' + escapeHtml(album.albumDesc || ('照片数量:' + (album.photoCount || 0))) +
'</p></div><div class="row-actions">' + actions + '</div></article>';
}
function renderPhotoList(data) {
var photos = Array.isArray(data) ? data.map(normalizeAlbumPhoto) : [];
if (photos.some(function (photo) { return !photo; })) return '<div class="api-empty">照片响应字段不完整</div>';
if (!photos.length) return '<div class="api-empty">暂无照片</div>';
return photos.map(function (photo) {
var action = canEditContent
? '<button class="pill is-danger" type="button" data-album-photo-delete-id="' +
escapeHtml(photo.photoId) + '">删除</button>'
: '';
return '<article class="module-row album-photo-row"><div><h3>' +
escapeHtml(photo.photoTitle || '未命名照片') + '</h3><p>' +
escapeHtml([photo.photoDesc, photo.photographer, photo.shootTime].filter(Boolean).join(' · ') || '文件已上传') +
'</p></div><div class="row-actions">' + action + '</div></article>';
}).join('');
}
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 showMessage(message) {
if (root.layui && root.layui.layer) root.layui.layer.msg(message);
else if (root.alert) root.alert(message);
}
function syncLinks() {
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) root.ProfileUI.syncGenealogyContextLinks();
}
function setStatus(selector, message) {
var target = query(selector);
if (target) target.textContent = message || '';
}
function setEditorEnabled(enabled, message) {
var placeholder = query('[data-album-editor-placeholder]');
queryAll('[data-album-editor], [data-album-editor-action], [data-album-photo-editor]').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-album-form] button, [data-album-form] input, [data-album-form] textarea, [data-album-photo-form] button, [data-album-photo-form] input, [data-album-photo-form] textarea, [data-album-delete-id], [data-album-photo-delete-id]').forEach(function (control) {
control.disabled = writePending;
});
}
function renderNoContext() {
var message = '请先选择家谱,再查看或维护相册。';
if (query('[data-album-list]')) query('[data-album-list]').innerHTML = '<div class="api-empty">' + message + '</div>';
if (query('[data-album-detail]')) query('[data-album-detail]').innerHTML = '<div class="api-empty">当前没有家谱上下文。</div>';
if (query('[data-album-photo-list]')) query('[data-album-photo-list]').innerHTML = '<div class="api-empty">当前没有家谱上下文。</div>';
setEditorEnabled(false, message);
setStatus('[data-album-form-status]', message);
setStatus('[data-album-photo-status]', message);
}
function requireGenealogyId() {
var genealogyId = getCurrentGenealogyId();
if (!genealogyId) renderNoContext();
return genealogyId;
}
async function loadCapability(api, genealogyId) {
var detail = await api.genealogyDetail(genealogyId);
canEditContent = Boolean(detail && (detail.canEditContent || detail.canManage));
queryAll('[data-album-create-link], [data-album-editor-action], [data-album-photo-editor]').forEach(function (element) {
element.hidden = !canEditContent;
});
return detail;
}
function renderAlbums(data) {
var container = query('[data-album-list]');
var albums = normalizeAlbums(data);
albumsById = Object.create(null);
albums.forEach(function (album) { albumsById[album.albumId] = album; });
if (!container) return albums;
if (!Array.isArray(data) || (data.length && !albums.length)) {
container.innerHTML = '<div class="api-empty">相册响应缺少稳定 AlbumVo 字段,请联系后端核对。</div>';
return [];
}
if (!albums.length) {
container.innerHTML = '<div class="api-empty">暂无相册</div>';
return [];
}
container.innerHTML = albums.map(renderAlbumRow).join('');
return albums;
}
async function loadAlbums() {
var api = getApi();
var genealogyId;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncLinks();
try {
await loadCapability(api, genealogyId);
renderAlbums(await api.albums(genealogyId));
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (query('[data-album-list]')) {
query('[data-album-list]').innerHTML = '<div class="api-empty">' +
(isForbidden(error) ? '当前账号无权查看该家谱的相册。' : '相册加载失败,请稍后重试。') +
'</div>';
}
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(formSelector, name, value) {
var field = query(formSelector + ' [name="' + name + '"]');
if (field) field.value = value === undefined || value === null ? '' : String(value);
}
function fillAlbumForm(album) {
if (!album || album.status !== '0') throw new Error('停用相册无法通过当前 PC 接口重新读取或编辑');
setFieldValue('[data-album-form]', 'albumName', album.albumName);
setFieldValue('[data-album-form]', 'albumDesc', album.albumDesc);
setFieldValue('[data-album-form]', 'coverOssId', album.coverOssId);
setFieldValue('[data-album-form]', 'sortOrder', album.sortOrder);
setFieldValue('[data-album-form]', 'status', '0');
}
async function loadAlbumEditor() {
var api = getApi();
var genealogyId;
var albumId;
var albums;
var album;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
albumId = getCurrentAlbumId();
syncLinks();
setEditorEnabled(false, '正在加载相册编辑信息…');
try {
await loadCapability(api, genealogyId);
if (!canEditContent) throw { status: 403, message: '当前账号无权维护该家谱的相册。' };
if (albumId) {
albums = await api.albums(genealogyId);
album = findAlbumById(albums, albumId);
if (!album) throw new Error('相册列表未返回当前相册');
fillAlbumForm(album);
if (query('[data-album-editor-title]')) query('[data-album-editor-title]').textContent = '编辑相册';
}
setEditorEnabled(true);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setEditorEnabled(false, isForbidden(error) ? '当前账号无权维护该家谱的相册。' : (error.message || '相册编辑信息加载失败'));
setStatus('[data-album-form-status]', isForbidden(error) ? '当前账号无权维护该家谱的相册。' : (error.message || '相册编辑信息加载失败'));
}
}
async function loadPhotos(api, genealogyId, albumId) {
var data = await api.albumPhotos(genealogyId, albumId);
var photos = normalizeAlbumPhotos(data);
var container = query('[data-album-photo-list]');
photosById = Object.create(null);
photos.forEach(function (photo) { photosById[photo.photoId] = photo; });
if (container) {
container.innerHTML = !Array.isArray(data) || (data.length && !photos.length)
? '<div class="api-empty">照片响应缺少稳定 AlbumPhotoVo 字段,请联系后端核对。</div>'
: renderPhotoList(photos);
}
return photos;
}
async function loadAlbumDetailPage() {
var api = getApi();
var genealogyId;
var albumId;
var albums;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
albumId = getCurrentAlbumId();
if (!genealogyId || !albumId) {
if (genealogyId && query('[data-album-detail]')) query('[data-album-detail]').innerHTML = '<div class="api-empty">缺少有效相册编号。</div>';
return;
}
syncLinks();
try {
await loadCapability(api, genealogyId);
albums = await api.albums(genealogyId);
currentAlbum = findAlbumById(albums, albumId);
if (!currentAlbum) throw new Error('相册列表未返回当前相册');
if (query('[data-album-detail]')) query('[data-album-detail]').innerHTML = renderAlbumDetail(currentAlbum);
if (query('[data-album-detail-title]')) query('[data-album-detail-title]').textContent = currentAlbum.albumName;
await loadPhotos(api, genealogyId, albumId);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (query('[data-album-detail]')) query('[data-album-detail]').innerHTML = '<div class="api-empty">' +
(isForbidden(error) ? '当前账号无权查看该相册。' : escapeHtml(error.message || '相册详情加载失败')) + '</div>';
showMessage(isForbidden(error) ? '当前账号无权查看该相册。' : (error.message || '相册详情加载失败'));
}
}
async function submitAlbum(form) {
var api = getApi();
var genealogyId = requireGenealogyId();
var albumId = getCurrentAlbumId();
var body;
var validation;
var result;
var saved;
var savedId;
var verified;
if (writePending || !genealogyId || redirectUnauthorized(api)) return;
body = buildAlbumBody(getFormValues(form));
validation = validateAlbumBody(body);
if (validation) {
setStatus('[data-album-form-status]', validation);
return;
}
delete body.invalidCoverOssId;
delete body.invalidSortOrder;
setWritePending(true);
setStatus('[data-album-form-status]', '正在保存相册…');
try {
result = albumId
? await api.updateAlbum(genealogyId, albumId, body)
: await api.createAlbum(genealogyId, body);
saved = normalizeAlbum(result);
savedId = albumId || (saved && saved.albumId);
if (!savedId) throw new Error('保存响应缺少 albumId');
verified = findAlbumById(await api.albums(genealogyId), savedId);
if (!verified) throw new Error('保存后相册列表未返回同一相册');
if (root.location) root.location.href = buildDetailUrl(genealogyId, savedId);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setStatus('[data-album-form-status]', isForbidden(error) ? '当前账号无权保存该相册。' : (error.message || '相册保存失败'));
} finally {
setWritePending(false);
}
}
async function submitPhoto(form) {
var api = getApi();
var genealogyId = requireGenealogyId();
var albumId = getCurrentAlbumId();
var body;
var validation;
var saved;
if (writePending || !genealogyId || !albumId || redirectUnauthorized(api)) return;
body = buildAlbumPhotoBody(getFormValues(form));
validation = validateAlbumPhotoBody(body);
if (validation) {
setStatus('[data-album-photo-status]', validation);
return;
}
delete body.invalidOssId;
delete body.invalidSortOrder;
setWritePending(true);
setStatus('[data-album-photo-status]', '正在添加照片…');
try {
saved = normalizeAlbumPhoto(await api.createAlbumPhoto(genealogyId, albumId, body));
if (!saved || saved.albumId !== albumId) throw new Error('添加照片响应缺少稳定 photoId');
if (!(await loadPhotos(api, genealogyId, albumId)).some(function (photo) { return photo.photoId === saved.photoId; })) {
throw new Error('添加后照片列表未返回同一照片');
}
form.reset();
setStatus('[data-album-photo-status]', '照片已添加');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setStatus('[data-album-photo-status]', isForbidden(error) ? '当前账号无权添加照片。' : (error.message || '照片添加失败'));
} finally {
setWritePending(false);
}
}
async function deleteAlbum(albumId) {
var api = getApi();
var genealogyId = requireGenealogyId();
var album = albumsById[albumId] || currentAlbum;
if (writePending || !genealogyId || !normalizeId(albumId) || redirectUnauthorized(api)) return;
if (root.confirm && !root.confirm('确认删除“' + (album ? album.albumName : '该相册') + '”及其中全部照片吗?删除后不可恢复。')) return;
setWritePending(true);
try {
await api.deleteAlbum(genealogyId, albumId);
if (query('[data-album-page]')) await loadAlbums();
else if (root.location) root.location.href = buildListUrl(genealogyId);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(isForbidden(error) ? '当前账号无权删除该相册。' : (error.message || '相册删除失败'));
} finally {
setWritePending(false);
}
}
async function deletePhoto(photoId) {
var api = getApi();
var genealogyId = requireGenealogyId();
var albumId = getCurrentAlbumId();
var photo = photosById[photoId];
if (writePending || !genealogyId || !albumId || !normalizeId(photoId) || redirectUnauthorized(api)) return;
if (root.confirm && !root.confirm('确认删除“' + (photo && photo.photoTitle || '该照片') + '”吗?删除后不可恢复。')) return;
setWritePending(true);
try {
await api.deleteAlbumPhoto(genealogyId, albumId, photoId);
await loadPhotos(api, genealogyId, albumId);
showMessage('照片已删除');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(isForbidden(error) ? '当前账号无权删除该照片。' : (error.message || '照片删除失败'));
} finally {
setWritePending(false);
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('click', function (event) {
var albumDelete = event.target.closest('[data-album-delete-id]');
var photoDelete = event.target.closest('[data-album-photo-delete-id]');
if (albumDelete) {
event.preventDefault();
deleteAlbum(albumDelete.getAttribute('data-album-delete-id'));
return;
}
if (photoDelete) {
event.preventDefault();
deletePhoto(photoDelete.getAttribute('data-album-photo-delete-id'));
}
});
documentRef.addEventListener('submit', function (event) {
var albumForm = event.target.closest('[data-album-form]');
var photoForm = event.target.closest('[data-album-photo-form]');
if (albumForm) {
event.preventDefault();
submitAlbum(albumForm);
} else if (photoForm) {
event.preventDefault();
submitPhoto(photoForm);
}
});
}
function init() {
if (!documentRef) return;
bindActions();
if (query('[data-album-page]')) loadAlbums();
if (query('[data-album-edit-page]')) loadAlbumEditor();
if (query('[data-album-detail-page]')) loadAlbumDetailPage();
}
return {
getCurrentGenealogyId: getCurrentGenealogyId,
getCurrentAlbumId: getCurrentAlbumId,
buildAlbumBody: buildAlbumBody,
validateAlbumBody: validateAlbumBody,
buildAlbumPhotoBody: buildAlbumPhotoBody,
validateAlbumPhotoBody: validateAlbumPhotoBody,
normalizeAlbum: normalizeAlbum,
normalizeAlbumPhoto: normalizeAlbumPhoto,
normalizeAlbums: normalizeAlbums,
normalizeAlbumPhotos: normalizeAlbumPhotos,
findAlbumById: findAlbumById,
renderAlbumDetail: renderAlbumDetail,
renderPhotoList: renderPhotoList,
shouldRedirectToLogin: shouldRedirectToLogin,
isForbidden: isForbidden,
init: init
};
});
+567
View File
@@ -0,0 +1,567 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.ArticlePages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.ArticlePages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
var articlesById = Object.create(null);
var writePending = false;
var canEditContent = 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 stringValue(value) {
return value === undefined || value === null ? '' : String(value);
}
function trimOrUndefined(value) {
var text = stringValue(value).trim();
return text || undefined;
}
function queryParam(search, name) {
return new URLSearchParams(stringValue(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 normalizeNullableId(value) {
if (value === undefined || value === null || value === '') return '';
return normalizeId(value);
}
function getCurrentGenealogyId(search) {
var source = search === undefined && root.location ? root.location.search : search;
var contextId;
if (search === undefined && root.ProfileUI && root.ProfileUI.getGenealogyId) {
contextId = root.ProfileUI.getGenealogyId();
if (contextId) return normalizeId(contextId);
}
return normalizeId(queryParam(source, 'genealogyId'));
}
function getCurrentArticleId(search) {
var source = search === undefined && root.location ? root.location.search : search;
return normalizeId(queryParam(source, 'articleId'));
}
function optionalSafeInteger(value) {
var number;
if (value === undefined || value === null || value === '') return undefined;
number = Number(value);
return Number.isSafeInteger(number) ? number : undefined;
}
function buildArticleBody(values) {
var source = values || {};
var body = {
articleTitle: stringValue(source.articleTitle).trim(),
articleContent: stringValue(source.articleContent).trim(),
status: '0'
};
var articleSummary = trimOrUndefined(source.articleSummary);
var coverOssId = normalizeNullableId(source.coverOssId);
var authorName = trimOrUndefined(source.authorName);
var sortOrder = optionalSafeInteger(source.sortOrder);
if (articleSummary !== undefined) body.articleSummary = articleSummary;
if (source.coverOssId !== undefined && source.coverOssId !== null && source.coverOssId !== '') {
if (coverOssId) body.coverOssId = coverOssId;
else body.invalidCoverOssId = true;
}
if (authorName !== undefined) body.authorName = authorName;
if (source.sortOrder !== undefined && source.sortOrder !== null && source.sortOrder !== '') {
if (sortOrder !== undefined) body.sortOrder = sortOrder;
else body.invalidSortOrder = true;
}
return body;
}
function validateArticleBody(body) {
if (!body || !body.articleTitle) return '请填写谱文标题';
if (!body.articleContent) return '请填写谱文正文';
if (body.invalidCoverOssId) return '封面文件编号无效,请重新选择文件';
if (body.invalidSortOrder) return '排序值必须是安全整数';
if (body.status === '1') return '当前 PC 无法重新读取停用谱文,暂不开放停用';
if (body.status !== '0') return '谱文状态只能是 0';
return '';
}
function normalizeArticle(item) {
var articleId = normalizeId(item && item.articleId);
var genealogyId = normalizeId(item && item.genealogyId);
var categoryId = normalizeNullableId(item && item.categoryId);
var coverOssId = normalizeNullableId(item && item.coverOssId);
var articleTitle = stringValue(item && item.articleTitle).trim();
var articleContent = stringValue(item && item.articleContent).trim();
var viewCount = optionalSafeInteger(item && item.viewCount);
var sortOrder = optionalSafeInteger(item && item.sortOrder);
var status = stringValue(item && item.status);
var article;
if (!articleId || !genealogyId || !articleTitle || !articleContent) return null;
if (item.categoryId !== undefined && item.categoryId !== null && item.categoryId !== '' && !categoryId) return null;
if (item.coverOssId !== undefined && item.coverOssId !== null && item.coverOssId !== '' && !coverOssId) return null;
if (item.viewCount !== undefined && item.viewCount !== null && item.viewCount !== '' && viewCount === undefined) return null;
if (item.sortOrder !== undefined && item.sortOrder !== null && item.sortOrder !== '' && sortOrder === undefined) return null;
if (status !== '0' && status !== '1') return null;
article = {
articleId: articleId,
genealogyId: genealogyId,
genealogyNo: stringValue(item.genealogyNo),
genealogyName: stringValue(item.genealogyName),
surname: stringValue(item.surname),
categoryName: stringValue(item.categoryName),
categoryCode: stringValue(item.categoryCode),
articleTitle: articleTitle,
articleSummary: stringValue(item.articleSummary),
articleContent: articleContent,
authorName: stringValue(item.authorName),
publishTime: stringValue(item.publishTime),
status: status,
remark: stringValue(item.remark)
};
if (categoryId) article.categoryId = categoryId;
if (coverOssId) article.coverOssId = coverOssId;
if (viewCount !== undefined) article.viewCount = viewCount;
if (sortOrder !== undefined) article.sortOrder = sortOrder;
return article;
}
function normalizeArticles(data) {
var normalized;
if (!Array.isArray(data)) return [];
normalized = data.map(normalizeArticle);
return normalized.some(function (article) { return !article; }) ? [] : normalized;
}
function matchesSavedArticle(item, expectedArticleId) {
var article = normalizeArticle(item);
return Boolean(article && article.articleId === normalizeId(expectedArticleId));
}
function escapeHtml(value) {
return stringValue(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function buildListUrl(genealogyId, articleId) {
var url = 'profile-article.html?genealogyId=' + encodeURIComponent(genealogyId);
return articleId ? url + '&articleId=' + encodeURIComponent(articleId) : url;
}
function buildEditUrl(genealogyId, articleId) {
var url = 'profile-article-edit.html?genealogyId=' + encodeURIComponent(genealogyId);
return articleId ? url + '&articleId=' + encodeURIComponent(articleId) : url;
}
function detailValue(label, value) {
return '<p><span>' + escapeHtml(label) + '</span><b>' + escapeHtml(value || '未提供') + '</b></p>';
}
function renderArticleDetail(article) {
var actions = '';
if (!article) return '<div class="api-empty">请选择一篇谱文查看详情</div>';
if (canEditContent) {
actions = '<div class="bottom-actions"><a class="btn ghost" href="' +
escapeHtml(buildEditUrl(article.genealogyId, article.articleId)) + '">编辑谱文</a>' +
'<button class="btn ghost" type="button" data-article-delete-id="' +
escapeHtml(article.articleId) + '">删除谱文</button></div>';
}
return '<div class="form-like article-detail">' +
detailValue('标题', article.articleTitle) +
detailValue('摘要', article.articleSummary) +
detailValue('正文', article.articleContent) +
detailValue('作者', article.authorName) +
detailValue('分类', article.categoryName) +
detailValue('发布时间', article.publishTime) +
detailValue('浏览量', article.viewCount === undefined ? '' : article.viewCount) +
detailValue('备注', article.remark) +
'</div>' + actions;
}
function renderArticleRow(article) {
var actions = '<button class="pill" type="button" data-article-detail-id="' +
escapeHtml(article.articleId) + '">查看详情</button>';
if (canEditContent) {
actions += '<a class="pill" href="' + escapeHtml(buildEditUrl(article.genealogyId, article.articleId)) +
'">编辑</a><button class="pill is-danger" type="button" data-article-delete-id="' +
escapeHtml(article.articleId) + '">删除</button>';
}
return '<article class="module-row article-row"><div><h3>' + escapeHtml(article.articleTitle) +
'</h3><p>' + escapeHtml(article.articleSummary || article.authorName || article.publishTime || '暂无摘要') +
'</p></div><div class="row-actions">' + actions + '</div></article>';
}
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 showMessage(message) {
if (root.layui && root.layui.layer) root.layui.layer.msg(message);
else if (root.alert) root.alert(message);
}
function syncLinks() {
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) root.ProfileUI.syncGenealogyContextLinks();
}
function setFormStatus(message) {
var target = query('[data-article-form-status]');
if (target) target.textContent = message || '';
}
function setEditorEnabled(enabled, message) {
var placeholder = query('[data-article-editor-placeholder]');
queryAll('[data-article-editor], [data-article-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-article-form] button, [data-article-form] input, [data-article-form] textarea, [data-article-delete-id]').forEach(function (control) {
control.disabled = writePending;
});
}
function renderNoContext() {
var message = '请先选择家谱,再查看或维护谱文。';
var list = query('[data-article-list]');
var detail = query('[data-article-detail]');
if (list) list.innerHTML = '<div class="api-empty">' + message + '</div>';
if (detail) detail.innerHTML = '<div class="api-empty">当前没有家谱上下文。</div>';
setEditorEnabled(false, message);
setFormStatus(message);
}
function requireGenealogyId() {
var genealogyId = getCurrentGenealogyId();
if (!genealogyId) renderNoContext();
return genealogyId;
}
async function loadCapability(api, genealogyId) {
var detail = await api.genealogyDetail(genealogyId);
canEditContent = Boolean(detail && (detail.canEditContent || detail.canManage));
queryAll('[data-article-create-link], [data-article-editor-action]').forEach(function (element) {
element.hidden = !canEditContent;
});
return detail;
}
function renderArticles(data) {
var container = query('[data-article-list]');
var articles = normalizeArticles(data);
articlesById = Object.create(null);
articles.forEach(function (article) {
articlesById[article.articleId] = article;
});
if (!container) return articles;
if (!Array.isArray(data) || (data.length && !articles.length)) {
container.innerHTML = '<div class="api-empty">谱文响应缺少稳定 ArticleVo 字段,请联系后端核对。</div>';
return [];
}
if (!articles.length) {
container.innerHTML = '<div class="api-empty">暂无谱文</div>';
return [];
}
container.innerHTML = articles.map(renderArticleRow).join('');
return articles;
}
function renderDetail(article) {
var container = query('[data-article-detail]');
if (container) container.innerHTML = renderArticleDetail(article);
}
async function loadArticleDetail(articleId) {
var api = getApi();
var genealogyId = requireGenealogyId();
var article;
if (!genealogyId || !normalizeId(articleId) || redirectUnauthorized(api)) return null;
try {
article = normalizeArticle(await api.articleDetail(genealogyId, articleId));
if (!article || article.articleId !== normalizeId(articleId)) {
throw new Error('谱文详情响应缺少稳定 ArticleVo 字段');
}
articlesById[article.articleId] = article;
renderDetail(article);
return article;
} catch (error) {
if (redirectUnauthorized(api, error)) return null;
renderDetail(null);
showMessage(isForbidden(error) ? '当前账号无权查看该谱文。' : (error.message || '谱文详情加载失败'));
return null;
}
}
async function loadArticles() {
var api = getApi();
var genealogyId;
var articles;
var requestedId;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncLinks();
try {
await loadCapability(api, genealogyId);
articles = renderArticles(await api.articles(genealogyId));
requestedId = getCurrentArticleId();
if (requestedId && articlesById[requestedId]) await loadArticleDetail(requestedId);
else if (articles.length) await loadArticleDetail(articles[0].articleId);
else renderDetail(null);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (query('[data-article-list]')) {
query('[data-article-list]').innerHTML = '<div class="api-empty">' +
(isForbidden(error) ? '当前账号无权查看该家谱的谱文。' : '谱文加载失败,请稍后重试。') +
'</div>';
}
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-article-form] [name="' + name + '"]');
if (field) field.value = value === undefined || value === null ? '' : String(value);
}
function fillArticleForm(article) {
var contentField;
var contentEditor;
if (!article || article.status !== '0') throw new Error('停用谱文无法通过当前 PC 接口重新读取或编辑');
setFieldValue('articleTitle', article.articleTitle);
setFieldValue('articleSummary', article.articleSummary);
setFieldValue('coverOssId', article.coverOssId);
setFieldValue('articleContent', article.articleContent);
setFieldValue('authorName', article.authorName);
setFieldValue('sortOrder', article.sortOrder);
setFieldValue('status', '0');
contentField = query('[name="articleContent"]');
if (root.AppRichEditor && root.AppRichEditor.init && contentField) {
contentEditor = root.AppRichEditor.init(contentField);
if (contentEditor && contentEditor.editor && contentEditor.editor.setHtml) {
contentEditor.editor.setHtml(article.articleContent);
contentEditor.sync();
}
}
}
async function loadArticleEditor() {
var api = getApi();
var genealogyId;
var articleId;
var article;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
articleId = getCurrentArticleId();
syncLinks();
setEditorEnabled(false, '正在加载谱文编辑信息…');
try {
await loadCapability(api, genealogyId);
if (!canEditContent) throw { status: 403, message: '当前账号无权维护该家谱的谱文。' };
if (articleId) {
article = normalizeArticle(await api.articleDetail(genealogyId, articleId));
if (!article || article.articleId !== articleId) throw new Error('谱文详情响应缺少稳定 ArticleVo 字段');
fillArticleForm(article);
if (query('[data-article-editor-title]')) query('[data-article-editor-title]').textContent = '编辑谱文';
}
setEditorEnabled(true);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setEditorEnabled(false, isForbidden(error)
? '当前账号无权维护该家谱的谱文。'
: (error.message || '谱文编辑信息加载失败'));
setFormStatus(isForbidden(error) ? '当前账号无权维护该家谱的谱文。' : (error.message || '谱文编辑信息加载失败'));
}
}
async function submitArticle(form) {
var api = getApi();
var genealogyId = requireGenealogyId();
var articleId = getCurrentArticleId();
var body;
var validation;
var result;
var saved;
var savedId;
if (writePending || !genealogyId || redirectUnauthorized(api)) return;
if (root.AppRichEditor && root.AppRichEditor.syncAll) root.AppRichEditor.syncAll();
body = buildArticleBody(getFormValues(form));
validation = validateArticleBody(body);
if (validation) {
setFormStatus(validation);
return;
}
delete body.invalidCoverOssId;
delete body.invalidSortOrder;
setWritePending(true);
setFormStatus('正在保存谱文…');
try {
result = articleId
? await api.updateArticle(genealogyId, articleId, body)
: await api.createArticle(genealogyId, body);
saved = normalizeArticle(result);
savedId = articleId || (saved && saved.articleId);
if (!savedId) throw new Error('保存响应缺少 articleId');
if (!matchesSavedArticle(await api.articleDetail(genealogyId, savedId), 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 deleteArticle(articleId) {
var api = getApi();
var genealogyId = requireGenealogyId();
var article = articlesById[articleId];
if (writePending || !genealogyId || !normalizeId(articleId) || redirectUnauthorized(api)) return;
if (root.confirm && !root.confirm('确认删除“' + (article ? article.articleTitle : '该谱文') + '”吗?删除后不可恢复。')) return;
setWritePending(true);
try {
await api.deleteArticle(genealogyId, articleId);
await loadArticles();
showMessage('谱文已删除');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(isForbidden(error) ? '当前账号无权删除该谱文。' : (error.message || '谱文删除失败'));
} finally {
setWritePending(false);
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('click', function (event) {
var detailButton = event.target.closest('[data-article-detail-id]');
var deleteButton = event.target.closest('[data-article-delete-id]');
if (detailButton) {
event.preventDefault();
loadArticleDetail(detailButton.getAttribute('data-article-detail-id'));
return;
}
if (deleteButton) {
event.preventDefault();
deleteArticle(deleteButton.getAttribute('data-article-delete-id'));
}
});
documentRef.addEventListener('submit', function (event) {
var form = event.target.closest('[data-article-form]');
if (!form) return;
event.preventDefault();
submitArticle(form);
});
}
function init() {
if (!documentRef) return;
bindActions();
if (query('[data-article-page]')) loadArticles();
if (query('[data-article-edit-page]')) loadArticleEditor();
}
return {
getCurrentGenealogyId: getCurrentGenealogyId,
getCurrentArticleId: getCurrentArticleId,
buildArticleBody: buildArticleBody,
validateArticleBody: validateArticleBody,
normalizeArticle: normalizeArticle,
normalizeArticles: normalizeArticles,
matchesSavedArticle: matchesSavedArticle,
renderArticleDetail: renderArticleDetail,
shouldRedirectToLogin: shouldRedirectToLogin,
isForbidden: isForbidden,
init: init
};
});
+920
View File
@@ -0,0 +1,920 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.CeremonyAdminPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.CeremonyAdminPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
var ceremoniesById = Object.create(null);
var giftsById = Object.create(null);
var inviteeOptionsByUserId = Object.create(null);
var currentCeremony = null;
var canEditContent = false;
var writePending = false;
var invitationStatuses = ['PENDING', 'ACCEPTED', 'DECLINED', 'CANCELED'];
var invitationStatusLabels = {
PENDING: '待响应',
ACCEPTED: '已接受',
DECLINED: '已拒绝',
CANCELED: '已取消'
};
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 stringValue(value) {
return value === undefined || value === null ? '' : String(value);
}
function trimOrUndefined(value) {
var text = stringValue(value).trim();
return text || undefined;
}
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 normalizeNullableId(value) {
if (value === undefined || value === null || value === '') return '';
return normalizeId(value);
}
function queryParam(search, name) {
return new URLSearchParams(stringValue(search).replace(/^\?/, '')).get(name) || '';
}
function getCurrentGenealogyId(search) {
var source = search === undefined && root.location ? root.location.search : search;
var contextId;
if (search === undefined && root.ProfileUI && root.ProfileUI.getGenealogyId) {
contextId = root.ProfileUI.getGenealogyId();
if (contextId) return normalizeId(contextId);
}
return normalizeId(queryParam(source, 'genealogyId'));
}
function getCurrentCeremonyId(search) {
var source = search === undefined && root.location ? root.location.search : search;
return normalizeId(queryParam(source, 'ceremonyId'));
}
function optionalSafeInteger(value) {
var number;
if (value === undefined || value === null || value === '') return undefined;
number = Number(value);
return Number.isSafeInteger(number) ? number : undefined;
}
function optionalFiniteNumber(value) {
var number;
if (value === undefined || value === null || value === '') return undefined;
number = Number(value);
return Number.isFinite(number) ? number : undefined;
}
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 = stringValue(value);
if (!/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/.test(text)) return '';
return text.slice(0, 16).replace(' ', 'T');
}
function isValidBackendDateTime(value) {
var match = stringValue(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 buildCeremonyBody(values) {
var source = values || {};
var body = {
ceremonyType: stringValue(source.ceremonyType).trim(),
ceremonyTitle: stringValue(source.ceremonyTitle).trim(),
status: '0'
};
var optionalNames = ['ceremonyDesc', 'location', 'locationAddress'];
var ceremonyTime = toBackendDateTime(source.ceremonyTime);
var longitude = optionalFiniteNumber(source.longitude);
var latitude = optionalFiniteNumber(source.latitude);
var coverOssId = normalizeNullableId(source.coverOssId);
var sortOrder = optionalSafeInteger(source.sortOrder);
optionalNames.forEach(function (name) {
var value = trimOrUndefined(source[name]);
if (value !== undefined) body[name] = value;
});
if (ceremonyTime !== undefined) body.ceremonyTime = ceremonyTime;
if (source.longitude !== undefined && source.longitude !== null && source.longitude !== '') {
if (longitude !== undefined) body.longitude = longitude;
else body.invalidLongitude = true;
}
if (source.latitude !== undefined && source.latitude !== null && source.latitude !== '') {
if (latitude !== undefined) body.latitude = latitude;
else body.invalidLatitude = true;
}
if (source.coverOssId !== undefined && source.coverOssId !== null && source.coverOssId !== '') {
if (coverOssId) body.coverOssId = coverOssId;
else body.invalidCoverOssId = true;
}
if (source.sortOrder !== undefined && source.sortOrder !== null && source.sortOrder !== '') {
if (sortOrder !== undefined) body.sortOrder = sortOrder;
else body.invalidSortOrder = true;
}
return body;
}
function validateCeremonyBody(body) {
if (!body || !body.ceremonyType) return '请填写活动类型';
if (!body.ceremonyTitle) return '请填写活动标题';
if (body.ceremonyTime !== undefined && !isValidBackendDateTime(body.ceremonyTime)) return '活动时间格式无效';
if ((body.longitude === undefined) !== (body.latitude === undefined)) return '经度和纬度必须同时提供';
if (body.invalidLongitude) return '经度必须是有效数字';
if (body.invalidLatitude) return '纬度必须是有效数字';
if (body.longitude !== undefined && (body.longitude < -180 || body.longitude > 180)) return '经度范围必须是 -180 到 180';
if (body.latitude !== undefined && (body.latitude < -90 || body.latitude > 90)) return '纬度范围必须是 -90 到 90';
if (stringValue(body.locationAddress).length > 300) return '详细地址不能超过 300 个字符';
if (body.invalidCoverOssId) return '封面文件编号无效,请重新选择文件';
if (body.invalidSortOrder) return '排序值必须是安全整数';
if (body.status === '1') return '当前 PC 无法重新读取停用活动,暂不开放停用';
if (body.status !== '0') return '活动状态只能是 0';
return '';
}
function buildCeremonyGiftBody(values) {
var source = values || {};
var body = {};
var giverName = trimOrUndefined(source.giverName);
var giftMessage = trimOrUndefined(source.giftMessage);
var giftAmount = optionalFiniteNumber(source.giftAmount);
if (giverName !== undefined) body.giverName = giverName;
if (source.giftAmount !== undefined && source.giftAmount !== null && source.giftAmount !== '') {
if (giftAmount !== undefined) body.giftAmount = giftAmount;
else body.invalidGiftAmount = true;
}
if (giftMessage !== undefined) body.giftMessage = giftMessage;
return body;
}
function validateCeremonyGiftBody(body) {
if (!body || (body.giftAmount === undefined && !body.invalidGiftAmount)) return '请填写礼金金额';
if (body.invalidGiftAmount) return '礼金金额必须是有效数字';
if (body.giftAmount < 0) return '礼金金额不能小于 0';
return '';
}
function normalizeCeremony(item) {
var ceremonyId = normalizeId(item && item.ceremonyId);
var genealogyId = normalizeId(item && item.genealogyId);
var sponsorUserId = normalizeNullableId(item && item.sponsorUserId);
var coverOssId = normalizeNullableId(item && item.coverOssId);
var ceremonyType = stringValue(item && item.ceremonyType).trim();
var ceremonyTitle = stringValue(item && item.ceremonyTitle).trim();
var longitude = optionalFiniteNumber(item && item.longitude);
var latitude = optionalFiniteNumber(item && item.latitude);
var giftCount = optionalSafeInteger(item && item.giftCount);
var giftAmount = optionalFiniteNumber(item && item.giftAmount);
var sortOrder = optionalSafeInteger(item && item.sortOrder);
var status = stringValue(item && item.status);
var ceremony;
if (!ceremonyId || !genealogyId || !ceremonyType || !ceremonyTitle) return null;
if (item.sponsorUserId !== undefined && item.sponsorUserId !== null && item.sponsorUserId !== '' && !sponsorUserId) return null;
if (item.coverOssId !== undefined && item.coverOssId !== null && item.coverOssId !== '' && !coverOssId) return null;
if ((item.longitude !== undefined && item.longitude !== null && item.longitude !== '' && longitude === undefined) ||
(item.latitude !== undefined && item.latitude !== null && item.latitude !== '' && latitude === undefined)) return null;
if ((longitude === undefined) !== (latitude === undefined)) return null;
if (item.giftCount !== undefined && item.giftCount !== null && item.giftCount !== '' && giftCount === undefined) return null;
if (item.giftAmount !== undefined && item.giftAmount !== null && item.giftAmount !== '' && giftAmount === undefined) return null;
if (item.sortOrder !== undefined && item.sortOrder !== null && item.sortOrder !== '' && sortOrder === undefined) return null;
if (status !== '0' && status !== '1') return null;
ceremony = {
ceremonyId: ceremonyId,
genealogyId: genealogyId,
genealogyNo: stringValue(item.genealogyNo),
genealogyName: stringValue(item.genealogyName),
surname: stringValue(item.surname),
sponsorNickName: stringValue(item.sponsorNickName),
sponsorPhone: stringValue(item.sponsorPhone),
ceremonyType: ceremonyType,
ceremonyTitle: ceremonyTitle,
ceremonyDesc: stringValue(item.ceremonyDesc),
ceremonyTime: stringValue(item.ceremonyTime),
location: stringValue(item.location),
locationAddress: stringValue(item.locationAddress),
status: status,
remark: stringValue(item.remark)
};
if (sponsorUserId) ceremony.sponsorUserId = sponsorUserId;
if (coverOssId) ceremony.coverOssId = coverOssId;
if (longitude !== undefined) ceremony.longitude = longitude;
if (latitude !== undefined) ceremony.latitude = latitude;
if (giftCount !== undefined) ceremony.giftCount = giftCount;
if (giftAmount !== undefined) ceremony.giftAmount = giftAmount;
if (sortOrder !== undefined) ceremony.sortOrder = sortOrder;
return ceremony;
}
function normalizeCeremonyGift(item) {
var giftId = normalizeId(item && item.giftId);
var genealogyId = normalizeId(item && item.genealogyId);
var ceremonyId = normalizeId(item && item.ceremonyId);
var giverUserId = normalizeNullableId(item && item.giverUserId);
var giftAmount = optionalFiniteNumber(item && item.giftAmount);
var status = stringValue(item && item.status);
var gift;
if (!giftId || !genealogyId || !ceremonyId || giftAmount === undefined || giftAmount < 0) return null;
if (item.giverUserId !== undefined && item.giverUserId !== null && item.giverUserId !== '' && !giverUserId) return null;
if (status !== '0' && status !== '1') return null;
gift = {
giftId: giftId,
genealogyId: genealogyId,
genealogyNo: stringValue(item.genealogyNo),
genealogyName: stringValue(item.genealogyName),
surname: stringValue(item.surname),
ceremonyId: ceremonyId,
ceremonyTitle: stringValue(item.ceremonyTitle),
giverNickName: stringValue(item.giverNickName),
giverPhone: stringValue(item.giverPhone),
giverName: stringValue(item.giverName),
giftAmount: giftAmount,
giftMessage: stringValue(item.giftMessage),
giftTime: stringValue(item.giftTime),
status: status,
remark: stringValue(item.remark)
};
if (giverUserId) gift.giverUserId = giverUserId;
return gift;
}
function normalizeInvitation(item) {
var invitationId = normalizeId(item && item.invitationId);
var genealogyId = normalizeId(item && item.genealogyId);
var ceremonyId = normalizeId(item && item.ceremonyId);
var inviteeUserId = normalizeId(item && item.inviteeUserId);
var inviteStatus = stringValue(item && item.inviteStatus);
var inviteVersion = optionalSafeInteger(item && item.inviteVersion);
var invitation;
if (!invitationId || !genealogyId || !ceremonyId || !inviteeUserId || invitationStatuses.indexOf(inviteStatus) < 0) return null;
if (item.inviteVersion !== undefined && item.inviteVersion !== null && item.inviteVersion !== '' && inviteVersion === undefined) return null;
invitation = {
invitationId: invitationId,
genealogyId: genealogyId,
ceremonyId: ceremonyId,
inviteeUserId: inviteeUserId,
inviteStatus: inviteStatus,
deliveredTime: stringValue(item.deliveredTime),
readTime: stringValue(item.readTime),
responseTime: stringValue(item.responseTime)
};
if (inviteVersion !== undefined) invitation.inviteVersion = inviteVersion;
return invitation;
}
function normalizeInviteeOption(item) {
var memberId = normalizeId(item && item.memberId);
var genealogyId = normalizeId(item && item.genealogyId);
var appUserId = normalizeNullableId(item && item.appUserId);
var status = stringValue(item && item.status);
if (!memberId || !genealogyId || !appUserId || status !== '0') return null;
return {
memberId: memberId,
genealogyId: genealogyId,
appUserId: appUserId,
appUserNickName: stringValue(item.appUserNickName),
memberName: stringValue(item.memberName),
roleType: stringValue(item.roleType),
status: status
};
}
function normalizeList(data, normalizer) {
var normalized;
if (!Array.isArray(data)) return [];
normalized = data.map(normalizer);
return normalized.some(function (item) { return !item; }) ? [] : normalized;
}
function buildInviteesBody(values) {
var seen = Object.create(null);
var ids = [];
(Array.isArray(values) ? values : []).forEach(function (value) {
var id = normalizeId(value);
if (id && !seen[id]) {
seen[id] = true;
ids.push(id);
}
});
return { inviteeUserIds: ids };
}
function escapeHtml(value) {
return stringValue(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function detailValue(label, value) {
return '<p><span>' + escapeHtml(label) + '</span><b>' + escapeHtml(value === '' || value === undefined ? '未提供' : value) + '</b></p>';
}
function buildListUrl(genealogyId) {
return 'profile-ceremony.html?genealogyId=' + encodeURIComponent(genealogyId);
}
function buildEditUrl(genealogyId, ceremonyId) {
var url = 'profile-gift-edit.html?genealogyId=' + encodeURIComponent(genealogyId);
return ceremonyId ? url + '&ceremonyId=' + encodeURIComponent(ceremonyId) : url;
}
function buildDetailUrl(genealogyId, ceremonyId) {
return 'profile-ceremony-detail.html?genealogyId=' + encodeURIComponent(genealogyId) +
'&ceremonyId=' + encodeURIComponent(ceremonyId);
}
function renderCeremonyDetail(ceremony) {
var mapLink = '';
var actions = '';
if (!ceremony) return '<div class="api-empty">未找到该活动</div>';
if (ceremony.longitude !== undefined && ceremony.latitude !== undefined) {
mapLink = ' <a class="pill" target="_blank" rel="noopener noreferrer" href="https://uri.amap.com/marker?position=' +
encodeURIComponent(ceremony.longitude + ',' + ceremony.latitude) + '&amp;name=' +
encodeURIComponent(ceremony.location || ceremony.locationAddress || ceremony.ceremonyTitle) + '">打开地图</a>';
}
if (canEditContent) {
actions = '<div class="bottom-actions"><a class="btn ghost" href="' +
escapeHtml(buildEditUrl(ceremony.genealogyId, ceremony.ceremonyId)) + '">编辑活动</a>' +
'<button class="btn ghost" type="button" data-ceremony-delete-id="' +
escapeHtml(ceremony.ceremonyId) + '">删除活动</button></div>';
}
return '<div class="form-like ceremony-detail">' +
detailValue('活动标题', ceremony.ceremonyTitle) +
detailValue('活动类型', ceremony.ceremonyType) +
detailValue('活动说明', ceremony.ceremonyDesc) +
detailValue('活动时间', ceremony.ceremonyTime) +
detailValue('地点', ceremony.location) +
'<p><span>详细地址</span><b>' + escapeHtml(ceremony.locationAddress || '未提供') + mapLink + '</b></p>' +
detailValue('发起人', ceremony.sponsorNickName) +
detailValue('祭品数量', ceremony.giftCount === undefined ? '' : ceremony.giftCount) +
detailValue('礼金合计', ceremony.giftAmount === undefined ? '' : ceremony.giftAmount) +
detailValue('备注', ceremony.remark) +
'</div>' + actions;
}
function renderCeremonyRow(ceremony) {
var actions = '<a class="pill" href="' + escapeHtml(buildDetailUrl(ceremony.genealogyId, ceremony.ceremonyId)) + '">查看详情</a>';
if (canEditContent) {
actions += '<a class="pill" href="' + escapeHtml(buildEditUrl(ceremony.genealogyId, ceremony.ceremonyId)) +
'">编辑</a><button class="pill is-danger" type="button" data-ceremony-delete-id="' +
escapeHtml(ceremony.ceremonyId) + '">删除</button>';
}
return '<article class="module-row ceremony-row"><div><h3>' + escapeHtml(ceremony.ceremonyTitle) +
'</h3><p>' + escapeHtml([ceremony.ceremonyType, ceremony.ceremonyTime, ceremony.location].filter(Boolean).join(' · ')) +
'</p></div><div class="row-actions">' + actions + '</div></article>';
}
function renderGiftList(data) {
var gifts = Array.isArray(data) ? data.map(normalizeCeremonyGift) : [];
if (gifts.some(function (gift) { return !gift; })) return '<div class="api-empty">祭品响应字段不完整</div>';
if (!gifts.length) return '<div class="api-empty">暂无祭品记录</div>';
return gifts.map(function (gift) {
var action = canEditContent
? '<button class="pill is-danger" type="button" data-ceremony-gift-delete-id="' +
escapeHtml(gift.giftId) + '">删除</button>'
: '';
return '<article class="module-row ceremony-gift-row"><div><h3>' +
escapeHtml(gift.giverName || gift.giverNickName || '匿名宗亲') + ' · ' +
escapeHtml(gift.giftAmount) + '</h3><p>' +
escapeHtml([gift.giftMessage, gift.giftTime].filter(Boolean).join(' · ') || '未提供留言') +
'</p></div><div class="row-actions">' + action + '</div></article>';
}).join('');
}
function renderInvitationList(data) {
var invitations = normalizeList(data, normalizeInvitation);
if (!Array.isArray(data) || (data.length && !invitations.length)) return '<div class="api-empty">邀请响应字段不完整</div>';
if (!invitations.length) return '<div class="api-empty">暂无邀请记录</div>';
return invitations.map(function (invitation) {
var option = inviteeOptionsByUserId[invitation.inviteeUserId];
var label = option && (option.memberName || option.appUserNickName) || '已邀请成员';
return '<article class="module-row ceremony-invitee-row"><div><h3>' + escapeHtml(label) +
'</h3><p>' + escapeHtml(invitationStatusLabels[invitation.inviteStatus]) +
(invitation.deliveredTime ? ' · ' + escapeHtml(invitation.deliveredTime) : '') +
'</p></div></article>';
}).join('');
}
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 showMessage(message) {
if (root.layui && root.layui.layer) root.layui.layer.msg(message);
else if (root.alert) root.alert(message);
}
function setStatus(selector, message) {
var target = query(selector);
if (target) target.textContent = message || '';
}
function syncLinks() {
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) root.ProfileUI.syncGenealogyContextLinks();
}
function setWritePending(value) {
writePending = Boolean(value);
queryAll('[data-ceremony-form] input, [data-ceremony-form] textarea, [data-ceremony-form] button, [data-ceremony-gift-form] input, [data-ceremony-gift-form] textarea, [data-ceremony-gift-form] button, [data-ceremony-invitee-form] input, [data-ceremony-invitee-form] button, [data-ceremony-delete-id], [data-ceremony-gift-delete-id]').forEach(function (control) {
control.disabled = writePending;
});
}
function setEditorEnabled(enabled, message) {
var placeholder = query('[data-ceremony-editor-placeholder]');
queryAll('[data-ceremony-editor], [data-ceremony-editor-action], [data-ceremony-admin-editor]').forEach(function (element) {
element.hidden = !enabled;
});
if (placeholder) {
placeholder.hidden = Boolean(enabled);
if (message) placeholder.textContent = message;
}
}
function renderNoContext() {
var message = '请先选择家谱,再查看或维护祭祀活动。';
if (query('[data-ceremony-list]')) query('[data-ceremony-list]').innerHTML = '<div class="api-empty">' + message + '</div>';
if (query('[data-ceremony-detail]')) query('[data-ceremony-detail]').innerHTML = '<div class="api-empty">当前没有家谱上下文。</div>';
setEditorEnabled(false, message);
setStatus('[data-ceremony-form-status]', message);
}
function requireGenealogyId() {
var genealogyId = getCurrentGenealogyId();
if (!genealogyId) renderNoContext();
return genealogyId;
}
async function loadCapability(api, genealogyId) {
var detail = await api.genealogyDetail(genealogyId);
canEditContent = Boolean(detail && (detail.canEditContent || detail.canManage));
queryAll('[data-ceremony-create-link], [data-ceremony-editor-action], [data-ceremony-admin-editor]').forEach(function (element) {
element.hidden = !canEditContent;
});
return detail;
}
function renderCeremonies(data) {
var container = query('[data-ceremony-list]');
var ceremonies = normalizeList(data, normalizeCeremony);
ceremoniesById = Object.create(null);
ceremonies.forEach(function (ceremony) { ceremoniesById[ceremony.ceremonyId] = ceremony; });
if (!container) return ceremonies;
if (!Array.isArray(data) || (data.length && !ceremonies.length)) {
container.innerHTML = '<div class="api-empty">活动响应缺少稳定 CeremonyVo 字段,请联系后端核对。</div>';
return [];
}
container.innerHTML = ceremonies.length
? ceremonies.map(renderCeremonyRow).join('')
: '<div class="api-empty">暂无祭祀活动</div>';
return ceremonies;
}
async function loadCeremonies() {
var api = getApi();
var genealogyId;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncLinks();
try {
await loadCapability(api, genealogyId);
renderCeremonies(await api.ceremonies(genealogyId));
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (query('[data-ceremony-list]')) query('[data-ceremony-list]').innerHTML = '<div class="api-empty">' +
(isForbidden(error) ? '当前账号无权查看该家谱的祭祀活动。' : '活动加载失败,请稍后重试。') + '</div>';
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(formSelector, name, value) {
var field = query(formSelector + ' [name="' + name + '"]');
if (field) field.value = value === undefined || value === null ? '' : String(value);
}
function fillCeremonyForm(ceremony) {
var contentField;
var contentEditor;
if (!ceremony || ceremony.status !== '0') throw new Error('停用活动无法通过当前 PC 接口重新读取或编辑');
['ceremonyType', 'ceremonyTitle', 'ceremonyDesc', 'location', 'locationAddress', 'longitude', 'latitude', 'coverOssId', 'sortOrder'].forEach(function (name) {
setFieldValue('[data-ceremony-form]', name, ceremony[name]);
});
setFieldValue('[data-ceremony-form]', 'ceremonyTime', toDateTimeInputValue(ceremony.ceremonyTime));
setFieldValue('[data-ceremony-form]', 'status', '0');
contentField = query('[data-ceremony-form] [name="ceremonyDesc"]');
if (contentField && root.AppRichEditor && root.AppRichEditor.init) {
contentEditor = root.AppRichEditor.init(contentField);
if (contentEditor && contentEditor.editor && contentEditor.editor.setHtml) {
contentEditor.editor.setHtml(ceremony.ceremonyDesc || '');
contentEditor.sync();
}
}
}
async function loadCeremonyEditor() {
var api = getApi();
var genealogyId;
var ceremonyId;
var ceremony;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
ceremonyId = getCurrentCeremonyId();
syncLinks();
setEditorEnabled(false, '正在加载活动编辑信息…');
try {
await loadCapability(api, genealogyId);
if (!canEditContent) throw { status: 403, message: '当前账号无权维护该家谱的祭祀活动。' };
if (ceremonyId) {
ceremony = normalizeCeremony(await api.ceremonyDetail(genealogyId, ceremonyId));
if (!ceremony || ceremony.ceremonyId !== ceremonyId) throw new Error('活动详情响应缺少稳定 ceremonyId');
fillCeremonyForm(ceremony);
if (query('[data-ceremony-editor-title]')) query('[data-ceremony-editor-title]').textContent = '编辑祭祀活动';
}
setEditorEnabled(true);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setEditorEnabled(false, isForbidden(error) ? '当前账号无权维护该家谱的祭祀活动。' : (error.message || '活动编辑信息加载失败'));
setStatus('[data-ceremony-form-status]', isForbidden(error) ? '当前账号无权维护该家谱的祭祀活动。' : (error.message || '活动编辑信息加载失败'));
}
}
async function loadGifts(api, genealogyId, ceremonyId) {
var data = await api.ceremonyGifts(genealogyId, ceremonyId);
var gifts = normalizeList(data, normalizeCeremonyGift);
var container = query('[data-ceremony-gift-list]');
giftsById = Object.create(null);
gifts.forEach(function (gift) { giftsById[gift.giftId] = gift; });
if (container) {
container.innerHTML = !Array.isArray(data) || (data.length && !gifts.length)
? '<div class="api-empty">祭品响应缺少稳定 CeremonyGiftVo 字段,请联系后端核对。</div>'
: renderGiftList(gifts);
}
return gifts;
}
function renderInviteeOptions(options, invitations) {
var container = query('[data-ceremony-invitee-options]');
var selected = Object.create(null);
invitations.forEach(function (invitation) {
if (invitation.inviteStatus !== 'CANCELED') selected[invitation.inviteeUserId] = true;
});
if (!container) return;
container.innerHTML = options.length ? options.map(function (option) {
var label = option.memberName || option.appUserNickName || '家谱成员';
return '<label class="editor-choice"><input type="checkbox" data-invitee-user-id="' +
escapeHtml(option.appUserId) + '"' + (selected[option.appUserId] ? ' checked' : '') +
' /> <span>' + escapeHtml(label) + '</span></label>';
}).join('') : '<div class="api-empty">暂无已绑定账号的正常成员</div>';
}
async function loadInvitees(api, genealogyId, ceremonyId) {
var results = await Promise.all([
api.genealogyMemberOptions(genealogyId),
api.ceremonyInvitations(genealogyId, ceremonyId)
]);
var options = normalizeList(results[0], normalizeInviteeOption);
var invitations = normalizeList(results[1], normalizeInvitation);
var list = query('[data-ceremony-invitation-list]');
inviteeOptionsByUserId = Object.create(null);
options.forEach(function (option) { inviteeOptionsByUserId[option.appUserId] = option; });
if (!Array.isArray(results[0]) || (results[0].length && !options.length)) {
if (query('[data-ceremony-invitee-options]')) query('[data-ceremony-invitee-options]').innerHTML = '<div class="api-empty">成员选项响应字段不完整</div>';
} else {
renderInviteeOptions(options, invitations);
}
if (list) list.innerHTML = renderInvitationList(results[1]);
return { options: options, invitations: invitations };
}
async function loadCeremonyDetailPage() {
var api = getApi();
var genealogyId;
var ceremonyId;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
ceremonyId = getCurrentCeremonyId();
if (!genealogyId || !ceremonyId) {
if (genealogyId && query('[data-ceremony-detail]')) query('[data-ceremony-detail]').innerHTML = '<div class="api-empty">缺少有效活动编号。</div>';
return;
}
syncLinks();
try {
await loadCapability(api, genealogyId);
currentCeremony = normalizeCeremony(await api.ceremonyDetail(genealogyId, ceremonyId));
if (!currentCeremony || currentCeremony.ceremonyId !== ceremonyId) throw new Error('活动详情响应缺少稳定 ceremonyId');
if (query('[data-ceremony-detail]')) query('[data-ceremony-detail]').innerHTML = renderCeremonyDetail(currentCeremony);
if (query('[data-ceremony-detail-title]')) query('[data-ceremony-detail-title]').textContent = currentCeremony.ceremonyTitle;
if (query('[data-ceremony-editor-action]')) query('[data-ceremony-editor-action]').href = buildEditUrl(genealogyId, ceremonyId);
await loadGifts(api, genealogyId, ceremonyId);
if (canEditContent) await loadInvitees(api, genealogyId, ceremonyId);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (query('[data-ceremony-detail]')) query('[data-ceremony-detail]').innerHTML = '<div class="api-empty">' +
(isForbidden(error) ? '当前账号无权查看该活动。' : escapeHtml(error.message || '活动详情加载失败')) + '</div>';
showMessage(isForbidden(error) ? '当前账号无权查看该活动。' : (error.message || '活动详情加载失败'));
}
}
async function submitCeremony(form) {
var api = getApi();
var genealogyId = requireGenealogyId();
var ceremonyId = getCurrentCeremonyId();
var body;
var validation;
var saved;
var savedId;
var verified;
if (writePending || !genealogyId || redirectUnauthorized(api)) return;
body = buildCeremonyBody(getFormValues(form));
validation = validateCeremonyBody(body);
if (validation) {
setStatus('[data-ceremony-form-status]', validation);
return;
}
delete body.invalidLongitude;
delete body.invalidLatitude;
delete body.invalidCoverOssId;
delete body.invalidSortOrder;
setWritePending(true);
setStatus('[data-ceremony-form-status]', '正在保存活动…');
try {
saved = normalizeCeremony(ceremonyId
? await api.updateCeremony(genealogyId, ceremonyId, body)
: await api.createCeremony(genealogyId, body));
savedId = ceremonyId || (saved && saved.ceremonyId);
if (!savedId) throw new Error('保存响应缺少 ceremonyId');
verified = normalizeCeremony(await api.ceremonyDetail(genealogyId, savedId));
if (!verified || verified.ceremonyId !== savedId) throw new Error('保存后未能重读同一活动');
if (root.location) root.location.href = buildDetailUrl(genealogyId, savedId);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setStatus('[data-ceremony-form-status]', isForbidden(error) ? '当前账号无权保存该活动。' : (error.message || '活动保存失败'));
} finally {
setWritePending(false);
}
}
async function submitGift(form) {
var api = getApi();
var genealogyId = requireGenealogyId();
var ceremonyId = getCurrentCeremonyId();
var body;
var validation;
var saved;
if (writePending || !genealogyId || !ceremonyId || redirectUnauthorized(api)) return;
body = buildCeremonyGiftBody(getFormValues(form));
validation = validateCeremonyGiftBody(body);
if (validation) {
setStatus('[data-ceremony-gift-status]', validation);
return;
}
delete body.invalidGiftAmount;
setWritePending(true);
setStatus('[data-ceremony-gift-status]', '正在添加祭品…');
try {
saved = normalizeCeremonyGift(await api.createCeremonyGift(genealogyId, ceremonyId, body));
if (!saved || saved.ceremonyId !== ceremonyId) throw new Error('添加祭品响应缺少稳定 giftId');
if (!(await loadGifts(api, genealogyId, ceremonyId)).some(function (gift) { return gift.giftId === saved.giftId; })) {
throw new Error('添加后祭品列表未返回同一记录');
}
form.reset();
setStatus('[data-ceremony-gift-status]', '祭品已添加');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setStatus('[data-ceremony-gift-status]', isForbidden(error) ? '当前账号无权添加祭品。' : (error.message || '祭品添加失败'));
} finally {
setWritePending(false);
}
}
async function submitInvitees() {
var api = getApi();
var genealogyId = requireGenealogyId();
var ceremonyId = getCurrentCeremonyId();
var body;
if (writePending || !genealogyId || !ceremonyId || redirectUnauthorized(api)) return;
body = buildInviteesBody(queryAll('[data-invitee-user-id]:checked').map(function (input) {
return input.getAttribute('data-invitee-user-id');
}));
setWritePending(true);
setStatus('[data-ceremony-invitee-status]', '正在更新受邀人…');
try {
await api.replaceCeremonyInvitees(genealogyId, ceremonyId, body.inviteeUserIds);
await loadInvitees(api, genealogyId, ceremonyId);
setStatus('[data-ceremony-invitee-status]', '受邀名单已更新');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setStatus('[data-ceremony-invitee-status]', isForbidden(error) ? '当前账号无权维护受邀名单。' : (error.message || '受邀名单更新失败'));
} finally {
setWritePending(false);
}
}
async function deleteCeremony(ceremonyId) {
var api = getApi();
var genealogyId = requireGenealogyId();
var ceremony = ceremoniesById[ceremonyId] || currentCeremony;
if (writePending || !genealogyId || !normalizeId(ceremonyId) || redirectUnauthorized(api)) return;
if (root.confirm && !root.confirm('确认删除“' + (ceremony ? ceremony.ceremonyTitle : '该活动') + '”吗?删除后不可恢复。')) return;
setWritePending(true);
try {
await api.deleteCeremony(genealogyId, ceremonyId);
if (query('[data-ceremony-list-page]')) await loadCeremonies();
else if (root.location) root.location.href = buildListUrl(genealogyId);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(isForbidden(error) ? '当前账号无权删除该活动。' : (error.message || '活动删除失败'));
} finally {
setWritePending(false);
}
}
async function deleteGift(giftId) {
var api = getApi();
var genealogyId = requireGenealogyId();
var ceremonyId = getCurrentCeremonyId();
var gift = giftsById[giftId];
if (writePending || !genealogyId || !ceremonyId || !normalizeId(giftId) || redirectUnauthorized(api)) return;
if (root.confirm && !root.confirm('确认删除“' + (gift && (gift.giverName || gift.giverNickName) || '该祭品') + '”的记录吗?')) return;
setWritePending(true);
try {
await api.deleteCeremonyGift(genealogyId, ceremonyId, giftId);
await loadGifts(api, genealogyId, ceremonyId);
showMessage('祭品记录已删除');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(isForbidden(error) ? '当前账号无权删除该祭品。' : (error.message || '祭品删除失败'));
} finally {
setWritePending(false);
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('click', function (event) {
var ceremonyDelete = event.target.closest('[data-ceremony-delete-id]');
var giftDelete = event.target.closest('[data-ceremony-gift-delete-id]');
if (ceremonyDelete) {
event.preventDefault();
deleteCeremony(ceremonyDelete.getAttribute('data-ceremony-delete-id'));
} else if (giftDelete) {
event.preventDefault();
deleteGift(giftDelete.getAttribute('data-ceremony-gift-delete-id'));
}
});
documentRef.addEventListener('submit', function (event) {
var ceremonyForm = event.target.closest('[data-ceremony-form]');
var giftForm = event.target.closest('[data-ceremony-gift-form]');
var inviteeForm = event.target.closest('[data-ceremony-invitee-form]');
if (ceremonyForm) {
event.preventDefault();
submitCeremony(ceremonyForm);
} else if (giftForm) {
event.preventDefault();
submitGift(giftForm);
} else if (inviteeForm) {
event.preventDefault();
submitInvitees();
}
});
}
function init() {
if (!documentRef) return;
bindActions();
if (query('[data-ceremony-list-page]')) loadCeremonies();
if (query('[data-ceremony-edit-page]')) loadCeremonyEditor();
if (query('[data-ceremony-detail-page]')) loadCeremonyDetailPage();
}
return {
getCurrentGenealogyId: getCurrentGenealogyId,
getCurrentCeremonyId: getCurrentCeremonyId,
buildCeremonyBody: buildCeremonyBody,
validateCeremonyBody: validateCeremonyBody,
buildCeremonyGiftBody: buildCeremonyGiftBody,
validateCeremonyGiftBody: validateCeremonyGiftBody,
normalizeCeremony: normalizeCeremony,
normalizeCeremonyGift: normalizeCeremonyGift,
normalizeInvitation: normalizeInvitation,
normalizeInviteeOption: normalizeInviteeOption,
buildInviteesBody: buildInviteesBody,
renderCeremonyDetail: renderCeremonyDetail,
renderGiftList: renderGiftList,
shouldRedirectToLogin: shouldRedirectToLogin,
isForbidden: isForbidden,
init: init
};
});
+316
View File
@@ -0,0 +1,316 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.CeremonyPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.CeremonyPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
var invitationsById = Object.create(null);
var selectedInvitationId = '';
var writePending = false;
var invitationStatuses = ['PENDING', 'ACCEPTED', 'DECLINED', 'CANCELED'];
var responseStatuses = ['ACCEPTED', 'DECLINED'];
var statusLabels = {
PENDING: '待响应',
ACCEPTED: '已接受',
DECLINED: '已拒绝',
CANCELED: '已取消'
};
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
return documentRef ? (rootNode || documentRef).querySelector(selector) : null;
}
function shouldRedirectToLogin(api, error) {
var status = error && (error.status || error.code);
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
}
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');
return true;
}
root.location.href = 'login.html';
return true;
}
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 stringValue(value) {
return value === undefined || value === null ? '' : String(value);
}
function optionalInteger(value) {
var number;
if (value === undefined || value === null || value === '') return undefined;
number = Number(value);
return Number.isSafeInteger(number) ? number : undefined;
}
function optionalNumber(value) {
var number;
if (value === undefined || value === null || value === '') return undefined;
number = Number(value);
return Number.isFinite(number) ? number : undefined;
}
function normalizeInvitation(item) {
var invitationId = normalizeId(item && item.invitationId);
var genealogyId = normalizeId(item && item.genealogyId);
var ceremonyId = normalizeId(item && item.ceremonyId);
var inviteeUserId = normalizeId(item && item.inviteeUserId);
var inviteStatus = stringValue(item && item.inviteStatus);
var invitation;
var inviteVersion;
var longitude;
var latitude;
if (!invitationId || !genealogyId || !ceremonyId || invitationStatuses.indexOf(inviteStatus) < 0) {
return null;
}
invitation = {
invitationId: invitationId,
genealogyId: genealogyId,
ceremonyId: ceremonyId,
inviteStatus: inviteStatus,
deliveredTime: stringValue(item.deliveredTime),
readTime: stringValue(item.readTime),
responseTime: stringValue(item.responseTime),
ceremonyTitle: stringValue(item.ceremonyTitle),
ceremonyTime: stringValue(item.ceremonyTime),
location: stringValue(item.location),
locationAddress: stringValue(item.locationAddress)
};
if (inviteeUserId) invitation.inviteeUserId = inviteeUserId;
inviteVersion = optionalInteger(item.inviteVersion);
longitude = optionalNumber(item.longitude);
latitude = optionalNumber(item.latitude);
if (inviteVersion !== undefined) invitation.inviteVersion = inviteVersion;
if (longitude !== undefined) invitation.longitude = longitude;
if (latitude !== undefined) invitation.latitude = latitude;
return invitation;
}
function normalizeList(data) {
return Array.isArray(data) ? data : [];
}
function buildInvitationResponseBody(values) {
return {
inviteStatus: stringValue(values && values.inviteStatus)
};
}
function validateInvitationResponseBody(body) {
return !body || responseStatuses.indexOf(body.inviteStatus) < 0 ? '邀请只能选择接受或拒绝' : '';
}
function canRespondToInvitation(invitation) {
return Boolean(invitation && invitation.inviteStatus === 'PENDING');
}
function escapeHtml(value) {
return stringValue(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function invitationTitle(invitation) {
return invitation.ceremonyTitle || '未命名贺礼活动';
}
function renderInvitationRow(invitation) {
var actions = '<button class="pill" type="button" data-invitation-detail="' +
escapeHtml(invitation.invitationId) + '">查看详情</button>';
if (canRespondToInvitation(invitation)) {
actions += '<button class="pill" type="button" data-invitation-response="ACCEPTED" data-invitation-id="' +
escapeHtml(invitation.invitationId) + '">接受邀请</button>' +
'<button class="pill is-danger" type="button" data-invitation-response="DECLINED" data-invitation-id="' +
escapeHtml(invitation.invitationId) + '">拒绝邀请</button>';
}
return '<article class="module-row ceremony-invitation-row" data-invitation-row="' +
escapeHtml(invitation.invitationId) + '"><div><h3>' + escapeHtml(invitationTitle(invitation)) +
'</h3><p>' + escapeHtml(statusLabels[invitation.inviteStatus]) +
(invitation.ceremonyTime ? ' · ' + escapeHtml(invitation.ceremonyTime) : '') +
(invitation.location ? ' · ' + escapeHtml(invitation.location) : '') +
'</p></div><div class="row-actions">' + actions + '</div></article>';
}
function renderDetailValue(label, value) {
return '<p><span>' + escapeHtml(label) + '</span><b>' + escapeHtml(value || '未提供') + '</b></p>';
}
function renderInvitationDetail(invitation) {
var mapLink = '';
if (!invitation) return '<div class="api-empty">请选择一条邀请查看详情</div>';
if (invitation.longitude !== undefined && invitation.latitude !== undefined) {
mapLink = ' <a class="pill" target="_blank" rel="noopener noreferrer" href="https://uri.amap.com/marker?position=' +
encodeURIComponent(invitation.longitude + ',' + invitation.latitude) + '&amp;name=' +
encodeURIComponent(invitation.location || invitation.locationAddress || invitationTitle(invitation)) +
'">打开地图</a>';
}
return '<div class="form-like invitation-detail">' +
renderDetailValue('活动标题', invitationTitle(invitation)) +
renderDetailValue('邀请状态', statusLabels[invitation.inviteStatus]) +
renderDetailValue('活动时间', invitation.ceremonyTime) +
renderDetailValue('地点', invitation.location) +
'<p><span>详细地址</span><b>' + escapeHtml(invitation.locationAddress || '未提供') + mapLink + '</b></p>' +
renderDetailValue('投递时间', invitation.deliveredTime) +
renderDetailValue('阅读时间', invitation.readTime) +
renderDetailValue('响应时间', invitation.responseTime) +
'</div>';
}
function renderDetail(invitation) {
var container = query('[data-my-invitation-detail]');
selectedInvitationId = invitation ? invitation.invitationId : '';
if (container) container.innerHTML = renderInvitationDetail(invitation);
}
function renderInvitations(data) {
var container = query('[data-my-invitation-list]');
var normalized = normalizeList(data).map(normalizeInvitation);
var invalidCount = normalized.filter(function (item) { return !item; }).length;
var invitations = normalized.filter(Boolean);
invitationsById = Object.create(null);
invitations.forEach(function (invitation) {
invitationsById[invitation.invitationId] = invitation;
});
if (!container) return invitations;
if (invalidCount) {
container.innerHTML = '<div class="api-empty">邀请响应缺少稳定 ID 或合法状态,请联系后端核对 DTO。</div>';
renderDetail(null);
return [];
}
if (!invitations.length) {
container.innerHTML = '<div class="api-empty">暂无贺礼邀请</div>';
renderDetail(null);
return [];
}
container.innerHTML = invitations.map(renderInvitationRow).join('');
renderDetail(invitationsById[selectedInvitationId] || invitations[0]);
return invitations;
}
function setWritePending(pending) {
var buttons;
writePending = pending;
if (!documentRef) return;
buttons = documentRef.querySelectorAll('[data-invitation-response]');
Array.prototype.forEach.call(buttons, function (button) {
button.disabled = pending;
});
}
function setListState(message) {
var container = query('[data-my-invitation-list]');
if (container) container.innerHTML = '<div class="api-empty">' + escapeHtml(message) + '</div>';
}
async function loadMyInvitations() {
var api = getApi();
if (redirectUnauthorized(api)) return;
setListState('正在加载我的邀请…');
try {
renderInvitations(await api.myCeremonyInvitations());
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setListState(Number(error && (error.status || error.code)) === 403 ? '当前账号无权查看邀请' : (error.message || '邀请加载失败'));
renderDetail(null);
}
}
async function respondToInvitation(invitationId, inviteStatus) {
var api = getApi();
var invitation = invitationsById[normalizeId(invitationId)];
var body = buildInvitationResponseBody({ inviteStatus: inviteStatus });
var validation = validateInvitationResponseBody(body);
if (writePending || redirectUnauthorized(api) || !invitation || !canRespondToInvitation(invitation)) return;
if (validation) return;
setWritePending(true);
try {
await api.respondCeremonyInvitation(invitation.genealogyId, invitation.ceremonyId, body);
selectedInvitationId = invitation.invitationId;
await loadMyInvitations();
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setListState(Number(error && (error.status || error.code)) === 403 ? '当前账号无权响应该邀请' : (error.message || '邀请响应失败'));
} finally {
setWritePending(false);
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('click', function (event) {
var detailButton = event.target.closest('[data-invitation-detail]');
var responseButton = event.target.closest('[data-invitation-response]');
if (detailButton) {
event.preventDefault();
renderDetail(invitationsById[detailButton.getAttribute('data-invitation-detail')] || null);
return;
}
if (!responseButton) return;
event.preventDefault();
respondToInvitation(
responseButton.getAttribute('data-invitation-id'),
responseButton.getAttribute('data-invitation-response')
);
});
}
function init() {
if (!documentRef || !query('[data-ceremony-page]')) return;
bindActions();
loadMyInvitations();
}
return {
normalizeInvitation: normalizeInvitation,
normalizeList: normalizeList,
buildInvitationResponseBody: buildInvitationResponseBody,
validateInvitationResponseBody: validateInvitationResponseBody,
canRespondToInvitation: canRespondToInvitation,
renderInvitationRow: renderInvitationRow,
renderInvitationDetail: renderInvitationDetail,
shouldRedirectToLogin: shouldRedirectToLogin,
loadMyInvitations: loadMyInvitations,
init: init
};
});
+462 -97
View File
@@ -15,6 +15,16 @@
'use strict';
var documentRef = root.document;
var PAGE_SIZE = 20;
var currentFeedPage = 1;
var currentCommentPages = {};
var currentReplyPages = {};
var pendingActions = {};
var viewerContext = {
userId: '',
canEditContent: false,
canManage: false
};
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
@@ -62,9 +72,14 @@
}
function getCurrentGenealogyId(search) {
var page = query('[data-feed-page], [data-feed-edit-page]');
var page = query('[data-feed-page], [data-feed-edit-page], [data-feed-detail-page]');
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 profileGenealogyId;
}
return getQueryParam(source, 'genealogyId') || (page && page.getAttribute('data-genealogy-id')) || '';
}
@@ -86,20 +101,86 @@
if (!text) return undefined;
number = Number(text);
return Number.isFinite(number) ? number : undefined;
return Number.isSafeInteger(number) && number >= 0 ? number : undefined;
}
function toId(value) {
var text;
if (value === undefined || value === null || value === '') return '';
text = String(value);
return /^\d+$/.test(text) ? text : '';
}
function setDefined(target, source, fields) {
fields.forEach(function (field) {
if (source[field] !== undefined && source[field] !== null) target[field] = source[field];
});
return target;
}
function buildPageQuery(pageNum) {
var number = Number(pageNum);
if (!Number.isSafeInteger(number) || number < 1) number = 1;
return {
pageNum: number,
pageSize: PAGE_SIZE
};
}
function buildFeedUrl(kind, genealogyId, feedId) {
var params = new URLSearchParams();
var page = 'profile-feed.html';
params.set('genealogyId', toId(genealogyId));
if (kind === 'detail' || kind === 'edit') {
params.set('feedId', toId(feedId));
page = kind === 'detail' ? 'profile-feed-detail.html' : 'profile-feed-edit.html';
}
return page + '?' + params.toString();
}
function buildPostSaveUrl(genealogyId, feedId, status) {
return String(status || '0') === '1'
? buildFeedUrl('list', genealogyId)
: buildFeedUrl('detail', genealogyId, feedId);
}
function withActionLock(key, task) {
var promise;
if (pendingActions[key]) return pendingActions[key];
try {
promise = Promise.resolve(task());
} catch (error) {
promise = Promise.reject(error);
}
pendingActions[key] = promise.finally(function () {
delete pendingActions[key];
});
return pendingActions[key];
}
function buildFeedBody(values) {
// FamilyFeedBody 只提交最新接口文档定义的字段。
var source = values || {};
var mediaOssIds = trimOrUndefined(source.mediaOssIds);
var status = trimOrUndefined(source.status);
var body = {
feedType: trimOrUndefined(source.feedType) || 'text',
feedContent: String(source.feedContent || '').trim(),
mediaOssIds: trimOrUndefined(source.mediaOssIds),
status: trimOrUndefined(source.status)
feedContent: String(source.feedContent || '').trim()
};
var sortOrder = toIntegerOrUndefined(source.sortOrder);
if (status !== undefined && status !== '0' && status !== '1') {
throw new Error('动态状态只能是 0 或 1');
}
if (trimOrUndefined(source.sortOrder) !== undefined && sortOrder === undefined) {
throw new Error('排序值必须是非负整数');
}
if (mediaOssIds !== undefined) body.mediaOssIds = mediaOssIds;
if (status !== undefined) body.status = status;
if (sortOrder !== undefined) body.sortOrder = sortOrder;
return body;
}
@@ -117,21 +198,11 @@
}
function getFeedId(item) {
var value = item && item.feedId;
var text;
if (value === undefined || value === null || value === '') return '';
text = String(value);
return /^\d+$/.test(text) ? text : '';
return toId(item && item.feedId);
}
function getCommentId(item) {
var value = item && item.commentId;
var text;
if (value === undefined || value === null || value === '') return '';
text = String(value);
return /^\d+$/.test(text) ? text : '';
return toId(item && item.commentId);
}
function normalizeFeed(item) {
@@ -140,13 +211,29 @@
var feed;
if (!feedId || feedContent === undefined || feedContent === null) return null;
feed = {
feed = setDefined({
feedId: feedId,
feedContent: String(feedContent)
};
if (trimOrUndefined(item.mediaOssIds)) feed.mediaOssIds = trimOrUndefined(item.mediaOssIds);
if (trimOrUndefined(item.feedType)) feed.feedType = trimOrUndefined(item.feedType);
if (trimOrUndefined(item.status)) feed.status = trimOrUndefined(item.status);
}, item, [
'genealogyNo',
'genealogyName',
'publisherNickName',
'publisherStatus',
'feedType',
'mediaOssIds',
'likedByMe',
'likeCount',
'commentCount',
'pinned',
'pinnedTime',
'sortOrder',
'status',
'remark',
'createTime',
'updateTime'
]);
if (toId(item.genealogyId)) feed.genealogyId = toId(item.genealogyId);
if (toId(item.publisherUserId)) feed.publisherUserId = toId(item.publisherUserId);
return feed;
}
@@ -157,13 +244,24 @@
if (!commentId || commentContent === undefined) return null;
userDeleted = String(item && item.userDeleted || '') === '1';
return {
var comment = {
commentId: commentId,
commentContent: commentContent === null ? '该评论已删除' : String(commentContent),
appUserNickName: trimOrUndefined(item && item.appUserNickName),
replyCount: item && item.replyCount,
userDeleted: userDeleted || commentContent === null
};
setDefined(comment, item, [
'appUserAvatar',
'parentAppUserNickName',
'commentLevel',
'status',
'createTime'
]);
['genealogyId', 'feedId', 'parentCommentId', 'appUserId', 'parentAppUserId'].forEach(function (field) {
if (toId(item && item[field])) comment[field] = toId(item[field]);
});
return comment;
}
function normalizeList(data) {
@@ -183,24 +281,38 @@
}
function getListUrl(genealogyId, feedId) {
var params = new URLSearchParams();
params.set('genealogyId', genealogyId);
if (feedId) params.set('feedId', feedId);
return 'profile-feed' + (feedId ? '-edit' : '') + '.html?' + params.toString();
return buildFeedUrl(feedId ? 'edit' : 'list', genealogyId, feedId);
}
function renderFeedRow(feed, genealogyId) {
var canDelete = viewerContext.userId && viewerContext.userId === feed.publisherUserId;
var mediaCount = feed.mediaOssIds ? String(feed.mediaOssIds).split(',').filter(Boolean).length : 0;
var title = feed.publisherNickName || feed.genealogyName || '家族动态';
var meta = [];
var actions = '';
if (feed.pinned === true || String(feed.pinned) === '1') meta.push('置顶');
if (feed.createTime) meta.push(feed.createTime);
if (mediaCount) meta.push('附件 ' + mediaCount);
meta.push('点赞 ' + Number(feed.likeCount || 0));
meta.push('评论 ' + Number(feed.commentCount || 0));
actions += feed.likedByMe
? '<button class="pill" type="button" data-feed-unlike="' + escapeHtml(feed.feedId) + '">取消点赞</button>'
: '<button class="pill" type="button" data-feed-like="' + escapeHtml(feed.feedId) + '">点赞</button>';
actions += '<a class="pill" href="' + escapeHtml(buildFeedUrl('detail', genealogyId, feed.feedId)) + '">查看详情</a>';
actions += '<button class="pill" type="button" data-feed-comments="' + escapeHtml(feed.feedId) + '">查看评论</button>';
actions += '<button class="pill" type="button" data-feed-comment="' + escapeHtml(feed.feedId) + '">发表评论</button>';
if (viewerContext.canEditContent) {
actions += '<a class="pill" href="' + escapeHtml(buildFeedUrl('edit', genealogyId, feed.feedId)) + '">编辑</a>';
}
if (canDelete) {
actions += '<button class="pill" type="button" data-feed-delete="' + escapeHtml(feed.feedId) + '">删除</button>';
}
return '<article class="module-row feed-row" data-feed-id="' + escapeHtml(feed.feedId) + '">' +
'<div><h3>家族动态</h3><p>' + escapeHtml(feed.feedContent) + '</p></div>' +
'<div class="row-actions">' +
'<button class="pill" type="button" data-feed-like="' + escapeHtml(feed.feedId) + '">点赞</button>' +
'<button class="pill" type="button" data-feed-unlike="' + escapeHtml(feed.feedId) + '">取消点赞</button>' +
'<button class="pill" type="button" data-feed-comments="' + escapeHtml(feed.feedId) + '">查看评论</button>' +
'<button class="pill" type="button" data-feed-comment="' + escapeHtml(feed.feedId) + '">发表评论</button>' +
'<a class="pill" href="' + escapeHtml(getListUrl(genealogyId, feed.feedId)) + '">编辑</a>' +
'<button class="pill" type="button" data-feed-delete="' + escapeHtml(feed.feedId) + '">删除</button>' +
'</div></article>';
'<div><h3>' + escapeHtml(title) + '</h3><p>' + escapeHtml(feed.feedContent) + '</p>' +
'<p class="module-meta">' + escapeHtml(meta.join(' · ')) + '</p></div>' +
'<div class="row-actions">' + actions + '</div></article>';
}
function renderFeeds(data, genealogyId) {
@@ -224,6 +336,24 @@
}).join('');
}
function getPageTotal(data) {
var total = Number(data && data.total);
return Number.isFinite(total) && total >= 0 ? total : 0;
}
function updateListPager(data) {
var total = getPageTotal(data);
var totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
var previous = query('[data-feed-page-prev]');
var next = query('[data-feed-page-next]');
var label = query('[data-feed-page-label]');
if (previous) previous.disabled = currentFeedPage <= 1;
if (next) next.disabled = currentFeedPage >= totalPages;
if (label) label.textContent = '第 ' + currentFeedPage + ' / ' + totalPages + ' 页,共 ' + total + ' 条';
}
function hasReplies(comment) {
var count = comment && comment.replyCount;
@@ -233,16 +363,20 @@
function renderCommentRow(feedId, comment) {
var author = comment.appUserNickName ? '<strong>' + escapeHtml(comment.appUserNickName) + '</strong> ' : '';
var actions = '';
var canDelete = viewerContext.userId && viewerContext.userId === comment.appUserId;
if (!comment.userDeleted) {
actions += '<button class="pill" type="button" data-feed-comment-reply="' + escapeHtml(comment.commentId) + '" data-feed-id="' + escapeHtml(feedId) + '">回复</button>';
actions += '<button class="pill" type="button" data-feed-comment-delete="' + escapeHtml(comment.commentId) + '" data-feed-id="' + escapeHtml(feedId) + '">删除评论</button>';
if (canDelete) {
actions += '<button class="pill" type="button" data-feed-comment-delete="' + escapeHtml(comment.commentId) + '" data-feed-id="' + escapeHtml(feedId) + '">删除评论</button>';
}
}
if (hasReplies(comment)) {
actions += '<button class="pill" type="button" data-feed-comment-replies="' + escapeHtml(comment.commentId) + '" data-feed-id="' + escapeHtml(feedId) + '">查看回复</button>';
}
return '<div class="module-row" data-feed-comment-node="' + escapeHtml(comment.commentId) + '"><p>' + author + escapeHtml(comment.commentContent) + '</p>' + actions + '</div>';
return '<div class="module-row" data-feed-comment-node="' + escapeHtml(comment.commentId) + '"><p>' + author + escapeHtml(comment.commentContent) + '</p>' +
(comment.createTime ? '<p class="module-meta">' + escapeHtml(comment.createTime) + '</p>' : '') + actions + '</div>';
}
function normalizeComments(data) {
@@ -255,25 +389,33 @@
}
function renderComments(feedId, data) {
var detailContainer = query('[data-feed-detail-comments]');
var row = query('[data-feed-id="' + feedId + '"]');
var result = normalizeComments(data);
var list = result.comments;
var previous = row && row.nextElementSibling;
var content;
if (!row) return;
if (!row && !detailContainer) return;
if (previous && previous.getAttribute('data-feed-comment-list') === feedId) previous.remove();
if (result.invalidCount) {
row.insertAdjacentHTML('afterend', '<div class="api-empty" data-feed-comment-list="' + escapeHtml(feedId) + '">评论响应缺少 commentId 或 commentContent,请联系后端补充 DTO。</div>');
content = '<div class="api-empty">评论响应缺少 commentId 或 commentContent,请联系后端补充 DTO。</div>';
if (detailContainer) detailContainer.innerHTML = content;
else row.insertAdjacentHTML('afterend', '<div data-feed-comment-list="' + escapeHtml(feedId) + '">' + content + '</div>');
return;
}
if (!list.length) {
row.insertAdjacentHTML('afterend', '<div class="api-empty" data-feed-comment-list="' + escapeHtml(feedId) + '">暂无评论</div>');
content = '<div class="api-empty">暂无评论</div>';
if (detailContainer) detailContainer.innerHTML = content;
else row.insertAdjacentHTML('afterend', '<div data-feed-comment-list="' + escapeHtml(feedId) + '">' + content + '</div>');
return;
}
row.insertAdjacentHTML('afterend', '<div class="module-list" data-feed-comment-list="' + escapeHtml(feedId) + '">' + list.map(function (comment) {
content = '<div class="module-list">' + list.map(function (comment) {
return renderCommentRow(feedId, comment);
}).join('') + '</div>');
}).join('') + '</div>' + renderPageControls('comments', feedId, '', currentCommentPages[feedId] || 1, getPageTotal(data));
if (detailContainer) detailContainer.innerHTML = content;
else row.insertAdjacentHTML('afterend', '<div data-feed-comment-list="' + escapeHtml(feedId) + '">' + content + '</div>');
}
function renderCommentReplies(feedId, parentCommentId, data) {
@@ -294,13 +436,27 @@
row.insertAdjacentHTML('afterend', '<div class="module-list" data-feed-reply-list="' + escapeHtml(parentCommentId) + '">' + result.comments.map(function (comment) {
return renderCommentRow(feedId, comment);
}).join('') + '</div>');
}).join('') + renderPageControls('replies', feedId, parentCommentId, currentReplyPages[parentCommentId] || 1, getPageTotal(data)) + '</div>');
}
function renderPageControls(kind, feedId, commentId, pageNum, total) {
var totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
var prefix = kind === 'comments' ? 'data-feed-comments-' : 'data-feed-replies-';
var ids = ' data-feed-id="' + escapeHtml(feedId) + '"' +
(commentId ? ' data-comment-id="' + escapeHtml(commentId) + '"' : '');
return '<div class="row-actions">' +
'<button class="pill" type="button" ' + prefix + 'prev' + ids + (pageNum <= 1 ? ' disabled' : '') + '>上一页</button>' +
'<span>第 ' + pageNum + ' / ' + totalPages + ' 页</span>' +
'<button class="pill" type="button" ' + prefix + 'next' + ids + (pageNum >= totalPages ? ' disabled' : '') + '>下一页</button>' +
'</div>';
}
function getFormValues(form) {
var values = {};
queryAll('[name]', form).forEach(function (field) {
if (field.disabled) return;
values[field.name] = field.value;
});
return values;
@@ -315,9 +471,70 @@
});
}
function normalizeViewerContext(genealogy, profile) {
return {
userId: toId(profile && profile.userId),
canEditContent: genealogy && genealogy.canEditContent === true,
canManage: genealogy && genealogy.canManage === true
};
}
async function loadViewerContext(api, genealogyId) {
var genealogy;
var profile;
try {
genealogy = await api.genealogyDetail(genealogyId);
} catch (error) {
genealogy = null;
}
try {
profile = await api.currentProfile();
} catch (error) {
profile = null;
}
viewerContext = normalizeViewerContext(genealogy, profile);
return viewerContext;
}
function renderFeedDetail(feed, genealogyId) {
var container = query('[data-feed-detail]');
var canDelete = viewerContext.userId && viewerContext.userId === feed.publisherUserId;
var actions = '';
if (!container) return;
actions += feed.likedByMe
? '<button class="pill" type="button" data-feed-unlike="' + escapeHtml(feed.feedId) + '">取消点赞</button>'
: '<button class="pill" type="button" data-feed-like="' + escapeHtml(feed.feedId) + '">点赞</button>';
actions += '<button class="pill" type="button" data-feed-comment="' + escapeHtml(feed.feedId) + '">发表评论</button>';
actions += '<button class="pill" type="button" data-feed-share>复制分享链接</button>';
if (viewerContext.canEditContent) {
actions += '<a class="pill" href="' + escapeHtml(buildFeedUrl('edit', genealogyId, feed.feedId)) + '">编辑</a>';
}
if (canDelete) {
actions += '<button class="pill" type="button" data-feed-delete="' + escapeHtml(feed.feedId) + '">删除</button>';
}
container.setAttribute('data-feed-id', feed.feedId);
container.innerHTML = '<h2>' + escapeHtml(feed.publisherNickName || feed.genealogyName || '家族动态') + '</h2>' +
'<p>' + escapeHtml(feed.feedContent) + '</p>' +
'<p class="module-meta">' +
escapeHtml([
feed.createTime || '',
'点赞 ' + Number(feed.likeCount || 0),
'评论 ' + Number(feed.commentCount || 0),
String(feed.status) === '1' ? '已停用' : '正常'
].filter(Boolean).join(' · ')) +
'</p><div class="row-actions">' + actions + '</div>';
}
function syncGenealogyLinks(genealogyId) {
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) {
root.ProfileUI.syncGenealogyContextLinks();
}
queryAll('[data-feed-create-link], [data-feed-list-link]').forEach(function (link) {
link.href = getListUrl(genealogyId);
link.href = link.hasAttribute('data-feed-create-link')
? buildFeedUrl('edit', genealogyId)
: buildFeedUrl('list', genealogyId);
});
}
@@ -333,18 +550,24 @@
return genealogyId;
}
async function loadFeeds() {
async function loadFeeds(pageNum) {
var api = getApi();
var genealogyId;
var data;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncGenealogyLinks(genealogyId);
currentFeedPage = buildPageQuery(pageNum || getQueryParam(root.location && root.location.search, 'pageNum')).pageNum;
try {
renderFeeds(await api.feedsPage(genealogyId, { pageNum: 1, pageSize: 20 }), genealogyId);
await loadViewerContext(api, genealogyId);
data = await api.feedsPage(genealogyId, buildPageQuery(currentFeedPage));
renderFeeds(data, genealogyId);
updateListPager(data);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (query('[data-feed-list]')) query('[data-feed-list]').innerHTML = '<div class="api-empty">家族动态加载失败,请稍后重试。</div>';
showMessage(error.message || '家族动态加载失败');
}
}
@@ -358,8 +581,15 @@
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncGenealogyLinks(genealogyId);
if (!feedId) return;
try {
await loadViewerContext(api, genealogyId);
if (query('[data-feed-advanced]')) {
query('[data-feed-advanced]').hidden = !viewerContext.canManage;
queryAll('[name]', query('[data-feed-advanced]')).forEach(function (field) {
field.disabled = !viewerContext.canManage;
});
}
if (!feedId) return;
fillFeedForm(await api.feedDetail(genealogyId, feedId));
} catch (error) {
if (redirectUnauthorized(api, error)) return;
@@ -367,6 +597,32 @@
}
}
async function loadFeedDetail() {
var api = getApi();
var genealogyId;
var feedId = getCurrentFeedId();
var feed;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId || !feedId) {
if (query('[data-feed-detail]')) query('[data-feed-detail]').innerHTML = '<div class="api-empty">动态链接缺少有效编号</div>';
return;
}
syncGenealogyLinks(genealogyId);
try {
await loadViewerContext(api, genealogyId);
feed = normalizeFeed(await api.feedDetail(genealogyId, feedId));
if (!feed) throw new Error('动态响应缺少 feedId 或 feedContent,请联系后端补充 DTO。');
renderFeedDetail(feed, genealogyId);
await showComments(feedId, currentCommentPages[feedId] || 1);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (query('[data-feed-detail]')) query('[data-feed-detail]').innerHTML = '<div class="api-empty">动态详情加载失败,请返回列表重试。</div>';
showMessage(error.message || '动态详情加载失败');
}
}
async function submitFeed(form) {
var api = getApi();
var genealogyId;
@@ -378,23 +634,42 @@
if (!genealogyId) return;
// 富文本编辑器在提交前同步回 textarea,保证发送的是用户当前输入。
if (root.AppRichEditor && root.AppRichEditor.syncAll) root.AppRichEditor.syncAll();
body = buildFeedBody(getFormValues(form));
try {
body = buildFeedBody(getFormValues(form));
} catch (error) {
showMessage(error.message || '动态表单校验失败');
return;
}
if (!body.feedContent) {
showMessage('请填写动态内容');
return;
}
try {
if (feedId) {
await api.updateFeed(genealogyId, feedId, body);
} else {
await api.createFeed(genealogyId, body);
return withActionLock('save:' + (feedId || 'new'), async function () {
var saved;
var savedFeedId;
var buttons = queryAll('[type="submit"]', form);
buttons.forEach(function (button) { button.disabled = true; });
try {
if (feedId) {
saved = await api.updateFeed(genealogyId, feedId, body);
} else {
saved = await api.createFeed(genealogyId, body);
}
savedFeedId = getFeedId(saved) || feedId;
if (!savedFeedId) throw new Error('动态保存响应缺少 feedId');
if (String(body.status || '0') === '0') {
await api.feedDetail(genealogyId, savedFeedId);
}
showMessage('动态已保存');
root.location.href = buildPostSaveUrl(genealogyId, savedFeedId, body.status);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(error.message || '动态保存失败');
} finally {
buttons.forEach(function (button) { button.disabled = false; });
}
showMessage('动态已保存');
root.location.href = getListUrl(genealogyId);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(error.message || '动态保存失败');
}
});
}
async function likeFeed(feedId, shouldLike) {
@@ -404,17 +679,20 @@
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId || !feedId) return;
try {
if (shouldLike) {
await api.likeFeed(genealogyId, feedId);
} else {
await api.unlikeFeed(genealogyId, feedId);
return withActionLock((shouldLike ? 'like:' : 'unlike:') + feedId, async function () {
try {
if (shouldLike) {
await api.likeFeed(genealogyId, feedId);
} else {
await api.unlikeFeed(genealogyId, feedId);
}
if (query('[data-feed-detail-page]')) await loadFeedDetail();
else await loadFeeds(currentFeedPage);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(error.message || (shouldLike ? '点赞失败' : '取消点赞失败'));
}
await loadFeeds();
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(error.message || (shouldLike ? '点赞失败' : '取消点赞失败'));
}
});
}
async function deleteFeed(feedId) {
@@ -425,41 +703,67 @@
genealogyId = requireGenealogyId();
if (!genealogyId || !feedId) return;
if (root.confirm && !root.confirm('确认删除这条动态吗?')) return;
try {
await api.deleteFeed(genealogyId, feedId);
await loadFeeds();
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(error.message || '删除动态失败');
}
return withActionLock('delete:' + feedId, async function () {
try {
await api.deleteFeed(genealogyId, feedId);
if (query('[data-feed-detail-page]')) root.location.href = buildFeedUrl('list', genealogyId);
else await loadFeeds(currentFeedPage);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(error.message || '删除动态失败');
}
});
}
async function showComments(feedId) {
async function showComments(feedId, pageNum) {
var api = getApi();
var genealogyId;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId || !feedId) return;
currentCommentPages[feedId] = buildPageQuery(pageNum).pageNum;
try {
renderComments(feedId, await api.feedComments(genealogyId, feedId));
renderComments(
feedId,
await api.feedCommentsPage(genealogyId, feedId, buildPageQuery(currentCommentPages[feedId]))
);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (query('[data-feed-detail-comments]')) {
query('[data-feed-detail-comments]').innerHTML = '<div class="api-empty">评论加载失败,请稍后重试。</div>';
}
showMessage(error.message || '评论加载失败');
}
}
async function showCommentReplies(feedId, commentId) {
async function showCommentReplies(feedId, commentId, pageNum) {
var api = getApi();
var genealogyId;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId || !feedId || !commentId) return;
currentReplyPages[commentId] = buildPageQuery(pageNum).pageNum;
try {
renderCommentReplies(feedId, commentId, await api.feedCommentReplies(genealogyId, feedId, commentId));
renderCommentReplies(
feedId,
commentId,
await api.feedCommentRepliesPage(
genealogyId,
feedId,
commentId,
buildPageQuery(currentReplyPages[commentId])
)
);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (query('[data-feed-comment-node="' + commentId + '"]')) {
query('[data-feed-comment-node="' + commentId + '"]').insertAdjacentHTML(
'afterend',
'<div class="api-empty">回复加载失败,请稍后重试。</div>'
);
}
showMessage(error.message || '回复加载失败');
}
}
@@ -476,13 +780,26 @@
content = root.prompt ? root.prompt(parentCommentId ? '请输入回复内容' : '请输入评论内容', '') : '';
body = buildCommentBody({ commentContent: content, parentCommentId: parentCommentId });
if (!body.commentContent) return;
try {
await api.createFeedComment(genealogyId, feedId, body);
await showComments(feedId);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(error.message || '评论发布失败');
if (body.commentContent.length > 1000) {
showMessage('评论不能超过 1000 字');
return;
}
return withActionLock('comment:' + feedId + ':' + (parentCommentId || 'root'), async function () {
try {
await api.createFeedComment(genealogyId, feedId, body);
await showComments(feedId, currentCommentPages[feedId] || 1);
if (parentCommentId) await showCommentReplies(feedId, parentCommentId, currentReplyPages[parentCommentId] || 1);
if (query('[data-feed-detail-page]')) {
var feed = normalizeFeed(await api.feedDetail(genealogyId, feedId));
if (feed) renderFeedDetail(feed, genealogyId);
} else {
await loadFeeds(currentFeedPage);
}
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(error.message || '评论发布失败');
}
});
}
async function deleteComment(feedId, commentId) {
@@ -493,13 +810,21 @@
genealogyId = requireGenealogyId();
if (!genealogyId || !feedId || !commentId) return;
if (root.confirm && !root.confirm('确认删除这条评论吗?')) return;
try {
await api.deleteFeedComment(genealogyId, feedId, commentId);
await showComments(feedId);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(error.message || '删除评论失败');
}
return withActionLock('delete-comment:' + commentId, async function () {
try {
await api.deleteFeedComment(genealogyId, feedId, commentId);
await showComments(feedId, currentCommentPages[feedId] || 1);
if (query('[data-feed-detail-page]')) {
var feed = normalizeFeed(await api.feedDetail(genealogyId, feedId));
if (feed) renderFeedDetail(feed, genealogyId);
} else {
await loadFeeds(currentFeedPage);
}
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(error.message || '删除评论失败');
}
});
}
function bindActions() {
@@ -515,6 +840,7 @@
var target = event.target;
var feedId;
var commentId;
var pageNum;
if (target.closest('[data-feed-like]')) {
event.preventDefault();
@@ -527,7 +853,7 @@
if (target.closest('[data-feed-comments]')) {
event.preventDefault();
feedId = target.closest('[data-feed-comments]').getAttribute('data-feed-comments');
showComments(feedId);
showComments(feedId, 1);
}
if (target.closest('[data-feed-comment]')) {
event.preventDefault();
@@ -544,7 +870,7 @@
event.preventDefault();
commentId = target.closest('[data-feed-comment-replies]').getAttribute('data-feed-comment-replies');
feedId = target.closest('[data-feed-comment-replies]').getAttribute('data-feed-id');
showCommentReplies(feedId, commentId);
showCommentReplies(feedId, commentId, 1);
}
if (target.closest('[data-feed-delete]')) {
event.preventDefault();
@@ -556,6 +882,39 @@
feedId = target.closest('[data-feed-comment-delete]').getAttribute('data-feed-id');
deleteComment(feedId, commentId);
}
if (target.closest('[data-feed-page-prev]')) {
event.preventDefault();
loadFeeds(Math.max(1, currentFeedPage - 1));
}
if (target.closest('[data-feed-page-next]')) {
event.preventDefault();
loadFeeds(currentFeedPage + 1);
}
if (target.closest('[data-feed-comments-prev], [data-feed-comments-next]')) {
event.preventDefault();
feedId = target.closest('[data-feed-id]').getAttribute('data-feed-id');
pageNum = currentCommentPages[feedId] || 1;
showComments(feedId, target.closest('[data-feed-comments-prev]') ? Math.max(1, pageNum - 1) : pageNum + 1);
}
if (target.closest('[data-feed-replies-prev], [data-feed-replies-next]')) {
event.preventDefault();
feedId = target.closest('[data-feed-id]').getAttribute('data-feed-id');
commentId = target.closest('[data-comment-id]').getAttribute('data-comment-id');
pageNum = currentReplyPages[commentId] || 1;
showCommentReplies(feedId, commentId, target.closest('[data-feed-replies-prev]') ? Math.max(1, pageNum - 1) : pageNum + 1);
}
if (target.closest('[data-feed-share]')) {
event.preventDefault();
if (root.navigator && root.navigator.clipboard && root.location) {
root.navigator.clipboard.writeText(root.location.href).then(function () {
showMessage('分享链接已复制');
}).catch(function () {
showMessage('复制失败,请从浏览器地址栏复制');
});
} else {
showMessage('请从浏览器地址栏复制分享链接');
}
}
});
}
@@ -564,6 +923,7 @@
bindActions();
if (query('[data-feed-page]')) loadFeeds();
if (query('[data-feed-edit-page]')) loadFeedForEdit();
if (query('[data-feed-detail-page]')) loadFeedDetail();
}
return {
@@ -571,8 +931,13 @@
getCurrentFeedId: getCurrentFeedId,
buildFeedBody: buildFeedBody,
buildCommentBody: buildCommentBody,
buildPageQuery: buildPageQuery,
buildFeedUrl: buildFeedUrl,
buildPostSaveUrl: buildPostSaveUrl,
withActionLock: withActionLock,
normalizeFeed: normalizeFeed,
normalizeComment: normalizeComment,
normalizeViewerContext: normalizeViewerContext,
normalizeComments: normalizeComments,
normalizeList: normalizeList,
shouldRedirectToLogin: shouldRedirectToLogin,
+268 -19
View File
@@ -13,16 +13,6 @@
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var TARGET_LABELS = {
'profile-family-home.html': '家谱主页',
'profile-feed.html': '家族圈动态',
'profile-growth.html': '成长记录',
'profile-relative.html': '亲友往来',
'profile-memo.html': '备忘录',
'profile-tree.html': '世系图',
'profile-generation.html': '字辈谱'
};
function getTargetPage(search) {
var params = new URLSearchParams(String(search === undefined && root.location ? root.location.search : search || '').replace(/^\?/, ''));
var target = params.get('next') || '';
@@ -30,38 +20,297 @@
return /^profile-[a-z-]+\.html$/.test(target) ? target : 'profile-family-home.html';
}
function buildStatus(quota, target) {
function buildStatus(quota) {
var source = quota || {};
var details = [];
var label = TARGET_LABELS[target] || '该家谱业务';
if (Number(source.createRemaining) >= 0) details.push('还可创建 ' + source.createRemaining + ' 部');
if (Number(source.joinRemaining) >= 0) details.push('还可加入 ' + source.joinRemaining + ' 部');
return '当前 PC 接口尚未提供“我的家谱”列表或家谱详情,不能伪造家谱编号进入“' + label + '”。' +
(details.length ? ' 已读取额度:' + details.join('') + '。' : '');
if (Number(source.createRemaining) === -1) details.push('创建不限');
else if (Number(source.createRemaining) >= 0) details.push('还可创建 ' + source.createRemaining + ' 部');
if (Number(source.joinRemaining) === -1) details.push('加入不限');
else if (Number(source.joinRemaining) >= 0) details.push('还可加入 ' + source.joinRemaining + '');
return details.length ? '家谱额度:' + details.join('') + '。' : '';
}
function escapeHtml(value) {
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function trimOrUndefined(value) {
var text;
if (value === undefined || value === null) return undefined;
text = String(value).trim();
return text || undefined;
}
function normalizeId(value) {
var text;
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
if (value === undefined || value === null) return '';
text = String(value).trim();
return /^[1-9][0-9]*$/.test(text) ? text : '';
}
function buildGenealogyCreateBody(values) {
var source = values || {};
var body = {
genealogyName: String(source.genealogyName || '').trim(),
surname: String(source.surname || '').trim(),
regionCode: String(source.regionCode || '').trim()
};
var optionalNames = ['ancestralHall', 'originPlace', 'addressDetail', 'intro'];
var coverOssId = normalizeId(source.coverOssId);
var visibility = trimOrUndefined(source.visibility);
var joinMode = trimOrUndefined(source.joinMode);
optionalNames.forEach(function (name) {
var value = trimOrUndefined(source[name]);
if (value !== undefined) body[name] = value;
});
if (source.coverOssId !== undefined && source.coverOssId !== null && source.coverOssId !== '') {
if (coverOssId) body.coverOssId = coverOssId;
else body.invalidCoverOssId = true;
}
if (visibility !== undefined) body.visibility = visibility;
if (joinMode !== undefined) body.joinMode = joinMode;
return body;
}
function validateGenealogyCreateBody(body) {
if (!body || !body.genealogyName) return '请填写谱名';
if (!body.surname) return '请填写姓氏';
if (!body.regionCode) return '请选择家谱所在地区';
if (body.visibility !== undefined && ['0', '1', '2'].indexOf(body.visibility) < 0) return '可见范围选项无效';
if (body.joinMode !== undefined && ['0', '1', '2'].indexOf(body.joinMode) < 0) return '加入方式选项无效';
if (body.invalidCoverOssId) return '封面文件无效,请重新选择';
return '';
}
function canCreateGenealogy(quota) {
var remaining = Number(quota && quota.createRemaining);
if (quota && typeof quota.canCreate === 'boolean') return quota.canCreate;
return Number.isInteger(remaining) && (remaining === -1 || remaining > 0);
}
function normalizeCreatedGenealogy(item) {
var genealogyId = normalizeId(item && item.genealogyId);
var genealogyName = String(item && item.genealogyName || '').trim();
var surname = String(item && item.surname || '').trim();
if (!genealogyId || !genealogyName || !surname) return null;
return {
genealogyId: genealogyId,
genealogyName: genealogyName,
surname: surname,
regionFullName: String(item && item.regionFullName || '')
};
}
function buildCreatedGenealogyUrl(item) {
var genealogy = normalizeCreatedGenealogy(item);
return genealogy ? buildEntryUrl(genealogy.genealogyId, 'profile-family-home.html') : '';
}
function shouldRedirectToLogin(api, error) {
var status = error && (error.status || error.code);
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
}
function redirectToLogin(api) {
if (api && api.clearToken) api.clearToken();
if (!root.location) return;
if (typeof root.location.replace === 'function') root.location.replace('login.html');
else root.location.href = 'login.html';
}
function getFormValues(form) {
var values = {};
Array.prototype.forEach.call(form.querySelectorAll('[name]'), function (field) {
values[field.name] = field.value;
});
return values;
}
function setCreateStatus(message) {
var status = root.document && root.document.querySelector('[data-genealogy-create-status]');
if (status) status.textContent = message || '';
}
function setCreatePending(pending) {
var button = root.document && root.document.querySelector('[data-genealogy-create-submit]');
if (button) button.disabled = Boolean(pending);
}
async function initGenealogyCreatePage() {
var form = root.document && root.document.querySelector('[data-genealogy-create-form]');
var quotaElement = root.document && root.document.querySelector('[data-genealogy-create-quota]');
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
var quota;
var writePending = false;
if (!form) return;
if (shouldRedirectToLogin(api)) {
redirectToLogin(api);
return;
}
try {
quota = await api.genealogyQuota();
if (quotaElement) quotaElement.textContent = buildStatus(quota) || '当前额度信息不可用';
if (!canCreateGenealogy(quota)) {
setCreateStatus('当前没有可用的家谱创建额度');
setCreatePending(true);
return;
}
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return;
}
if (quotaElement) quotaElement.textContent = '家谱额度读取失败';
setCreateStatus(error.message || '无法读取家谱创建额度');
setCreatePending(true);
return;
}
if (form.__genealogyCreateBound) return;
form.__genealogyCreateBound = true;
form.addEventListener('submit', async function (event) {
var body;
var validation;
var created;
var verified;
event.preventDefault();
if (writePending) return;
body = buildGenealogyCreateBody(getFormValues(form));
validation = validateGenealogyCreateBody(body);
if (validation) {
setCreateStatus(validation);
return;
}
delete body.invalidCoverOssId;
writePending = true;
setCreatePending(true);
setCreateStatus('正在创建家谱…');
try {
created = normalizeCreatedGenealogy(await api.createGenealogy(body));
if (!created) throw new Error('创建响应缺少有效家谱信息');
verified = normalizeCreatedGenealogy(await api.genealogyDetail(created.genealogyId));
if (!verified || verified.genealogyId !== created.genealogyId) throw new Error('创建后无法读取同一家谱');
if (root.location) root.location.href = buildCreatedGenealogyUrl(verified);
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return;
}
setCreateStatus(error.message || '创建家谱失败,请稍后重试');
} finally {
writePending = false;
setCreatePending(false);
}
});
}
function normalizeGenealogy(item) {
var source = item || {};
var id = source.genealogyId === undefined || source.genealogyId === null
? ''
: String(source.genealogyId);
if (!/^[1-9][0-9]*$/.test(id) || !source.genealogyName) return null;
return {
genealogyId: id,
genealogyName: String(source.genealogyName),
surname: source.surname === undefined || source.surname === null ? '' : String(source.surname),
regionFullName: source.regionFullName === undefined || source.regionFullName === null ? '' : String(source.regionFullName),
memberCount: source.memberCount,
personCount: source.personCount,
roleType: source.roleType === undefined || source.roleType === null ? '' : String(source.roleType),
canManage: source.canManage === true
};
}
function buildEntryUrl(genealogyId, target) {
var params = new URLSearchParams();
params.set('genealogyId', String(genealogyId));
return (target || 'profile-family-home.html') + '?' + params.toString();
}
function renderGenealogies(container, data, target) {
var items = Array.isArray(data) ? data.map(normalizeGenealogy).filter(Boolean) : [];
if (!container) return;
if (!items.length) {
container.innerHTML = '<div class="api-empty">当前账号还没有可进入的家谱</div>';
return;
}
container.innerHTML = items.map(function (item) {
var meta = [
item.surname ? item.surname + '氏' : '',
item.regionFullName,
item.memberCount === undefined || item.memberCount === null ? '' : item.memberCount + ' 位成员',
item.personCount === undefined || item.personCount === null ? '' : item.personCount + ' 位世系人物'
].filter(Boolean).join(' · ');
return '<a class="module-row" data-genealogy-id="' + escapeHtml(item.genealogyId) +
'" href="' + escapeHtml(buildEntryUrl(item.genealogyId, target)) + '">' +
'<div><h3>' + escapeHtml(item.genealogyName) + '</h3><p>' + escapeHtml(meta) +
'</p></div><span class="pill">' + (item.canManage ? '管理并进入' : '进入家谱') + '</span></a>';
}).join('');
}
async function init() {
if (root.document && root.document.querySelector('[data-genealogy-create-page]')) {
await initGenealogyCreatePage();
return;
}
var status = root.document && root.document.querySelector('[data-genealogy-entry-status]');
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
var target = getTargetPage();
var container = root.document && root.document.querySelector('[data-genealogy-list="mine"]');
var result;
if (!status) return;
if (!api || !api.genealogyQuota) {
if (!api || !api.genealogyQuota || !api.genealogiesMine) {
status.textContent = '家谱入口接口初始化失败,请刷新后重试。';
return;
}
try {
status.textContent = buildStatus(await api.genealogyQuota(), target);
result = await Promise.all([api.genealogiesMine(), api.genealogyQuota()]);
renderGenealogies(container, result[0], target);
status.textContent = buildStatus(result[1]);
} catch (error) {
status.textContent = '无法读取家谱额度,请稍后重试。当前 PC 接口仍未提供家谱列表或详情,不能伪造家谱编号。';
status.textContent = '无法读取我的家谱,请稍后重试。';
}
}
return {
getTargetPage: getTargetPage,
buildStatus: buildStatus,
buildGenealogyCreateBody: buildGenealogyCreateBody,
validateGenealogyCreateBody: validateGenealogyCreateBody,
canCreateGenealogy: canCreateGenealogy,
normalizeCreatedGenealogy: normalizeCreatedGenealogy,
buildCreatedGenealogyUrl: buildCreatedGenealogyUrl,
shouldRedirectToLogin: shouldRedirectToLogin,
initGenealogyCreatePage: initGenealogyCreatePage,
normalizeGenealogy: normalizeGenealogy,
buildEntryUrl: buildEntryUrl,
renderGenealogies: renderGenealogies,
init: init
};
});
+111 -19
View File
@@ -17,7 +17,7 @@
var documentRef = root.document;
var poemsById = Object.create(null);
var lastPreviewSignature = '';
var managementAccess = true;
var managementAccess = false;
var writePending = false;
function getApi() {
@@ -150,11 +150,16 @@
}
function splitPoemText(value) {
return String(value || '').trim().split(/[\s,;;、/|]+/).filter(Boolean);
var text = String(value || '').trim();
if (!text) return [];
if (/[\s,;;、/|]/.test(text)) {
return text.split(/[\s,;;、/|]+/).filter(Boolean);
}
return Array.from(text);
}
function getPoemId(item) {
var value = item && item.poemId;
function normalizeId(value) {
var text;
if (value === undefined || value === null || value === '') return '';
@@ -163,8 +168,13 @@
return /^\d+$/.test(text) ? text : '';
}
function getPoemId(item) {
return normalizeId(item && item.poemId);
}
function normalizeGenerationPoem(item) {
var poemId = getPoemId(item);
var genealogyId = normalizeId(item && item.genealogyId);
var generationNo = toIntegerOrUndefined(item && item.generationNo);
var generationText = trimOrUndefined(item && item.generationText);
var status = trimOrUndefined(item && item.status);
@@ -177,11 +187,24 @@
generationText: generationText,
status: status
};
if (genealogyId) poem.genealogyId = genealogyId;
if (item.genealogyNo !== undefined && item.genealogyNo !== null) poem.genealogyNo = String(item.genealogyNo);
if (item.genealogyName !== undefined && item.genealogyName !== null) poem.genealogyName = String(item.genealogyName);
if (item.description !== undefined && item.description !== null) poem.description = String(item.description);
if (toIntegerOrUndefined(item.sortOrder) !== undefined) poem.sortOrder = toIntegerOrUndefined(item.sortOrder);
if (item.remark !== undefined && item.remark !== null) poem.remark = String(item.remark);
return poem;
}
function normalizeGenerationAccess(genealogy) {
var canEditContent = Boolean(genealogy && genealogy.canEditContent === true);
return {
canEditContent: canEditContent,
listMethod: canEditContent ? 'generationPoemsManagement' : 'generationPoems'
};
}
function normalizeList(data) {
if (Array.isArray(data)) return data;
if (data && Array.isArray(data.rows)) return data.rows;
@@ -200,16 +223,20 @@
function renderGenerationPoem(poem) {
var details = [];
var stateText = poem.status === '1' ? '已停用' : '正常';
var actions = '';
if (poem.description) details.push('说明:' + poem.description);
if (poem.sortOrder !== undefined) details.push('排序:' + poem.sortOrder);
details.push('状态:' + stateText);
if (managementAccess) {
actions = '<div class="row-actions">' +
'<button class="pill" type="button" data-generation-edit="' + escapeHtml(poem.poemId) + '">编辑</button>' +
'<button class="pill" type="button" data-generation-status="' + escapeHtml(poem.poemId) + '">' + (poem.status === '1' ? '恢复' : '停用') + '</button>' +
'</div>';
}
return '<div class="module-row" data-generation-poem-id="' + escapeHtml(poem.poemId) + '">' +
'<div><h3>第 ' + escapeHtml(poem.generationNo) + ' 代:' + escapeHtml(poem.generationText) + '</h3><p>' + escapeHtml(details.join(' · ')) + '</p></div>' +
'<div class="row-actions">' +
'<button class="pill" type="button" data-generation-edit="' + escapeHtml(poem.poemId) + '">编辑</button>' +
'<button class="pill" type="button" data-generation-status="' + escapeHtml(poem.poemId) + '">' + (poem.status === '1' ? '恢复' : '停用') + '</button>' +
'</div></div>';
actions + '</div>';
}
function renderGenerationPoems(data) {
@@ -234,16 +261,71 @@
container.innerHTML = poems.map(renderGenerationPoem).join('');
}
function normalizePreview(data) {
if (!data || !Array.isArray(data.items)) return null;
function normalizeCount(value) {
var count = toIntegerOrUndefined(value);
return {
createCount: toIntegerOrUndefined(data.createCount),
updateCount: toIntegerOrUndefined(data.updateCount),
keepCount: toIntegerOrUndefined(data.keepCount),
disableCount: toIntegerOrUndefined(data.disableCount),
items: data.items
return count !== undefined && count >= 0 ? count : undefined;
}
function normalizePreviewItem(item) {
var action = item && item.action;
var generationNo = toIntegerOrUndefined(item && item.generationNo);
var poemId = getPoemId(item);
var oldText = trimOrUndefined(item && item.oldGenerationText);
var newText = trimOrUndefined(item && item.newGenerationText);
var oldStatus = trimOrUndefined(item && item.oldStatus);
var newStatus = trimOrUndefined(item && item.newStatus);
var normalized;
if (!generationNo || ['create', 'update', 'keep', 'disable'].indexOf(action) === -1) return null;
if (newStatus !== '0' && newStatus !== '1') return null;
if (action === 'create' && !newText) return null;
if (action !== 'create' && (!poemId || !oldText || (oldStatus !== '0' && oldStatus !== '1'))) return null;
if ((action === 'update' || action === 'keep') && !newText) return null;
normalized = {
generationNo: generationNo
};
if (poemId) normalized.poemId = poemId;
if (oldText) normalized.oldGenerationText = oldText;
if (newText) normalized.newGenerationText = newText;
if (oldStatus !== undefined) normalized.oldStatus = oldStatus;
normalized.newStatus = newStatus;
normalized.action = action;
if (item.warning !== undefined && item.warning !== null) normalized.warning = String(item.warning);
return normalized;
}
function normalizePreview(data) {
var counts;
var items;
var actualCounts = { create: 0, update: 0, keep: 0, disable: 0 };
if (!data || !Array.isArray(data.items) || !data.items.length) return null;
counts = {
createCount: normalizeCount(data.createCount),
updateCount: normalizeCount(data.updateCount),
keepCount: normalizeCount(data.keepCount),
disableCount: normalizeCount(data.disableCount)
};
if (Object.keys(counts).some(function (key) { return counts[key] === undefined; })) return null;
items = data.items.map(normalizePreviewItem);
if (items.some(function (item) { return !item; })) return null;
items.forEach(function (item) { actualCounts[item.action] += 1; });
if (counts.createCount !== actualCounts.create ||
counts.updateCount !== actualCounts.update ||
counts.keepCount !== actualCounts.keep ||
counts.disableCount !== actualCounts.disable) return null;
counts.items = items;
return counts;
}
function buildBatchConfirmMessage(disableMissing) {
if (disableMissing) {
return '确认按当前预览保存字辈,并停用未出现在文本中的后续世代吗?历史记录不会删除。';
}
return '确认按当前预览保存字辈吗?';
}
function renderBatchPreview(data) {
@@ -307,9 +389,15 @@
function refreshManagementControls() {
var disabled = !managementAccess || writePending;
queryAll('[data-generation-management]').forEach(function (element) {
element.hidden = !managementAccess;
});
queryAll('[data-generation-add], [data-generation-batch-action], [data-generation-edit], [data-generation-status]').forEach(function (control) {
control.disabled = disabled;
});
queryAll('[data-generation-management] textarea, [data-generation-management] input').forEach(function (control) {
control.disabled = disabled;
});
}
function setManagementEnabled(enabled) {
@@ -385,14 +473,16 @@
async function loadGenerationPoems() {
var api = getApi();
var genealogyId;
var access;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncGenealogyLinks(genealogyId);
try {
renderGenerationPoems(await api.generationPoemsManagement(genealogyId));
setManagementEnabled(true);
access = normalizeGenerationAccess(await api.genealogyDetail(genealogyId));
setManagementEnabled(access.canEditContent);
renderGenerationPoems(await api[access.listMethod](genealogyId));
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (isForbidden(error)) {
@@ -533,7 +623,7 @@
setBatchStatus('请先预览当前内容,再保存');
return;
}
if (root.confirm && !root.confirm('确认按当前预览保存字辈吗?')) return;
if (root.confirm && !root.confirm(buildBatchConfirmMessage(body.disableMissing))) return;
setBatchStatus('正在保存…');
setWritePending(true);
try {
@@ -592,7 +682,9 @@
validateBatchBody: validateBatchBody,
splitPoemText: splitPoemText,
normalizeGenerationPoem: normalizeGenerationPoem,
normalizeGenerationAccess: normalizeGenerationAccess,
normalizePreview: normalizePreview,
buildBatchConfirmMessage: buildBatchConfirmMessage,
normalizeList: normalizeList,
shouldRedirectToLogin: shouldRedirectToLogin,
isForbidden: isForbidden,
+466 -111
View File
@@ -1,5 +1,4 @@
(function (root, factory) {
// 成长记录页面只消费 ApiClient 中已在 Apifox PC 目录核验的方法。
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
@@ -15,6 +14,7 @@
'use strict';
var documentRef = root.document;
var recordsById = Object.create(null);
var writePending = false;
function getApi() {
@@ -29,37 +29,70 @@
return documentRef ? Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector)) : [];
}
function trimOrUndefined(value) {
var text = String(value === undefined || value === null ? '' : value).trim();
return text || undefined;
}
function toSafeIntegerOrUndefined(value) {
var text = trimOrUndefined(value);
var number;
if (!text) return undefined;
number = Number(text);
return Number.isInteger(number) && Number.isSafeInteger(number) ? number : undefined;
}
function getQueryParam(search, name) {
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return params.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 page = query('[data-growth-page], [data-growth-edit-page]');
var source = search === undefined && root.location ? root.location.search : search;
var profileGenealogyId;
var fromProfile;
if (search === undefined && root.ProfileUI && root.ProfileUI.getGenealogyId) {
profileGenealogyId = root.ProfileUI.getGenealogyId();
if (profileGenealogyId) return profileGenealogyId;
fromProfile = root.ProfileUI.getGenealogyId();
if (fromProfile) return normalizeId(fromProfile);
}
return getQueryParam(source, 'genealogyId') || (page && page.getAttribute('data-genealogy-id')) || '';
return normalizeId(getQueryParam(source, 'genealogyId'));
}
function getCurrentRecordId(search) {
var source = search === undefined && root.location ? root.location.search : search;
return normalizeId(getQueryParam(source, 'recordId'));
}
function trimOrUndefined(value) {
var text = String(value === undefined || value === null ? '' : value).trim();
return text || undefined;
}
function toSafeInteger(value) {
var text = trimOrUndefined(value);
var number;
if (text === undefined) return undefined;
number = Number(text);
return Number.isSafeInteger(number) ? number : undefined;
}
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 toDateInputValue(value) {
var text = String(value || '');
return /^\d{4}-\d{2}-\d{2}/.test(text) ? text.slice(0, 10) : '';
}
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 buildGrowthRecordBody(values) {
@@ -71,10 +104,10 @@
var recordType = trimOrUndefined(source.recordType);
var recordContent = trimOrUndefined(source.recordContent);
var recordDate = trimOrUndefined(source.recordDate);
var remindTime = trimOrUndefined(source.remindTime);
var remindTime = toBackendDateTime(source.remindTime);
var mediaOssIds = trimOrUndefined(source.mediaOssIds);
var status = trimOrUndefined(source.status);
var sortOrderText = trimOrUndefined(source.sortOrder);
var status = trimOrUndefined(source.status);
if (lineagePersonId !== undefined) body.lineagePersonId = lineagePersonId;
if (recordType !== undefined) body.recordType = recordType;
@@ -82,27 +115,105 @@
if (recordDate !== undefined) body.recordDate = recordDate;
if (remindTime !== undefined) body.remindTime = remindTime;
if (mediaOssIds !== undefined) body.mediaOssIds = mediaOssIds;
if (sortOrderText !== undefined) body.sortOrder = toSafeIntegerOrUndefined(sortOrderText);
if (sortOrderText !== undefined) {
body.sortOrder = toSafeInteger(sortOrderText);
if (body.sortOrder === undefined) body.invalidSortOrder = true;
}
if (status !== undefined) body.status = status;
return body;
}
function validateGrowthRecordBody(body) {
if (!body || !body.recordTitle) return '请填写记录标题';
if (body.lineagePersonId !== undefined && !/^\d+$/.test(body.lineagePersonId)) return '世系人物 ID 必须是整数';
if (body.lineagePersonId !== undefined && !normalizeId(body.lineagePersonId)) return '请选择有效的世系人物';
if (body.mediaOssIds !== undefined && !/^[1-9][0-9]*(,[1-9][0-9]*)*$/.test(body.mediaOssIds)) {
return '附件 OSS ID 请使用英文逗号分隔的正整数';
return '附件上传结果无效';
}
if (Object.prototype.hasOwnProperty.call(body, 'sortOrder') && body.sortOrder === undefined) return '排序值必须是安全整数';
if (body.invalidSortOrder || (Object.prototype.hasOwnProperty.call(body, 'sortOrder') && !Number.isSafeInteger(body.sortOrder))) {
return '排序值必须是安全整数';
}
if (body.recordDate !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(body.recordDate)) return '记录日期格式无效';
if (body.remindTime !== undefined && !/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(body.remindTime)) {
return '提醒时间格式无效';
}
if (body.status === '1') return '当前 PC 无法重新读取停用记录,暂不开放停用';
if (body.status !== undefined && body.status !== '0') return '记录状态只能是 0';
return '';
}
function normalizeGrowthList(data) {
return Array.isArray(data) ? data : [];
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 normalizeGrowthRecord(item) {
var recordId = normalizeId(item && item.recordId);
var genealogyId = normalizeId(item && item.genealogyId);
var appUserId = normalizeId(item && item.appUserId);
var lineagePersonId = normalizeId(item && item.lineagePersonId);
var title = String(item && item.recordTitle || '').trim();
var status = stringValue(item && item.status);
var mediaOssIds = stringValue(item && item.mediaOssIds);
var sortOrder = optionalSafeInteger(item && item.sortOrder);
var record;
if (!recordId || !genealogyId || !title || (status !== '0' && status !== '1')) return null;
if (item.appUserId !== undefined && item.appUserId !== null && item.appUserId !== '' && !appUserId) return null;
if (item.lineagePersonId !== undefined && item.lineagePersonId !== null && item.lineagePersonId !== '' && !lineagePersonId) 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;
record = {
recordId: recordId,
genealogyId: genealogyId,
genealogyNo: stringValue(item.genealogyNo),
genealogyName: stringValue(item.genealogyName),
surname: stringValue(item.surname),
appUserNickName: stringValue(item.appUserNickName),
appUserPhone: stringValue(item.appUserPhone),
lineagePersonNo: stringValue(item.lineagePersonNo),
lineagePersonName: stringValue(item.lineagePersonName),
recordType: stringValue(item.recordType),
recordTitle: title,
recordContent: stringValue(item.recordContent),
recordDate: stringValue(item.recordDate),
remindTime: stringValue(item.remindTime),
mediaOssIds: mediaOssIds,
status: status,
remark: stringValue(item.remark)
};
if (appUserId) record.appUserId = appUserId;
if (lineagePersonId) record.lineagePersonId = lineagePersonId;
if (sortOrder !== undefined) record.sortOrder = sortOrder;
return record;
}
function matchesSavedRecord(item, expectedRecordId) {
var record = normalizeGrowthRecord(item);
return Boolean(record && record.recordId === normalizeId(expectedRecordId));
}
function normalizeLineageOption(item) {
var personId = normalizeId(item && (item.personId || item.lineagePersonId));
var name = String(item && (item.name || item.lineagePersonName) || '').trim();
if (!personId || !name) return null;
return {
personId: personId,
name: name,
generationName: stringValue(item.generationName)
};
}
function escapeHtml(value) {
return String(value === undefined || value === null ? '' : value)
return stringValue(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
@@ -110,32 +221,76 @@
.replace(/'/g, '&#39;');
}
function stringifyRecord(record) {
try {
return typeof record === 'string' ? record : JSON.stringify(record);
} catch (error) {
return String(record);
function renderLineageOptions(data, selectedId, fallbackName) {
var selected = normalizeId(selectedId);
var options = Array.isArray(data) ? data.map(normalizeLineageOption).filter(Boolean) : [];
var selectedFound = options.some(function (option) { return option.personId === selected; });
if (selected && !selectedFound && fallbackName) {
options.push({ personId: selected, name: String(fallbackName), generationName: '' });
}
return '<option value="">不关联世系人物</option>' + options.map(function (option) {
return '<option value="' + escapeHtml(option.personId) + '"' +
(option.personId === selected ? ' selected' : '') + '>' +
escapeHtml(option.name + (option.generationName ? ' · ' + option.generationName : '')) +
'</option>';
}).join('');
}
function renderGrowthRecords(data) {
var container = query('[data-growth-list]');
var records = normalizeGrowthList(data);
function attachmentCount(mediaOssIds) {
return mediaOssIds ? mediaOssIds.split(',').filter(Boolean).length : 0;
}
if (!container) return;
if (!Array.isArray(data)) {
container.innerHTML = '<div class="api-empty">成长记录响应未按 Apifox ListResult 返回数组,无法安全展示或操作记录。</div>';
return;
}
if (!records.length) {
container.innerHTML = '<div class="api-empty">暂无成长记录</div>';
return;
}
// ListResult 的元素 DTO 尚未展开,不能假设 recordId、标题或权限字段。
container.innerHTML = records.map(function (record, index) {
return '<article class="module-row"><div><h3>成长记录 ' + (index + 1) + '</h3><p>' +
escapeHtml(stringifyRecord(record)) + '</p></div></article>';
}).join('');
function buildListUrl(genealogyId, recordId) {
var url = 'profile-growth.html?genealogyId=' + encodeURIComponent(genealogyId);
return recordId ? url + '&recordId=' + encodeURIComponent(recordId) : url;
}
function buildEditUrl(genealogyId, recordId) {
var url = 'profile-growth-edit.html?genealogyId=' + encodeURIComponent(genealogyId);
return recordId ? url + '&recordId=' + encodeURIComponent(recordId) : url;
}
function renderGrowthRow(record) {
var summary = [];
if (record.lineagePersonName) summary.push(record.lineagePersonName);
if (record.recordType) summary.push(record.recordType);
if (record.recordDate) summary.push(record.recordDate);
if (!summary.length) summary.push('未填写日期和关联人物');
return '<article class="module-row growth-row"><div><h3>' + escapeHtml(record.recordTitle) + '</h3><p>' +
escapeHtml(summary.join(' · ')) + '</p></div><div class="row-actions">' +
'<button class="pill" type="button" data-growth-detail-id="' + escapeHtml(record.recordId) + '">查看详情</button>' +
'<a class="pill" href="' + escapeHtml(buildEditUrl(record.genealogyId, record.recordId)) + '">编辑</a>' +
'<button class="pill is-danger" type="button" data-growth-delete-id="' + escapeHtml(record.recordId) + '">删除</button>' +
'</div></article>';
}
function detailValue(label, value) {
return '<p><span>' + escapeHtml(label) + '</span><b>' + escapeHtml(value || '未提供') + '</b></p>';
}
function renderGrowthDetail(record) {
var count;
if (!record) return '<div class="api-empty">请选择一条成长记录查看详情</div>';
count = attachmentCount(record.mediaOssIds);
return '<div class="form-like growth-detail">' +
detailValue('标题', record.recordTitle) +
detailValue('关联人物', record.lineagePersonName || record.lineagePersonNo) +
detailValue('记录类型', record.recordType) +
detailValue('记录日期', record.recordDate) +
detailValue('提醒时间', record.remindTime) +
detailValue('记录内容', record.recordContent) +
detailValue('附件', count ? count + ' 个附件' : '未上传') +
detailValue('记录人', record.appUserNickName) +
detailValue('状态', record.status === '0' ? '正常' : '停用') +
detailValue('备注', record.remark) +
'</div><div class="bottom-actions"><a class="btn ghost" href="' +
escapeHtml(buildEditUrl(record.genealogyId, record.recordId)) + '">编辑记录</a>' +
'<button class="btn ghost" type="button" data-growth-delete-id="' + escapeHtml(record.recordId) + '">删除记录</button></div>';
}
function showMessage(message) {
@@ -152,6 +307,10 @@
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();
@@ -164,6 +323,133 @@
return true;
}
function syncGenealogyLinks() {
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) root.ProfileUI.syncGenealogyContextLinks();
}
function setFormStatus(message) {
var status = query('[data-growth-form-status]');
if (status) status.textContent = message || '';
}
function setEditorEnabled(enabled, message) {
var placeholder = query('[data-growth-editor-placeholder]');
queryAll('[data-growth-editor], [data-growth-editor-action]').forEach(function (element) {
element.hidden = !enabled;
});
if (placeholder) {
placeholder.hidden = Boolean(enabled);
if (message) placeholder.textContent = message;
}
}
function renderNoContext() {
var list = query('[data-growth-list]');
var detail = query('[data-growth-detail]');
var message = '请先选择家谱,再查看或维护成长记录。';
if (list) list.innerHTML = '<div class="api-empty">' + message + '</div>';
if (detail) detail.innerHTML = '<div class="api-empty">当前没有家谱上下文。</div>';
setEditorEnabled(false, message);
setFormStatus(message);
}
function requireGenealogyId() {
var genealogyId = getCurrentGenealogyId();
if (!genealogyId) renderNoContext();
return genealogyId;
}
function setWritePending(pending) {
writePending = Boolean(pending);
queryAll('[data-growth-form] input, [data-growth-form] textarea, [data-growth-form] select, [data-growth-form] button, [data-growth-delete-id]').forEach(function (control) {
control.disabled = writePending;
});
}
function renderGrowthRecords(data) {
var container = query('[data-growth-list]');
var normalized = Array.isArray(data) ? data.map(normalizeGrowthRecord) : [];
var invalidCount = normalized.filter(function (record) { return !record; }).length;
var records = normalized.filter(Boolean);
recordsById = Object.create(null);
records.forEach(function (record) {
recordsById[record.recordId] = record;
});
if (!container) return records;
if (!Array.isArray(data) || invalidCount) {
container.innerHTML = '<div class="api-empty">成长记录响应缺少稳定 GrowthRecordVo 字段,请联系后端核对。</div>';
return [];
}
if (!records.length) {
container.innerHTML = '<div class="api-empty">暂无成长记录</div>';
return [];
}
container.innerHTML = records.map(renderGrowthRow).join('');
return records;
}
function renderDetail(record) {
var container = query('[data-growth-detail]');
if (container) container.innerHTML = renderGrowthDetail(record);
}
async function loadGrowthDetail(recordId) {
var api = getApi();
var genealogyId = requireGenealogyId();
var record;
if (!genealogyId || !normalizeId(recordId) || redirectUnauthorized(api)) return null;
try {
record = normalizeGrowthRecord(await api.growthRecordDetail(genealogyId, recordId));
if (!record) throw new Error('成长记录详情响应缺少稳定 GrowthRecordVo 字段');
recordsById[record.recordId] = record;
renderDetail(record);
return record;
} catch (error) {
if (redirectUnauthorized(api, error)) return null;
renderDetail(null);
showMessage(isForbidden(error) ? '当前账号无权查看该成长记录。' : (error.message || '成长记录详情加载失败'));
return null;
}
}
async function loadGrowthRecords() {
var api = getApi();
var genealogyId;
var records;
var requestedRecordId;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncGenealogyLinks();
try {
records = renderGrowthRecords(await api.growthRecords(genealogyId));
requestedRecordId = getCurrentRecordId();
if (requestedRecordId && recordsById[requestedRecordId]) {
await loadGrowthDetail(requestedRecordId);
} else if (records.length) {
await loadGrowthDetail(records[0].recordId);
} else {
renderDetail(null);
}
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (query('[data-growth-list]')) {
query('[data-growth-list]').innerHTML = '<div class="api-empty">' +
(isForbidden(error) ? '当前账号无权查看该家谱的成长记录。' : '成长记录加载失败,请稍后重试。') +
'</div>';
}
showMessage(isForbidden(error) ? '当前账号无权查看该家谱的成长记录。' : (error.message || '成长记录加载失败'));
}
}
function getFormValues(form) {
var values = {};
@@ -173,102 +459,171 @@
return values;
}
function setFormStatus(message) {
var status = query('[data-growth-form-status]');
function setFieldValue(name, value) {
var field = query('[data-growth-form] [name="' + name + '"]');
if (status) status.textContent = message || '';
if (field) field.value = value === undefined || value === null ? '' : String(value);
}
function setWritePending(pending) {
writePending = Boolean(pending);
queryAll('[data-growth-form] button, [data-growth-form] input, [data-growth-form] textarea').forEach(function (control) {
control.disabled = writePending;
});
}
function fillGrowthForm(record) {
var contentField;
var editorInstance;
function syncGenealogyLinks(genealogyId) {
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) {
root.ProfileUI.syncGenealogyContextLinks();
return;
setFieldValue('lineagePersonId', record.lineagePersonId);
setFieldValue('recordType', record.recordType);
setFieldValue('recordTitle', record.recordTitle);
setFieldValue('recordContent', record.recordContent);
setFieldValue('recordDate', toDateInputValue(record.recordDate));
setFieldValue('remindTime', toDateTimeInputValue(record.remindTime));
setFieldValue('mediaOssIds', record.mediaOssIds);
setFieldValue('sortOrder', record.sortOrder);
setFieldValue('status', '0');
contentField = query('[data-growth-form] [name="recordContent"]');
if (contentField && root.AppRichEditor && root.AppRichEditor.init) {
editorInstance = root.AppRichEditor.init(contentField);
if (editorInstance && editorInstance.editor && editorInstance.editor.setHtml) {
editorInstance.editor.setHtml(record.recordContent || '');
}
}
queryAll('[data-genealogy-context-link]').forEach(function (link) {
var href = link.getAttribute('href');
var base;
if (!href || href === '#') return;
base = href.split('?')[0];
link.href = base + '?genealogyId=' + encodeURIComponent(genealogyId);
});
}
function renderNoContext() {
var list = query('[data-growth-list]');
if (list) list.innerHTML = '<div class="api-empty">等待家谱入口接口;请从具体家谱进入成长记录。</div>';
setFormStatus('等待家谱入口接口;请从具体家谱进入成长记录。');
}
function requireGenealogyId() {
var genealogyId = getCurrentGenealogyId();
if (!genealogyId) {
renderNoContext();
showMessage('等待家谱入口接口;请从具体家谱进入成长记录。');
if (query('[data-growth-media-status]')) {
query('[data-growth-media-status]').textContent = record.mediaOssIds
? '已保留 ' + attachmentCount(record.mediaOssIds) + ' 个附件,可继续选择追加'
: '未选择文件';
}
return genealogyId;
}
function buildListUrl(genealogyId) {
return 'profile-growth.html?genealogyId=' + encodeURIComponent(genealogyId);
function applyLineageOptions(data, selectedId, fallbackName) {
var select = query('[data-growth-lineage-person]');
if (select) select.innerHTML = renderLineageOptions(data, selectedId, fallbackName);
}
async function loadGrowthRecords() {
async function loadGrowthEditor() {
var api = getApi();
var genealogyId;
var recordId;
var result;
var record;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncGenealogyLinks(genealogyId);
recordId = getCurrentRecordId();
syncGenealogyLinks();
setEditorEnabled(false, '正在加载成长记录编辑信息…');
try {
renderGrowthRecords(await api.growthRecords(genealogyId));
result = await Promise.all([
api.lineagePersonOptions(genealogyId),
recordId ? api.growthRecordDetail(genealogyId, recordId) : Promise.resolve(null)
]);
record = recordId ? normalizeGrowthRecord(result[1]) : null;
if (recordId && !record) throw new Error('成长记录详情响应缺少稳定 GrowthRecordVo 字段');
applyLineageOptions(result[0], record && record.lineagePersonId, record && record.lineagePersonName);
if (record) {
fillGrowthForm(record);
if (query('[data-growth-editor-title]')) query('[data-growth-editor-title]').textContent = '编辑成长记录';
}
setEditorEnabled(true);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
renderGrowthRecords(null);
showMessage(error.message || '成长记录加载失败');
setEditorEnabled(false, isForbidden(error)
? '当前账号无权维护该家谱的成长记录。'
: (error.message || '成长记录编辑信息加载失败'));
setFormStatus(error.message || '成长记录编辑信息加载失败');
}
}
async function submitGrowthRecord(form) {
var api = getApi();
var genealogyId;
var genealogyId = requireGenealogyId();
var recordId = getCurrentRecordId();
var body;
var validation;
var result;
var saved;
var savedId;
var verifiedDetail;
if (writePending || redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
if (writePending || !genealogyId || redirectUnauthorized(api)) return;
body = buildGrowthRecordBody(getFormValues(form));
validation = validateGrowthRecordBody(body);
if (validation) {
setFormStatus(validation);
return;
}
delete body.invalidSortOrder;
setWritePending(true);
setFormStatus('正在保存成长记录...');
setFormStatus('正在保存成长记录');
try {
await api.createGrowthRecord(genealogyId, body);
if (root.location) root.location.href = buildListUrl(genealogyId);
result = recordId
? await api.updateGrowthRecord(genealogyId, recordId, body)
: await api.createGrowthRecord(genealogyId, body);
saved = normalizeGrowthRecord(result);
savedId = recordId || (saved && saved.recordId);
if (!savedId) throw new Error('保存响应缺少 recordId');
verifiedDetail = await api.growthRecordDetail(genealogyId, savedId);
if (!matchesSavedRecord(verifiedDetail, savedId)) throw new Error('保存后重读未返回同一条成长记录');
if (root.location) root.location.href = buildListUrl(genealogyId, savedId);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setFormStatus(error.message || '成长记录保存失败');
setFormStatus(isForbidden(error) ? '当前账号无权保存该成长记录。' : (error.message || '成长记录保存失败'));
} finally {
setWritePending(false);
}
}
async function deleteGrowthRecord(recordId) {
var api = getApi();
var genealogyId = requireGenealogyId();
var record = recordsById[recordId];
if (writePending || !genealogyId || !normalizeId(recordId) || redirectUnauthorized(api)) return;
if (root.confirm && !root.confirm('确认删除“' + (record ? record.recordTitle : '该成长记录') + '”吗?删除后不可恢复,并会释放附件引用。')) return;
setWritePending(true);
try {
await api.deleteGrowthRecord(genealogyId, recordId);
await loadGrowthRecords();
showMessage('成长记录已删除');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(isForbidden(error) ? '当前账号无权删除该成长记录。' : (error.message || '成长记录删除失败'));
} finally {
setWritePending(false);
}
}
function clearMediaSelection() {
var target = query('[data-growth-form] [name="mediaOssIds"]');
var status = query('[data-growth-media-status]');
var fileInput = query('[data-growth-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-growth-detail-id]');
var deleteButton = event.target.closest('[data-growth-delete-id]');
var clearMediaButton = event.target.closest('[data-growth-clear-media]');
if (detailButton) {
event.preventDefault();
loadGrowthDetail(detailButton.getAttribute('data-growth-detail-id'));
return;
}
if (deleteButton) {
event.preventDefault();
deleteGrowthRecord(deleteButton.getAttribute('data-growth-delete-id'));
return;
}
if (clearMediaButton) {
event.preventDefault();
clearMediaSelection();
}
});
documentRef.addEventListener('submit', function (event) {
var form = event.target.closest('[data-growth-form]');
@@ -279,23 +634,23 @@
}
function init() {
var genealogyId;
if (!documentRef) return;
bindActions();
if (query('[data-growth-page]')) loadGrowthRecords();
if (query('[data-growth-edit-page]')) {
genealogyId = requireGenealogyId();
if (genealogyId) syncGenealogyLinks(genealogyId);
}
if (query('[data-growth-edit-page]')) loadGrowthEditor();
}
return {
getCurrentGenealogyId: getCurrentGenealogyId,
getCurrentRecordId: getCurrentRecordId,
buildGrowthRecordBody: buildGrowthRecordBody,
validateGrowthRecordBody: validateGrowthRecordBody,
normalizeGrowthList: normalizeGrowthList,
normalizeGrowthRecord: normalizeGrowthRecord,
matchesSavedRecord: matchesSavedRecord,
renderLineageOptions: renderLineageOptions,
renderGrowthDetail: renderGrowthDetail,
shouldRedirectToLogin: shouldRedirectToLogin,
isForbidden: isForbidden,
init: init
};
});
+308 -35
View File
@@ -22,8 +22,9 @@
var currentPageNum = 1;
var currentPageSize = 20;
var currentPageTotal = null;
var managementAccess = true;
var managementAccess = false;
var writePending = false;
var currentUserId = '';
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
@@ -101,8 +102,7 @@
return Number.isInteger(number) && Number.isSafeInteger(number) ? number : undefined;
}
function getPersonId(item) {
var value = item && item.personId;
function normalizeId(value) {
var text;
if (value === undefined || value === null || value === '') return '';
@@ -111,22 +111,52 @@
return /^\d+$/.test(text) ? text : '';
}
function getPersonId(item) {
return normalizeId(item && item.personId);
}
function formatLineageDate(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 buildLineagePersonBody(values) {
var source = values || {};
var body = { name: trimOrUndefined(source.name) };
var bindingMode = trimOrUndefined(source.bindingMode);
var body = {
bindingMode: bindingMode,
name: trimOrUndefined(source.name)
};
var optionalFields = [
'personNo', 'aliasName', 'sex', 'generationName', 'birthDate', 'birthLunar', 'birthPlace',
'deathDate', 'deathLunar', 'deathPlace', 'burialPlace', 'personStatus', 'biography',
'personNo', 'aliasName', 'sex', 'generationName', 'birthLunar', 'birthPlace',
'deathLunar', 'deathPlace', 'burialPlace', 'personStatus', 'biography',
'remark', 'relationName'
];
var idFields = ['avatarOssId', 'fatherId', 'motherId'];
var generation = trimOrUndefined(source.generation);
var sortOrder = trimOrUndefined(source.sortOrder);
var birthDate = formatLineageDate(source.birthDate);
var deathDate = formatLineageDate(source.deathDate);
optionalFields.forEach(function (field) {
var value = trimOrUndefined(source[field]);
if (value !== undefined) body[field] = value;
});
idFields.forEach(function (field) {
var value = normalizeId(source[field]);
if (value) body[field] = value;
});
if (bindingMode === 'SPECIFIED') {
body.appUserId = normalizeId(source.appUserId) || trimOrUndefined(source.appUserId);
}
if (birthDate !== undefined) body.birthDate = birthDate;
if (deathDate !== undefined) body.deathDate = deathDate;
if (generation !== undefined) body.generation = Number(generation);
if (sortOrder !== undefined) body.sortOrder = Number(sortOrder);
return body;
@@ -134,6 +164,17 @@
function validateLineagePersonBody(body) {
if (!body || !body.name) return '请填写成员姓名';
if (!body.bindingMode) return '请选择账号绑定方式';
if (['NONE', 'SELF', 'SPECIFIED'].indexOf(body.bindingMode) === -1) return '账号绑定方式无效';
if (body.bindingMode === 'SPECIFIED' && !normalizeId(body.appUserId)) return '指定账号绑定缺少用户选项';
if (body.bindingMode !== 'SPECIFIED' && body.appUserId !== undefined) return '当前绑定方式不能提交指定用户';
if (body.sex !== undefined && ['0', '1', '2'].indexOf(body.sex) === -1) return '性别选项无效';
if (body.personStatus !== undefined && ['0', '1', '2'].indexOf(body.personStatus) === -1) return '人物状态选项无效';
if (body.birthLunar !== undefined && ['0', '1'].indexOf(body.birthLunar) === -1) return '出生历法选项无效';
if (body.deathLunar !== undefined && ['0', '1'].indexOf(body.deathLunar) === -1) return '逝世历法选项无效';
if (['avatarOssId', 'fatherId', 'motherId'].some(function (field) {
return body[field] !== undefined && !normalizeId(body[field]);
})) return '人物关联字段无效';
if (body.generation !== undefined && (!Number.isInteger(body.generation) || !Number.isSafeInteger(body.generation))) {
return '世代序号必须是整数';
}
@@ -147,6 +188,7 @@
var personId = getPersonId(item);
var name = trimOrUndefined(item && item.name);
var person;
var invalidId = false;
if (!personId || !name) return null;
person = { personId: personId, name: name };
@@ -160,15 +202,70 @@
});
['appUserId', 'genealogyId', 'fatherId', 'motherId', 'avatarOssId'].forEach(function (field) {
if (item && item[field] !== undefined && item[field] !== null) {
if (typeof item[field] === 'number' && !Number.isSafeInteger(item[field])) return;
person[field] = String(item[field]);
var value = normalizeId(item[field]);
if (!value) {
invalidId = true;
return;
}
person[field] = value;
}
});
if (invalidId) return null;
if (toIntegerOrUndefined(item && item.generation) !== undefined) person.generation = toIntegerOrUndefined(item.generation);
if (toIntegerOrUndefined(item && item.sortOrder) !== undefined) person.sortOrder = toIntegerOrUndefined(item.sortOrder);
return person;
}
function normalizeLineageAccess(genealogy) {
return {
canEditContent: Boolean(genealogy && genealogy.canEditContent === true)
};
}
function normalizeGenerationOption(item) {
var generation = toIntegerOrUndefined(item && item.generationNo);
var generationName = trimOrUndefined(item && item.generationText);
if (!generation || !generationName) return null;
return {
generation: generation,
generationName: generationName
};
}
function normalizeGenealogyMemberOption(item) {
var memberId = normalizeId(item && item.memberId);
var genealogyId = normalizeId(item && item.genealogyId);
var appUserId = normalizeId(item && item.appUserId);
var status = trimOrUndefined(item && item.status);
if (!memberId || !genealogyId || !appUserId || status !== '0') return null;
return {
memberId: memberId,
genealogyId: genealogyId,
appUserId: appUserId,
appUserNickName: String(item.appUserNickName === undefined || item.appUserNickName === null ? '' : item.appUserNickName),
memberName: String(item.memberName === undefined || item.memberName === null ? '' : item.memberName),
roleType: String(item.roleType === undefined || item.roleType === null ? '' : item.roleType),
status: status
};
}
function normalizeGenealogyMemberOptions(data) {
if (!Array.isArray(data)) return [];
return data.map(normalizeGenealogyMemberOption).filter(Boolean);
}
function normalizeLineageBinding(person, userId) {
var appUserId = normalizeId(person && person.appUserId);
var normalizedUserId = normalizeId(userId);
if (!appUserId) return { bindingMode: 'NONE', appUserId: '' };
if (normalizedUserId && appUserId === normalizedUserId) return { bindingMode: 'SELF', appUserId: '' };
return { bindingMode: 'SPECIFIED', appUserId: appUserId };
}
function normalizeList(data) {
if (Array.isArray(data)) return data;
if (data && Array.isArray(data.rows)) return data.rows;
@@ -259,12 +356,68 @@
function renderOptions(data) {
var select = query('[data-lineage-person-options]');
var people = normalizeList(data).map(normalizeLineagePerson).filter(Boolean);
if (!select) return;
select.innerHTML = '<option value="">请选择当前成员</option>' + people.map(function (person) {
var options = people.map(function (person) {
return '<option value="' + escapeHtml(person.personId) + '">' + escapeHtml(person.name + (person.generationName ? ' · ' + person.generationName : '')) + '</option>';
}).join('');
if (!select) return;
select.innerHTML = '<option value="">请选择当前成员</option>' + options;
select.value = selectedPersonId;
queryAll('[data-lineage-parent-option]').forEach(function (parentSelect) {
var emptyLabel = parentSelect.getAttribute('data-empty-label') || '请选择人物';
parentSelect.innerHTML = '<option value="">' + escapeHtml(emptyLabel) + '</option>' + options;
});
}
function renderGenerationOptions(data) {
var select = query('[data-lineage-generation]');
var filter = query('[data-lineage-generation-filter]');
var selectedGeneration = select ? select.value : '';
var selectedFilter = filter ? filter.value : '';
var options = normalizeList(data).map(normalizeGenerationOption).filter(Boolean);
var optionHtml = options.map(function (option) {
return '<option value="' + escapeHtml(option.generation) + '" data-generation-name="' + escapeHtml(option.generationName) + '">' +
escapeHtml('第 ' + option.generation + ' 世 · ' + option.generationName) + '</option>';
}).join('');
if (select) {
select.innerHTML = '<option value="">请选择世代</option>' + optionHtml;
select.value = selectedGeneration;
}
if (filter) {
filter.innerHTML = '<option value="">全部世代</option>' + optionHtml;
filter.value = selectedFilter;
}
}
function updateBindingFields() {
var bindingMode = query('[name="bindingMode"]');
var memberSelect = query('[data-lineage-member-options]');
var specified = Boolean(bindingMode && bindingMode.value === 'SPECIFIED');
if (!memberSelect) return;
memberSelect.hidden = !specified;
memberSelect.required = specified;
if (!specified) memberSelect.value = '';
}
function renderGenealogyMemberOptions(data) {
var select = query('[data-lineage-member-options]');
var selectedValue = select ? select.value : '';
var options = normalizeGenealogyMemberOptions(data).filter(function (option) {
return option.appUserId !== currentUserId;
});
if (!select) return options;
select.innerHTML = '<option value="">请选择已绑定账号的家谱成员</option>' + options.map(function (option) {
var label = option.memberName || option.appUserNickName || '家谱成员';
return '<option value="' + escapeHtml(option.appUserId) + '">' + escapeHtml(label) + '</option>';
}).join('');
select.value = selectedValue;
updateBindingFields();
return options;
}
function renderListSummary(data) {
@@ -281,7 +434,7 @@
var previous = query('[data-lineage-page-action="previous"]');
var next = query('[data-lineage-page-action="next"]');
var status = query('[data-lineage-page-status]');
var disabled = !managementAccess || writePending;
var disabled = writePending;
if (!container) return;
if (currentPageTotal === null) {
@@ -294,6 +447,10 @@
if (status) status.textContent = '第 ' + currentPageNum + ' 页,共 ' + currentPageTotal + ' 人';
}
function canUseLineagePagination(pending) {
return !Boolean(pending);
}
function renderPagination(data) {
var total = data && Number(data.total);
@@ -305,6 +462,7 @@
var container = query('[data-lineage-detail]');
var person = normalizeLineagePerson(data);
var values;
var actions = '';
if (!container) return;
if (!person) {
@@ -314,17 +472,30 @@
selectedPersonId = person.personId;
peopleById[person.personId] = person;
values = [
person.sex ? '性别:' + person.sex : '',
person.sex !== undefined ? '性别:' + ({ '0': '男', '1': '女', '2': '未知' }[person.sex] || person.sex) : '',
person.generation !== undefined ? '世代:' + person.generation : '',
person.generationName ? '字辈:' + person.generationName : '',
person.personNo ? '人物编号:' + person.personNo : '',
person.aliasName ? '别名:' + person.aliasName : '',
person.fatherName ? '父亲:' + person.fatherName : '',
person.motherName ? '母亲:' + person.motherName : '',
person.spouseNames ? '配偶:' + person.spouseNames : '',
person.appUserNickName ? '绑定账号:' + person.appUserNickName : '',
person.birthDate ? '出生:' + person.birthDate : '',
person.birthPlace ? '出生地:' + person.birthPlace : '',
person.deathDate ? '逝世:' + person.deathDate : '',
person.biography ? '简介' + person.biography : ''
person.deathPlace ? '逝世地' + person.deathPlace : '',
person.burialPlace ? '安葬地:' + person.burialPlace : '',
person.personStatus !== undefined ? '人物状态:' + ({ '0': '健在', '1': '已故', '2': '未知' }[person.personStatus] || person.personStatus) : '',
person.biography ? '简介:' + person.biography : '',
person.remark ? '备注:' + person.remark : ''
].filter(Boolean);
if (managementAccess) {
actions = '<div class="row-actions"><button class="pill" type="button" data-lineage-edit="' + escapeHtml(person.personId) + '">编辑</button>' +
'<button class="pill is-danger" type="button" data-lineage-disable="' + escapeHtml(person.personId) + '">停用</button></div>';
}
container.innerHTML = '<div class="module-row"><div><h3>' + escapeHtml(person.name) + '</h3><p>' + escapeHtml(values.join(' · ') || '暂无更多资料') + '</p></div>' +
'<div class="row-actions"><button class="pill" type="button" data-lineage-edit="' + escapeHtml(person.personId) + '">编辑</button>' +
'<button class="pill is-danger" type="button" data-lineage-disable="' + escapeHtml(person.personId) + '">停用</button></div></div>';
actions + '</div>';
renderLineageList(Object.keys(peopleById).map(function (id) { return peopleById[id]; }));
if (query('[data-lineage-person-options]')) query('[data-lineage-person-options]').value = person.personId;
refreshControls();
@@ -377,6 +548,7 @@
});
if (query('[data-lineage-list-summary]')) query('[data-lineage-list-summary]').textContent = '';
currentPageTotal = null;
setLineageEnabled(false);
updatePaginationControls();
}
@@ -391,10 +563,16 @@
}
function refreshControls() {
var disabled = !managementAccess || writePending;
var writeDisabled = !managementAccess || writePending;
queryAll('[data-lineage-relation], [data-lineage-search], [data-lineage-form] button, [data-lineage-person-options], [data-lineage-edit], [data-lineage-disable]').forEach(function (control) {
control.disabled = disabled;
queryAll('[data-lineage-management]').forEach(function (element) {
element.hidden = !managementAccess;
});
queryAll('[data-lineage-relation], [data-lineage-form] button, [data-lineage-form] input, [data-lineage-form] select, [data-lineage-form] textarea, [data-lineage-edit], [data-lineage-disable]').forEach(function (control) {
control.disabled = writeDisabled;
});
queryAll('[data-lineage-search], [data-lineage-person-options]').forEach(function (control) {
control.disabled = writePending;
});
updatePaginationControls();
}
@@ -427,10 +605,38 @@
return input ? input.value : '';
}
function buildLineagePageQuery(values, pageNum, pageSize) {
var source = values || {};
var queryValues = {
pageNum: Number(pageNum),
pageSize: Number(pageSize)
};
var keyword = trimOrUndefined(source.keyword);
var generation = toIntegerOrUndefined(source.generation);
var personStatus = trimOrUndefined(source.personStatus);
if (keyword !== undefined) queryValues.keyword = keyword;
if (generation !== undefined && generation > 0) queryValues.generation = generation;
if (['0', '1', '2'].indexOf(personStatus) !== -1) queryValues.personStatus = personStatus;
return queryValues;
}
function getCurrentFilters(keyword) {
var generation = query('[data-lineage-generation-filter]');
var status = query('[data-lineage-status-filter]');
return {
keyword: keyword,
generation: generation ? generation.value : '',
personStatus: status ? status.value : ''
};
}
async function loadLineage(keyword, pageNum) {
var api = getApi();
var genealogyId;
var results;
var access;
var searchKeyword = trimOrUndefined(keyword);
var requestedPage = Number(pageNum);
@@ -441,27 +647,41 @@
syncGenealogyLinks(genealogyId);
try {
results = await Promise.all([
api.genealogyDetail(genealogyId),
api.currentProfile(),
api.lineageTree(genealogyId),
api.lineagePersons(genealogyId),
api.lineagePersonsPage(genealogyId, {
pageNum: currentPageNum,
pageSize: currentPageSize,
keyword: searchKeyword
}),
api.lineagePersonOptions(genealogyId)
api.lineagePersonsPage(genealogyId, buildLineagePageQuery(
getCurrentFilters(searchKeyword),
currentPageNum,
currentPageSize
)),
api.lineagePersonOptions(genealogyId),
api.generationPoems(genealogyId)
]);
renderTree(results[0]);
renderListSummary(results[1]);
renderLineageList(results[2]);
renderPagination(results[2]);
renderOptions(results[3]);
setLineageEnabled(true);
access = normalizeLineageAccess(results[0]);
currentUserId = normalizeId(results[1] && results[1].userId);
setLineageEnabled(access.canEditContent);
renderTree(results[2]);
renderListSummary(results[3]);
renderLineageList(results[4]);
renderPagination(results[4]);
renderOptions(results[5]);
renderGenerationOptions(results[6]);
if (access.canEditContent) {
renderGenealogyMemberOptions(await api.genealogyMemberOptions(genealogyId));
}
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (isForbidden(error)) {
renderForbiddenState();
return;
}
['[data-lineage-tree]', '[data-lineage-list]'].forEach(function (selector) {
var container = query(selector);
if (container) container.innerHTML = '<div class="api-empty">世系人物加载失败,请稍后重试</div>';
});
showMessage(error.message || '世系人物加载失败');
}
}
@@ -496,34 +716,63 @@
}
relationMode = mode;
editingPersonId = '';
setRelationFieldVisibility(mode === 'spouses');
if (submitButton) submitButton.textContent = '新增' + relationLabel(mode);
setFormStatus('当前将为“' + person.name + '”新增' + relationLabel(mode));
}
function toLocalDateTime(value) {
var text = trimOrUndefined(value);
return text ? text.slice(0, 16).replace(' ', 'T') : '';
}
function setRelationFieldVisibility(visible) {
var field = query('[data-lineage-spouse-field]');
if (field) field.hidden = !visible;
}
function fillForm(person) {
var form = query('[data-lineage-form]');
var values;
var submitButton;
var binding;
if (!form || !person) return;
binding = normalizeLineageBinding(person, currentUserId);
values = {
bindingMode: binding.bindingMode,
appUserId: binding.appUserId,
avatarOssId: person.avatarOssId,
name: person.name,
sex: person.sex,
generation: person.generation,
generationName: person.generationName,
fatherId: person.fatherId,
motherId: person.motherId,
personNo: person.personNo,
aliasName: person.aliasName,
birthDate: person.birthDate ? person.birthDate.slice(0, 10) : '',
deathDate: person.deathDate ? person.deathDate.slice(0, 10) : '',
birthDate: toLocalDateTime(person.birthDate),
birthLunar: person.birthLunar,
birthPlace: person.birthPlace,
deathDate: toLocalDateTime(person.deathDate),
deathLunar: person.deathLunar,
deathPlace: person.deathPlace,
burialPlace: person.burialPlace,
personStatus: person.personStatus,
sortOrder: person.sortOrder,
relationName: person.relationName,
biography: person.biography
biography: person.biography,
remark: person.remark
};
queryAll('[name]', form).forEach(function (field) {
field.value = values[field.name] === undefined || values[field.name] === null ? '' : values[field.name];
});
updateBindingFields();
relationMode = '';
editingPersonId = person.personId;
setRelationFieldVisibility(false);
submitButton = query('[data-lineage-form] button[type="submit"]');
if (submitButton) submitButton.textContent = '保存修改';
setFormStatus('正在编辑“' + person.name + '”');
@@ -534,8 +783,10 @@
relationMode = '';
editingPersonId = '';
setRelationFieldVisibility(false);
if (submitButton) submitButton.textContent = '保存成员';
setFormStatus('');
if (root.setTimeout) root.setTimeout(updateBindingFields, 0);
}
async function submitPerson(form) {
@@ -627,8 +878,23 @@
});
documentRef.addEventListener('change', function (event) {
var select = event.target.closest('[data-lineage-person-options]');
var generationSelect = event.target.closest('[data-lineage-generation]');
var bindingSelect = event.target.closest('[name="bindingMode"]');
var selectedOption;
var generationName;
var appUserId;
if (select && select.value) loadPersonDetail(select.value);
if (generationSelect) {
selectedOption = generationSelect.options[generationSelect.selectedIndex];
generationName = query('[name="generationName"]', generationSelect.form);
if (generationName) generationName.value = selectedOption ? selectedOption.getAttribute('data-generation-name') || '' : '';
}
if (bindingSelect) {
appUserId = query('[name="appUserId"]', bindingSelect.form);
if (bindingSelect.value !== 'SPECIFIED' && appUserId) appUserId.value = '';
updateBindingFields();
}
});
documentRef.addEventListener('click', function (event) {
var target = event.target;
@@ -645,7 +911,7 @@
}
if (pageButton) {
event.preventDefault();
if (!managementAccess || writePending) return;
if (!canUseLineagePagination(writePending)) return;
if (pageButton.getAttribute('data-lineage-page-action') === 'previous' && currentPageNum > 1) {
loadLineage(getCurrentKeyword(), currentPageNum - 1);
}
@@ -688,6 +954,13 @@
buildLineagePersonBody: buildLineagePersonBody,
validateLineagePersonBody: validateLineagePersonBody,
normalizeLineagePerson: normalizeLineagePerson,
normalizeLineageAccess: normalizeLineageAccess,
canUseLineagePagination: canUseLineagePagination,
normalizeGenerationOption: normalizeGenerationOption,
normalizeGenealogyMemberOption: normalizeGenealogyMemberOption,
normalizeGenealogyMemberOptions: normalizeGenealogyMemberOptions,
normalizeLineageBinding: normalizeLineageBinding,
buildLineagePageQuery: buildLineagePageQuery,
normalizeList: normalizeList,
relationLabel: relationLabel,
shouldRedirectToLogin: shouldRedirectToLogin,
+581
View File
@@ -0,0 +1,581 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.MemberAdminPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.MemberAdminPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
var membersById = Object.create(null);
var selectedMemberId = '';
var currentMemberId = '';
var actorRole = '';
var managementAccess = false;
var writePending = false;
var responseRoles = ['owner', 'admin', 'editor', 'member', 'visitor'];
var assignableRoles = ['admin', 'editor', 'member'];
var roleLabels = {
owner: '谱主',
admin: '管理员',
editor: '编辑',
member: '成员',
visitor: '访客'
};
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 stringValue(value) {
return value === undefined || value === null ? '' : String(value);
}
function trimOrUndefined(value) {
var text = stringValue(value).trim();
return text || undefined;
}
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 contextId;
var params;
if (search === undefined && root.ProfileUI && root.ProfileUI.getGenealogyId) {
contextId = root.ProfileUI.getGenealogyId();
if (contextId) return normalizeId(contextId);
}
params = new URLSearchParams(stringValue(source).replace(/^\?/, ''));
return normalizeId(params.get('genealogyId'));
}
function normalizeMember(item) {
var memberId = normalizeId(item && item.memberId);
var genealogyId = normalizeId(item && item.genealogyId);
var appUserId = normalizeId(item && item.appUserId);
var lineagePersonId = normalizeId(item && item.lineagePersonId);
var inviterUserId = normalizeId(item && item.inviterUserId);
var roleType = stringValue(item && item.roleType);
var status = stringValue(item && item.status);
var member;
if (!memberId || !genealogyId || !appUserId || responseRoles.indexOf(roleType) < 0 || status !== '0') return null;
if (item.lineagePersonId !== undefined && item.lineagePersonId !== null && item.lineagePersonId !== '' && !lineagePersonId) return null;
if (item.inviterUserId !== undefined && item.inviterUserId !== null && item.inviterUserId !== '' && !inviterUserId) return null;
member = {
memberId: memberId,
genealogyId: genealogyId,
genealogyNo: stringValue(item.genealogyNo),
genealogyName: stringValue(item.genealogyName),
surname: stringValue(item.surname),
appUserId: appUserId,
appUserNickName: stringValue(item.appUserNickName),
appUserPhone: stringValue(item.appUserPhone),
lineagePersonNo: stringValue(item.lineagePersonNo),
lineagePersonName: stringValue(item.lineagePersonName),
memberName: stringValue(item.memberName),
roleType: roleType,
relationName: stringValue(item.relationName),
joinSource: stringValue(item.joinSource),
inviterNickName: stringValue(item.inviterNickName),
inviterPhone: stringValue(item.inviterPhone),
joinTime: stringValue(item.joinTime),
status: status
};
if (lineagePersonId) member.lineagePersonId = lineagePersonId;
if (inviterUserId) member.inviterUserId = inviterUserId;
return member;
}
function normalizeMembers(data) {
var normalized;
if (!Array.isArray(data)) return [];
normalized = data.map(normalizeMember);
return normalized.some(function (member) { return !member; }) ? [] : normalized;
}
function normalizeLineageOption(item) {
var personId = normalizeId(item && item.personId);
var name = trimOrUndefined(item && item.name);
if (!personId || !name) return null;
return {
personId: personId,
name: name,
generationName: stringValue(item.generationName)
};
}
function buildMemberUpdateBody(values) {
var source = values || {};
var body = {};
var memberName = trimOrUndefined(source.memberName);
var relationName = source.relationName === undefined || source.relationName === null
? undefined
: stringValue(source.relationName).trim();
var roleType = trimOrUndefined(source.roleType);
var lineagePersonId = normalizeId(source.lineagePersonId);
if (roleType !== undefined && assignableRoles.indexOf(roleType) < 0) {
throw new Error('成员角色只能是管理员、编辑或成员');
}
if (memberName !== undefined) body.memberName = memberName;
if (relationName !== undefined) body.relationName = relationName;
if (roleType !== undefined) body.roleType = roleType;
if (source.lineagePersonId !== undefined && source.lineagePersonId !== null && source.lineagePersonId !== '') {
if (lineagePersonId) body.lineagePersonId = lineagePersonId;
else body.invalidLineagePersonId = true;
}
return body;
}
function validateMemberUpdateBody(body) {
if (!body) return '成员修改内容无效';
if (body.memberName !== undefined && body.memberName.length > 50) return '成员名称不能超过 50 个字符';
if (body.relationName !== undefined && body.relationName.length > 100) return '关系名称不能超过 100 个字符';
if (body.roleType !== undefined && assignableRoles.indexOf(body.roleType) < 0) return '成员角色只能是管理员、编辑或成员';
if (body.invalidLineagePersonId) return '世系人物选项无效';
return '';
}
function canEditMember(role, targetRole) {
if (targetRole === 'owner') return false;
if (targetRole === 'admin') return role === 'owner';
return role === 'owner' || role === 'admin';
}
function canRemoveMember(role, targetRole) {
return canEditMember(role, targetRole);
}
function canLeaveGenealogy(role) {
return Boolean(role && role !== 'owner');
}
function canTransferOwner(role) {
return role === 'owner';
}
function buildOwnerTransferBody(targetMemberId) {
return { targetMemberId: normalizeId(targetMemberId) };
}
function validateOwnerTransferBody(body, ownerMemberId) {
if (!body || !body.targetMemberId) return '请选择新谱主';
if (normalizeId(ownerMemberId) && body.targetMemberId === normalizeId(ownerMemberId)) return '不能转让给自己';
return '';
}
function escapeHtml(value) {
return stringValue(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function memberDisplayName(member) {
return member.memberName || member.lineagePersonName || member.appUserNickName || '未命名成员';
}
function renderMemberRow(member, context) {
var settings = context || {};
var meta = [
roleLabels[member.roleType] || member.roleType,
member.relationName,
member.lineagePersonName ? '世系人物:' + member.lineagePersonName : '',
member.joinTime ? '加入:' + member.joinTime : ''
].filter(Boolean).join(' · ');
var actions = '';
if (settings.canManage && canEditMember(settings.actorRole, member.roleType)) {
actions = '<button class="pill" type="button" data-member-edit-id="' + escapeHtml(member.memberId) +
'">编辑</button><button class="pill is-danger" type="button" data-member-remove-id="' +
escapeHtml(member.memberId) + '">移出家谱</button>';
}
return '<article class="module-row member-admin-row"><div><h3>' + escapeHtml(memberDisplayName(member)) +
(member.memberId === settings.currentMemberId ? ' <span class="pill">当前账号</span>' : '') +
'</h3><p>' + escapeHtml(meta) + '</p></div><div class="row-actions">' + actions + '</div></article>';
}
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 showMessage(message) {
if (root.layui && root.layui.layer) root.layui.layer.msg(message);
else if (root.alert) root.alert(message);
}
function setStatus(selector, message) {
var target = query(selector);
if (target) target.textContent = message || '';
}
function syncLinks() {
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) root.ProfileUI.syncGenealogyContextLinks();
}
function setWritePending(value) {
writePending = Boolean(value);
queryAll('[data-member-form] input, [data-member-form] select, [data-member-form] button, [data-owner-transfer-form] select, [data-owner-transfer-form] button, [data-member-edit-id], [data-member-remove-id], [data-member-leave]').forEach(function (control) {
control.disabled = writePending;
});
}
function renderLineageOptions(data) {
var select = query('[data-member-lineage-options]');
var options = Array.isArray(data) ? data.map(normalizeLineageOption).filter(Boolean) : [];
if (!select) return options;
select.innerHTML = '<option value="">不修改世系人物绑定</option>' + options.map(function (option) {
var label = option.name + (option.generationName ? ' · ' + option.generationName : '');
return '<option value="' + escapeHtml(option.personId) + '">' + escapeHtml(label) + '</option>';
}).join('');
return options;
}
function renderTransferOptions(members) {
var select = query('[data-owner-transfer-target]');
var targets = members.filter(function (member) {
return member.memberId !== currentMemberId && member.roleType !== 'owner';
});
if (!select) return targets;
select.innerHTML = '<option value="">请选择新谱主</option>' + targets.map(function (member) {
return '<option value="' + escapeHtml(member.memberId) + '">' +
escapeHtml(memberDisplayName(member) + ' · ' + (roleLabels[member.roleType] || member.roleType)) +
'</option>';
}).join('');
return targets;
}
function renderMembers(data) {
var container = query('[data-member-list]');
var members = normalizeMembers(data);
membersById = Object.create(null);
members.forEach(function (member) { membersById[member.memberId] = member; });
if (!container) return members;
if (!Array.isArray(data) || (data.length && !members.length)) {
container.innerHTML = '<div class="api-empty">成员响应缺少稳定 GenealogyMemberVo 字段,请联系后端核对。</div>';
return [];
}
container.innerHTML = members.length ? members.map(function (member) {
return renderMemberRow(member, {
actorRole: actorRole,
currentMemberId: currentMemberId,
canManage: managementAccess
});
}).join('') : '<div class="api-empty">暂无正常成员</div>';
renderTransferOptions(members);
return members;
}
function renderRoleOptions(targetRole) {
var select = query('[data-member-role-options]');
var roles = actorRole === 'owner' ? assignableRoles : ['editor', 'member'];
if (!select) return;
select.innerHTML = roles.map(function (role) {
return '<option value="' + role + '">' + roleLabels[role] + '</option>';
}).join('');
select.value = roles.indexOf(targetRole) >= 0 ? targetRole : roles[roles.length - 1];
}
function fillMemberForm(member) {
var form = query('[data-member-form]');
if (!form || !member || !managementAccess || !canEditMember(actorRole, member.roleType)) return;
selectedMemberId = member.memberId;
form.hidden = false;
query('[name="memberName"]', form).value = member.memberName;
query('[name="relationName"]', form).value = member.relationName;
renderRoleOptions(member.roleType);
query('[name="lineagePersonId"]', form).value = member.lineagePersonId || '';
if (query('[data-member-form-title]')) query('[data-member-form-title]').textContent = '编辑“' + memberDisplayName(member) + '”';
setStatus('[data-member-form-status]', '');
}
function resetMemberForm() {
var form = query('[data-member-form]');
selectedMemberId = '';
if (form) {
form.reset();
form.hidden = true;
}
setStatus('[data-member-form-status]', '');
}
async function fetchAndRenderMembers(api, genealogyId) {
var data = await api.genealogyMembers(genealogyId);
var members = renderMembers(data);
if (Array.isArray(data) && data.length && !members.length) throw new Error('成员响应字段不完整');
return members;
}
function renderNoContext() {
if (query('[data-member-list]')) query('[data-member-list]').innerHTML = '<div class="api-empty">请先选择家谱,再管理成员。</div>';
setStatus('[data-member-overview]', '当前没有家谱上下文。');
queryAll('[data-member-management], [data-member-leave-section], [data-owner-transfer-section]').forEach(function (element) {
element.hidden = true;
});
}
async function loadPage() {
var api = getApi();
var genealogyId;
var results;
var detail;
var profileUserId;
var members;
var currentMember;
if (redirectUnauthorized(api)) return;
genealogyId = getCurrentGenealogyId();
if (!genealogyId) {
renderNoContext();
return;
}
syncLinks();
try {
results = await Promise.all([
api.genealogyDetail(genealogyId),
api.currentProfile(),
api.genealogyMembers(genealogyId),
api.lineagePersonOptions(genealogyId)
]);
detail = results[0] || {};
profileUserId = normalizeId(results[1] && results[1].userId);
actorRole = responseRoles.indexOf(stringValue(detail.roleType)) >= 0 ? stringValue(detail.roleType) : '';
managementAccess = detail.canManage === true;
members = normalizeMembers(results[2]);
currentMember = members.find(function (member) { return member.appUserId === profileUserId; }) || null;
currentMemberId = currentMember ? currentMember.memberId : '';
if (!actorRole && currentMember) actorRole = currentMember.roleType;
setStatus('[data-member-overview]', (detail.genealogyName || '当前家谱') + ' · ' + members.length + ' 位正常成员 · 当前角色:' + (roleLabels[actorRole] || '成员'));
queryAll('[data-member-management]').forEach(function (element) { element.hidden = !managementAccess; });
queryAll('[data-member-leave-section]').forEach(function (element) { element.hidden = !currentMember || !canLeaveGenealogy(actorRole); });
queryAll('[data-owner-transfer-section]').forEach(function (element) { element.hidden = !currentMember || !canTransferOwner(actorRole); });
renderLineageOptions(results[3]);
renderMembers(results[2]);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (query('[data-member-list]')) query('[data-member-list]').innerHTML = '<div class="api-empty">' +
(isForbidden(error) ? '当前账号无权查看该家谱成员。' : '成员加载失败,请稍后重试。') + '</div>';
showMessage(isForbidden(error) ? '当前账号无权查看该家谱成员。' : (error.message || '成员加载失败'));
}
}
function getFormValues(form) {
var values = {};
queryAll('[name]', form).forEach(function (field) { values[field.name] = field.value; });
return values;
}
async function submitMember(form) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var target = membersById[selectedMemberId];
var body;
var validation;
var saved;
if (writePending || !genealogyId || !target || !managementAccess || !canEditMember(actorRole, target.roleType) || redirectUnauthorized(api)) return;
body = buildMemberUpdateBody(getFormValues(form));
validation = validateMemberUpdateBody(body);
if (validation) {
setStatus('[data-member-form-status]', validation);
return;
}
delete body.invalidLineagePersonId;
setWritePending(true);
setStatus('[data-member-form-status]', '正在保存成员…');
try {
saved = normalizeMember(await api.updateGenealogyMember(genealogyId, selectedMemberId, body));
if (!saved || saved.memberId !== selectedMemberId) throw new Error('成员修改响应缺少稳定 memberId');
if (!(await fetchAndRenderMembers(api, genealogyId)).some(function (member) { return member.memberId === saved.memberId; })) {
throw new Error('修改后成员列表未返回同一成员');
}
resetMemberForm();
showMessage('成员信息已更新');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setStatus('[data-member-form-status]', isForbidden(error) ? '当前账号无权修改该成员。' : (error.message || '成员修改失败'));
} finally {
setWritePending(false);
}
}
async function removeMember(memberId) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var target = membersById[memberId];
if (writePending || !genealogyId || !target || !managementAccess || !canRemoveMember(actorRole, target.roleType) || redirectUnauthorized(api)) return;
if (root.confirm && !root.confirm('确认将“' + memberDisplayName(target) + '”移出当前家谱吗?该操作不会删除其世系人物资料。')) return;
setWritePending(true);
try {
await api.removeGenealogyMember(genealogyId, memberId);
if ((await fetchAndRenderMembers(api, genealogyId)).some(function (member) { return member.memberId === memberId; })) {
throw new Error('移出后成员仍存在于正常成员列表');
}
if (selectedMemberId === memberId) resetMemberForm();
showMessage('成员已移出家谱');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(isForbidden(error) ? '当前账号无权移出该成员。' : (error.message || '移出成员失败'));
} finally {
setWritePending(false);
}
}
async function leaveGenealogy() {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
if (writePending || !genealogyId || !currentMemberId || !canLeaveGenealogy(actorRole) || redirectUnauthorized(api)) return;
if (root.confirm && !root.confirm('确认退出当前家谱吗?退出后需重新受邀才能进入。世系人物资料不会被删除。')) return;
setWritePending(true);
try {
await api.leaveGenealogy(genealogyId);
if (root.location) root.location.href = 'profile-families.html';
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(isForbidden(error) ? '当前账号无权退出该家谱。' : (error.message || '退出家谱失败'));
} finally {
setWritePending(false);
}
}
async function transferOwner(form) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var body = buildOwnerTransferBody(query('[name="targetMemberId"]', form).value);
var validation = validateOwnerTransferBody(body, currentMemberId);
var target = membersById[body.targetMemberId];
if (writePending || !genealogyId || !canTransferOwner(actorRole) || redirectUnauthorized(api)) return;
if (validation || !target) {
setStatus('[data-owner-transfer-status]', validation || '请选择成员列表中的新谱主');
return;
}
if (root.confirm && !root.confirm('确认将谱主转让给“' + memberDisplayName(target) + '”吗?转让后当前账号将变为管理员。')) return;
setWritePending(true);
setStatus('[data-owner-transfer-status]', '正在转让谱主…');
try {
await api.transferGenealogyOwner(genealogyId, body);
await loadPage();
setStatus('[data-owner-transfer-status]', '谱主已转让');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setStatus('[data-owner-transfer-status]', isForbidden(error) ? '只有当前谱主可以执行转让。' : (error.message || '谱主转让失败'));
} finally {
setWritePending(false);
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('click', function (event) {
var editButton = event.target.closest('[data-member-edit-id]');
var removeButton = event.target.closest('[data-member-remove-id]');
var leaveButton = event.target.closest('[data-member-leave]');
if (editButton) {
event.preventDefault();
fillMemberForm(membersById[editButton.getAttribute('data-member-edit-id')]);
} else if (removeButton) {
event.preventDefault();
removeMember(removeButton.getAttribute('data-member-remove-id'));
} else if (leaveButton) {
event.preventDefault();
leaveGenealogy();
}
});
documentRef.addEventListener('submit', function (event) {
var memberForm = event.target.closest('[data-member-form]');
var transferForm = event.target.closest('[data-owner-transfer-form]');
if (memberForm) {
event.preventDefault();
submitMember(memberForm);
} else if (transferForm) {
event.preventDefault();
transferOwner(transferForm);
}
});
documentRef.addEventListener('reset', function (event) {
if (event.target.closest('[data-member-form]')) resetMemberForm();
});
}
function init() {
if (!documentRef || !query('[data-member-admin-page]')) return;
bindActions();
loadPage();
}
return {
getCurrentGenealogyId: getCurrentGenealogyId,
normalizeMember: normalizeMember,
normalizeMembers: normalizeMembers,
normalizeLineageOption: normalizeLineageOption,
buildMemberUpdateBody: buildMemberUpdateBody,
validateMemberUpdateBody: validateMemberUpdateBody,
canEditMember: canEditMember,
canRemoveMember: canRemoveMember,
canLeaveGenealogy: canLeaveGenealogy,
canTransferOwner: canTransferOwner,
buildOwnerTransferBody: buildOwnerTransferBody,
validateOwnerTransferBody: validateOwnerTransferBody,
renderMemberRow: renderMemberRow,
shouldRedirectToLogin: shouldRedirectToLogin,
isForbidden: isForbidden,
init: init
};
});
+437 -128
View File
@@ -14,18 +14,19 @@
'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, rootNode) {
return documentRef ? (rootNode || documentRef).querySelector(selector) : null;
function query(selector, node) {
return documentRef ? (node || documentRef).querySelector(selector) : null;
}
function queryAll(selector, rootNode) {
return documentRef ? Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector)) : [];
function queryAll(selector, node) {
return documentRef ? Array.prototype.slice.call((node || documentRef).querySelectorAll(selector)) : [];
}
function trimOrUndefined(value) {
@@ -34,63 +35,176 @@
return text || undefined;
}
function toSafeIntegerOrUndefined(value) {
var text = trimOrUndefined(value);
var number;
if (!text) return undefined;
number = Number(text);
return Number.isInteger(number) && Number.isSafeInteger(number) ? number : undefined;
function queryParam(search, name) {
return new URLSearchParams(String(search || '').replace(/^\?/, '')).get(name) || '';
}
function getQueryParam(search, name) {
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return params.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 page = query('[data-memo-page], [data-memo-edit-page]');
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 profileGenealogyId;
if (profileGenealogyId) return normalizeId(profileGenealogyId);
}
return getQueryParam(source, 'genealogyId') || (page && page.getAttribute('data-genealogy-id')) || '';
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 fields = ['memoContent', 'remindTime', 'completed', 'mediaOssIds', 'status'];
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);
fields.forEach(function (field) {
var value = trimOrUndefined(source[field]);
if (value !== undefined) body[field] = value;
});
if (sortOrderText !== undefined) body.sortOrder = toSafeIntegerOrUndefined(sortOrderText);
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.mediaOssIds !== undefined && !/^[1-9][0-9]*(,[1-9][0-9]*)*$/.test(body.mediaOssIds)) {
return '附件 OSS ID 请使用英文逗号分隔的正整数';
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 (Object.prototype.hasOwnProperty.call(body, 'sortOrder') && body.sortOrder === undefined) 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 normalizeMemoList(data) {
return Array.isArray(data) ? data : [];
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 String(value === undefined || value === null ? '' : value)
return stringValue(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
@@ -98,40 +212,61 @@
.replace(/'/g, '&#39;');
}
function stringifyMemo(memo) {
try {
return typeof memo === 'string' ? memo : JSON.stringify(memo);
} catch (error) {
return String(memo);
}
function attachmentCount(mediaOssIds) {
return mediaOssIds ? mediaOssIds.split(',').filter(Boolean).length : 0;
}
function renderMemos(data) {
var container = query('[data-memo-list]');
var memos = normalizeMemoList(data);
function buildListUrl(genealogyId, memoId) {
var url = 'profile-memo.html?genealogyId=' + encodeURIComponent(genealogyId);
if (!container) return;
if (!Array.isArray(data)) {
container.innerHTML = '<div class="api-empty">备忘录响应未按 Apifox ListResult 返回数组,无法安全展示或操作记录。</div>';
return;
}
if (!memos.length) {
container.innerHTML = '<div class="api-empty">暂无备忘录</div>';
return;
}
// ListResult 的元素 DTO 尚未展开,不能假设 memoId、标题或权限字段。
container.innerHTML = memos.map(function (memo, index) {
return '<article class="module-row"><div><h3>备忘录 ' + (index + 1) + '</h3><p>' +
escapeHtml(stringifyMemo(memo)) + '</p></div></article>';
}).join('');
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 '<article class="module-row memo-row"><div><h3>' + escapeHtml(memo.memoTitle) + '</h3><p>' +
escapeHtml(summary.join(' · ')) + '</p></div><div class="row-actions">' +
'<button class="pill" type="button" data-memo-detail-id="' + escapeHtml(memo.memoId) + '">查看详情</button>' +
'<a class="pill" href="' + escapeHtml(buildEditUrl(memo.genealogyId, memo.memoId)) + '">编辑</a>' +
'<button class="pill is-danger" type="button" data-memo-delete-id="' + escapeHtml(memo.memoId) + '">删除</button>' +
'</div></article>';
}
function detailValue(label, value) {
return '<p><span>' + escapeHtml(label) + '</span><b>' + escapeHtml(value || '未提供') + '</b></p>';
}
function renderMemoDetail(memo) {
var count;
if (!memo) return '<div class="api-empty">请选择一条备忘录查看详情</div>';
count = attachmentCount(memo.mediaOssIds);
return '<div class="form-like memo-detail">' +
detailValue('备忘标题', memo.memoTitle) +
detailValue('备忘内容', memo.memoContent) +
detailValue('提醒时间', memo.remindTime) +
detailValue('完成状态', memo.completed === '1' ? '已完成' : '未完成') +
detailValue('附件', count ? count + ' 个附件' : '未上传') +
detailValue('记录人', memo.appUserNickName) +
detailValue('备注', memo.remark) +
'</div><div class="bottom-actions"><a class="btn ghost" href="' +
escapeHtml(buildEditUrl(memo.genealogyId, memo.memoId)) + '">编辑备忘录</a>' +
'<button class="btn ghost" type="button" data-memo-delete-id="' + escapeHtml(memo.memoId) + '">删除备忘录</button></div>';
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
if (root.alert) root.alert(message);
if (root.layui && root.layui.layer) root.layui.layer.msg(message);
else if (root.alert) root.alert(message);
}
function shouldRedirectToLogin(api, error) {
@@ -140,18 +275,146 @@
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');
return true;
}
root.location.href = 'login.html';
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 = '<div class="api-empty">' + message + '</div>';
if (detail) detail.innerHTML = '<div class="api-empty">当前没有家谱上下文。</div>';
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 = '<div class="api-empty">备忘录响应缺少稳定 MemoVo 字段,请联系后端核对。</div>';
return [];
}
if (!memos.length) {
container.innerHTML = '<div class="api-empty">暂无备忘录</div>';
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 = '<div class="api-empty">' +
(isForbidden(error) ? '当前账号无权查看该家谱的备忘录。' : '备忘录加载失败,请稍后重试。') +
'</div>';
}
showMessage(isForbidden(error) ? '当前账号无权查看该家谱的备忘录。' : (error.message || '备忘录加载失败'));
}
}
function getFormValues(form) {
var values = {};
@@ -161,102 +424,148 @@
return values;
}
function setFormStatus(message) {
var status = query('[data-memo-form-status]');
function setFieldValue(name, value) {
var field = query('[data-memo-form] [name="' + name + '"]');
if (status) status.textContent = message || '';
if (field) field.value = value === undefined || value === null ? '' : String(value);
}
function setWritePending(pending) {
writePending = Boolean(pending);
queryAll('[data-memo-form] button, [data-memo-form] input, [data-memo-form] textarea, [data-memo-form] select').forEach(function (control) {
control.disabled = writePending;
});
}
function syncGenealogyLinks(genealogyId) {
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) {
root.ProfileUI.syncGenealogyContextLinks();
return;
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) + ' 个附件,可继续选择追加'
: '未选择文件';
}
queryAll('[data-genealogy-context-link]').forEach(function (link) {
var href = link.getAttribute('href');
var base;
if (!href || href === '#') return;
base = href.split('?')[0];
link.href = base + '?genealogyId=' + encodeURIComponent(genealogyId);
});
}
function renderNoContext() {
var list = query('[data-memo-list]');
if (list) list.innerHTML = '<div class="api-empty">等待家谱入口接口;请从具体家谱进入备忘录。</div>';
setFormStatus('等待家谱入口接口;请从具体家谱进入备忘录。');
}
function requireGenealogyId() {
var genealogyId = getCurrentGenealogyId();
if (!genealogyId) {
renderNoContext();
showMessage('等待家谱入口接口;请从具体家谱进入备忘录。');
}
return genealogyId;
}
function buildListUrl(genealogyId) {
return 'profile-memo.html?genealogyId=' + encodeURIComponent(genealogyId);
}
async function loadMemos() {
async function loadMemoEditor() {
var api = getApi();
var genealogyId;
var memoId;
var memo;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncGenealogyLinks(genealogyId);
memoId = getCurrentMemoId();
syncLinks();
setEditorEnabled(false, '正在加载备忘录编辑信息…');
try {
renderMemos(await api.memos(genealogyId));
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;
renderMemos(null);
showMessage(error.message || '备忘录加载失败');
setEditorEnabled(false, isForbidden(error)
? '当前账号无权维护该家谱的备忘录。'
: (error.message || '备忘录编辑信息加载失败'));
setFormStatus(error.message || '备忘录编辑信息加载失败');
}
}
async function submitMemo(form) {
var api = getApi();
var genealogyId;
var genealogyId = requireGenealogyId();
var memoId = getCurrentMemoId();
var body;
var validation;
var result;
var saved;
var savedId;
var verifiedDetail;
if (writePending || redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
if (writePending || !genealogyId || redirectUnauthorized(api)) return;
body = buildMemoBody(getFormValues(form));
validation = validateMemoBody(body);
if (validation) {
setFormStatus(validation);
return;
}
delete body.invalidSortOrder;
setWritePending(true);
setFormStatus('正在保存备忘录...');
setFormStatus('正在保存备忘录');
try {
await api.createMemo(genealogyId, body);
if (root.location) root.location.href = buildListUrl(genealogyId);
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(error.message || '备忘录保存失败');
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]');
@@ -267,23 +576,23 @@
}
function init() {
var genealogyId;
if (!documentRef) return;
bindActions();
if (query('[data-memo-page]')) loadMemos();
if (query('[data-memo-edit-page]')) {
genealogyId = requireGenealogyId();
if (genealogyId) syncGenealogyLinks(genealogyId);
}
if (query('[data-memo-edit-page]')) loadMemoEditor();
}
return {
getCurrentGenealogyId: getCurrentGenealogyId,
getCurrentMemoId: getCurrentMemoId,
buildMemoBody: buildMemoBody,
validateMemoBody: validateMemoBody,
normalizeMemoList: normalizeMemoList,
normalizeMemo: normalizeMemo,
matchesSavedMemo: matchesSavedMemo,
isEditableMemo: isEditableMemo,
renderMemoDetail: renderMemoDetail,
shouldRedirectToLogin: shouldRedirectToLogin,
isForbidden: isForbidden,
init: init
};
});
+644
View File
@@ -0,0 +1,644 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.MeritPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.MeritPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
var recordsById = Object.create(null);
var writePending = false;
var meritTypes = {
donation: '捐赠',
repair: '修缮',
public: '公益',
other: '其他'
};
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 getCurrentMeritId(search) {
var source = search === undefined && root.location ? root.location.search : search;
return normalizeId(queryParam(source, 'meritId'));
}
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 toFiniteNumber(value) {
var text = trimOrUndefined(value);
var number;
if (!text) return undefined;
number = Number(text);
return Number.isFinite(number) ? number : undefined;
}
function toSafeInteger(value) {
var number = toFiniteNumber(value);
return Number.isSafeInteger(number) ? number : undefined;
}
function decimalKey(value) {
var text = String(value).trim().replace(/^([+-]?)\./, '$10.');
var match = text.match(/^([+-]?)(\d+)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/);
var coefficient;
var scale;
if (!match) return '';
coefficient = BigInt((match[2] + (match[3] || '')).replace(/^0+(?=\d)/, ''));
scale = (match[3] || '').length - Number(match[4] || 0);
if (coefficient === 0n) return '0:0';
if (scale < 0) {
coefficient *= 10n ** BigInt(-scale);
scale = 0;
}
while (scale > 0 && coefficient % 10n === 0n) {
coefficient /= 10n;
scale -= 1;
}
if (match[1] === '-') coefficient = -coefficient;
return String(coefficient) + ':' + String(scale);
}
function isExactJsonNumber(sourceText, number) {
return Number.isFinite(number) && decimalKey(sourceText) === decimalKey(JSON.stringify(number));
}
function buildMeritRecordBody(values) {
var source = values || {};
var body = {
donorName: String(source.donorName || '').trim(),
meritTitle: String(source.meritTitle || '').trim()
};
var meritType = trimOrUndefined(source.meritType);
var meritContent = trimOrUndefined(source.meritContent);
var amountText = trimOrUndefined(source.amount);
var meritTime = toBackendDateTime(source.meritTime);
var sortOrderText = trimOrUndefined(source.sortOrder);
var status = trimOrUndefined(source.status);
if (meritType !== undefined) body.meritType = meritType;
if (meritContent !== undefined) body.meritContent = meritContent;
if (amountText !== undefined) {
body.amount = toFiniteNumber(amountText);
if (body.amount === undefined) body.invalidAmount = true;
else if (!isExactJsonNumber(amountText, body.amount)) body.invalidAmountPrecision = true;
}
if (meritTime !== undefined) body.meritTime = meritTime;
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 validateMeritRecordBody(body) {
if (!body || !body.donorName) return '请填写功德人姓名';
if (!body.meritTitle) return '请填写功德标题';
if (body.meritType !== undefined && !Object.prototype.hasOwnProperty.call(meritTypes, body.meritType)) {
return '功德类型无效';
}
if (body.invalidAmount || (Object.prototype.hasOwnProperty.call(body, 'amount') && !Number.isFinite(body.amount))) {
return '功德金额必须是有限数字';
}
if (body.invalidAmountPrecision) return '功德金额超出浏览器可安全提交的精度';
if (body.meritTime !== undefined && !isValidBackendDateTime(body.meritTime)) 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 normalizeAmount(value) {
var text;
if (value === undefined || value === null || value === '') return '';
if (typeof value === 'number') return Number.isFinite(value) ? String(value) : '';
text = String(value).trim();
return /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(text) ? text : '';
}
function optionalSafeInteger(value) {
var number;
if (value === undefined || value === null || value === '') return undefined;
number = Number(value);
return Number.isSafeInteger(number) ? number : undefined;
}
function normalizeMeritRecord(item) {
var meritId = normalizeId(item && item.meritId);
var genealogyId = normalizeId(item && item.genealogyId);
var appUserId = normalizeId(item && item.appUserId);
var donorName = String(item && item.donorName || '').trim();
var meritType = stringValue(item && item.meritType);
var meritTitle = String(item && item.meritTitle || '').trim();
var amount = normalizeAmount(item && item.amount);
var meritTime = stringValue(item && item.meritTime);
var sortOrder = optionalSafeInteger(item && item.sortOrder);
var status = stringValue(item && item.status);
var record;
if (!meritId || !genealogyId || !donorName || !meritTitle || !Object.prototype.hasOwnProperty.call(meritTypes, meritType)) {
return null;
}
if (item.appUserId !== undefined && item.appUserId !== null && item.appUserId !== '' && !appUserId) return null;
if (item.amount !== undefined && item.amount !== null && item.amount !== '' && amount === '') return null;
if (meritTime && !isValidBackendDateTime(meritTime)) return null;
if (item.sortOrder !== undefined && item.sortOrder !== null && item.sortOrder !== '' && sortOrder === undefined) return null;
if (status !== '0' && status !== '1') return null;
record = {
meritId: meritId,
genealogyId: genealogyId,
genealogyNo: stringValue(item.genealogyNo),
genealogyName: stringValue(item.genealogyName),
surname: stringValue(item.surname),
appUserNickName: stringValue(item.appUserNickName),
appUserPhone: stringValue(item.appUserPhone),
donorName: donorName,
meritType: meritType,
meritTitle: meritTitle,
meritContent: stringValue(item.meritContent),
amount: amount,
meritTime: meritTime,
status: status,
remark: stringValue(item.remark)
};
if (appUserId) record.appUserId = appUserId;
if (sortOrder !== undefined) record.sortOrder = sortOrder;
return record;
}
function matchesSavedMerit(item, expectedMeritId) {
var record = normalizeMeritRecord(item);
return Boolean(record && record.meritId === normalizeId(expectedMeritId));
}
function isEditableMerit(record) {
return Boolean(record && record.status === '0');
}
function escapeHtml(value) {
return stringValue(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function buildListUrl(genealogyId, meritId) {
var url = 'profile-merit.html?genealogyId=' + encodeURIComponent(genealogyId);
return meritId ? url + '&meritId=' + encodeURIComponent(meritId) : url;
}
function buildEditUrl(genealogyId, meritId) {
var url = 'profile-merit-edit.html?genealogyId=' + encodeURIComponent(genealogyId);
return meritId ? url + '&meritId=' + encodeURIComponent(meritId) : url;
}
function renderMeritRow(record) {
var summary = [meritTypes[record.meritType]];
if (record.meritTime) summary.push(record.meritTime);
if (record.amount) summary.push('金额:' + record.amount);
return '<article class="module-row merit-row"><div><h3>' + escapeHtml(record.meritTitle) + '</h3><p>' +
escapeHtml(record.donorName + ' · ' + summary.join(' · ')) + '</p></div><div class="row-actions">' +
'<button class="pill" type="button" data-merit-detail-id="' + escapeHtml(record.meritId) + '">查看详情</button>' +
'<a class="pill" href="' + escapeHtml(buildEditUrl(record.genealogyId, record.meritId)) + '">编辑</a>' +
'<button class="pill is-danger" type="button" data-merit-delete-id="' + escapeHtml(record.meritId) + '">删除</button>' +
'</div></article>';
}
function detailValue(label, value) {
return '<p><span>' + escapeHtml(label) + '</span><b>' + escapeHtml(value || '未提供') + '</b></p>';
}
function renderMeritDetail(record) {
if (!record) return '<div class="api-empty">请选择一条功德记录查看详情</div>';
return '<div class="form-like merit-detail">' +
detailValue('功德人', record.donorName) +
detailValue('功德类型', meritTypes[record.meritType]) +
detailValue('功德标题', record.meritTitle) +
detailValue('事迹内容', record.meritContent) +
detailValue('金额', record.amount) +
detailValue('功德时间', record.meritTime) +
detailValue('记录人', record.appUserNickName) +
detailValue('备注', record.remark) +
'</div><div class="bottom-actions"><a class="btn ghost" href="' +
escapeHtml(buildEditUrl(record.genealogyId, record.meritId)) + '">编辑功德记录</a>' +
'<button class="btn ghost" type="button" data-merit-delete-id="' + escapeHtml(record.meritId) + '">删除功德记录</button></div>';
}
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-merit-form-status]');
if (target) target.textContent = message || '';
}
function setEditorEnabled(enabled, message) {
var placeholder = query('[data-merit-editor-placeholder]');
queryAll('[data-merit-editor], [data-merit-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-merit-form] button, [data-merit-form] input, [data-merit-form] textarea, [data-merit-form] select, [data-merit-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-merit-list]');
var detail = query('[data-merit-detail]');
var message = '请先选择家谱,再查看或维护功德记录。';
if (list) list.innerHTML = '<div class="api-empty">' + message + '</div>';
if (detail) detail.innerHTML = '<div class="api-empty">当前没有家谱上下文。</div>';
setEditorEnabled(false, message);
setFormStatus(message);
}
function requireGenealogyId() {
var genealogyId = getCurrentGenealogyId();
if (!genealogyId) renderNoContext();
return genealogyId;
}
function renderMeritRecords(data) {
var container = query('[data-merit-list]');
var normalized = Array.isArray(data) ? data.map(normalizeMeritRecord) : [];
var invalidCount = normalized.filter(function (record) { return !record; }).length;
var records = normalized.filter(Boolean);
recordsById = Object.create(null);
records.forEach(function (record) {
recordsById[record.meritId] = record;
});
if (!container) return records;
if (!Array.isArray(data) || invalidCount) {
container.innerHTML = '<div class="api-empty">功德记录响应缺少稳定 MeritRecordVo 字段,请联系后端核对。</div>';
return [];
}
if (!records.length) {
container.innerHTML = '<div class="api-empty">暂无功德记录</div>';
return [];
}
container.innerHTML = records.map(renderMeritRow).join('');
return records;
}
function renderDetail(record) {
var container = query('[data-merit-detail]');
if (container) container.innerHTML = renderMeritDetail(record);
}
async function loadMeritDetail(meritId) {
var api = getApi();
var genealogyId = requireGenealogyId();
var record;
if (!genealogyId || !normalizeId(meritId) || redirectUnauthorized(api)) return null;
try {
record = normalizeMeritRecord(await api.meritRecordDetail(genealogyId, meritId));
if (!record) throw new Error('功德记录详情响应缺少稳定 MeritRecordVo 字段');
recordsById[record.meritId] = record;
renderDetail(record);
return record;
} catch (error) {
if (redirectUnauthorized(api, error)) return null;
renderDetail(null);
showMessage(isForbidden(error) ? '当前账号无权查看该功德记录。' : (error.message || '功德记录详情加载失败'));
return null;
}
}
async function loadMeritRecords() {
var api = getApi();
var genealogyId;
var records;
var requestedMeritId;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncLinks();
try {
records = renderMeritRecords(await api.meritRecords(genealogyId));
requestedMeritId = getCurrentMeritId();
if (requestedMeritId && recordsById[requestedMeritId]) {
await loadMeritDetail(requestedMeritId);
} else if (records.length) {
await loadMeritDetail(records[0].meritId);
} else {
renderDetail(null);
}
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (query('[data-merit-list]')) {
query('[data-merit-list]').innerHTML = '<div class="api-empty">' +
(isForbidden(error) ? '当前账号无权查看该家谱的功德记录。' : '功德记录加载失败,请稍后重试。') +
'</div>';
}
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-merit-form] [name="' + name + '"]');
if (field) field.value = value === undefined || value === null ? '' : String(value);
}
function fillMeritForm(record) {
var contentField;
var contentEditor;
if (!isEditableMerit(record)) throw new Error('停用功德记录无法通过当前 PC 接口重新读取或编辑');
setFieldValue('donorName', record.donorName);
setFieldValue('meritType', record.meritType);
setFieldValue('meritTitle', record.meritTitle);
setFieldValue('meritContent', record.meritContent);
setFieldValue('amount', record.amount);
setFieldValue('meritTime', toDateTimeInputValue(record.meritTime));
setFieldValue('sortOrder', record.sortOrder);
setFieldValue('status', '0');
contentField = query('[name="meritContent"]');
if (root.AppRichEditor && root.AppRichEditor.init && contentField) {
contentEditor = root.AppRichEditor.init(contentField);
if (contentEditor && contentEditor.editor && contentEditor.editor.setHtml) {
contentEditor.editor.setHtml(record.meritContent);
contentEditor.sync();
}
}
}
async function loadMeritEditor() {
var api = getApi();
var genealogyId;
var meritId;
var record;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
meritId = getCurrentMeritId();
syncLinks();
setEditorEnabled(false, '正在加载功德记录编辑信息…');
try {
if (meritId) {
record = normalizeMeritRecord(await api.meritRecordDetail(genealogyId, meritId));
if (!record) throw new Error('功德记录详情响应缺少稳定 MeritRecordVo 字段');
fillMeritForm(record);
if (query('[data-merit-editor-title]')) query('[data-merit-editor-title]').textContent = '编辑功德记录';
}
setEditorEnabled(true);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setEditorEnabled(false, isForbidden(error)
? '当前账号无权维护该家谱的功德记录。'
: (error.message || '功德记录编辑信息加载失败'));
setFormStatus(error.message || '功德记录编辑信息加载失败');
}
}
async function submitMeritRecord(form) {
var api = getApi();
var genealogyId = requireGenealogyId();
var meritId = getCurrentMeritId();
var body;
var validation;
var result;
var saved;
var savedId;
var verifiedDetail;
if (writePending || !genealogyId || redirectUnauthorized(api)) return;
body = buildMeritRecordBody(getFormValues(form));
validation = validateMeritRecordBody(body);
if (validation) {
setFormStatus(validation);
return;
}
delete body.invalidAmount;
delete body.invalidAmountPrecision;
delete body.invalidSortOrder;
setWritePending(true);
setFormStatus('正在保存功德记录…');
try {
result = meritId
? await api.updateMeritRecord(genealogyId, meritId, body)
: await api.createMeritRecord(genealogyId, body);
saved = normalizeMeritRecord(result);
savedId = meritId || (saved && saved.meritId);
if (!savedId) throw new Error('保存响应缺少 meritId');
verifiedDetail = await api.meritRecordDetail(genealogyId, savedId);
if (!matchesSavedMerit(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 deleteMeritRecord(meritId) {
var api = getApi();
var genealogyId = requireGenealogyId();
var record = recordsById[meritId];
if (writePending || !genealogyId || !normalizeId(meritId) || redirectUnauthorized(api)) return;
if (root.confirm && !root.confirm('确认删除“' + (record ? record.meritTitle : '该功德记录') + '”吗?删除后不可恢复。')) return;
setWritePending(true);
try {
await api.deleteMeritRecord(genealogyId, meritId);
await loadMeritRecords();
showMessage('功德记录已删除');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(isForbidden(error) ? '当前账号无权删除该功德记录。' : (error.message || '功德记录删除失败'));
} finally {
setWritePending(false);
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('click', function (event) {
var detailButton = event.target.closest('[data-merit-detail-id]');
var deleteButton = event.target.closest('[data-merit-delete-id]');
if (detailButton) {
event.preventDefault();
loadMeritDetail(detailButton.getAttribute('data-merit-detail-id'));
return;
}
if (deleteButton) {
event.preventDefault();
deleteMeritRecord(deleteButton.getAttribute('data-merit-delete-id'));
}
});
documentRef.addEventListener('submit', function (event) {
var form = event.target.closest('[data-merit-form]');
if (!form) return;
event.preventDefault();
submitMeritRecord(form);
});
}
function init() {
if (!documentRef) return;
bindActions();
if (query('[data-merit-page]')) loadMeritRecords();
if (query('[data-merit-edit-page]')) loadMeritEditor();
}
return {
getCurrentGenealogyId: getCurrentGenealogyId,
getCurrentMeritId: getCurrentMeritId,
buildMeritRecordBody: buildMeritRecordBody,
validateMeritRecordBody: validateMeritRecordBody,
normalizeMeritRecord: normalizeMeritRecord,
matchesSavedMerit: matchesSavedMerit,
isEditableMerit: isEditableMerit,
renderMeritDetail: renderMeritDetail,
shouldRedirectToLogin: shouldRedirectToLogin,
isForbidden: isForbidden,
init: init
};
});
+287 -45
View File
@@ -14,118 +14,360 @@
'use strict';
var documentRef = root.document;
var notificationsById = Object.create(null);
var selectedNotificationId = '';
var writePending = false;
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function normalizeNotifications(data) {
return Array.isArray(data) ? data : [];
function query(selector, node) {
return documentRef ? (node || documentRef).querySelector(selector) : null;
}
function formatNotification(data) {
return JSON.stringify(data || {}, null, 2);
function queryAll(selector, node) {
return documentRef ? Array.prototype.slice.call((node || documentRef).querySelectorAll(selector)) : [];
}
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 normalizeNullableId(value) {
var id;
if (value === undefined || value === null || value === '') return null;
id = normalizeId(value);
return id || undefined;
}
function stringValue(value) {
return value === undefined || value === null ? '' : String(value);
}
function buildNotificationQuery(readStatus) {
return readStatus === '0' || readStatus === '1' ? { readStatus: readStatus } : {};
}
function normalizeNotification(item) {
var notificationId = normalizeId(item && item.notificationId);
var genealogyId = normalizeNullableId(item && item.genealogyId);
var senderUserId = normalizeNullableId(item && item.senderUserId);
var bizId = normalizeNullableId(item && item.bizId);
var readStatus = stringValue(item && item.readStatus);
var status = stringValue(item && item.status);
if (!notificationId || genealogyId === undefined || senderUserId === undefined || bizId === undefined) return null;
if ((readStatus !== '0' && readStatus !== '1') || (status !== '0' && status !== '1')) return null;
return {
notificationId: notificationId,
genealogyId: genealogyId,
genealogyNo: stringValue(item.genealogyNo),
genealogyName: stringValue(item.genealogyName),
senderUserId: senderUserId,
senderNickName: stringValue(item.senderNickName),
senderPhone: stringValue(item.senderPhone),
noticeType: stringValue(item.noticeType),
noticeTitle: stringValue(item.noticeTitle),
noticeContent: stringValue(item.noticeContent),
bizType: stringValue(item.bizType),
bizId: bizId,
bizSummary: stringValue(item.bizSummary),
publishTime: stringValue(item.publishTime),
readStatus: readStatus,
readTime: item.readTime === undefined || item.readTime === null ? null : String(item.readTime),
status: status,
remark: stringValue(item.remark)
};
}
function normalizeNotifications(data) {
var normalized;
if (!Array.isArray(data)) return [];
normalized = data.map(normalizeNotification);
return normalized.some(function (item) { return !item; }) ? [] : normalized;
}
function getUnreadCount(data) {
var count = Number(data);
return Number.isFinite(count) && count >= 0 ? count : 0;
return Number.isSafeInteger(count) && count >= 0 ? count : 0;
}
function query(selector, rootNode) {
return (rootNode || documentRef).querySelector(selector);
function matchesNotification(item, expectedNotificationId) {
var notification = normalizeNotification(item);
return Boolean(notification && notification.notificationId === normalizeId(expectedNotificationId));
}
function canMarkRead(notification) {
return Boolean(notification && notification.status === '0' && notification.readStatus === '0');
}
function escapeHtml(value) {
return stringValue(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function renderNotificationRow(notification) {
var statusText = notification.readStatus === '0' ? '未读' : '已读';
var action = canMarkRead(notification)
? '<button class="pill" type="button" data-notification-read-id="' + escapeHtml(notification.notificationId) + '">标记已读</button>'
: '';
return '<article class="module-row notification-row' + (notification.readStatus === '0' ? ' is-unread' : '') + '">' +
'<div><h3>' + escapeHtml(notification.noticeTitle || '通知') + '</h3><p>' +
escapeHtml(notification.noticeContent || '暂无通知内容') + '</p><p>' +
escapeHtml([notification.publishTime, statusText].filter(Boolean).join(' · ')) +
'</p></div><div class="row-actions"><button class="pill" type="button" data-notification-detail-id="' +
escapeHtml(notification.notificationId) + '">查看详情</button>' + action + '</div></article>';
}
function detailValue(label, value) {
return '<p><span>' + escapeHtml(label) + '</span><b>' + escapeHtml(value || '未提供') + '</b></p>';
}
function renderNotificationDetail(notification) {
var action;
if (!notification) return '<div class="api-empty">请选择一条通知查看详情</div>';
action = canMarkRead(notification)
? '<div class="bottom-actions"><button class="btn ghost" type="button" data-notification-read-id="' +
escapeHtml(notification.notificationId) + '">标记为已读</button></div>'
: '';
return '<div class="form-like notification-detail">' +
detailValue('通知标题', notification.noticeTitle) +
detailValue('通知内容', notification.noticeContent) +
detailValue('通知类型', notification.noticeType) +
detailValue('家谱', notification.genealogyName) +
detailValue('发送人', notification.senderNickName) +
detailValue('业务类型', notification.bizType) +
detailValue('业务摘要', notification.bizSummary) +
detailValue('发布时间', notification.publishTime) +
detailValue('阅读状态', notification.readStatus === '0' ? '未读' : '已读') +
detailValue('阅读时间', notification.readTime) +
detailValue('备注', notification.remark) +
'</div>' + action;
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
if (root.alert) root.alert(message);
if (root.layui && root.layui.layer) root.layui.layer.msg(message);
else if (root.alert) root.alert(message);
}
function renderNotifications(container, notifications) {
if (!container) return;
container.innerHTML = '';
function shouldRedirectToLogin(api, error) {
var status = error && (error.status || error.code);
if (!notifications.length) {
container.textContent = '暂无通知';
return;
}
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
}
notifications.forEach(function (notification) {
var row = documentRef.createElement('pre');
row.className = 'notification-raw';
row.textContent = formatNotification(notification);
container.appendChild(row);
});
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 updateStatus(message) {
var status = query('[data-notification-status]');
if (status) status.textContent = message;
if (status) status.textContent = message || '';
}
function setWritePending(value) {
writePending = Boolean(value);
queryAll('[data-notification-read-id], [data-notification-read-all], [data-notification-refresh], [data-notification-filter]').forEach(function (control) {
control.disabled = writePending;
});
}
function renderNotifications(data) {
var container = query('[data-notification-list]');
var normalized = Array.isArray(data) ? data.map(normalizeNotification) : [];
var invalidCount = normalized.filter(function (item) { return !item; }).length;
var notifications = normalized.filter(Boolean);
notificationsById = Object.create(null);
notifications.forEach(function (notification) {
notificationsById[notification.notificationId] = notification;
});
if (!container) return notifications;
if (!Array.isArray(data) || invalidCount) {
container.innerHTML = '<div class="api-empty">通知列表响应缺少稳定 NotificationView 字段,请联系后端核对。</div>';
return [];
}
if (!notifications.length) {
container.innerHTML = '<div class="api-empty">暂无通知</div>';
return [];
}
container.innerHTML = notifications.map(renderNotificationRow).join('');
return notifications;
}
function renderDetail(notification) {
var container = query('[data-notification-detail]');
if (container) container.innerHTML = renderNotificationDetail(notification);
}
async function loadNotificationDetail(notificationId) {
var api = getApi();
var notification;
if (!normalizeId(notificationId) || redirectUnauthorized(api)) return null;
try {
notification = normalizeNotification(await api.notificationDetail(notificationId));
if (!notification || !matchesNotification(notification, notificationId)) {
throw new Error('通知详情未返回同一条稳定 NotificationView');
}
selectedNotificationId = notification.notificationId;
notificationsById[notification.notificationId] = notification;
renderDetail(notification);
return notification;
} catch (error) {
if (redirectUnauthorized(api, error)) return null;
renderDetail(null);
updateStatus(error.message || '通知详情加载失败');
return null;
}
}
function currentFilter() {
var filter = query('[data-notification-filter]');
return filter ? filter.value : '';
}
async function loadNotifications() {
var api = getApi();
var container = query('[data-notification-list]');
var countTarget = query('[data-notification-unread-count]');
var result;
var notifications;
var nextSelectedId;
if (!api || !api.notifications || !api.unreadNotificationCount) {
if (redirectUnauthorized(api)) return;
if (!api.notifications || !api.notificationDetail || !api.unreadNotificationCount) {
updateStatus('消息接口初始化失败,请刷新后重试');
return;
}
updateStatus('正在同步消息…');
try {
result = await Promise.all([api.notifications(), api.unreadNotificationCount()]);
renderNotifications(container, normalizeNotifications(result[0]));
result = await Promise.all([
api.notifications(buildNotificationQuery(currentFilter())),
api.unreadNotificationCount()
]);
notifications = renderNotifications(result[0]);
if (countTarget) countTarget.textContent = String(getUnreadCount(result[1]));
updateStatus('已同步消息。列表元素 DTO 尚未在 PC 契约展开,以下保留服务端原始记录。');
nextSelectedId = selectedNotificationId && notificationsById[selectedNotificationId]
? selectedNotificationId
: (notifications[0] && notifications[0].notificationId);
if (nextSelectedId) await loadNotificationDetail(nextSelectedId);
else renderDetail(null);
updateStatus('消息已同步');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
renderNotifications(null);
renderDetail(null);
updateStatus(error.message || '消息同步失败');
}
}
async function markAllRead(button) {
async function markNotificationRead(notificationId) {
var api = getApi();
var notification = notificationsById[notificationId];
if (writePending || !normalizeId(notificationId) || !canMarkRead(notification) || redirectUnauthorized(api)) return;
setWritePending(true);
try {
await api.markNotificationRead(notificationId);
selectedNotificationId = notificationId;
showMessage('通知已标记为已读');
await loadNotifications();
} catch (error) {
if (redirectUnauthorized(api, error)) return;
updateStatus(error.message || '标记通知已读失败');
} finally {
setWritePending(false);
}
}
async function markAllRead() {
var api = getApi();
if (!api || !api.markAllNotificationsRead) {
if (writePending || redirectUnauthorized(api)) return;
if (!api.markAllNotificationsRead) {
updateStatus('消息接口初始化失败,请刷新后重试');
return;
}
button.disabled = true;
setWritePending(true);
try {
await api.markAllNotificationsRead();
showMessage('已全部标记为已读');
await loadNotifications();
} catch (error) {
if (redirectUnauthorized(api, error)) return;
updateStatus(error.message || '全部标记已读失败');
} finally {
button.disabled = false;
setWritePending(false);
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('click', function (event) {
var detailButton = event.target.closest('[data-notification-detail-id]');
var readButton = event.target.closest('[data-notification-read-id]');
if (detailButton) {
event.preventDefault();
loadNotificationDetail(detailButton.getAttribute('data-notification-detail-id'));
return;
}
if (readButton) {
event.preventDefault();
markNotificationRead(readButton.getAttribute('data-notification-read-id'));
}
});
if (query('[data-notification-read-all]')) {
query('[data-notification-read-all]').addEventListener('click', markAllRead);
}
if (query('[data-notification-refresh]')) {
query('[data-notification-refresh]').addEventListener('click', loadNotifications);
}
if (query('[data-notification-filter]')) {
query('[data-notification-filter]').addEventListener('change', function () {
selectedNotificationId = '';
loadNotifications();
});
}
}
function init() {
var readAll = query('[data-notification-read-all]');
var refresh = query('[data-notification-refresh]');
if (!documentRef || !query('[data-notification-list]')) return;
if (readAll) readAll.addEventListener('click', function () { markAllRead(readAll); });
if (refresh) refresh.addEventListener('click', loadNotifications);
bindActions();
loadNotifications();
}
return {
buildNotificationQuery: buildNotificationQuery,
normalizeNotification: normalizeNotification,
normalizeNotifications: normalizeNotifications,
formatNotification: formatNotification,
getUnreadCount: getUnreadCount,
matchesNotification: matchesNotification,
canMarkRead: canMarkRead,
renderNotificationRow: renderNotificationRow,
renderNotificationDetail: renderNotificationDetail,
shouldRedirectToLogin: shouldRedirectToLogin,
loadNotifications: loadNotifications,
init: init
};
+9 -1
View File
@@ -121,11 +121,17 @@
});
}
function normalizeGenealogyId(value) {
var text = String(value === undefined || value === null ? '' : value).trim();
return /^[1-9][0-9]*$/.test(text) ? text : '';
}
function getGenealogyId(search) {
var source = search === undefined ? (window.location && window.location.search) : search;
var params = new URLSearchParams(String(source || '').replace(/^\?/, ''));
return params.get('genealogyId') || '';
return normalizeGenealogyId(params.get('genealogyId'));
}
function withGenealogyId(href, genealogyId) {
@@ -135,6 +141,7 @@
var parts;
var params;
genealogyId = normalizeGenealogyId(genealogyId);
if (!genealogyId || !value || value.charAt(0) === '#' || /^(?:https?:|mailto:|tel:)/i.test(value)) return value;
hashIndex = value.indexOf('#');
if (hashIndex >= 0) {
@@ -278,6 +285,7 @@
closeLogout: closeLegacyLogout,
initLayui: initLayui,
getGenealogyId: getGenealogyId,
normalizeGenealogyId: normalizeGenealogyId,
withGenealogyId: withGenealogyId,
getGenealogyEntryUrl: getGenealogyEntryUrl,
syncGenealogyContextLinks: syncGenealogyContextLinks
+572 -82
View File
@@ -3,34 +3,67 @@
module.exports = factory(root);
return;
}
root.RelativePages = factory(root);
if (root.document) root.document.addEventListener('DOMContentLoaded', function () { root.RelativePages.init(); });
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.RelativePages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
var recordsById = 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 trim(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 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 page = query('[data-relative-page], [data-relative-edit-page]');
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 profileGenealogyId;
if (profileGenealogyId) return normalizeId(profileGenealogyId);
}
return queryParam(source, 'genealogyId') || (page && page.getAttribute('data-genealogy-id')) || '';
return normalizeId(queryParam(source, 'genealogyId'));
}
function toSafeNumber(value) {
var text = trim(value);
function getCurrentRelativeId(search) {
var source = search === undefined && root.location ? root.location.search : search;
return normalizeId(queryParam(source, 'relativeId'));
}
function toFiniteNumber(value) {
var text = trimOrUndefined(value);
var number;
if (!text) return undefined;
@@ -39,130 +72,587 @@
}
function toSafeInteger(value) {
var number = toSafeNumber(value);
var number = toFiniteNumber(value);
return Number.isInteger(number) && Number.isSafeInteger(number) ? number : undefined;
return Number.isSafeInteger(number) ? number : undefined;
}
function decimalKey(value) {
var text = String(value).trim().replace(/^([+-]?)\./, '$10.');
var match = text.match(/^([+-]?)(\d+)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/);
var coefficient;
var scale;
if (!match) return '';
coefficient = BigInt((match[2] + (match[3] || '')).replace(/^0+(?=\d)/, ''));
scale = (match[3] || '').length - Number(match[4] || 0);
if (coefficient === 0n) return '0:0';
if (scale < 0) {
coefficient *= 10n ** BigInt(-scale);
scale = 0;
}
while (scale > 0 && coefficient % 10n === 0n) {
coefficient /= 10n;
scale -= 1;
}
if (match[1] === '-') coefficient = -coefficient;
return String(coefficient) + ':' + String(scale);
}
function isExactJsonNumber(sourceText, number) {
return Number.isFinite(number) && decimalKey(sourceText) === decimalKey(JSON.stringify(number));
}
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 buildRelativeRecordBody(values) {
var source = values || {};
var body = { relativeName: String(source.relativeName || '').trim() };
var fields = ['relationName', 'eventName', 'eventTime', 'recordContent', 'mediaOssIds', 'status'];
var amountText = trim(source.giftAmount);
var sortOrderText = trim(source.sortOrder);
var relationName = trimOrUndefined(source.relationName);
var eventName = trimOrUndefined(source.eventName);
var eventTime = toBackendDateTime(source.eventTime);
var amountText = trimOrUndefined(source.giftAmount);
var recordContent = trimOrUndefined(source.recordContent);
var mediaOssIds = trimOrUndefined(source.mediaOssIds);
var sortOrderText = trimOrUndefined(source.sortOrder);
var status = trimOrUndefined(source.status);
fields.forEach(function (field) {
var value = trim(source[field]);
if (value !== undefined) body[field] = value;
});
if (amountText !== undefined) body.giftAmount = toSafeNumber(amountText);
if (sortOrderText !== undefined) body.sortOrder = toSafeInteger(sortOrderText);
if (relationName !== undefined) body.relationName = relationName;
if (eventName !== undefined) body.eventName = eventName;
if (eventTime !== undefined) body.eventTime = eventTime;
if (amountText !== undefined) {
body.giftAmount = toFiniteNumber(amountText);
if (body.giftAmount === undefined) body.invalidGiftAmount = true;
else if (!isExactJsonNumber(amountText, body.giftAmount)) body.invalidGiftPrecision = true;
}
if (recordContent !== undefined) body.recordContent = recordContent;
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 validateRelativeRecordBody(body) {
if (!body || !body.relativeName) return '请填写亲友姓名';
if (body.mediaOssIds !== undefined && !/^[1-9][0-9]*(,[1-9][0-9]*)*$/.test(body.mediaOssIds)) {
return '附件 OSS ID 请使用英文逗号分隔的正整数';
if (body.invalidGiftAmount || (Object.prototype.hasOwnProperty.call(body, 'giftAmount') && !Number.isFinite(body.giftAmount))) {
return '礼金金额必须是有限数字';
}
if (Object.prototype.hasOwnProperty.call(body, 'giftAmount') && body.giftAmount === undefined) return '礼金金额必须是数字';
if (Object.prototype.hasOwnProperty.call(body, 'sortOrder') && body.sortOrder === undefined) return '排序值必须是安全整数';
if (body.invalidGiftPrecision) return '礼金金额超出浏览器可安全提交的精度';
if (body.eventTime !== undefined && !isValidBackendDateTime(body.eventTime)) {
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 normalizeRelativeList(data) { return Array.isArray(data) ? data : []; }
function escapeHtml(value) { return String(value === undefined || value === null ? '' : value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;'); }
function stringify(value) { try { return typeof value === 'string' ? value : JSON.stringify(value); } catch (error) { return String(value); } }
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;
function renderRelativeRecords(data) {
var container = query('[data-relative-list]');
var records = normalizeRelativeList(data);
if (!container) return;
if (!Array.isArray(data)) {
container.innerHTML = '<div class="api-empty">亲友往来响应未按 Apifox ListResult 返回数组,无法安全展示或操作记录。</div>';
return;
}
if (!records.length) {
container.innerHTML = '<div class="api-empty">暂无亲友往来记录</div>';
return;
}
container.innerHTML = records.map(function (record, index) {
return '<article class="module-row"><div><h3>亲友往来 ' + (index + 1) + '</h3><p>' + escapeHtml(stringify(record)) + '</p></div></article>';
}).join('');
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 stringValue(value) {
return value === undefined || value === null ? '' : String(value);
}
function normalizeAmount(value) {
var text;
if (value === undefined || value === null || value === '') return '';
if (typeof value === 'number') return Number.isFinite(value) ? String(value) : '';
text = String(value).trim();
return /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(text) ? text : '';
}
function optionalSafeInteger(value) {
var number;
if (value === undefined || value === null || value === '') return undefined;
number = Number(value);
return Number.isSafeInteger(number) ? number : undefined;
}
function normalizeRelativeRecord(item) {
var relativeId = normalizeId(item && item.relativeId);
var genealogyId = normalizeId(item && item.genealogyId);
var appUserId = normalizeId(item && item.appUserId);
var relativeName = String(item && item.relativeName || '').trim();
var status = stringValue(item && item.status);
var mediaOssIds = stringValue(item && item.mediaOssIds);
var giftAmount = normalizeAmount(item && item.giftAmount);
var sortOrder = optionalSafeInteger(item && item.sortOrder);
var record;
if (!relativeId || !genealogyId || !relativeName || (status !== '0' && status !== '1')) return null;
if (item.appUserId !== undefined && item.appUserId !== null && item.appUserId !== '' && !appUserId) return null;
if (item.giftAmount !== undefined && item.giftAmount !== null && item.giftAmount !== '' && giftAmount === '') 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;
record = {
relativeId: relativeId,
genealogyId: genealogyId,
genealogyNo: stringValue(item.genealogyNo),
genealogyName: stringValue(item.genealogyName),
surname: stringValue(item.surname),
appUserNickName: stringValue(item.appUserNickName),
appUserPhone: stringValue(item.appUserPhone),
relativeName: relativeName,
relationName: stringValue(item.relationName),
eventName: stringValue(item.eventName),
eventTime: stringValue(item.eventTime),
giftAmount: giftAmount,
recordContent: stringValue(item.recordContent),
mediaOssIds: mediaOssIds,
status: status,
remark: stringValue(item.remark)
};
if (appUserId) record.appUserId = appUserId;
if (sortOrder !== undefined) record.sortOrder = sortOrder;
return record;
}
function matchesSavedRecord(item, expectedRelativeId) {
var record = normalizeRelativeRecord(item);
return Boolean(record && record.relativeId === normalizeId(expectedRelativeId));
}
function isEditableRecord(record) {
return Boolean(record && record.status === '0');
}
function escapeHtml(value) {
return stringValue(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function attachmentCount(mediaOssIds) {
return mediaOssIds ? mediaOssIds.split(',').filter(Boolean).length : 0;
}
function buildListUrl(genealogyId, relativeId) {
var url = 'profile-relative.html?genealogyId=' + encodeURIComponent(genealogyId);
return relativeId ? url + '&relativeId=' + encodeURIComponent(relativeId) : url;
}
function buildEditUrl(genealogyId, relativeId) {
var url = 'profile-relative-edit.html?genealogyId=' + encodeURIComponent(genealogyId);
return relativeId ? url + '&relativeId=' + encodeURIComponent(relativeId) : url;
}
function renderRelativeRow(record) {
var summary = [];
if (record.relationName) summary.push(record.relationName);
if (record.eventName) summary.push(record.eventName);
if (record.eventTime) summary.push(record.eventTime);
if (!summary.length) summary.push('未填写关系和事件');
return '<article class="module-row relative-row"><div><h3>' + escapeHtml(record.relativeName) + '</h3><p>' +
escapeHtml(summary.join(' · ')) + '</p></div><div class="row-actions">' +
'<button class="pill" type="button" data-relative-detail-id="' + escapeHtml(record.relativeId) + '">查看详情</button>' +
'<a class="pill" href="' + escapeHtml(buildEditUrl(record.genealogyId, record.relativeId)) + '">编辑</a>' +
'<button class="pill is-danger" type="button" data-relative-delete-id="' + escapeHtml(record.relativeId) + '">删除</button>' +
'</div></article>';
}
function detailValue(label, value) {
return '<p><span>' + escapeHtml(label) + '</span><b>' + escapeHtml(value || '未提供') + '</b></p>';
}
function renderRelativeDetail(record) {
var count;
if (!record) return '<div class="api-empty">请选择一条亲友记录查看详情</div>';
count = attachmentCount(record.mediaOssIds);
return '<div class="form-like relative-detail">' +
detailValue('亲友姓名', record.relativeName) +
detailValue('关系名称', record.relationName) +
detailValue('事件名称', record.eventName) +
detailValue('事件时间', record.eventTime) +
detailValue('礼金金额', record.giftAmount) +
detailValue('记录内容', record.recordContent) +
detailValue('附件', count ? count + ' 个附件' : '未上传') +
detailValue('记录人', record.appUserNickName) +
detailValue('状态', record.status === '0' ? '正常' : '停用') +
detailValue('备注', record.remark) +
'</div><div class="bottom-actions"><a class="btn ghost" href="' +
escapeHtml(buildEditUrl(record.genealogyId, record.relativeId)) + '">编辑记录</a>' +
'<button class="btn ghost" type="button" data-relative-delete-id="' + escapeHtml(record.relativeId) + '">删除记录</button></div>';
}
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 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 redirectUnauthorized(api, error) {
if (!shouldRedirectToLogin(api, error)) return false;
if (api && api.clearToken) api.clearToken();
if (root.location && typeof root.location.replace === 'function') root.location.replace('login.html');
else if (root.location) root.location.href = 'login.html';
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-relative-form-status]'); if (target) target.textContent = message || ''; }
function setWritePending(value) { writePending = Boolean(value); queryAll('[data-relative-form] button, [data-relative-form] input, [data-relative-form] textarea').forEach(function (control) { control.disabled = writePending; }); }
function syncLinks(genealogyId) {
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) { root.ProfileUI.syncGenealogyContextLinks(); return; }
queryAll('[data-genealogy-context-link]').forEach(function (link) { var href = link.getAttribute('href'); if (href) link.href = href.split('?')[0] + '?genealogyId=' + encodeURIComponent(genealogyId); });
function setFormStatus(message) {
var target = query('[data-relative-form-status]');
if (target) target.textContent = message || '';
}
function setEditorEnabled(enabled, message) {
var placeholder = query('[data-relative-editor-placeholder]');
queryAll('[data-relative-editor], [data-relative-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-relative-form] button, [data-relative-form] input, [data-relative-form] textarea, [data-relative-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-relative-list]');
var detail = query('[data-relative-detail]');
var message = '请先选择家谱,再查看或维护亲友记录。';
if (list) list.innerHTML = '<div class="api-empty">' + message + '</div>';
if (detail) detail.innerHTML = '<div class="api-empty">当前没有家谱上下文。</div>';
setEditorEnabled(false, message);
setFormStatus(message);
}
function requireGenealogyId() {
var genealogyId = getCurrentGenealogyId();
var list = query('[data-relative-list]');
if (genealogyId) return genealogyId;
if (list) list.innerHTML = '<div class="api-empty">等待家谱入口接口;请从具体家谱进入亲友往来。</div>';
setFormStatus('等待家谱入口接口;请从具体家谱进入亲友往来。');
showMessage('等待家谱入口接口;请从具体家谱进入亲友往来。');
return '';
if (!genealogyId) renderNoContext();
return genealogyId;
}
function renderRelativeRecords(data) {
var container = query('[data-relative-list]');
var normalized = Array.isArray(data) ? data.map(normalizeRelativeRecord) : [];
var invalidCount = normalized.filter(function (record) { return !record; }).length;
var records = normalized.filter(Boolean);
recordsById = Object.create(null);
records.forEach(function (record) {
recordsById[record.relativeId] = record;
});
if (!container) return records;
if (!Array.isArray(data) || invalidCount) {
container.innerHTML = '<div class="api-empty">亲友记录响应缺少稳定 RelativeRecordVo 字段,请联系后端核对。</div>';
return [];
}
if (!records.length) {
container.innerHTML = '<div class="api-empty">暂无亲友往来记录</div>';
return [];
}
container.innerHTML = records.map(renderRelativeRow).join('');
return records;
}
function renderDetail(record) {
var container = query('[data-relative-detail]');
if (container) container.innerHTML = renderRelativeDetail(record);
}
async function loadRelativeDetail(relativeId) {
var api = getApi();
var genealogyId = requireGenealogyId();
var record;
if (!genealogyId || !normalizeId(relativeId) || redirectUnauthorized(api)) return null;
try {
record = normalizeRelativeRecord(await api.relativeRecordDetail(genealogyId, relativeId));
if (!record) throw new Error('亲友记录详情响应缺少稳定 RelativeRecordVo 字段');
recordsById[record.relativeId] = record;
renderDetail(record);
return record;
} catch (error) {
if (redirectUnauthorized(api, error)) return null;
renderDetail(null);
showMessage(isForbidden(error) ? '当前账号无权查看该亲友记录。' : (error.message || '亲友记录详情加载失败'));
return null;
}
}
async function loadRelativeRecords() {
var api = getApi();
var genealogyId;
var records;
var requestedRelativeId;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncLinks(genealogyId);
try { renderRelativeRecords(await api.relativeRecords(genealogyId)); }
catch (error) { if (!redirectUnauthorized(api, error)) { renderRelativeRecords(null); showMessage(error.message || '亲友往来加载失败'); } }
syncLinks();
try {
records = renderRelativeRecords(await api.relativeRecords(genealogyId));
requestedRelativeId = getCurrentRelativeId();
if (requestedRelativeId && recordsById[requestedRelativeId]) {
await loadRelativeDetail(requestedRelativeId);
} else if (records.length) {
await loadRelativeDetail(records[0].relativeId);
} else {
renderDetail(null);
}
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (query('[data-relative-list]')) {
query('[data-relative-list]').innerHTML = '<div class="api-empty">' +
(isForbidden(error) ? '当前账号无权查看该家谱的亲友记录。' : '亲友记录加载失败,请稍后重试。') +
'</div>';
}
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-relative-form] [name="' + name + '"]');
if (field) field.value = value === undefined || value === null ? '' : String(value);
}
function fillRelativeForm(record) {
if (!isEditableRecord(record)) throw new Error('停用亲友记录无法通过当前 PC 接口重新读取或编辑');
setFieldValue('relativeName', record.relativeName);
setFieldValue('relationName', record.relationName);
setFieldValue('eventName', record.eventName);
setFieldValue('eventTime', toDateTimeInputValue(record.eventTime));
setFieldValue('giftAmount', record.giftAmount);
setFieldValue('recordContent', record.recordContent);
setFieldValue('mediaOssIds', record.mediaOssIds);
setFieldValue('sortOrder', record.sortOrder);
setFieldValue('status', '0');
if (query('[data-relative-media-status]')) {
query('[data-relative-media-status]').textContent = record.mediaOssIds
? '已保留 ' + attachmentCount(record.mediaOssIds) + ' 个附件,可继续选择追加'
: '未选择文件';
}
}
async function loadRelativeEditor() {
var api = getApi();
var genealogyId;
var relativeId;
var record;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
relativeId = getCurrentRelativeId();
syncLinks();
setEditorEnabled(false, '正在加载亲友记录编辑信息…');
try {
if (relativeId) {
record = normalizeRelativeRecord(await api.relativeRecordDetail(genealogyId, relativeId));
if (!record) throw new Error('亲友记录详情响应缺少稳定 RelativeRecordVo 字段');
fillRelativeForm(record);
if (query('[data-relative-editor-title]')) query('[data-relative-editor-title]').textContent = '编辑亲友记录';
}
setEditorEnabled(true);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setEditorEnabled(false, isForbidden(error)
? '当前账号无权维护该家谱的亲友记录。'
: (error.message || '亲友记录编辑信息加载失败'));
setFormStatus(error.message || '亲友记录编辑信息加载失败');
}
}
async function submitRelativeRecord(form) {
var api = getApi();
var genealogyId;
var values = {};
var genealogyId = requireGenealogyId();
var relativeId = getCurrentRelativeId();
var body;
var validation;
var result;
var saved;
var savedId;
var verifiedDetail;
if (writePending || redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
queryAll('[name]', form).forEach(function (field) { values[field.name] = field.value; });
body = buildRelativeRecordBody(values);
if (writePending || !genealogyId || redirectUnauthorized(api)) return;
body = buildRelativeRecordBody(getFormValues(form));
validation = validateRelativeRecordBody(body);
if (validation) { setFormStatus(validation); return; }
if (validation) {
setFormStatus(validation);
return;
}
delete body.invalidGiftAmount;
delete body.invalidGiftPrecision;
delete body.invalidSortOrder;
setWritePending(true);
setFormStatus('正在保存亲友往来...');
setFormStatus('正在保存亲友记录…');
try {
await api.createRelativeRecord(genealogyId, body);
if (root.location) root.location.href = 'profile-relative.html?genealogyId=' + encodeURIComponent(genealogyId);
result = relativeId
? await api.updateRelativeRecord(genealogyId, relativeId, body)
: await api.createRelativeRecord(genealogyId, body);
saved = normalizeRelativeRecord(result);
savedId = relativeId || (saved && saved.relativeId);
if (!savedId) throw new Error('保存响应缺少 relativeId');
verifiedDetail = await api.relativeRecordDetail(genealogyId, savedId);
if (!matchesSavedRecord(verifiedDetail, savedId)) throw new Error('保存后重读未返回同一条亲友记录');
if (root.location) root.location.href = buildListUrl(genealogyId, savedId);
} catch (error) {
if (!redirectUnauthorized(api, error)) setFormStatus(error.message || '亲友往来保存失败');
} finally { setWritePending(false); }
if (redirectUnauthorized(api, error)) return;
setFormStatus(isForbidden(error) ? '当前账号无权保存该亲友记录。' : (error.message || '亲友记录保存失败'));
} finally {
setWritePending(false);
}
}
async function deleteRelativeRecord(relativeId) {
var api = getApi();
var genealogyId = requireGenealogyId();
var record = recordsById[relativeId];
if (writePending || !genealogyId || !normalizeId(relativeId) || redirectUnauthorized(api)) return;
if (root.confirm && !root.confirm('确认删除“' + (record ? record.relativeName : '该亲友记录') + '”吗?删除后不可恢复,并会释放附件引用。')) return;
setWritePending(true);
try {
await api.deleteRelativeRecord(genealogyId, relativeId);
await loadRelativeRecords();
showMessage('亲友记录已删除');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(isForbidden(error) ? '当前账号无权删除该亲友记录。' : (error.message || '亲友记录删除失败'));
} finally {
setWritePending(false);
}
}
function clearMediaSelection() {
var target = query('[data-relative-form] [name="mediaOssIds"]');
var status = query('[data-relative-media-status]');
var fileInput = query('[data-relative-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-relative-detail-id]');
var deleteButton = event.target.closest('[data-relative-delete-id]');
var clearMediaButton = event.target.closest('[data-relative-clear-media]');
if (detailButton) {
event.preventDefault();
loadRelativeDetail(detailButton.getAttribute('data-relative-detail-id'));
return;
}
if (deleteButton) {
event.preventDefault();
deleteRelativeRecord(deleteButton.getAttribute('data-relative-delete-id'));
return;
}
if (clearMediaButton) {
event.preventDefault();
clearMediaSelection();
}
});
documentRef.addEventListener('submit', function (event) {
var form = event.target.closest('[data-relative-form]');
if (!form) return;
event.preventDefault();
submitRelativeRecord(form);
});
}
function init() {
var genealogyId;
if (!documentRef) return;
documentRef.addEventListener('submit', function (event) { var form = event.target.closest('[data-relative-form]'); if (form) { event.preventDefault(); submitRelativeRecord(form); } });
bindActions();
if (query('[data-relative-page]')) loadRelativeRecords();
if (query('[data-relative-edit-page]')) { genealogyId = requireGenealogyId(); if (genealogyId) syncLinks(genealogyId); }
if (query('[data-relative-edit-page]')) loadRelativeEditor();
}
return { getCurrentGenealogyId: getCurrentGenealogyId, buildRelativeRecordBody: buildRelativeRecordBody, validateRelativeRecordBody: validateRelativeRecordBody, normalizeRelativeList: normalizeRelativeList, shouldRedirectToLogin: shouldRedirectToLogin, init: init };
return {
getCurrentGenealogyId: getCurrentGenealogyId,
getCurrentRelativeId: getCurrentRelativeId,
buildRelativeRecordBody: buildRelativeRecordBody,
validateRelativeRecordBody: validateRelativeRecordBody,
normalizeRelativeRecord: normalizeRelativeRecord,
matchesSavedRecord: matchesSavedRecord,
isEditableRecord: isEditableRecord,
renderRelativeDetail: renderRelativeDetail,
shouldRedirectToLogin: shouldRedirectToLogin,
isForbidden: isForbidden,
init: init
};
});
+616
View File
@@ -0,0 +1,616 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.VideoPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.VideoPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
var videosById = Object.create(null);
var canEditContent = false;
var writePending = false;
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
return documentRef ? (rootNode || documentRef).querySelector(selector) : null;
}
function queryAll(selector, rootNode) {
return documentRef ? Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector)) : [];
}
function getQueryParam(search, name) {
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return params.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 fromProfile;
if (search === undefined && root.ProfileUI && root.ProfileUI.getGenealogyId) {
fromProfile = root.ProfileUI.getGenealogyId();
if (fromProfile) return normalizeId(fromProfile);
}
return normalizeId(getQueryParam(source, 'genealogyId'));
}
function getCurrentVideoId(search) {
var source = search === undefined && root.location ? root.location.search : search;
return normalizeId(getQueryParam(source, 'videoId'));
}
function trimOrUndefined(value) {
var text = String(value === undefined || value === null ? '' : value).trim();
return text || undefined;
}
function toSafeInteger(value) {
var text = trimOrUndefined(value);
var number;
if (text === undefined) return undefined;
number = Number(text);
return Number.isSafeInteger(number) ? number : undefined;
}
function buildVideoBody(values) {
var source = values || {};
var body = {
videoTitle: String(source.videoTitle || '').trim()
};
var videoDesc = trimOrUndefined(source.videoDesc);
var coverOssId = trimOrUndefined(source.coverOssId);
var videoOssId = trimOrUndefined(source.videoOssId);
var durationText = trimOrUndefined(source.durationSeconds);
var sortOrderText = trimOrUndefined(source.sortOrder);
var status = trimOrUndefined(source.status);
if (videoDesc !== undefined) body.videoDesc = videoDesc;
if (coverOssId !== undefined) body.coverOssId = coverOssId;
if (videoOssId !== undefined) body.videoOssId = videoOssId;
if (durationText !== undefined) {
body.durationSeconds = toSafeInteger(durationText);
if (body.durationSeconds === undefined) body.invalidDuration = true;
}
if (sortOrderText !== undefined) {
body.sortOrder = toSafeInteger(sortOrderText);
if (body.sortOrder === undefined) body.invalidSortOrder = true;
}
if (status !== undefined) body.status = status;
return body;
}
function validateVideoBody(body) {
if (!body || !body.videoTitle) return '请填写视频标题';
if (!body.videoOssId) return '请先选择并上传视频文件';
if (!normalizeId(body.videoOssId)) return '视频文件上传结果无效';
if (body.coverOssId !== undefined && !normalizeId(body.coverOssId)) return '封面上传结果无效';
if (body.invalidDuration || (body.durationSeconds !== undefined && !Number.isSafeInteger(body.durationSeconds))) {
return '视频时长必须是安全整数';
}
if (body.durationSeconds !== undefined && body.durationSeconds < 0) return '视频时长不能小于 0';
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 optionalInteger(value, minimum) {
var number;
if (value === undefined || value === null || value === '') return undefined;
number = Number(value);
if (!Number.isSafeInteger(number) || (minimum !== undefined && number < minimum)) return undefined;
return number;
}
function stringValue(value) {
return value === undefined || value === null ? '' : String(value);
}
function normalizeVideo(item) {
var videoId = normalizeId(item && item.videoId);
var genealogyId = normalizeId(item && item.genealogyId);
var videoOssId = normalizeId(item && item.videoOssId);
var coverOssId = normalizeId(item && item.coverOssId);
var publisherUserId = normalizeId(item && item.publisherUserId);
var title = String(item && item.videoTitle || '').trim();
var status = stringValue(item && item.status);
var video;
var duration;
var viewCount;
var sortOrder;
if (!videoId || !genealogyId || !videoOssId || !title || (status !== '0' && status !== '1')) return null;
if (item.coverOssId !== undefined && item.coverOssId !== null && item.coverOssId !== '' && !coverOssId) return null;
if (item.publisherUserId !== undefined && item.publisherUserId !== null && item.publisherUserId !== '' && !publisherUserId) return null;
video = {
videoId: videoId,
genealogyId: genealogyId,
genealogyNo: stringValue(item.genealogyNo),
genealogyName: stringValue(item.genealogyName),
surname: stringValue(item.surname),
videoTitle: title,
videoDesc: stringValue(item.videoDesc),
videoOssId: videoOssId,
publisherNickName: stringValue(item.publisherNickName),
publisherPhone: stringValue(item.publisherPhone),
publishTime: stringValue(item.publishTime),
status: status,
remark: stringValue(item.remark)
};
if (coverOssId) video.coverOssId = coverOssId;
if (publisherUserId) video.publisherUserId = publisherUserId;
duration = optionalInteger(item.durationSeconds, 0);
viewCount = optionalInteger(item.viewCount, 0);
sortOrder = optionalInteger(item.sortOrder);
if (duration !== undefined) video.durationSeconds = duration;
if (viewCount !== undefined) video.viewCount = viewCount;
if (sortOrder !== undefined) video.sortOrder = sortOrder;
return video;
}
function normalizeDurationSeconds(value) {
var number = Number(value);
return Number.isFinite(number) && number >= 0 ? Math.ceil(number) : undefined;
}
function escapeHtml(value) {
return stringValue(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function formatDuration(seconds) {
var value = optionalInteger(seconds, 0);
var minutes;
var remaining;
if (value === undefined) return '未提供';
minutes = Math.floor(value / 60);
remaining = String(value % 60).padStart(2, '0');
return minutes + ':' + remaining;
}
function buildListUrl(genealogyId, videoId) {
var url = 'profile-video.html?genealogyId=' + encodeURIComponent(genealogyId);
return videoId ? url + '&videoId=' + encodeURIComponent(videoId) : url;
}
function buildEditUrl(genealogyId, videoId) {
var url = 'profile-video-edit.html?genealogyId=' + encodeURIComponent(genealogyId);
return videoId ? url + '&videoId=' + encodeURIComponent(videoId) : url;
}
function renderVideoRow(video, managementAccess) {
var actions = '<button class="pill" type="button" data-video-detail-id="' + escapeHtml(video.videoId) + '">查看详情</button>';
if (managementAccess) {
actions += '<a class="pill" href="' + escapeHtml(buildEditUrl(video.genealogyId, video.videoId)) + '">编辑</a>' +
'<button class="pill is-danger" type="button" data-video-delete-id="' + escapeHtml(video.videoId) + '">删除</button>';
}
return '<article class="module-row video-row"><div><h3>' + escapeHtml(video.videoTitle) + '</h3><p>' +
escapeHtml(video.publisherNickName || '未知发布人') +
(video.publishTime ? ' · ' + escapeHtml(video.publishTime) : '') +
' · 时长 ' + escapeHtml(formatDuration(video.durationSeconds)) +
'</p></div><div class="row-actions">' + actions + '</div></article>';
}
function detailValue(label, value) {
return '<p><span>' + escapeHtml(label) + '</span><b>' + escapeHtml(value || '未提供') + '</b></p>';
}
function renderVideoDetail(video, managementAccess) {
var actions = '';
if (!video) return '<div class="api-empty">请选择一个视频查看详情</div>';
if (managementAccess) {
actions = '<div class="bottom-actions"><a class="btn ghost" href="' +
escapeHtml(buildEditUrl(video.genealogyId, video.videoId)) + '">编辑视频</a>' +
'<button class="btn ghost" type="button" data-video-delete-id="' + escapeHtml(video.videoId) + '">删除视频</button></div>';
}
return '<div class="form-like video-detail">' +
detailValue('视频标题', video.videoTitle) +
detailValue('视频说明', video.videoDesc) +
detailValue('所属家谱', video.genealogyName || video.genealogyNo) +
detailValue('发布人', video.publisherNickName) +
detailValue('发布时间', video.publishTime) +
detailValue('视频文件', '已上传') +
detailValue('封面文件', video.coverOssId ? '已上传' : '未上传') +
detailValue('视频时长', formatDuration(video.durationSeconds)) +
detailValue('浏览量', video.viewCount === undefined ? '' : String(video.viewCount)) +
detailValue('状态', video.status === '0' ? '正常' : '停用') +
detailValue('备注', video.remark) +
'</div>' + actions;
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
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');
return true;
}
root.location.href = 'login.html';
return true;
}
function syncGenealogyLinks() {
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) root.ProfileUI.syncGenealogyContextLinks();
}
function renderNoContext() {
var list = query('[data-video-list]');
var detail = query('[data-video-detail]');
var placeholder = query('[data-video-permission-placeholder]');
if (list) list.innerHTML = '<div class="api-empty">请先选择家谱,再查看家族视频。</div>';
if (detail) detail.innerHTML = '<div class="api-empty">当前没有家谱上下文。</div>';
if (placeholder) placeholder.textContent = '请先选择家谱,再发布视频。';
setFormStatus('请先选择家谱,再发布视频。');
setManagementAccess(false);
}
function requireGenealogyId() {
var genealogyId = getCurrentGenealogyId();
if (!genealogyId) renderNoContext();
return genealogyId;
}
function setManagementAccess(enabled) {
var placeholder = query('[data-video-permission-placeholder]');
canEditContent = Boolean(enabled);
queryAll('[data-video-management]').forEach(function (element) {
element.hidden = !canEditContent;
});
if (placeholder) placeholder.hidden = canEditContent;
}
function setFormStatus(message) {
var status = query('[data-video-form-status]');
if (status) status.textContent = message || '';
}
function setWritePending(pending) {
writePending = Boolean(pending);
queryAll('[data-video-form] input, [data-video-form] textarea, [data-video-form] button, [data-video-delete-id]').forEach(function (control) {
control.disabled = writePending;
});
}
function renderVideos(data) {
var container = query('[data-video-list]');
var normalized = Array.isArray(data) ? data.map(normalizeVideo) : [];
var invalidCount = normalized.filter(function (item) { return !item; }).length;
var videos = normalized.filter(Boolean);
videosById = Object.create(null);
videos.forEach(function (video) {
videosById[video.videoId] = video;
});
if (!container) return videos;
if (!Array.isArray(data) || invalidCount) {
container.innerHTML = '<div class="api-empty">视频响应缺少稳定 VideoVo 字段,请联系后端核对。</div>';
return [];
}
if (!videos.length) {
container.innerHTML = '<div class="api-empty">暂无家族视频</div>';
return [];
}
container.innerHTML = videos.map(function (video) {
return renderVideoRow(video, canEditContent);
}).join('');
return videos;
}
function renderDetail(video) {
var container = query('[data-video-detail]');
if (container) container.innerHTML = renderVideoDetail(video, canEditContent);
}
async function loadVideoDetail(videoId) {
var api = getApi();
var genealogyId = requireGenealogyId();
var video;
if (!genealogyId || !normalizeId(videoId) || redirectUnauthorized(api)) return null;
try {
video = normalizeVideo(await api.videoDetail(genealogyId, videoId));
if (!video) throw new Error('视频详情响应缺少稳定 VideoVo 字段');
videosById[video.videoId] = video;
renderDetail(video);
return video;
} catch (error) {
if (redirectUnauthorized(api, error)) return null;
renderDetail(null);
showMessage(error.message || '视频详情加载失败');
return null;
}
}
async function loadVideos() {
var api = getApi();
var genealogyId;
var result;
var videos;
var requestedVideoId;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncGenealogyLinks();
try {
result = await Promise.all([api.genealogyDetail(genealogyId), api.videos(genealogyId)]);
setManagementAccess(Boolean(result[0] && result[0].canEditContent === true));
videos = renderVideos(result[1]);
requestedVideoId = getCurrentVideoId();
if (requestedVideoId && videosById[requestedVideoId]) {
await loadVideoDetail(requestedVideoId);
} else if (videos.length) {
await loadVideoDetail(videos[0].videoId);
} else {
renderDetail(null);
}
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (isForbidden(error)) {
setManagementAccess(false);
if (query('[data-video-list]')) query('[data-video-list]').innerHTML = '<div class="api-empty">当前账号无权查看该家谱视频。</div>';
return;
}
if (query('[data-video-list]')) query('[data-video-list]').innerHTML = '<div class="api-empty">视频加载失败,请稍后重试。</div>';
showMessage(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-video-form] [name="' + name + '"]');
if (field) field.value = value === undefined || value === null ? '' : String(value);
}
function fillVideoForm(video) {
setFieldValue('videoTitle', video.videoTitle);
setFieldValue('videoDesc', video.videoDesc);
setFieldValue('coverOssId', video.coverOssId);
setFieldValue('videoOssId', video.videoOssId);
setFieldValue('durationSeconds', video.durationSeconds);
setFieldValue('sortOrder', video.sortOrder);
setFieldValue('status', '0');
if (query('[data-video-file-status]')) query('[data-video-file-status]').textContent = '已保留原视频文件,可重新选择替换';
if (query('[data-video-cover-status]')) query('[data-video-cover-status]').textContent = video.coverOssId ? '已保留原封面,可重新选择替换' : '未选择封面';
}
async function loadVideoEditor() {
var api = getApi();
var genealogyId;
var videoId;
var result;
var video;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
videoId = getCurrentVideoId();
syncGenealogyLinks();
try {
result = await Promise.all([
api.genealogyDetail(genealogyId),
videoId ? api.videoDetail(genealogyId, videoId) : Promise.resolve(null)
]);
setManagementAccess(Boolean(result[0] && result[0].canEditContent === true));
if (!canEditContent) {
if (query('[data-video-permission-placeholder]')) {
query('[data-video-permission-placeholder]').textContent = '当前账号没有视频维护权限。';
}
setFormStatus('当前账号没有视频维护权限。');
return;
}
if (videoId) {
video = normalizeVideo(result[1]);
if (!video) throw new Error('视频详情响应缺少稳定 VideoVo 字段');
fillVideoForm(video);
if (query('[data-video-editor-title]')) query('[data-video-editor-title]').textContent = '编辑视频';
}
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setManagementAccess(false);
if (query('[data-video-permission-placeholder]')) {
query('[data-video-permission-placeholder]').textContent = isForbidden(error)
? '当前账号没有视频维护权限。'
: (error.message || '视频编辑信息加载失败');
}
setFormStatus(isForbidden(error) ? '当前账号没有视频维护权限。' : (error.message || '视频编辑信息加载失败'));
}
}
async function submitVideo(form) {
var api = getApi();
var genealogyId = requireGenealogyId();
var videoId = getCurrentVideoId();
var body = buildVideoBody(getFormValues(form));
var validation = validateVideoBody(body);
var result;
var saved;
var savedId;
if (writePending || !canEditContent || !genealogyId || redirectUnauthorized(api)) return;
if (validation) {
setFormStatus(validation);
return;
}
delete body.invalidDuration;
delete body.invalidSortOrder;
setWritePending(true);
setFormStatus('正在保存视频…');
try {
result = videoId ? await api.updateVideo(genealogyId, videoId, body) : await api.createVideo(genealogyId, body);
saved = normalizeVideo(result);
savedId = videoId || (saved && saved.videoId);
if (!savedId) throw new Error('保存响应缺少 videoId');
await api.videoDetail(genealogyId, savedId);
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 deleteVideo(videoId) {
var api = getApi();
var genealogyId = requireGenealogyId();
var video = videosById[videoId];
if (writePending || !canEditContent || !genealogyId || !normalizeId(videoId) || redirectUnauthorized(api)) return;
if (root.confirm && !root.confirm('确认删除“' + (video ? video.videoTitle : '该视频') + '”吗?删除会释放视频和封面文件引用。')) return;
setWritePending(true);
try {
await api.deleteVideo(genealogyId, videoId);
await loadVideos();
showMessage('视频已删除');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(isForbidden(error) ? '当前账号没有视频维护权限。' : (error.message || '视频删除失败'));
} finally {
setWritePending(false);
}
}
function readVideoDuration(input) {
var file = input && input.files && input.files[0];
var targetSelector = input && input.getAttribute('data-video-duration-target');
var target = targetSelector ? query(targetSelector) : null;
var media;
var objectUrl;
if (!file || !target || !documentRef || !root.URL || !root.URL.createObjectURL) return;
media = documentRef.createElement('video');
objectUrl = root.URL.createObjectURL(file);
media.preload = 'metadata';
media.onloadedmetadata = function () {
var seconds = normalizeDurationSeconds(media.duration);
target.value = seconds === undefined ? '' : String(seconds);
root.URL.revokeObjectURL(objectUrl);
};
media.onerror = function () {
target.value = '';
root.URL.revokeObjectURL(objectUrl);
};
media.src = objectUrl;
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('click', function (event) {
var detailButton = event.target.closest('[data-video-detail-id]');
var deleteButton = event.target.closest('[data-video-delete-id]');
if (detailButton) {
event.preventDefault();
loadVideoDetail(detailButton.getAttribute('data-video-detail-id'));
return;
}
if (deleteButton) {
event.preventDefault();
deleteVideo(deleteButton.getAttribute('data-video-delete-id'));
}
});
documentRef.addEventListener('submit', function (event) {
var form = event.target.closest('[data-video-form]');
if (!form) return;
event.preventDefault();
submitVideo(form);
});
documentRef.addEventListener('change', function (event) {
var input = event.target.closest('[data-video-duration-target]');
if (input && input.type === 'file') readVideoDuration(input);
});
}
function init() {
if (!documentRef) return;
bindActions();
if (query('[data-video-page]')) loadVideos();
if (query('[data-video-edit-page]')) loadVideoEditor();
}
return {
getCurrentGenealogyId: getCurrentGenealogyId,
getCurrentVideoId: getCurrentVideoId,
buildVideoBody: buildVideoBody,
validateVideoBody: validateVideoBody,
normalizeVideo: normalizeVideo,
normalizeDurationSeconds: normalizeDurationSeconds,
renderVideoRow: renderVideoRow,
renderVideoDetail: renderVideoDetail,
shouldRedirectToLogin: shouldRedirectToLogin,
isForbidden: isForbidden,
init: init
};
});