修改功能
This commit is contained in:
+25
-11
@@ -13,6 +13,15 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function confirmAction(message) {
|
||||
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
|
||||
return root.ProfileUI.confirmAction(message);
|
||||
}
|
||||
return Promise.resolve(!root['confirm'] || root['confirm'](message));
|
||||
}
|
||||
|
||||
var MediaDisplay = root.MediaDisplay || (typeof require === 'function' ? require('./media-display.js') : null);
|
||||
|
||||
var documentRef = root.document;
|
||||
var albumsById = Object.create(null);
|
||||
var photosById = Object.create(null);
|
||||
@@ -180,7 +189,7 @@
|
||||
function normalizeAlbum(item) {
|
||||
var albumId = normalizeId(item && item.albumId);
|
||||
var genealogyId = normalizeId(item && item.genealogyId);
|
||||
var coverOssId = normalizeNullableId(item && item.coverOssId);
|
||||
var coverFile = MediaDisplay && MediaDisplay.normalizeFileAccess(item && item.coverFile);
|
||||
var albumName = stringValue(item && item.albumName).trim();
|
||||
var photoCount = optionalSafeInteger(item && item.photoCount);
|
||||
var sortOrder = optionalSafeInteger(item && item.sortOrder);
|
||||
@@ -188,7 +197,7 @@
|
||||
var album;
|
||||
|
||||
if (!albumId || !genealogyId || !albumName) return null;
|
||||
if (item.coverOssId !== undefined && item.coverOssId !== null && item.coverOssId !== '' && !coverOssId) return null;
|
||||
if (item.coverFile !== undefined && item.coverFile !== null && !coverFile) 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;
|
||||
@@ -203,7 +212,7 @@
|
||||
status: status,
|
||||
remark: stringValue(item.remark)
|
||||
};
|
||||
if (coverOssId) album.coverOssId = coverOssId;
|
||||
if (coverFile) album.coverFile = coverFile;
|
||||
if (photoCount !== undefined) album.photoCount = photoCount;
|
||||
if (sortOrder !== undefined) album.sortOrder = sortOrder;
|
||||
return album;
|
||||
@@ -213,12 +222,12 @@
|
||||
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 photoFile = MediaDisplay && MediaDisplay.normalizeFileAccess(item && item.photoFile);
|
||||
var sortOrder = optionalSafeInteger(item && item.sortOrder);
|
||||
var status = stringValue(item && item.status);
|
||||
var photo;
|
||||
|
||||
if (!photoId || !genealogyId || !albumId || !ossId) return null;
|
||||
if (!photoId || !genealogyId || !albumId || !photoFile) return null;
|
||||
if (item.sortOrder !== undefined && item.sortOrder !== null && item.sortOrder !== '' && sortOrder === undefined) return null;
|
||||
if (status !== '0' && status !== '1') return null;
|
||||
photo = {
|
||||
@@ -229,7 +238,7 @@
|
||||
surname: stringValue(item.surname),
|
||||
albumId: albumId,
|
||||
albumName: stringValue(item.albumName),
|
||||
ossId: ossId,
|
||||
photoFile: photoFile,
|
||||
photoTitle: stringValue(item.photoTitle),
|
||||
photoDesc: stringValue(item.photoDesc),
|
||||
photographer: stringValue(item.photographer),
|
||||
@@ -299,10 +308,12 @@
|
||||
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="' +
|
||||
'<button class="btn danger" type="button" data-album-delete-id="' +
|
||||
escapeHtml(album.albumId) + '">删除相册</button></div>';
|
||||
}
|
||||
return '<div class="form-like album-detail">' +
|
||||
return (MediaDisplay ? MediaDisplay.renderImage(album.coverFile, {
|
||||
alt: album.albumName + '封面', className: 'album-cover-media'
|
||||
}) : '') + '<div class="form-like album-detail">' +
|
||||
detailValue('相册名称', album.albumName) +
|
||||
detailValue('相册说明', album.albumDesc) +
|
||||
detailValue('照片数量', album.photoCount === undefined ? '' : album.photoCount) +
|
||||
@@ -334,7 +345,10 @@
|
||||
? '<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>' +
|
||||
return '<article class="module-row album-photo-row">' +
|
||||
(MediaDisplay ? MediaDisplay.renderImage(photo.photoFile, {
|
||||
alt: photo.photoTitle || '家族照片', className: 'album-photo-media'
|
||||
}) : '') + '<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>';
|
||||
@@ -706,7 +720,7 @@
|
||||
var album = albumsById[albumId] || currentAlbum;
|
||||
|
||||
if (writePending || !canEditContent || !genealogyId || !normalizeId(albumId) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认删除“' + (album ? album.albumName : '该相册') + '”及其中全部照片吗?删除后不可恢复。')) return;
|
||||
if (!await confirmAction('确认删除“' + (album ? album.albumName : '该相册') + '”及其中全部照片吗?删除后不可恢复。')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
await api.deleteAlbum(genealogyId, albumId);
|
||||
@@ -727,7 +741,7 @@
|
||||
var photo = photosById[photoId];
|
||||
|
||||
if (writePending || !canEditContent || !genealogyId || !albumId || !normalizeId(photoId) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认删除“' + (photo && photo.photoTitle || '该照片') + '”吗?删除后不可恢复。')) return;
|
||||
if (!await confirmAction('确认删除“' + (photo && photo.photoTitle || '该照片') + '”吗?删除后不可恢复。')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
await api.deleteAlbumPhoto(genealogyId, albumId, photoId);
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
var MediaDisplay = root.MediaDisplay || (typeof require === 'function' ? require('./media-display.js') : null);
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/&/g, '&')
|
||||
@@ -52,14 +54,19 @@
|
||||
var source = item || {};
|
||||
var promotionId = normalizeId(source.promotionId);
|
||||
var promotionTitle = text(source.promotionTitle);
|
||||
var coverFile = MediaDisplay && MediaDisplay.normalizeFileAccess(source.coverFile);
|
||||
var promotion;
|
||||
|
||||
if (!promotionId || !promotionTitle || String(source.status) !== '0') return null;
|
||||
return {
|
||||
if (source.coverFile !== undefined && source.coverFile !== null && !coverFile) return null;
|
||||
promotion = {
|
||||
promotionId: promotionId,
|
||||
promotionTitle: promotionTitle,
|
||||
promotionDesc: text(source.promotionDesc),
|
||||
targetUrl: normalizeTargetUrl(source.targetUrl)
|
||||
};
|
||||
if (coverFile) promotion.coverFile = coverFile;
|
||||
return promotion;
|
||||
}
|
||||
|
||||
function normalizePromotionList(data) {
|
||||
@@ -71,7 +78,9 @@
|
||||
}
|
||||
|
||||
function renderPromotionCard(item) {
|
||||
var content = '<div><h3>' + escapeHtml(item.promotionTitle) + '</h3>' +
|
||||
var content = (MediaDisplay ? MediaDisplay.renderImage(item.coverFile, {
|
||||
alt: item.promotionTitle + '封面', className: 'promotion-cover-media'
|
||||
}) : '') + '<div><h3>' + escapeHtml(item.promotionTitle) + '</h3>' +
|
||||
'<p>' + escapeHtml(item.promotionDesc) + '</p></div>';
|
||||
|
||||
if (!item.targetUrl) {
|
||||
@@ -138,7 +147,7 @@
|
||||
setPromotionState(list, 'empty', '当前暂无应用推广。');
|
||||
return;
|
||||
}
|
||||
list.innerHTML = renderPromotionList(items);
|
||||
list.innerHTML = items.map(renderPromotionCard).join('');
|
||||
} catch (error) {
|
||||
if (isUnauthorized(error)) {
|
||||
if (api.clearToken) api.clearToken();
|
||||
|
||||
@@ -13,6 +13,15 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function confirmAction(message) {
|
||||
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
|
||||
return root.ProfileUI.confirmAction(message);
|
||||
}
|
||||
return Promise.resolve(!root['confirm'] || root['confirm'](message));
|
||||
}
|
||||
|
||||
var MediaDisplay = root.MediaDisplay || (typeof require === 'function' ? require('./media-display.js') : null);
|
||||
|
||||
var documentRef = root.document;
|
||||
var articlesById = Object.create(null);
|
||||
var writePending = false;
|
||||
@@ -119,7 +128,7 @@
|
||||
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 coverFile = MediaDisplay && MediaDisplay.normalizeFileAccess(item && item.coverFile);
|
||||
var articleTitle = stringValue(item && item.articleTitle).trim();
|
||||
var articleContent = stringValue(item && item.articleContent).trim();
|
||||
var viewCount = optionalSafeInteger(item && item.viewCount);
|
||||
@@ -129,7 +138,7 @@
|
||||
|
||||
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.coverFile !== undefined && item.coverFile !== null && !coverFile) 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;
|
||||
@@ -151,7 +160,7 @@
|
||||
remark: stringValue(item.remark)
|
||||
};
|
||||
if (categoryId) article.categoryId = categoryId;
|
||||
if (coverOssId) article.coverOssId = coverOssId;
|
||||
if (coverFile) article.coverFile = coverFile;
|
||||
if (viewCount !== undefined) article.viewCount = viewCount;
|
||||
if (sortOrder !== undefined) article.sortOrder = sortOrder;
|
||||
return article;
|
||||
@@ -207,10 +216,12 @@
|
||||
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="' +
|
||||
'<button class="btn danger" type="button" data-article-delete-id="' +
|
||||
escapeHtml(article.articleId) + '">删除谱文</button></div>';
|
||||
}
|
||||
return '<div class="form-like article-detail">' +
|
||||
return (MediaDisplay ? MediaDisplay.renderImage(article.coverFile, {
|
||||
alt: article.articleTitle + '封面', className: 'article-cover-media'
|
||||
}) : '') + '<div class="form-like article-detail">' +
|
||||
detailValue('标题', article.articleTitle) +
|
||||
detailValue('摘要', article.articleSummary) +
|
||||
detailValue('正文', article.articleContent) +
|
||||
@@ -556,7 +567,7 @@
|
||||
var article = articlesById[articleId];
|
||||
|
||||
if (writePending || !canEditContent || !genealogyId || !normalizeId(articleId) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认删除“' + (article ? article.articleTitle : '该谱文') + '”吗?删除后不可恢复。')) return;
|
||||
if (!await confirmAction('确认删除“' + (article ? article.articleTitle : '该谱文') + '”吗?删除后不可恢复。')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
await api.deleteArticle(genealogyId, articleId);
|
||||
|
||||
@@ -13,6 +13,15 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function confirmAction(message) {
|
||||
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
|
||||
return root.ProfileUI.confirmAction(message);
|
||||
}
|
||||
return Promise.resolve(!root['confirm'] || root['confirm'](message));
|
||||
}
|
||||
|
||||
var MediaDisplay = root.MediaDisplay || (typeof require === 'function' ? require('./media-display.js') : null);
|
||||
|
||||
var documentRef = root.document;
|
||||
var ceremoniesById = Object.create(null);
|
||||
var giftsById = Object.create(null);
|
||||
@@ -212,7 +221,7 @@
|
||||
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 coverFile = MediaDisplay && MediaDisplay.normalizeFileAccess(item && item.coverFile);
|
||||
var ceremonyType = stringValue(item && item.ceremonyType).trim();
|
||||
var ceremonyTitle = stringValue(item && item.ceremonyTitle).trim();
|
||||
var longitude = optionalFiniteNumber(item && item.longitude);
|
||||
@@ -225,7 +234,7 @@
|
||||
|
||||
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.coverFile !== undefined && item.coverFile !== null && !coverFile) 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;
|
||||
@@ -251,7 +260,7 @@
|
||||
remark: stringValue(item.remark)
|
||||
};
|
||||
if (sponsorUserId) ceremony.sponsorUserId = sponsorUserId;
|
||||
if (coverOssId) ceremony.coverOssId = coverOssId;
|
||||
if (coverFile) ceremony.coverFile = coverFile;
|
||||
if (longitude !== undefined) ceremony.longitude = longitude;
|
||||
if (latitude !== undefined) ceremony.latitude = latitude;
|
||||
if (giftCount !== undefined) ceremony.giftCount = giftCount;
|
||||
@@ -399,10 +408,12 @@
|
||||
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="' +
|
||||
'<button class="btn danger" type="button" data-ceremony-delete-id="' +
|
||||
escapeHtml(ceremony.ceremonyId) + '">删除活动</button></div>';
|
||||
}
|
||||
return '<div class="form-like ceremony-detail">' +
|
||||
return (MediaDisplay ? MediaDisplay.renderImage(ceremony.coverFile, {
|
||||
alt: ceremony.ceremonyTitle + '封面', className: 'ceremony-cover-media'
|
||||
}) : '') + '<div class="form-like ceremony-detail">' +
|
||||
detailValue('活动标题', ceremony.ceremonyTitle) +
|
||||
detailValue('活动类型', ceremony.ceremonyType) +
|
||||
detailValue('活动说明', ceremony.ceremonyDesc) +
|
||||
@@ -920,10 +931,18 @@
|
||||
var api = getApi();
|
||||
var genealogyId = requireGenealogyId();
|
||||
var ceremonyId = getCurrentCeremonyId();
|
||||
var availableInviteeInputs;
|
||||
var body;
|
||||
|
||||
if (writePending || !canEditContent || !genealogyId || !ceremonyId || redirectUnauthorized(api)) return;
|
||||
body = buildInviteesBody(queryAll('[data-invitee-user-id]:checked').map(function (input) {
|
||||
availableInviteeInputs = queryAll('[data-invitee-user-id]');
|
||||
if (!availableInviteeInputs.length) {
|
||||
setStatus('[data-ceremony-invitee-status]', '当前没有可邀请的家谱成员', 'empty');
|
||||
return;
|
||||
}
|
||||
body = buildInviteesBody(availableInviteeInputs.filter(function (input) {
|
||||
return input.checked;
|
||||
}).map(function (input) {
|
||||
return input.getAttribute('data-invitee-user-id');
|
||||
}));
|
||||
setWritePending(true);
|
||||
@@ -950,7 +969,7 @@
|
||||
var ceremony = ceremoniesById[ceremonyId] || currentCeremony;
|
||||
|
||||
if (writePending || !canEditContent || !genealogyId || !normalizeId(ceremonyId) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认删除“' + (ceremony ? ceremony.ceremonyTitle : '该活动') + '”吗?删除后不可恢复。')) return;
|
||||
if (!await confirmAction('确认删除“' + (ceremony ? ceremony.ceremonyTitle : '该活动') + '”吗?删除后不可恢复。')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
await api.deleteCeremony(genealogyId, ceremonyId);
|
||||
@@ -971,7 +990,7 @@
|
||||
var gift = giftsById[giftId];
|
||||
|
||||
if (writePending || !canEditContent || !genealogyId || !ceremonyId || !normalizeId(giftId) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认删除“' + (gift && (gift.giverName || gift.giverNickName) || '该祭品') + '”的记录吗?')) return;
|
||||
if (!await confirmAction('确认删除“' + (gift && (gift.giverName || gift.giverNickName) || '该祭品') + '”的记录吗?')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
await api.deleteCeremonyGift(genealogyId, ceremonyId, giftId);
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
(function (root, factory) {
|
||||
if (typeof module === 'object' && module.exports) {
|
||||
module.exports = factory(root);
|
||||
return;
|
||||
}
|
||||
root.EarningPages = factory(root);
|
||||
if (root.document) root.document.addEventListener('DOMContentLoaded', root.EarningPages.init);
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function confirmAction(message) {
|
||||
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
|
||||
return root.ProfileUI.confirmAction(message);
|
||||
}
|
||||
return Promise.resolve(!root['confirm'] || root['confirm'](message));
|
||||
}
|
||||
|
||||
var WITHDRAWAL_STATUS = {
|
||||
PENDING: '待审核', APPROVED: '已通过', PAYING: '打款中', PAID: '已到账',
|
||||
REJECTED: '已驳回', FAILED: '打款失败', CANCELLED: '已取消'
|
||||
};
|
||||
var ENTRY_TYPE = {
|
||||
PAYMENT_REWARD: '推广奖励', PAYMENT_REFUND_REVERSAL: '退款冲销', WITHDRAWAL_FREEZE: '提现冻结',
|
||||
WITHDRAWAL_CANCEL: '取消提现', WITHDRAWAL_REJECT: '提现驳回', WITHDRAWAL_PAID: '提现到账',
|
||||
WITHDRAWAL_FAILED: '提现失败'
|
||||
};
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function normalizeMoney(value) {
|
||||
var text = String(value === undefined || value === null ? '' : value).trim();
|
||||
return /^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,2})?$/.test(text) ? text : '';
|
||||
}
|
||||
|
||||
function createRequestId() {
|
||||
if (root.crypto && typeof root.crypto.randomUUID === 'function') return root.crypto.randomUUID();
|
||||
return 'pc-' + Date.now() + '-' + Math.random().toString(16).slice(2);
|
||||
}
|
||||
|
||||
function moneyToCents(value) {
|
||||
var text = normalizeMoney(value);
|
||||
var parts;
|
||||
|
||||
if (!text) return null;
|
||||
parts = text.split('.');
|
||||
return BigInt(parts[0]) * 100n + BigInt((parts[1] || '').padEnd(2, '0'));
|
||||
}
|
||||
|
||||
function buildWithdrawalBody(values, requestId) {
|
||||
var source = values || {};
|
||||
return {
|
||||
requestId: String(requestId || '').trim(),
|
||||
amount: normalizeMoney(source.amount),
|
||||
payoutQrOssId: String(source.payoutQrOssId || '').trim(),
|
||||
payoutAccountName: String(source.payoutAccountName || '').trim()
|
||||
};
|
||||
}
|
||||
|
||||
function validateWithdrawalBody(body, availableAmount) {
|
||||
if (!body.requestId) return '提现请求号生成失败,请刷新页面重试';
|
||||
if (!body.amount || Number(body.amount) <= 0) return '请输入有效提现金额,最多两位小数';
|
||||
if (moneyToCents(availableAmount) !== null && moneyToCents(body.amount) > moneyToCents(availableAmount)) {
|
||||
return '提现金额不能超过可用余额';
|
||||
}
|
||||
if (!/^[1-9][0-9]*$/.test(body.payoutQrOssId)) return '请上传收款码';
|
||||
if (!body.payoutAccountName) return '请填写收款人姓名';
|
||||
if (body.payoutAccountName.length > 64) return '收款人姓名不能超过 64 个字符';
|
||||
return '';
|
||||
}
|
||||
|
||||
function renderSummary(value) {
|
||||
var source = value || {};
|
||||
return '<div class="module-grid three"><div class="module-card"><h3>可用收益</h3><strong>¥' +
|
||||
escapeHtml(source.availableAmount || '0.00') + '</strong></div><div class="module-card"><h3>冻结金额</h3><strong>¥' +
|
||||
escapeHtml(source.frozenAmount || '0.00') + '</strong></div><div class="module-card"><h3>最低提现</h3><strong>' +
|
||||
escapeHtml(source.minimumWithdrawal ? '¥' + source.minimumWithdrawal : '以提交校验为准') + '</strong></div></div>';
|
||||
}
|
||||
|
||||
function renderLedger(page) {
|
||||
var rows = page && Array.isArray(page.rows) ? page.rows : [];
|
||||
if (!rows.length) return '<div class="api-empty">当前没有收益流水</div>';
|
||||
return rows.map(function (item) {
|
||||
return '<article class="module-row"><div><h3>' + escapeHtml(ENTRY_TYPE[item.entryType] || item.entryType || '收益变动') +
|
||||
'</h3><p>' + escapeHtml([item.createTime, item.remark].filter(Boolean).join(' · ')) +
|
||||
'</p></div><strong>' + escapeHtml(String(item.availableDelta || '0.00')) + '</strong></article>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderWithdrawals(page) {
|
||||
var rows = page && Array.isArray(page.rows) ? page.rows : [];
|
||||
if (!rows.length) return '<div class="api-empty">当前没有提现记录</div>';
|
||||
return rows.map(function (item) {
|
||||
var id = String(item.withdrawalId || '').trim();
|
||||
return '<article class="module-row"><div><h3>提现 ¥' + escapeHtml(item.amount || '0.00') +
|
||||
'</h3><p>' + escapeHtml([item.withdrawalNo, item.payoutAccountName, item.createTime, item.auditRemark, item.failureReason].filter(Boolean).join(' · ')) +
|
||||
'</p></div><span class="pill">' + escapeHtml(WITHDRAWAL_STATUS[item.withdrawalStatus] || item.withdrawalStatus || '未知') + '</span>' +
|
||||
(item.withdrawalStatus === 'PENDING' && /^[1-9][0-9]*$/.test(id) ? '<button class="btn outline" type="button" data-withdrawal-cancel="' + escapeHtml(id) + '">取消</button>' : '') + '</article>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function init() {
|
||||
var page = root.document && root.document.querySelector('[data-earning-page]');
|
||||
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
var summaryBox = page && page.querySelector('[data-earning-summary]');
|
||||
var ledgerBox = page && page.querySelector('[data-earning-ledger]');
|
||||
var withdrawalBox = page && page.querySelector('[data-earning-withdrawals]');
|
||||
var form = page && page.querySelector('[data-withdrawal-form]');
|
||||
var status = page && page.querySelector('[data-earning-status]');
|
||||
var availableAmount = '';
|
||||
var requestId = createRequestId();
|
||||
var pending = false;
|
||||
|
||||
if (!page) return;
|
||||
if (!api || !api.getToken || !api.getToken()) {
|
||||
if (root.location) root.location.href = 'login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
function setStatus(type, message) {
|
||||
if (type) root.ProfileUI.setApiState(status, type, message);
|
||||
else status.textContent = message || '';
|
||||
}
|
||||
|
||||
async function loadAll() {
|
||||
var values = await Promise.all([
|
||||
api.earningSummary(), api.earningLedger({ pageNum: 1, pageSize: 20 }),
|
||||
api.earningWithdrawals({ pageNum: 1, pageSize: 20 })
|
||||
]);
|
||||
availableAmount = String(values[0] && values[0].availableAmount || '');
|
||||
summaryBox.innerHTML = renderSummary(values[0]);
|
||||
ledgerBox.innerHTML = renderLedger(values[1]);
|
||||
withdrawalBox.innerHTML = renderWithdrawals(values[2]);
|
||||
form.querySelector('[data-withdrawal-submit]').disabled = values[0] && values[0].withdrawalEnabled === false;
|
||||
return values;
|
||||
}
|
||||
|
||||
try { await loadAll(); setStatus('', '收益数据已加载'); }
|
||||
catch (error) { setStatus('error', error.message || '收益数据读取失败'); return; }
|
||||
|
||||
form.addEventListener('submit', async function (event) {
|
||||
var body;
|
||||
var validation;
|
||||
event.preventDefault();
|
||||
if (pending) return;
|
||||
body = buildWithdrawalBody({
|
||||
amount: form.elements.amount.value,
|
||||
payoutAccountName: form.elements.payoutAccountName.value,
|
||||
payoutQrOssId: form.elements.payoutQrOssId.value
|
||||
}, requestId);
|
||||
validation = validateWithdrawalBody(body, availableAmount);
|
||||
if (validation) return setStatus('error', validation);
|
||||
pending = true; setStatus('loading', '正在提交提现申请…');
|
||||
try {
|
||||
await api.createEarningWithdrawal(body);
|
||||
requestId = createRequestId(); form.reset(); form.elements.payoutQrOssId.value = '';
|
||||
await loadAll(); setStatus('', '提现申请已提交');
|
||||
} catch (error) { setStatus('error', error.message || '提现申请失败'); }
|
||||
finally { pending = false; }
|
||||
});
|
||||
|
||||
withdrawalBox.addEventListener('click', async function (event) {
|
||||
var button = event.target.closest('[data-withdrawal-cancel]');
|
||||
if (!button || pending || !await confirmAction('确认取消这笔待审核提现吗?')) return;
|
||||
pending = true; setStatus('loading', '正在取消提现…');
|
||||
try { await api.cancelEarningWithdrawal(button.getAttribute('data-withdrawal-cancel')); await loadAll(); setStatus('', '提现已取消'); }
|
||||
catch (error) { setStatus('error', error.message || '取消提现失败'); }
|
||||
finally { pending = false; }
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
normalizeMoney: normalizeMoney,
|
||||
createRequestId: createRequestId,
|
||||
buildWithdrawalBody: buildWithdrawalBody,
|
||||
validateWithdrawalBody: validateWithdrawalBody,
|
||||
renderSummary: renderSummary,
|
||||
renderLedger: renderLedger,
|
||||
renderWithdrawals: renderWithdrawals,
|
||||
init: init
|
||||
};
|
||||
});
|
||||
@@ -13,6 +13,8 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
var MediaDisplay = root.MediaDisplay || (typeof require === 'function' ? require('./media-display.js') : null);
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/&/g, '&')
|
||||
@@ -48,13 +50,15 @@
|
||||
var expectedId = normalizeId(expectedGenealogyId);
|
||||
var memberCount = normalizeCount(source.memberCount);
|
||||
var personCount = normalizeCount(source.personCount);
|
||||
var coverFile = MediaDisplay && MediaDisplay.normalizeFileAccess(source.coverFile);
|
||||
var overview;
|
||||
|
||||
if (!genealogyId || genealogyId !== expectedId ||
|
||||
!text(source.genealogyName) || String(source.status) !== '0' ||
|
||||
memberCount === null || personCount === null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
overview = {
|
||||
genealogyId: genealogyId,
|
||||
genealogyNo: text(source.genealogyNo),
|
||||
genealogyName: text(source.genealogyName),
|
||||
@@ -67,6 +71,9 @@
|
||||
canManage: source.canManage === true,
|
||||
canEditContent: source.canEditContent === true
|
||||
};
|
||||
if (source.coverFile !== undefined && source.coverFile !== null && !coverFile) return null;
|
||||
if (coverFile) overview.coverFile = coverFile;
|
||||
return overview;
|
||||
}
|
||||
|
||||
function renderFamilyOverview(overview) {
|
||||
@@ -79,7 +86,10 @@
|
||||
source.regionFullName ? '地区 ' + source.regionFullName : ''
|
||||
].filter(Boolean).join(' · ');
|
||||
|
||||
return '<h3>' + escapeHtml(source.genealogyName) + '</h3><p>' +
|
||||
return (MediaDisplay ? MediaDisplay.renderImage(source.coverFile, {
|
||||
alt: source.genealogyName + '封面',
|
||||
className: 'family-cover-media'
|
||||
}) : '') + '<h3>' + escapeHtml(source.genealogyName) + '</h3><p>' +
|
||||
escapeHtml(details) + '</p><p>成员 ' + escapeHtml(source.memberCount) +
|
||||
' 人 · 世系人物 ' + escapeHtml(source.personCount) + ' 人</p>';
|
||||
}
|
||||
@@ -154,6 +164,7 @@
|
||||
var element = query('[data-family-home-status]');
|
||||
|
||||
if (!element) return;
|
||||
element.hidden = !message;
|
||||
if (!message) {
|
||||
element.innerHTML = '';
|
||||
return;
|
||||
@@ -239,7 +250,7 @@
|
||||
} else {
|
||||
query('[data-lineage-home-tree]').innerHTML = treeHtml;
|
||||
}
|
||||
setHomeStatus('家谱概览已更新');
|
||||
setHomeStatus('');
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
(function (root, factory) {
|
||||
if (typeof module === 'object' && module.exports) {
|
||||
module.exports = factory(root);
|
||||
return;
|
||||
}
|
||||
|
||||
root.FamilyInvitePages = factory(root);
|
||||
if (root.document) root.document.addEventListener('DOMContentLoaded', root.FamilyInvitePages.init);
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
var STATUS_TEXT = { ACTIVE: '待使用', REDEEMED: '已使用', REVOKED: '已撤销', EXPIRED: '已过期' };
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function normalizeId(value) {
|
||||
var text = String(value === undefined || value === null ? '' : value).trim();
|
||||
return /^[1-9][0-9]*$/.test(text) ? text : '';
|
||||
}
|
||||
|
||||
function normalizeInvitation(item) {
|
||||
var source = item || {};
|
||||
var inviteId = normalizeId(source.inviteId);
|
||||
var genealogyId = normalizeId(source.genealogyId);
|
||||
|
||||
if (!inviteId || !genealogyId || !String(source.genealogyName || '').trim()) return null;
|
||||
return {
|
||||
inviteId: inviteId,
|
||||
genealogyId: genealogyId,
|
||||
genealogyName: String(source.genealogyName).trim(),
|
||||
lineagePersonName: String(source.lineagePersonName || '').trim(),
|
||||
status: String(source.status || '').toUpperCase(),
|
||||
token: String(source.token || '').trim(),
|
||||
expiresAt: String(source.expiresAt || '').trim(),
|
||||
createTime: String(source.createTime || '').trim()
|
||||
};
|
||||
}
|
||||
|
||||
function invitationLink(token) {
|
||||
var base;
|
||||
|
||||
if (!token) return '';
|
||||
if (!root.location) return 'profile-invite.html?token=' + encodeURIComponent(token);
|
||||
base = String(root.location.href || '').split('?')[0].split('#')[0];
|
||||
return base + '?token=' + encodeURIComponent(token);
|
||||
}
|
||||
|
||||
function renderInvitations(items) {
|
||||
var list = Array.isArray(items) ? items.map(normalizeInvitation).filter(Boolean) : [];
|
||||
|
||||
if (!list.length) return '<div class="api-empty">还没有发出的邀请</div>';
|
||||
return list.map(function (item) {
|
||||
var canRevoke = item.status === 'ACTIVE';
|
||||
return '<article class="module-row"><div><h3>' + escapeHtml(item.genealogyName) +
|
||||
(item.lineagePersonName ? ' · ' + escapeHtml(item.lineagePersonName) : '') + '</h3><p>' +
|
||||
escapeHtml([item.createTime, item.expiresAt ? '有效期至 ' + item.expiresAt : ''].filter(Boolean).join(' · ')) +
|
||||
'</p></div><div class="row-actions"><span class="pill">' +
|
||||
escapeHtml(STATUS_TEXT[item.status] || item.status || '未知') + '</span>' +
|
||||
(canRevoke ? '<button class="btn outline" type="button" data-invite-revoke="' +
|
||||
escapeHtml(item.inviteId) + '">撤销</button>' : '') + '</div></article>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function redirectToLogin(api) {
|
||||
if (api && api.clearToken) api.clearToken();
|
||||
if (root.location) root.location.href = 'login.html';
|
||||
}
|
||||
|
||||
async function init() {
|
||||
var page = root.document && root.document.querySelector('[data-family-invite-page]');
|
||||
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
var genealogyId = root.ProfileUI && root.ProfileUI.getGenealogyId ? root.ProfileUI.getGenealogyId() : '';
|
||||
var form = page && page.querySelector('[data-invite-form]');
|
||||
var personSelect = page && page.querySelector('[data-invite-person]');
|
||||
var status = page && page.querySelector('[data-invite-status]');
|
||||
var result = page && page.querySelector('[data-invite-result]');
|
||||
var list = page && page.querySelector('[data-invite-list]');
|
||||
var redeem = page && page.querySelector('[data-invite-redeem]');
|
||||
var token = root.location ? new URLSearchParams(root.location.search).get('token') : '';
|
||||
var pending = false;
|
||||
|
||||
if (!page) return;
|
||||
if (!api || !api.getToken || !api.getToken()) return redirectToLogin(api);
|
||||
if (token && redeem) {
|
||||
form.hidden = true;
|
||||
list.hidden = true;
|
||||
redeem.hidden = false;
|
||||
redeem.querySelector('[data-invite-token]').textContent = token;
|
||||
try {
|
||||
var preview = normalizeInvitation(await api.previewGenealogyInvitation(token));
|
||||
if (!preview) throw new Error('邀请预览响应无效');
|
||||
redeem.querySelector('h3').textContent = '接受“' + preview.genealogyName + '”邀请';
|
||||
setStatus('', preview.expiresAt ? '邀请有效期至 ' + preview.expiresAt : '请确认是否加入该家谱');
|
||||
} catch (error) {
|
||||
setStatus('error', error.message || '邀请预览失败');
|
||||
redeem.querySelector('[data-invite-redeem-submit]').disabled = true;
|
||||
return;
|
||||
}
|
||||
redeem.querySelector('[data-invite-redeem-submit]').addEventListener('click', async function () {
|
||||
if (pending) return;
|
||||
pending = true;
|
||||
setStatus('loading', '正在接受邀请…');
|
||||
try {
|
||||
await api.redeemGenealogyInvitation({ token: token });
|
||||
setStatus('', '邀请已接受,你已加入该家谱。');
|
||||
} catch (error) {
|
||||
setStatus('error', error.message || '接受邀请失败');
|
||||
} finally { pending = false; }
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!genealogyId) {
|
||||
root.ProfileUI.setApiState(status, 'error', '缺少家谱上下文,请从“我的家谱”重新进入。');
|
||||
return;
|
||||
}
|
||||
|
||||
function setStatus(type, message) {
|
||||
if (type) root.ProfileUI.setApiState(status, type, message);
|
||||
else status.textContent = message || '';
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
var invitations = await api.myGenealogyInvitations();
|
||||
list.innerHTML = renderInvitations(invitations);
|
||||
}
|
||||
|
||||
try {
|
||||
var initial = await Promise.all([api.lineagePersonOptions(genealogyId), api.myGenealogyInvitations()]);
|
||||
personSelect.innerHTML = '<option value="">不指定人物,加入后再关联</option>' +
|
||||
(Array.isArray(initial[0]) ? initial[0] : []).map(function (item) {
|
||||
var id = normalizeId(item.personId || item.lineagePersonId);
|
||||
var name = String(item.displayName || item.personName || item.name || '').trim();
|
||||
return id && name ? '<option value="' + escapeHtml(id) + '">' + escapeHtml(name) + '</option>' : '';
|
||||
}).join('');
|
||||
list.innerHTML = renderInvitations(initial[1]);
|
||||
setStatus('', '可生成一次性邀请链接');
|
||||
} catch (error) {
|
||||
setStatus('error', error.message || '邀请服务读取失败');
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async function (event) {
|
||||
var body = {};
|
||||
var created;
|
||||
var link;
|
||||
|
||||
event.preventDefault();
|
||||
if (pending) return;
|
||||
if (personSelect.value) body.lineagePersonId = personSelect.value;
|
||||
pending = true;
|
||||
setStatus('loading', '正在生成邀请…');
|
||||
try {
|
||||
created = normalizeInvitation(await api.issueGenealogyInvitation(genealogyId, body));
|
||||
if (!created || !created.token) throw new Error('邀请已创建,但响应缺少一次性令牌');
|
||||
link = invitationLink(created.token);
|
||||
result.hidden = false;
|
||||
result.querySelector('[data-invite-link]').value = link;
|
||||
setStatus('', '邀请已生成,请及时复制给家人。');
|
||||
await loadList();
|
||||
} catch (error) {
|
||||
setStatus('error', error.message || '邀请生成失败');
|
||||
} finally { pending = false; }
|
||||
});
|
||||
|
||||
result.querySelector('[data-invite-copy]').addEventListener('click', async function () {
|
||||
var value = result.querySelector('[data-invite-link]').value;
|
||||
try {
|
||||
await root.navigator.clipboard.writeText(value);
|
||||
setStatus('', '邀请链接已复制');
|
||||
} catch (error) {
|
||||
result.querySelector('[data-invite-link]').select();
|
||||
setStatus('', '请按 Ctrl+C 复制邀请链接');
|
||||
}
|
||||
});
|
||||
|
||||
list.addEventListener('click', async function (event) {
|
||||
var button = event.target.closest('[data-invite-revoke]');
|
||||
if (!button || pending) return;
|
||||
pending = true;
|
||||
setStatus('loading', '正在撤销邀请…');
|
||||
try {
|
||||
await api.revokeGenealogyInvitation(button.getAttribute('data-invite-revoke'));
|
||||
await loadList();
|
||||
setStatus('', '邀请已撤销');
|
||||
} catch (error) { setStatus('error', error.message || '撤销邀请失败'); }
|
||||
finally { pending = false; }
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
normalizeInvitation: normalizeInvitation,
|
||||
invitationLink: invitationLink,
|
||||
renderInvitations: renderInvitations,
|
||||
init: init
|
||||
};
|
||||
});
|
||||
+28
-7
@@ -14,6 +14,15 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function confirmAction(message) {
|
||||
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
|
||||
return root.ProfileUI.confirmAction(message);
|
||||
}
|
||||
return Promise.resolve(!root['confirm'] || root['confirm'](message));
|
||||
}
|
||||
|
||||
var MediaDisplay = root.MediaDisplay || (typeof require === 'function' ? require('./media-display.js') : null);
|
||||
|
||||
var documentRef = root.document;
|
||||
var PAGE_SIZE = 20;
|
||||
var currentFeedPage = 1;
|
||||
@@ -339,7 +348,6 @@
|
||||
'publisherNickName',
|
||||
'publisherStatus',
|
||||
'feedType',
|
||||
'mediaOssIds',
|
||||
'likedByMe',
|
||||
'likeCount',
|
||||
'commentCount',
|
||||
@@ -351,6 +359,8 @@
|
||||
'createTime',
|
||||
'updateTime'
|
||||
]);
|
||||
var mediaFiles = MediaDisplay ? MediaDisplay.normalizeFileList(item && item.mediaFiles) : [];
|
||||
if (mediaFiles.length) feed.mediaFiles = mediaFiles;
|
||||
if (toId(item.genealogyId)) feed.genealogyId = toId(item.genealogyId);
|
||||
if (toId(item.publisherUserId)) feed.publisherUserId = toId(item.publisherUserId);
|
||||
return feed;
|
||||
@@ -438,7 +448,7 @@
|
||||
|
||||
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 mediaCount = Array.isArray(feed.mediaFiles) ? feed.mediaFiles.length : 0;
|
||||
var title = feed.publisherNickName || feed.genealogyName || '家族动态';
|
||||
var meta = [];
|
||||
var actions = '';
|
||||
@@ -458,11 +468,12 @@
|
||||
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>';
|
||||
actions += '<button class="pill is-danger" type="button" data-feed-delete="' + escapeHtml(feed.feedId) + '">删除</button>';
|
||||
}
|
||||
|
||||
return '<article class="module-row feed-row" data-feed-id="' + escapeHtml(feed.feedId) + '">' +
|
||||
'<div><h3>' + escapeHtml(title) + '</h3><p>' + escapeHtml(feed.feedContent) + '</p>' +
|
||||
(MediaDisplay ? MediaDisplay.renderGallery(feed.mediaFiles, { label: '动态附件', className: 'feed-media-gallery' }) : '') +
|
||||
'<p class="module-meta">' + escapeHtml(meta.join(' · ')) + '</p></div>' +
|
||||
'<div class="row-actions">' + actions + '</div></article>';
|
||||
}
|
||||
@@ -520,7 +531,7 @@
|
||||
if (!comment.userDeleted) {
|
||||
actions += '<button class="pill" type="button" data-feed-comment-reply="' + 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>';
|
||||
actions += '<button class="pill is-danger" type="button" data-feed-comment-delete="' + escapeHtml(comment.commentId) + '" data-feed-id="' + escapeHtml(feedId) + '">删除评论</button>';
|
||||
}
|
||||
}
|
||||
if (hasReplies(comment)) {
|
||||
@@ -671,11 +682,12 @@
|
||||
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>';
|
||||
actions += '<button class="pill is-danger" 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>' +
|
||||
(MediaDisplay ? MediaDisplay.renderGallery(feed.mediaFiles, { label: '动态附件', className: 'feed-media-gallery' }) : '') +
|
||||
'<p class="module-meta">' +
|
||||
escapeHtml([
|
||||
feed.createTime || '',
|
||||
@@ -744,6 +756,15 @@
|
||||
var api = getApi();
|
||||
var genealogyId;
|
||||
var feedId = getCurrentFeedId();
|
||||
var isEditing = Boolean(feedId);
|
||||
var editorTitle = isEditing ? '编辑动态' : '发布动态';
|
||||
|
||||
if (root.document) root.document.title = editorTitle + ' - 个人中心 - 代代相传';
|
||||
if (query('[data-feed-editor-kicker]')) query('[data-feed-editor-kicker]').textContent = editorTitle;
|
||||
if (query('[data-feed-editor-title]')) query('[data-feed-editor-title]').textContent = editorTitle;
|
||||
queryAll('[data-feed-editor-action]').forEach(function (button) {
|
||||
button.textContent = isEditing ? '保存修改' : '发布动态';
|
||||
});
|
||||
|
||||
if (redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
@@ -905,7 +926,7 @@
|
||||
if (redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId || !feedId) return;
|
||||
if (root.confirm && !root.confirm('确认删除这条动态吗?')) return;
|
||||
if (!await confirmAction('确认删除这条动态吗?')) return;
|
||||
return withActionLock('delete:' + feedId, async function () {
|
||||
try {
|
||||
await api.deleteFeed(genealogyId, feedId);
|
||||
@@ -1044,7 +1065,7 @@
|
||||
if (redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId || !feedId || !commentId) return;
|
||||
if (root.confirm && !root.confirm('确认删除这条评论吗?')) return;
|
||||
if (!await confirmAction('确认删除这条评论吗?')) return;
|
||||
return withActionLock('delete-comment:' + commentId, async function () {
|
||||
try {
|
||||
await api.deleteFeedComment(genealogyId, feedId, commentId);
|
||||
|
||||
+47
-13
@@ -13,12 +13,7 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
var FEEDBACK_TYPES = {
|
||||
advice: '意见建议',
|
||||
bug: '问题反馈',
|
||||
complaint: '投诉反馈',
|
||||
other: '其他反馈'
|
||||
};
|
||||
var FEEDBACK_OPTION_STATES = ['ACTIVE', 'DISABLED', 'UNKNOWN'];
|
||||
var HANDLE_STATUS = {
|
||||
'0': '待处理',
|
||||
'1': '处理中',
|
||||
@@ -68,26 +63,41 @@
|
||||
return body;
|
||||
}
|
||||
|
||||
function validateFeedbackBody(body) {
|
||||
function validateFeedbackBody(body, allowedFeedbackTypes) {
|
||||
var source = body || {};
|
||||
var allowedTypes = Array.isArray(allowedFeedbackTypes) ? allowedFeedbackTypes : [];
|
||||
|
||||
if (!optionalText(source.feedbackContent)) return '请填写反馈内容';
|
||||
if (source.feedbackType && !Object.prototype.hasOwnProperty.call(FEEDBACK_TYPES, source.feedbackType)) {
|
||||
if (source.feedbackType && allowedTypes.length && allowedTypes.indexOf(source.feedbackType) < 0) {
|
||||
return '反馈类型无效';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function normalizeFeedbackTypeOptions(data) {
|
||||
if (!Array.isArray(data)) return [];
|
||||
return data.reduce(function (options, item) {
|
||||
var source = item || {};
|
||||
var value = optionalText(source.value);
|
||||
var label = optionalText(source.label);
|
||||
|
||||
if (value && label && source.enabled === true) options.push({ value: value, label: label });
|
||||
return options;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function normalizeFeedback(item) {
|
||||
var source = item || {};
|
||||
var feedbackId = normalizeId(source.feedbackId);
|
||||
var feedbackType = optionalText(source.feedbackType);
|
||||
var feedbackTypeLabel = optionalText(source.feedbackTypeLabel) || feedbackType;
|
||||
var feedbackTypeOptionState = optionalText(source.feedbackTypeOptionState);
|
||||
var feedbackContent = optionalText(source.feedbackContent);
|
||||
var handleStatus = String(source.handleStatus === undefined || source.handleStatus === null ? '' : source.handleStatus);
|
||||
var status = String(source.status === undefined || source.status === null ? '' : source.status);
|
||||
|
||||
if (!feedbackId || !feedbackContent ||
|
||||
!Object.prototype.hasOwnProperty.call(FEEDBACK_TYPES, feedbackType) ||
|
||||
if (!feedbackId || !feedbackContent || !feedbackType || !feedbackTypeLabel ||
|
||||
FEEDBACK_OPTION_STATES.indexOf(feedbackTypeOptionState) < 0 ||
|
||||
!Object.prototype.hasOwnProperty.call(HANDLE_STATUS, handleStatus) ||
|
||||
['0', '1'].indexOf(status) < 0) {
|
||||
return null;
|
||||
@@ -95,6 +105,8 @@
|
||||
return {
|
||||
feedbackId: feedbackId,
|
||||
feedbackType: feedbackType,
|
||||
feedbackTypeLabel: feedbackTypeLabel,
|
||||
feedbackTypeOptionState: feedbackTypeOptionState,
|
||||
feedbackContent: feedbackContent,
|
||||
contactInfo: optionalText(source.contactInfo),
|
||||
handleStatus: handleStatus,
|
||||
@@ -142,7 +154,7 @@
|
||||
return items.map(function (item) {
|
||||
return '<a class="module-row" href="' +
|
||||
escapeHtml(buildFeedbackDetailUrl(item.feedbackId, detailPage)) +
|
||||
'"><div><h3>' + escapeHtml(FEEDBACK_TYPES[item.feedbackType]) +
|
||||
'"><div><h3>' + escapeHtml(item.feedbackTypeLabel) +
|
||||
'</h3><p>' + escapeHtml(item.feedbackContent) +
|
||||
'</p></div><span class="pill">' + HANDLE_STATUS[item.handleStatus] +
|
||||
'</span></a>';
|
||||
@@ -163,7 +175,7 @@
|
||||
].filter(Boolean).join('');
|
||||
|
||||
return '<article class="module-panel"><div class="module-kicker">' +
|
||||
escapeHtml(FEEDBACK_TYPES[feedback.feedbackType]) +
|
||||
escapeHtml(feedback.feedbackTypeLabel) +
|
||||
'</div><h2>问题说明</h2><p>' + escapeHtml(feedback.feedbackContent) +
|
||||
'</p><dl class="detail-list">' + details + '</dl></article>';
|
||||
}
|
||||
@@ -225,6 +237,18 @@
|
||||
button.textContent = pending ? label : button.dataset.defaultLabel;
|
||||
}
|
||||
|
||||
async function loadFeedbackTypeOptions(api, select) {
|
||||
var options = normalizeFeedbackTypeOptions(await api.businessDictionary('gen_feedback_type'));
|
||||
|
||||
if (!options.length) throw new Error('反馈类型选项响应无效');
|
||||
if (select) {
|
||||
select.innerHTML = '<option value="">请选择反馈类型(可选)</option>' + options.map(function (item) {
|
||||
return '<option value="' + escapeHtml(item.value) + '">' + escapeHtml(item.label) + '</option>';
|
||||
}).join('');
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
async function readMyFeedback(api) {
|
||||
var data = await api.myFeedback();
|
||||
var items = normalizeFeedbackList(data);
|
||||
@@ -242,6 +266,8 @@
|
||||
var page = root.document && root.document.querySelector('[data-feedback-page]');
|
||||
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
var pageMode = page && page.getAttribute('data-feedback-page');
|
||||
var feedbackTypeSelect = form && form.querySelector('[data-feedback-type-options]');
|
||||
var allowedFeedbackTypes = [];
|
||||
var writePending = false;
|
||||
|
||||
if (!form) return;
|
||||
@@ -249,6 +275,13 @@
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
allowedFeedbackTypes = (await loadFeedbackTypeOptions(api, feedbackTypeSelect)).map(function (item) {
|
||||
return item.value;
|
||||
});
|
||||
} catch (error) {
|
||||
if (feedbackTypeSelect) feedbackTypeSelect.innerHTML = '<option value="">暂不选择反馈类型</option>';
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
var items;
|
||||
@@ -290,7 +323,7 @@
|
||||
event.preventDefault();
|
||||
if (writePending) return;
|
||||
body = buildFeedbackBody(getFormValues(form));
|
||||
validation = validateFeedbackBody(body);
|
||||
validation = validateFeedbackBody(body, allowedFeedbackTypes);
|
||||
if (validation) {
|
||||
setStatus('error', validation);
|
||||
return;
|
||||
@@ -416,6 +449,7 @@
|
||||
getFeedbackId: getFeedbackId,
|
||||
buildFeedbackBody: buildFeedbackBody,
|
||||
validateFeedbackBody: validateFeedbackBody,
|
||||
normalizeFeedbackTypeOptions: normalizeFeedbackTypeOptions,
|
||||
normalizeFeedback: normalizeFeedback,
|
||||
normalizeFeedbackList: normalizeFeedbackList,
|
||||
findFeedbackById: findFeedbackById,
|
||||
|
||||
@@ -352,14 +352,19 @@
|
||||
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
var target = getTargetPage();
|
||||
var container = root.document && root.document.querySelector('[data-genealogy-list="mine"]');
|
||||
var sortSelect = root.document && root.document.querySelector('[data-genealogy-sort-select]');
|
||||
var sortUp = root.document && root.document.querySelector('[data-genealogy-sort-up]');
|
||||
var sortDown = root.document && root.document.querySelector('[data-genealogy-sort-down]');
|
||||
var sortSave = root.document && root.document.querySelector('[data-genealogy-sort-save]');
|
||||
var result;
|
||||
var sortPending = false;
|
||||
|
||||
if (!status) return;
|
||||
if (shouldRedirectToLogin(api)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
if (!api || !api.genealogyQuota || !api.genealogiesMine) {
|
||||
if (!api || !api.genealogyQuota || !api.genealogiesMine || !api.saveGenealogyOrder) {
|
||||
setEntryStatus('家谱入口接口初始化失败,请刷新后重试。', 'error');
|
||||
setGenealogyListState(container, 'error', '家谱列表接口初始化失败,请刷新后重试。');
|
||||
return;
|
||||
@@ -369,6 +374,15 @@
|
||||
try {
|
||||
result = await api.genealogiesMine();
|
||||
renderGenealogies(container, result, target);
|
||||
if (sortSelect) {
|
||||
var sortItems = Array.isArray(result) ? result.map(normalizeGenealogy).filter(Boolean) : [];
|
||||
sortSelect.innerHTML = sortItems.map(function (item) {
|
||||
return '<option value="' + escapeHtml(item.genealogyId) + '">' + escapeHtml(item.genealogyName) + '</option>';
|
||||
}).join('');
|
||||
[sortSelect, sortUp, sortDown, sortSave].forEach(function (control) {
|
||||
if (control) control.disabled = sortItems.length < 2;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
@@ -380,6 +394,41 @@
|
||||
getApiStateType(error) === 'forbidden' ? '当前账号无权查看我的家谱。' : '无法读取我的家谱,请稍后重试。'
|
||||
);
|
||||
}
|
||||
|
||||
function moveSelected(offset) {
|
||||
var index;
|
||||
var targetIndex;
|
||||
var option;
|
||||
|
||||
if (!sortSelect || sortPending) return;
|
||||
index = sortSelect.selectedIndex;
|
||||
targetIndex = index + offset;
|
||||
if (index < 0 || targetIndex < 0 || targetIndex >= sortSelect.options.length) return;
|
||||
option = sortSelect.options[index];
|
||||
if (offset < 0) sortSelect.insertBefore(option, sortSelect.options[targetIndex]);
|
||||
else sortSelect.insertBefore(sortSelect.options[targetIndex], option);
|
||||
sortSelect.selectedIndex = targetIndex;
|
||||
}
|
||||
|
||||
if (sortUp) sortUp.addEventListener('click', function () { moveSelected(-1); });
|
||||
if (sortDown) sortDown.addEventListener('click', function () { moveSelected(1); });
|
||||
if (sortSave) sortSave.addEventListener('click', async function () {
|
||||
if (sortPending || !sortSelect) return;
|
||||
sortPending = true;
|
||||
[sortSelect, sortUp, sortDown, sortSave].forEach(function (control) { if (control) control.disabled = true; });
|
||||
setEntryStatus('正在保存家谱顺序…', 'loading');
|
||||
try {
|
||||
await api.saveGenealogyOrder(Array.prototype.map.call(sortSelect.options, function (option) { return option.value; }));
|
||||
setEntryStatus('家谱顺序已保存');
|
||||
} catch (error) {
|
||||
setEntryStatus(error.message || '家谱顺序保存失败,请稍后重试。', getApiStateType(error));
|
||||
} finally {
|
||||
sortPending = false;
|
||||
[sortSelect, sortUp, sortDown, sortSave].forEach(function (control) {
|
||||
if (control) control.disabled = sortSelect.options.length < 2;
|
||||
});
|
||||
}
|
||||
});
|
||||
try {
|
||||
result = await api.genealogyQuota();
|
||||
setEntryStatus(buildStatus(result));
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
(function (root, factory) {
|
||||
if (typeof module === 'object' && module.exports) {
|
||||
module.exports = factory(root);
|
||||
return;
|
||||
}
|
||||
root.GenealogyLifecyclePages = factory(root);
|
||||
if (root.document) root.document.addEventListener('DOMContentLoaded', root.GenealogyLifecyclePages.init);
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function confirmAction(message) {
|
||||
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
|
||||
return root.ProfileUI.confirmAction(message);
|
||||
}
|
||||
return Promise.resolve(!root['confirm'] || root['confirm'](message));
|
||||
}
|
||||
|
||||
var DISABLED_REASON_TEXT = {
|
||||
NOT_OWNER: '只有谱主可以永久删除家谱',
|
||||
NO_VERIFIED_MOBILE: '谱主账号尚未绑定已验证手机号',
|
||||
ACTIVE_PAYMENT: '当前家谱存在未完成的支付单',
|
||||
DELETION_IN_PROGRESS: '永久删除任务正在处理中'
|
||||
};
|
||||
|
||||
function normalizeCapability(value) {
|
||||
var source = value || {};
|
||||
return {
|
||||
canDeletePermanently: source.canDeletePermanently === true,
|
||||
verifiedMobileMasked: String(source.verifiedMobileMasked || '').trim(),
|
||||
disabledReasons: Array.isArray(source.disabledReasons) ? source.disabledReasons.map(String) : []
|
||||
};
|
||||
}
|
||||
|
||||
function capabilityMessage(capability) {
|
||||
var data = normalizeCapability(capability);
|
||||
if (data.canDeletePermanently) {
|
||||
return data.verifiedMobileMasked ? '验证码将发送至 ' + data.verifiedMobileMasked : '当前账号具备永久删除资格';
|
||||
}
|
||||
return data.disabledReasons.map(function (code) {
|
||||
return DISABLED_REASON_TEXT[code] || code;
|
||||
}).join(';') || '当前家谱不允许永久删除';
|
||||
}
|
||||
|
||||
async function init() {
|
||||
var page = root.document && root.document.querySelector('[data-genealogy-lifecycle-page]');
|
||||
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
var genealogyId = root.ProfileUI && root.ProfileUI.getGenealogyId ? root.ProfileUI.getGenealogyId() : '';
|
||||
var status = page && page.querySelector('[data-lifecycle-status]');
|
||||
var archive = page && page.querySelector('[data-genealogy-archive]');
|
||||
var restore = page && page.querySelector('[data-genealogy-restore]');
|
||||
var deleteForm = page && page.querySelector('[data-genealogy-delete-form]');
|
||||
var sendCode = page && page.querySelector('[data-genealogy-delete-code]');
|
||||
var submitDelete = page && page.querySelector('[data-genealogy-delete-submit]');
|
||||
var nameInput = page && page.querySelector('[data-genealogy-delete-name]');
|
||||
var codeInput = page && page.querySelector('[data-genealogy-delete-sms]');
|
||||
var capability;
|
||||
var pending = false;
|
||||
|
||||
if (!page) return;
|
||||
if (!api || !api.getToken || !api.getToken()) {
|
||||
if (root.location) root.location.href = 'login.html';
|
||||
return;
|
||||
}
|
||||
if (!genealogyId) {
|
||||
root.ProfileUI.setApiState(status, 'error', '缺少家谱上下文,请从“我的家谱”重新进入。');
|
||||
return;
|
||||
}
|
||||
|
||||
function setStatus(type, message) {
|
||||
if (type) root.ProfileUI.setApiState(status, type, message);
|
||||
else status.textContent = message || '';
|
||||
}
|
||||
|
||||
function setPending(value) {
|
||||
pending = value;
|
||||
[archive, restore, sendCode, submitDelete].forEach(function (button) {
|
||||
if (button) button.disabled = value ||
|
||||
((button === submitDelete || button === sendCode) && capability && !capability.canDeletePermanently);
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
var initial = await Promise.all([api.genealogyDetail(genealogyId), api.genealogyDeletionCapability(genealogyId)]);
|
||||
capability = normalizeCapability(initial[1]);
|
||||
nameInput.placeholder = '请输入“' + String(initial[0] && initial[0].genealogyName || '家谱全名') + '”确认';
|
||||
submitDelete.disabled = !capability.canDeletePermanently;
|
||||
sendCode.disabled = !capability.canDeletePermanently;
|
||||
setStatus(capability.canDeletePermanently ? '' : 'forbidden', capabilityMessage(capability));
|
||||
} catch (error) {
|
||||
setStatus('error', error.message || '家谱维护能力读取失败');
|
||||
return;
|
||||
}
|
||||
|
||||
archive.addEventListener('click', async function () {
|
||||
if (pending || !await confirmAction('归档后家谱将停止日常维护,确认归档吗?')) return;
|
||||
setPending(true); setStatus('loading', '正在归档家谱…');
|
||||
try { await api.archiveGenealogy(genealogyId); setStatus('', '家谱已归档,需要时可在此恢复。'); }
|
||||
catch (error) { setStatus('error', error.message || '归档失败'); }
|
||||
finally { setPending(false); }
|
||||
});
|
||||
|
||||
restore.addEventListener('click', async function () {
|
||||
if (pending) return;
|
||||
setPending(true); setStatus('loading', '正在恢复家谱…');
|
||||
try { await api.restoreGenealogy(genealogyId); setStatus('', '家谱已恢复。'); }
|
||||
catch (error) { setStatus('error', error.message || '恢复失败'); }
|
||||
finally { setPending(false); }
|
||||
});
|
||||
|
||||
sendCode.addEventListener('click', async function () {
|
||||
if (pending || !capability.canDeletePermanently) return;
|
||||
setPending(true); setStatus('loading', '正在发送验证码…');
|
||||
try { await api.sendGenealogyDeletionCode(genealogyId, {}); setStatus('', '验证码已发送至谱主手机号。'); }
|
||||
catch (error) { setStatus('error', error.message || '验证码发送失败'); }
|
||||
finally { setPending(false); }
|
||||
});
|
||||
|
||||
deleteForm.addEventListener('submit', async function (event) {
|
||||
event.preventDefault();
|
||||
if (pending || !capability.canDeletePermanently) return;
|
||||
if (!nameInput.value.trim() || !codeInput.value.trim()) {
|
||||
setStatus('error', '请填写家谱全名和短信验证码');
|
||||
return;
|
||||
}
|
||||
if (!await confirmAction('永久删除不可恢复,确认继续吗?')) return;
|
||||
setPending(true); setStatus('loading', '正在提交永久删除任务…');
|
||||
try {
|
||||
await api.permanentlyDeleteGenealogy(genealogyId, {
|
||||
genealogyName: nameInput.value.trim(), smsCode: codeInput.value.trim()
|
||||
});
|
||||
if (root.ProfileUI && root.ProfileUI.clearCurrentGenealogyContext) root.ProfileUI.clearCurrentGenealogyContext();
|
||||
if (root.location) root.location.href = 'profile-families.html';
|
||||
} catch (error) { setStatus('error', error.message || '永久删除提交失败'); }
|
||||
finally { setPending(false); }
|
||||
});
|
||||
}
|
||||
|
||||
return { normalizeCapability: normalizeCapability, capabilityMessage: capabilityMessage, init: init };
|
||||
});
|
||||
@@ -14,6 +14,13 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function confirmAction(message) {
|
||||
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
|
||||
return root.ProfileUI.confirmAction(message);
|
||||
}
|
||||
return Promise.resolve(!root['confirm'] || root['confirm'](message));
|
||||
}
|
||||
|
||||
var documentRef = root.document;
|
||||
var poemsById = Object.create(null);
|
||||
var lastPreviewSignature = '';
|
||||
@@ -695,7 +702,7 @@
|
||||
setBatchStatus('请先预览当前内容,再保存');
|
||||
return;
|
||||
}
|
||||
if (root.confirm && !root.confirm(buildBatchConfirmMessage(body.disableMissing))) return;
|
||||
if (!await confirmAction(buildBatchConfirmMessage(body.disableMissing))) return;
|
||||
setBatchStatus('正在保存…');
|
||||
setWritePending(true);
|
||||
try {
|
||||
|
||||
+28
-10
@@ -13,6 +13,15 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function confirmAction(message) {
|
||||
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
|
||||
return root.ProfileUI.confirmAction(message);
|
||||
}
|
||||
return Promise.resolve(!root['confirm'] || root['confirm'](message));
|
||||
}
|
||||
|
||||
var MediaDisplay = root.MediaDisplay || (typeof require === 'function' ? require('./media-display.js') : null);
|
||||
|
||||
var documentRef = root.document;
|
||||
var recordsById = Object.create(null);
|
||||
var canEditContent = false;
|
||||
@@ -161,14 +170,13 @@
|
||||
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 mediaFiles = MediaDisplay ? MediaDisplay.normalizeFileList(item && item.mediaFiles) : [];
|
||||
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,
|
||||
@@ -185,10 +193,10 @@
|
||||
recordContent: stringValue(item.recordContent),
|
||||
recordDate: stringValue(item.recordDate),
|
||||
remindTime: stringValue(item.remindTime),
|
||||
mediaOssIds: mediaOssIds,
|
||||
status: status,
|
||||
remark: stringValue(item.remark)
|
||||
};
|
||||
if (mediaFiles.length) record.mediaFiles = mediaFiles;
|
||||
if (appUserId) record.appUserId = appUserId;
|
||||
if (lineagePersonId) record.lineagePersonId = lineagePersonId;
|
||||
if (sortOrder !== undefined) record.sortOrder = sortOrder;
|
||||
@@ -238,8 +246,8 @@
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function attachmentCount(mediaOssIds) {
|
||||
return mediaOssIds ? mediaOssIds.split(',').filter(Boolean).length : 0;
|
||||
function attachmentCount(mediaFiles) {
|
||||
return Array.isArray(mediaFiles) ? mediaFiles.length : 0;
|
||||
}
|
||||
|
||||
function buildListUrl(genealogyId, recordId) {
|
||||
@@ -285,14 +293,15 @@
|
||||
var actions = '';
|
||||
|
||||
if (!record) return stateHtml('empty', '请选择一条成长记录查看详情');
|
||||
count = attachmentCount(record.mediaOssIds);
|
||||
count = attachmentCount(record.mediaFiles);
|
||||
if (canEditContent) {
|
||||
actions = '<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="' +
|
||||
'<button class="btn danger" type="button" data-growth-delete-id="' +
|
||||
escapeHtml(record.recordId) + '">删除记录</button></div>';
|
||||
}
|
||||
return '<div class="form-like growth-detail">' +
|
||||
return (MediaDisplay ? MediaDisplay.renderGallery(record.mediaFiles, { label: '成长记录附件' }) : '') +
|
||||
'<div class="form-like growth-detail">' +
|
||||
detailValue('标题', record.recordTitle) +
|
||||
detailValue('关联人物', record.lineagePersonName || record.lineagePersonNo) +
|
||||
detailValue('记录类型', record.recordType) +
|
||||
@@ -664,7 +673,7 @@
|
||||
var record = recordsById[recordId];
|
||||
|
||||
if (writePending || !canEditContent || !genealogyId || !normalizeId(recordId) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认删除“' + (record ? record.recordTitle : '该成长记录') + '”吗?删除后不可恢复,并会释放附件引用。')) return;
|
||||
if (!await confirmAction('确认删除“' + (record ? record.recordTitle : '该成长记录') + '”吗?删除后不可恢复,并会释放附件引用。')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
await api.deleteGrowthRecord(genealogyId, recordId);
|
||||
@@ -694,10 +703,19 @@
|
||||
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]');
|
||||
var recordId;
|
||||
var genealogyId;
|
||||
|
||||
if (detailButton) {
|
||||
event.preventDefault();
|
||||
loadGrowthDetail(detailButton.getAttribute('data-growth-detail-id'));
|
||||
recordId = detailButton.getAttribute('data-growth-detail-id');
|
||||
genealogyId = requireGenealogyId();
|
||||
if (root.history && genealogyId) root.history.replaceState(null, '', buildListUrl(genealogyId, recordId));
|
||||
loadGrowthDetail(recordId).then(function (record) {
|
||||
if (record && root.ProfileUI && root.ProfileUI.revealDetail) {
|
||||
root.ProfileUI.revealDetail(query('[data-growth-detail-heading]'));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (deleteButton) {
|
||||
|
||||
@@ -13,6 +13,13 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function confirmAction(message) {
|
||||
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
|
||||
return root.ProfileUI.confirmAction(message);
|
||||
}
|
||||
return Promise.resolve(!root['confirm'] || root['confirm'](message));
|
||||
}
|
||||
|
||||
var APPLY_STATUS = {
|
||||
'0': '待审核',
|
||||
'1': '已通过',
|
||||
@@ -403,7 +410,7 @@
|
||||
if (!button || !list.contains(button) || writePending) return;
|
||||
applyId = normalizeId(button.getAttribute('data-join-cancel-id'));
|
||||
if (!applyId) return;
|
||||
if (root.confirm && !root.confirm('确认撤销这条加入申请吗?')) return;
|
||||
if (!await confirmAction('确认撤销这条加入申请吗?')) return;
|
||||
writePending = true;
|
||||
button.disabled = true;
|
||||
setText('[data-my-join-applies-status]', '正在撤销申请…', 'loading');
|
||||
|
||||
@@ -14,6 +14,15 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function confirmAction(message) {
|
||||
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
|
||||
return root.ProfileUI.confirmAction(message);
|
||||
}
|
||||
return Promise.resolve(!root['confirm'] || root['confirm'](message));
|
||||
}
|
||||
|
||||
var MediaDisplay = root.MediaDisplay || (typeof require === 'function' ? require('./media-display.js') : null);
|
||||
|
||||
var documentRef = root.document;
|
||||
var selectedPersonId = '';
|
||||
var editingPersonId = '';
|
||||
@@ -200,7 +209,7 @@
|
||||
].forEach(function (field) {
|
||||
if (item && item[field] !== undefined && item[field] !== null) person[field] = String(item[field]);
|
||||
});
|
||||
['appUserId', 'genealogyId', 'fatherId', 'motherId', 'avatarOssId'].forEach(function (field) {
|
||||
['appUserId', 'genealogyId', 'fatherId', 'motherId'].forEach(function (field) {
|
||||
if (item && item[field] !== undefined && item[field] !== null) {
|
||||
var value = normalizeId(item[field]);
|
||||
|
||||
@@ -212,6 +221,10 @@
|
||||
}
|
||||
});
|
||||
if (invalidId) return null;
|
||||
if (item && item.avatarFile !== undefined && item.avatarFile !== null) {
|
||||
person.avatarFile = MediaDisplay && MediaDisplay.normalizeFileAccess(item.avatarFile);
|
||||
if (!person.avatarFile) 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;
|
||||
@@ -315,6 +328,7 @@
|
||||
var selected = person.personId === selectedPersonId ? ' is-selected' : '';
|
||||
|
||||
return '<button class="module-row lineage-row' + selected + '" type="button" data-lineage-person="' + escapeHtml(person.personId) + '">' +
|
||||
(MediaDisplay ? MediaDisplay.renderAvatar(person.avatarFile, { name: person.name, className: 'lineage-avatar-media' }) : '') +
|
||||
'<div><h3>' + escapeHtml(person.name) + '</h3><p>' + escapeHtml(getPersonSummary(person)) + '</p></div>' +
|
||||
'<span class="pill">查看</span></button>';
|
||||
}).join('');
|
||||
@@ -338,6 +352,7 @@
|
||||
if (spouses) branches += '<ul><li><span>配偶</span><ul>' + spouses + '</ul></li></ul>';
|
||||
if (children) branches += '<ul><li><span>子女</span><ul>' + children + '</ul></li></ul>';
|
||||
return '<li><button class="lineage-node" type="button" data-lineage-person="' + escapeHtml(person.personId) + '">' +
|
||||
(MediaDisplay ? MediaDisplay.renderAvatar(person.avatarFile, { name: person.name, className: 'lineage-node-avatar' }) : '') +
|
||||
'<strong>' + escapeHtml(person.name) + '</strong><span>' + escapeHtml(getPersonSummary(person)) + '</span></button>' + branches + '</li>';
|
||||
}
|
||||
|
||||
@@ -630,6 +645,9 @@
|
||||
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-relation]').forEach(function (control) {
|
||||
control.disabled = writeDisabled || !selectedPersonId;
|
||||
});
|
||||
queryAll('[data-lineage-search], [data-lineage-person-options]').forEach(function (control) {
|
||||
control.disabled = writePending;
|
||||
});
|
||||
@@ -906,7 +924,7 @@
|
||||
if (writePending || !managementAccess || redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId || !personId) return;
|
||||
if (root.confirm && !root.confirm('确认停用“' + (person ? person.name : '该成员') + '”吗?有正常子女时后端会拒绝停用。')) return;
|
||||
if (!await confirmAction('确认停用“' + (person ? person.name : '该成员') + '”吗?有正常子女时后端会拒绝停用。')) return;
|
||||
setWritePending(true);
|
||||
setActionStatus('正在停用成员…', 'loading');
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
(function (root, factory) {
|
||||
if (typeof module === 'object' && module.exports) {
|
||||
module.exports = factory(root);
|
||||
return;
|
||||
}
|
||||
|
||||
root.MediaDisplay = factory(root);
|
||||
if (root.document) {
|
||||
root.document.addEventListener('DOMContentLoaded', function () {
|
||||
root.MediaDisplay.init(root.document);
|
||||
});
|
||||
}
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function stringValue(value) {
|
||||
return value === undefined || value === null ? '' : String(value);
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return stringValue(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function normalizeId(value) {
|
||||
var text;
|
||||
|
||||
if (value === undefined || value === null || value === '') return '';
|
||||
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
|
||||
text = String(value).trim();
|
||||
return /^[1-9][0-9]*$/.test(text) ? text : '';
|
||||
}
|
||||
|
||||
function normalizeAccessUrl(value) {
|
||||
var url;
|
||||
|
||||
try {
|
||||
url = new URL(stringValue(value).trim());
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
return url.protocol === 'https:' || url.protocol === 'http:' ? url.href : '';
|
||||
}
|
||||
|
||||
function normalizeFileAccess(value) {
|
||||
var source = value || {};
|
||||
var accessUrl = normalizeAccessUrl(source.accessUrl);
|
||||
var fileId = normalizeId(source.fileId);
|
||||
var fileSize;
|
||||
var normalized;
|
||||
|
||||
if (!accessUrl) return null;
|
||||
if (source.fileId !== undefined && source.fileId !== null && source.fileId !== '' && !fileId) return null;
|
||||
normalized = {
|
||||
fileId: fileId,
|
||||
fileName: stringValue(source.fileName).trim(),
|
||||
mediaType: stringValue(source.mediaType).trim().toLowerCase(),
|
||||
accessUrl: accessUrl,
|
||||
expiresAt: stringValue(source.expiresAt).trim()
|
||||
};
|
||||
fileSize = Number(source.fileSize);
|
||||
if (source.fileSize !== undefined && source.fileSize !== null && source.fileSize !== '' &&
|
||||
Number.isSafeInteger(fileSize) && fileSize >= 0) {
|
||||
normalized.fileSize = fileSize;
|
||||
}
|
||||
|
||||
if (!normalized.fileId) delete normalized.fileId;
|
||||
if (!normalized.fileName) delete normalized.fileName;
|
||||
if (!normalized.mediaType) delete normalized.mediaType;
|
||||
if (!normalized.expiresAt) delete normalized.expiresAt;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeFileList(values) {
|
||||
if (!Array.isArray(values)) return [];
|
||||
return values.map(normalizeFileAccess).filter(Boolean);
|
||||
}
|
||||
|
||||
function classNames(base, value) {
|
||||
var additions = stringValue(value).split(/\s+/).filter(function (name) {
|
||||
return /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name);
|
||||
});
|
||||
|
||||
return [base].concat(additions).join(' ');
|
||||
}
|
||||
|
||||
function renderImage(value, options) {
|
||||
var file = normalizeFileAccess(value);
|
||||
var settings = options || {};
|
||||
var alt;
|
||||
var loading;
|
||||
var caption;
|
||||
|
||||
if (!file) return '';
|
||||
alt = stringValue(settings.alt || file.fileName || '图片').trim();
|
||||
loading = settings.eager === true ? 'eager' : 'lazy';
|
||||
caption = stringValue(settings.caption).trim();
|
||||
return '<figure class="' + escapeHtml(classNames('media-frame', settings.className)) +
|
||||
'" data-media-kind="image"><button class="media-preview" type="button" data-media-preview-url="' +
|
||||
escapeHtml(file.accessUrl) + '" data-media-preview-alt="' + escapeHtml(alt) + '">' +
|
||||
'<img src="' + escapeHtml(file.accessUrl) + '" alt="' + escapeHtml(alt) +
|
||||
'" loading="' + loading + '" decoding="async" /></button>' +
|
||||
'<span class="media-fallback" hidden>图片暂时无法显示</span>' +
|
||||
(caption ? '<figcaption>' + escapeHtml(caption) + '</figcaption>' : '') + '</figure>';
|
||||
}
|
||||
|
||||
function renderAvatar(value, options) {
|
||||
var file = normalizeFileAccess(value);
|
||||
var settings = options || {};
|
||||
var name = stringValue(settings.name || '用户').trim() || '用户';
|
||||
var initial = Array.from(name)[0] || '家';
|
||||
var classes = classNames('media-avatar', settings.className);
|
||||
|
||||
if (!file) {
|
||||
return '<span class="' + escapeHtml(classes) + '" aria-label="' +
|
||||
escapeHtml(name + '的头像') + '">' + escapeHtml(initial) + '</span>';
|
||||
}
|
||||
return '<span class="' + escapeHtml(classes) + '"><img src="' + escapeHtml(file.accessUrl) +
|
||||
'" alt="' + escapeHtml(name + '的头像') + '" loading="lazy" decoding="async" />' +
|
||||
'<span class="media-avatar-fallback" aria-hidden="true">' + escapeHtml(initial) + '</span></span>';
|
||||
}
|
||||
|
||||
function renderVideo(value, options) {
|
||||
var file = normalizeFileAccess(value);
|
||||
var settings = options || {};
|
||||
var poster = normalizeFileAccess(settings.posterFile);
|
||||
var title;
|
||||
|
||||
if (!file) return '';
|
||||
title = stringValue(settings.title || file.fileName || '家族视频').trim();
|
||||
return '<figure class="' + escapeHtml(classNames('media-frame media-video', settings.className)) +
|
||||
'" data-media-kind="video"><video controls preload="metadata" src="' + escapeHtml(file.accessUrl) + '"' +
|
||||
(poster ? ' poster="' + escapeHtml(poster.accessUrl) + '"' : '') +
|
||||
' aria-label="' + escapeHtml(title) + '"></video>' +
|
||||
'<span class="media-fallback" hidden>视频暂时无法播放</span>' +
|
||||
(settings.caption ? '<figcaption>' + escapeHtml(settings.caption) + '</figcaption>' : '') + '</figure>';
|
||||
}
|
||||
|
||||
function mediaKind(file) {
|
||||
var type = stringValue(file && file.mediaType).toLowerCase();
|
||||
|
||||
if (type.indexOf('image/') === 0) return 'image';
|
||||
if (type.indexOf('video/') === 0) return 'video';
|
||||
return 'file';
|
||||
}
|
||||
|
||||
function renderDownload(file) {
|
||||
var label = file.fileName || '附件';
|
||||
|
||||
return '<a class="media-download" href="' + escapeHtml(file.accessUrl) +
|
||||
'" target="_blank" rel="noopener noreferrer" download>下载 ' + escapeHtml(label) + '</a>';
|
||||
}
|
||||
|
||||
function renderGallery(values, options) {
|
||||
var files = normalizeFileList(values);
|
||||
var settings = options || {};
|
||||
var label = stringValue(settings.label || '媒体附件').trim();
|
||||
var content;
|
||||
|
||||
if (!files.length) return '';
|
||||
content = files.map(function (file) {
|
||||
var kind = mediaKind(file);
|
||||
|
||||
if (kind === 'image') return renderImage(file, { alt: file.fileName || label });
|
||||
if (kind === 'video') return renderVideo(file, { title: file.fileName || label });
|
||||
return renderDownload(file);
|
||||
}).join('');
|
||||
return '<div class="' + escapeHtml(classNames('media-gallery', settings.className)) +
|
||||
'" role="group" aria-label="' + escapeHtml(label) + '">' + content + '</div>';
|
||||
}
|
||||
|
||||
function ensurePreviewDialog(documentRef) {
|
||||
var dialog = documentRef.querySelector('[data-media-preview-dialog]');
|
||||
var close;
|
||||
|
||||
if (dialog) return dialog;
|
||||
dialog = documentRef.createElement('dialog');
|
||||
dialog.className = 'media-dialog';
|
||||
dialog.setAttribute('data-media-preview-dialog', '');
|
||||
dialog.setAttribute('aria-label', '图片预览');
|
||||
dialog.innerHTML = '<button class="media-dialog-close" type="button" aria-label="关闭图片预览">关闭</button>' +
|
||||
'<img alt="" data-media-dialog-image />';
|
||||
documentRef.body.appendChild(dialog);
|
||||
close = dialog.querySelector('.media-dialog-close');
|
||||
close.addEventListener('click', function () {
|
||||
if (typeof dialog.close === 'function') dialog.close();
|
||||
else dialog.removeAttribute('open');
|
||||
});
|
||||
dialog.addEventListener('click', function (event) {
|
||||
if (event.target !== dialog) return;
|
||||
if (typeof dialog.close === 'function') dialog.close();
|
||||
else dialog.removeAttribute('open');
|
||||
});
|
||||
return dialog;
|
||||
}
|
||||
|
||||
function init(documentRef) {
|
||||
if (!documentRef || documentRef.documentElement.getAttribute('data-media-display-ready') === 'true') return;
|
||||
documentRef.documentElement.setAttribute('data-media-display-ready', 'true');
|
||||
documentRef.addEventListener('error', function (event) {
|
||||
var target = event.target;
|
||||
var frame;
|
||||
var fallback;
|
||||
|
||||
if (!target || !/^(IMG|VIDEO)$/.test(target.tagName)) return;
|
||||
frame = target.closest && target.closest('.media-frame, .media-avatar');
|
||||
if (!frame) return;
|
||||
frame.classList.add('is-media-error');
|
||||
target.hidden = true;
|
||||
fallback = frame.querySelector('.media-fallback, .media-avatar-fallback');
|
||||
if (fallback) fallback.hidden = false;
|
||||
}, true);
|
||||
documentRef.addEventListener('click', function (event) {
|
||||
var trigger = event.target.closest && event.target.closest('[data-media-preview-url]');
|
||||
var dialog;
|
||||
var image;
|
||||
var url;
|
||||
|
||||
if (!trigger) return;
|
||||
url = normalizeAccessUrl(trigger.getAttribute('data-media-preview-url'));
|
||||
if (!url) return;
|
||||
dialog = ensurePreviewDialog(documentRef);
|
||||
image = dialog.querySelector('[data-media-dialog-image]');
|
||||
image.src = url;
|
||||
image.alt = trigger.getAttribute('data-media-preview-alt') || '图片预览';
|
||||
if (typeof dialog.showModal === 'function') dialog.showModal();
|
||||
else dialog.setAttribute('open', '');
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
normalizeFileAccess: normalizeFileAccess,
|
||||
normalizeFileList: normalizeFileList,
|
||||
renderImage: renderImage,
|
||||
renderAvatar: renderAvatar,
|
||||
renderVideo: renderVideo,
|
||||
renderGallery: renderGallery,
|
||||
init: init
|
||||
};
|
||||
});
|
||||
@@ -13,6 +13,13 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function confirmAction(message) {
|
||||
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
|
||||
return root.ProfileUI.confirmAction(message);
|
||||
}
|
||||
return Promise.resolve(!root['confirm'] || root['confirm'](message));
|
||||
}
|
||||
|
||||
var documentRef = root.document;
|
||||
var membersById = Object.create(null);
|
||||
var selectedMemberId = '';
|
||||
@@ -496,7 +503,7 @@
|
||||
var target = membersById[memberId];
|
||||
|
||||
if (writePending || !genealogyId || !target || !managementAccess || !canRemoveMember(actorRole, target.roleType) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认将“' + memberDisplayName(target) + '”移出当前家谱吗?该操作不会删除其世系人物资料。')) return;
|
||||
if (!await confirmAction('确认将“' + memberDisplayName(target) + '”移出当前家谱吗?该操作不会删除其世系人物资料。')) return;
|
||||
setWritePending(true);
|
||||
setStatus('[data-member-action-status]', '正在移出成员…', 'loading');
|
||||
try {
|
||||
@@ -520,7 +527,7 @@
|
||||
var genealogyId = getCurrentGenealogyId();
|
||||
|
||||
if (writePending || !genealogyId || !currentMemberId || !canLeaveGenealogy(actorRole) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认退出当前家谱吗?退出后需重新受邀才能进入。世系人物资料不会被删除。')) return;
|
||||
if (!await confirmAction('确认退出当前家谱吗?退出后需重新受邀才能进入。世系人物资料不会被删除。')) return;
|
||||
setWritePending(true);
|
||||
setStatus('[data-member-action-status]', '正在退出当前家谱…', 'loading');
|
||||
try {
|
||||
@@ -547,7 +554,7 @@
|
||||
setStatus('[data-owner-transfer-status]', validation || '请选择成员列表中的新谱主', 'error');
|
||||
return;
|
||||
}
|
||||
if (root.confirm && !root.confirm('确认将谱主转让给“' + memberDisplayName(target) + '”吗?转让后当前账号将变为管理员。')) return;
|
||||
if (!await confirmAction('确认将谱主转让给“' + memberDisplayName(target) + '”吗?转让后当前账号将变为管理员。')) return;
|
||||
setWritePending(true);
|
||||
setStatus('[data-owner-transfer-status]', '正在转让谱主…', 'loading');
|
||||
try {
|
||||
|
||||
+28
-10
@@ -13,6 +13,15 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function confirmAction(message) {
|
||||
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
|
||||
return root.ProfileUI.confirmAction(message);
|
||||
}
|
||||
return Promise.resolve(!root['confirm'] || root['confirm'](message));
|
||||
}
|
||||
|
||||
var MediaDisplay = root.MediaDisplay || (typeof require === 'function' ? require('./media-display.js') : null);
|
||||
|
||||
var documentRef = root.document;
|
||||
var memosById = Object.create(null);
|
||||
var canEditContent = false;
|
||||
@@ -161,7 +170,7 @@
|
||||
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 mediaFiles = MediaDisplay ? MediaDisplay.normalizeFileList(item && item.mediaFiles) : [];
|
||||
var status = stringValue(item && item.status);
|
||||
var sortOrder = optionalSafeInteger(item && item.sortOrder);
|
||||
var memo;
|
||||
@@ -170,7 +179,6 @@
|
||||
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 = {
|
||||
@@ -185,10 +193,10 @@
|
||||
memoContent: stringValue(item.memoContent),
|
||||
remindTime: stringValue(item.remindTime),
|
||||
completed: completed,
|
||||
mediaOssIds: mediaOssIds,
|
||||
status: status,
|
||||
remark: stringValue(item.remark)
|
||||
};
|
||||
if (mediaFiles.length) memo.mediaFiles = mediaFiles;
|
||||
if (appUserId) memo.appUserId = appUserId;
|
||||
if (sortOrder !== undefined) memo.sortOrder = sortOrder;
|
||||
return memo;
|
||||
@@ -213,8 +221,8 @@
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function attachmentCount(mediaOssIds) {
|
||||
return mediaOssIds ? mediaOssIds.split(',').filter(Boolean).length : 0;
|
||||
function attachmentCount(mediaFiles) {
|
||||
return Array.isArray(mediaFiles) ? mediaFiles.length : 0;
|
||||
}
|
||||
|
||||
function buildListUrl(genealogyId, memoId) {
|
||||
@@ -259,14 +267,15 @@
|
||||
var actions = '';
|
||||
|
||||
if (!memo) return stateHtml('empty', '请选择一条备忘录查看详情');
|
||||
count = attachmentCount(memo.mediaOssIds);
|
||||
count = attachmentCount(memo.mediaFiles);
|
||||
if (canEditContent) {
|
||||
actions = '<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="' +
|
||||
'<button class="btn danger" type="button" data-memo-delete-id="' +
|
||||
escapeHtml(memo.memoId) + '">删除备忘录</button></div>';
|
||||
}
|
||||
return '<div class="form-like memo-detail">' +
|
||||
return (MediaDisplay ? MediaDisplay.renderGallery(memo.mediaFiles, { label: '备忘录附件' }) : '') +
|
||||
'<div class="form-like memo-detail">' +
|
||||
detailValue('备忘标题', memo.memoTitle) +
|
||||
detailValue('备忘内容', memo.memoContent) +
|
||||
detailValue('提醒时间', memo.remindTime) +
|
||||
@@ -606,7 +615,7 @@
|
||||
var memo = memosById[memoId];
|
||||
|
||||
if (writePending || !canEditContent || !genealogyId || !normalizeId(memoId) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认删除“' + (memo ? memo.memoTitle : '该备忘录') + '”吗?删除后不可恢复,并会释放附件引用。')) return;
|
||||
if (!await confirmAction('确认删除“' + (memo ? memo.memoTitle : '该备忘录') + '”吗?删除后不可恢复,并会释放附件引用。')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
await api.deleteMemo(genealogyId, memoId);
|
||||
@@ -636,10 +645,19 @@
|
||||
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]');
|
||||
var memoId;
|
||||
var genealogyId;
|
||||
|
||||
if (detailButton) {
|
||||
event.preventDefault();
|
||||
loadMemoDetail(detailButton.getAttribute('data-memo-detail-id'));
|
||||
memoId = detailButton.getAttribute('data-memo-detail-id');
|
||||
genealogyId = requireGenealogyId();
|
||||
if (root.history && genealogyId) root.history.replaceState(null, '', buildListUrl(genealogyId, memoId));
|
||||
loadMemoDetail(memoId).then(function (memo) {
|
||||
if (memo && root.ProfileUI && root.ProfileUI.revealDetail) {
|
||||
root.ProfileUI.revealDetail(query('[data-memo-detail-heading]'));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (deleteButton) {
|
||||
|
||||
@@ -13,6 +13,15 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function confirmAction(message) {
|
||||
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
|
||||
return root.ProfileUI.confirmAction(message);
|
||||
}
|
||||
return Promise.resolve(!root['confirm'] || root['confirm'](message));
|
||||
}
|
||||
|
||||
var MediaDisplay = root.MediaDisplay || (typeof require === 'function' ? require('./media-display.js') : null);
|
||||
|
||||
var documentRef = root.document;
|
||||
var recordsById = Object.create(null);
|
||||
var canEditContent = false;
|
||||
@@ -249,6 +258,8 @@
|
||||
status: status,
|
||||
remark: stringValue(item.remark)
|
||||
};
|
||||
var mediaFiles = MediaDisplay ? MediaDisplay.normalizeFileList(item && item.mediaFiles) : [];
|
||||
if (mediaFiles.length) record.mediaFiles = mediaFiles;
|
||||
if (appUserId) record.appUserId = appUserId;
|
||||
if (sortOrder !== undefined) record.sortOrder = sortOrder;
|
||||
return record;
|
||||
@@ -316,10 +327,11 @@
|
||||
if (canEditContent) {
|
||||
actions = '<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="' +
|
||||
'<button class="btn danger" type="button" data-merit-delete-id="' +
|
||||
escapeHtml(record.meritId) + '">删除功德记录</button></div>';
|
||||
}
|
||||
return '<div class="form-like merit-detail">' +
|
||||
return (MediaDisplay ? MediaDisplay.renderGallery(record.mediaFiles, { label: '功德记录附件' }) : '') +
|
||||
'<div class="form-like merit-detail">' +
|
||||
detailValue('功德人', record.donorName) +
|
||||
detailValue('功德类型', meritTypes[record.meritType]) +
|
||||
detailValue('功德标题', record.meritTitle) +
|
||||
@@ -670,7 +682,7 @@
|
||||
var record = recordsById[meritId];
|
||||
|
||||
if (writePending || !canEditContent || !genealogyId || !normalizeId(meritId) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认删除“' + (record ? record.meritTitle : '该功德记录') + '”吗?删除后不可恢复。')) return;
|
||||
if (!await confirmAction('确认删除“' + (record ? record.meritTitle : '该功德记录') + '”吗?删除后不可恢复。')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
await api.deleteMeritRecord(genealogyId, meritId);
|
||||
@@ -689,10 +701,19 @@
|
||||
documentRef.addEventListener('click', function (event) {
|
||||
var detailButton = event.target.closest('[data-merit-detail-id]');
|
||||
var deleteButton = event.target.closest('[data-merit-delete-id]');
|
||||
var meritId;
|
||||
var genealogyId;
|
||||
|
||||
if (detailButton) {
|
||||
event.preventDefault();
|
||||
loadMeritDetail(detailButton.getAttribute('data-merit-detail-id'));
|
||||
meritId = detailButton.getAttribute('data-merit-detail-id');
|
||||
genealogyId = requireGenealogyId();
|
||||
if (root.history && genealogyId) root.history.replaceState(null, '', buildListUrl(genealogyId, meritId));
|
||||
loadMeritDetail(meritId).then(function (record) {
|
||||
if (record && root.ProfileUI && root.ProfileUI.revealDetail) {
|
||||
root.ProfileUI.revealDetail(query('[data-merit-detail-heading]'));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (deleteButton) {
|
||||
|
||||
@@ -1,4 +1,94 @@
|
||||
(function () {
|
||||
function syncPublicNavigation() {
|
||||
let token = "";
|
||||
|
||||
try {
|
||||
token = window.localStorage.getItem("genealogy_auth_token") || "";
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
if (!token) return;
|
||||
|
||||
document.querySelectorAll(".nav-actions").forEach((actions) => {
|
||||
const loginLink = actions.querySelector('a[href="login.html"]');
|
||||
const registerLink = actions.querySelector('a[href="register.html"]');
|
||||
const createLink = actions.querySelector('a[href="create-genealogy.html"]');
|
||||
|
||||
if (loginLink) {
|
||||
loginLink.href = "profile.html";
|
||||
loginLink.textContent = "个人中心";
|
||||
}
|
||||
if (registerLink) {
|
||||
registerLink.href = "profile-data.html";
|
||||
registerLink.textContent = "账户资料";
|
||||
}
|
||||
if (createLink) createLink.href = "profile-create-family.html";
|
||||
});
|
||||
}
|
||||
|
||||
function initSiteMenu() {
|
||||
const body = document.body;
|
||||
const header = document.querySelector(".site-header");
|
||||
const nav = header && header.querySelector(".nav");
|
||||
const navLinks = nav && nav.querySelector(".nav-links");
|
||||
const navActions = nav && nav.querySelector(".nav-actions");
|
||||
|
||||
if (!body || body.matches(".page-profile, .page-profile-module")) return;
|
||||
if (!header || !nav || !navLinks || nav.querySelector("[data-site-menu-toggle]")) return;
|
||||
|
||||
const menuId = "site-mobile-menu";
|
||||
const toggle = document.createElement("button");
|
||||
const menu = document.createElement("div");
|
||||
const inner = document.createElement("div");
|
||||
|
||||
toggle.className = "site-menu-toggle";
|
||||
toggle.type = "button";
|
||||
toggle.textContent = "菜单";
|
||||
toggle.setAttribute("data-site-menu-toggle", "");
|
||||
toggle.setAttribute("aria-expanded", "false");
|
||||
toggle.setAttribute("aria-controls", menuId);
|
||||
|
||||
menu.className = "site-mobile-menu";
|
||||
menu.id = menuId;
|
||||
menu.hidden = true;
|
||||
menu.setAttribute("data-site-mobile-menu", "");
|
||||
menu.setAttribute("aria-hidden", "true");
|
||||
|
||||
inner.className = "container site-mobile-menu-inner";
|
||||
[...navLinks.querySelectorAll("a"), ...(navActions ? navActions.querySelectorAll("a") : [])].forEach((link) => {
|
||||
inner.appendChild(link.cloneNode(true));
|
||||
});
|
||||
menu.appendChild(inner);
|
||||
nav.appendChild(toggle);
|
||||
header.appendChild(menu);
|
||||
body.classList.add("has-site-mobile-menu");
|
||||
|
||||
const setOpen = (isOpen) => {
|
||||
menu.hidden = !isOpen;
|
||||
menu.classList.toggle("is-open", isOpen);
|
||||
menu.setAttribute("aria-hidden", isOpen ? "false" : "true");
|
||||
toggle.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
||||
};
|
||||
|
||||
toggle.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
setOpen(toggle.getAttribute("aria-expanded") !== "true");
|
||||
});
|
||||
menu.addEventListener("click", (event) => {
|
||||
if (event.target.closest("a")) setOpen(false);
|
||||
event.stopPropagation();
|
||||
});
|
||||
document.addEventListener("click", () => setOpen(false));
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key !== "Escape" || menu.hidden) return;
|
||||
setOpen(false);
|
||||
toggle.focus();
|
||||
});
|
||||
}
|
||||
|
||||
syncPublicNavigation();
|
||||
initSiteMenu();
|
||||
|
||||
// 只在精确指针设备上启用微动效,触屏和减少动效偏好下保持静态。
|
||||
const finePointer = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
|
||||
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
(function (root, factory) {
|
||||
if (typeof module === 'object' && module.exports) {
|
||||
module.exports = factory(root);
|
||||
return;
|
||||
}
|
||||
root.PersonDocumentPages = factory(root);
|
||||
if (root.document) root.document.addEventListener('DOMContentLoaded', root.PersonDocumentPages.init);
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function confirmAction(message) {
|
||||
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
|
||||
return root.ProfileUI.confirmAction(message);
|
||||
}
|
||||
return Promise.resolve(!root['confirm'] || root['confirm'](message));
|
||||
}
|
||||
|
||||
function promptAction(title) {
|
||||
if (root.ProfileUI && typeof root.ProfileUI.promptAction === 'function') {
|
||||
return root.ProfileUI.promptAction({ title: title, formType: 1, maxlength: 100 });
|
||||
}
|
||||
return Promise.resolve(root['prompt'] ? root['prompt'](title) : null);
|
||||
}
|
||||
|
||||
var USAGE_LABELS = { FRONT: '正面', BACK: '反面', ATTACHMENT: '附件' };
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function normalizeId(value) {
|
||||
var text = String(value === undefined || value === null ? '' : value).trim();
|
||||
return /^[1-9][0-9]*$/.test(text) ? text : '';
|
||||
}
|
||||
|
||||
function normalizeDocument(item) {
|
||||
var source = item || {};
|
||||
var documentId = normalizeId(source.documentId);
|
||||
var personId = normalizeId(source.lineagePersonId);
|
||||
var title = String(source.documentTitle || '').trim();
|
||||
|
||||
if (!documentId || !personId || !title || !Array.isArray(source.resources)) return null;
|
||||
return {
|
||||
documentId: documentId,
|
||||
lineagePersonId: personId,
|
||||
lineagePersonName: String(source.lineagePersonName || '').trim(),
|
||||
documentType: String(source.documentType || '').trim(),
|
||||
documentTypeLabel: String(source.documentTypeLabel || source.documentType || '').trim(),
|
||||
documentTitle: title,
|
||||
maskedIdentifier: String(source.maskedIdentifier || '').trim(),
|
||||
description: String(source.description || '').trim(),
|
||||
contentProtected: source.contentProtected === true,
|
||||
contentUnlocked: source.contentUnlocked === true,
|
||||
canEdit: source.canEdit === true,
|
||||
canDelete: source.canDelete === true,
|
||||
resources: source.resources.map(function (resource) {
|
||||
var resourceId = normalizeId(resource && resource.resourceId);
|
||||
var usageType = String(resource && resource.usageType || '').trim();
|
||||
return resourceId && USAGE_LABELS[usageType] ? { resourceId: resourceId, usageType: usageType } : null;
|
||||
}).filter(Boolean)
|
||||
};
|
||||
}
|
||||
|
||||
function buildDocumentBody(values) {
|
||||
var source = values || {};
|
||||
return {
|
||||
lineagePersonId: normalizeId(source.lineagePersonId),
|
||||
documentType: String(source.documentType || '').trim(),
|
||||
documentTitle: String(source.documentTitle || '').trim(),
|
||||
maskedIdentifier: String(source.maskedIdentifier || '').trim(),
|
||||
description: String(source.description || '').trim(),
|
||||
status: '0'
|
||||
};
|
||||
}
|
||||
|
||||
function validateDocumentBody(body) {
|
||||
if (!body.lineagePersonId) return '请选择关联人物';
|
||||
if (!body.documentType) return '请选择证件类型';
|
||||
if (!body.documentTitle) return '请填写证件标题';
|
||||
if (body.documentTitle.length > 100) return '证件标题不能超过 100 个字符';
|
||||
if (body.maskedIdentifier.length > 100) return '脱敏标识不能超过 100 个字符';
|
||||
if (body.description.length > 1000) return '说明不能超过 1000 个字符';
|
||||
return '';
|
||||
}
|
||||
|
||||
function renderDocuments(data) {
|
||||
var items = Array.isArray(data) ? data.map(normalizeDocument).filter(Boolean) : [];
|
||||
|
||||
if (!items.length) return '<div class="api-empty">当前家谱还没有重要证件档案</div>';
|
||||
return items.map(function (item) {
|
||||
var resources = item.resources.map(function (resource) {
|
||||
return '<button class="btn outline" type="button" data-document-resource="' +
|
||||
escapeHtml(resource.resourceId) + '" data-document-id="' + escapeHtml(item.documentId) + '">' +
|
||||
escapeHtml(USAGE_LABELS[resource.usageType]) + '</button>';
|
||||
}).join('');
|
||||
return '<article class="module-card" data-document-row="' + escapeHtml(item.documentId) + '"><h3>' +
|
||||
escapeHtml(item.documentTitle) + '</h3><p>' +
|
||||
escapeHtml([item.lineagePersonName, item.documentTypeLabel, item.maskedIdentifier, item.description].filter(Boolean).join(' · ')) +
|
||||
'</p><div class="row-actions">' + resources +
|
||||
(item.contentProtected && !item.contentUnlocked ? '<button class="btn outline" type="button" data-document-unlock="' + item.documentId + '">解锁</button>' : '') +
|
||||
(item.canEdit ? '<button class="btn outline" type="button" data-document-protect="' + item.documentId + '">' + (item.contentProtected ? '取消密码' : '设置密码') + '</button>' : '') +
|
||||
(item.canDelete ? '<button class="btn danger" type="button" data-document-delete="' + item.documentId + '">删除</button>' : '') +
|
||||
'</div></article>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function init() {
|
||||
var page = root.document && root.document.querySelector('[data-person-document-page]');
|
||||
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
var genealogyId = root.ProfileUI && root.ProfileUI.getGenealogyId ? root.ProfileUI.getGenealogyId() : '';
|
||||
var form = page && page.querySelector('[data-document-form]');
|
||||
var list = page && page.querySelector('[data-document-list]');
|
||||
var status = page && page.querySelector('[data-document-status]');
|
||||
var personSelect = page && page.querySelector('[data-document-person]');
|
||||
var typeSelect = page && page.querySelector('[data-document-type]');
|
||||
var ossInput = page && page.querySelector('[data-document-oss-id]');
|
||||
var usageSelect = page && page.querySelector('[data-document-usage]');
|
||||
var documents = [];
|
||||
var accessTokens = {};
|
||||
var pending = false;
|
||||
|
||||
if (!page) return;
|
||||
if (!api || !api.getToken || !api.getToken()) {
|
||||
if (root.location) root.location.href = 'login.html';
|
||||
return;
|
||||
}
|
||||
if (!genealogyId) {
|
||||
root.ProfileUI.setApiState(status, 'error', '缺少家谱上下文,请从“我的家谱”重新进入。');
|
||||
return;
|
||||
}
|
||||
|
||||
function setStatus(type, message) {
|
||||
if (type) root.ProfileUI.setApiState(status, type, message);
|
||||
else status.textContent = message || '';
|
||||
}
|
||||
|
||||
async function loadDocuments() {
|
||||
documents = (await api.personDocuments(genealogyId)).map(normalizeDocument).filter(Boolean);
|
||||
list.innerHTML = renderDocuments(documents);
|
||||
}
|
||||
|
||||
try {
|
||||
var initial = await Promise.all([
|
||||
api.lineagePersonOptions(genealogyId),
|
||||
api.businessDictionary('gen_person_document_type'),
|
||||
api.personDocuments(genealogyId)
|
||||
]);
|
||||
personSelect.innerHTML = '<option value="">请选择人物</option>' + (Array.isArray(initial[0]) ? initial[0] : []).map(function (item) {
|
||||
var id = normalizeId(item.personId || item.lineagePersonId);
|
||||
var name = String(item.displayName || item.personName || item.name || '').trim();
|
||||
return id && name ? '<option value="' + escapeHtml(id) + '">' + escapeHtml(name) + '</option>' : '';
|
||||
}).join('');
|
||||
typeSelect.innerHTML = '<option value="">请选择证件类型</option>' + (Array.isArray(initial[1]) ? initial[1] : []).filter(function (item) {
|
||||
return item && item.enabled === true && String(item.value || '').trim();
|
||||
}).map(function (item) {
|
||||
return '<option value="' + escapeHtml(item.value) + '">' + escapeHtml(item.label || item.value) + '</option>';
|
||||
}).join('');
|
||||
documents = (Array.isArray(initial[2]) ? initial[2] : []).map(normalizeDocument).filter(Boolean);
|
||||
list.innerHTML = renderDocuments(documents);
|
||||
setStatus('', '重要证件档案已加载');
|
||||
} catch (error) {
|
||||
setStatus('error', error.message || '重要证件档案读取失败');
|
||||
return;
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async function (event) {
|
||||
var body;
|
||||
var validation;
|
||||
var created;
|
||||
var ossId;
|
||||
|
||||
event.preventDefault();
|
||||
if (pending) return;
|
||||
body = buildDocumentBody({
|
||||
lineagePersonId: personSelect.value,
|
||||
documentType: typeSelect.value,
|
||||
documentTitle: form.elements.documentTitle.value,
|
||||
maskedIdentifier: form.elements.maskedIdentifier.value,
|
||||
description: form.elements.description.value
|
||||
});
|
||||
validation = validateDocumentBody(body);
|
||||
if (validation) return setStatus('error', validation);
|
||||
ossId = normalizeId(ossInput.value);
|
||||
if (!ossId) return setStatus('error', '请先上传证件文件');
|
||||
pending = true; setStatus('loading', '正在保存证件档案…');
|
||||
try {
|
||||
created = normalizeDocument(await api.createPersonDocument(genealogyId, body));
|
||||
if (!created) throw new Error('创建响应缺少有效档案信息');
|
||||
await api.addPersonDocumentResource(genealogyId, created.documentId, {
|
||||
ossId: ossId, usageType: usageSelect.value
|
||||
});
|
||||
form.reset(); ossInput.value = '';
|
||||
await loadDocuments();
|
||||
setStatus('', '证件档案已保存');
|
||||
} catch (error) { setStatus('error', error.message || '证件档案保存失败'); }
|
||||
finally { pending = false; }
|
||||
});
|
||||
|
||||
list.addEventListener('click', async function (event) {
|
||||
var resourceButton = event.target.closest('[data-document-resource]');
|
||||
var unlockButton = event.target.closest('[data-document-unlock]');
|
||||
var protectButton = event.target.closest('[data-document-protect]');
|
||||
var deleteButton = event.target.closest('[data-document-delete]');
|
||||
var documentId;
|
||||
var password;
|
||||
var documentItem;
|
||||
var accessWindow;
|
||||
|
||||
if (pending) return;
|
||||
try {
|
||||
if (resourceButton) {
|
||||
accessWindow = root.open('about:blank', '_blank', 'noopener');
|
||||
documentId = resourceButton.getAttribute('data-document-id');
|
||||
var access = await api.personDocumentResourceAccess(
|
||||
genealogyId, documentId, resourceButton.getAttribute('data-document-resource'), accessTokens[documentId]
|
||||
);
|
||||
if (!access || !access.accessUrl) throw new Error('文件访问响应缺少地址');
|
||||
if (accessWindow) accessWindow.location.replace(access.accessUrl);
|
||||
else root.location.href = access.accessUrl;
|
||||
return;
|
||||
}
|
||||
if (unlockButton) {
|
||||
documentId = unlockButton.getAttribute('data-document-unlock');
|
||||
password = await promptAction('请输入档案访问密码');
|
||||
if (!password) return;
|
||||
var grant = await api.unlockPersonDocument(genealogyId, documentId, password);
|
||||
if (!grant || !grant.accessToken) throw new Error('解锁响应缺少访问令牌');
|
||||
accessTokens[documentId] = grant.accessToken;
|
||||
await loadDocuments(); setStatus('', '档案已临时解锁'); return;
|
||||
}
|
||||
if (protectButton) {
|
||||
documentId = protectButton.getAttribute('data-document-protect');
|
||||
documentItem = documents.find(function (item) { return item.documentId === documentId; });
|
||||
if (!documentItem) return;
|
||||
if (documentItem.contentProtected) {
|
||||
if (!await confirmAction('确认取消此档案的密码保护吗?')) return;
|
||||
await api.unprotectPersonDocument(genealogyId, documentId);
|
||||
} else {
|
||||
password = await promptAction('设置档案访问密码');
|
||||
if (!password) return;
|
||||
await api.protectPersonDocument(genealogyId, documentId, password);
|
||||
}
|
||||
await loadDocuments(); setStatus('', '密码保护设置已更新'); return;
|
||||
}
|
||||
if (deleteButton) {
|
||||
documentId = deleteButton.getAttribute('data-document-delete');
|
||||
if (!await confirmAction('确认删除这份证件档案吗?')) return;
|
||||
await api.deletePersonDocument(genealogyId, documentId);
|
||||
await loadDocuments(); setStatus('', '证件档案已删除');
|
||||
}
|
||||
} catch (error) {
|
||||
if (accessWindow) accessWindow.close();
|
||||
setStatus('error', error.message || '操作失败,请稍后重试');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
normalizeDocument: normalizeDocument,
|
||||
buildDocumentBody: buildDocumentBody,
|
||||
validateDocumentBody: validateDocumentBody,
|
||||
renderDocuments: renderDocuments,
|
||||
init: init
|
||||
};
|
||||
});
|
||||
+240
-19
@@ -7,6 +7,69 @@
|
||||
var genealogyContextStorageKey = 'genealogy_current_context_v1';
|
||||
var mobileMenuId = 'profile-mobile-menu';
|
||||
var apiStateTypes = ['loading', 'empty', 'error', 'forbidden'];
|
||||
var familyWorkspacePages = [
|
||||
'profile-family-home.html', 'profile-tree.html', 'profile-generation.html',
|
||||
'profile-family-admin.html', 'profile-admin-permissions.html', 'profile-join-review.html',
|
||||
'profile-content.html', 'profile-article.html', 'profile-article-edit.html',
|
||||
'profile-album.html', 'profile-album-detail.html', 'profile-album-edit.html',
|
||||
'profile-video.html', 'profile-video-edit.html', 'profile-feed.html',
|
||||
'profile-feed-detail.html', 'profile-feed-edit.html', 'profile-ceremony.html',
|
||||
'profile-ceremony-detail.html', 'profile-gift-edit.html', 'profile-gift.html',
|
||||
'profile-growth.html', 'profile-growth-edit.html', 'profile-merit.html',
|
||||
'profile-merit-edit.html', 'profile-memo.html', 'profile-memo-edit.html',
|
||||
'profile-relative.html', 'profile-relative-edit.html', 'profile-invite.html',
|
||||
'profile-data-reminders.html', 'profile-documents.html', 'profile-family-settings.html'
|
||||
];
|
||||
var familyWorkspaceNavigation = [
|
||||
{ label: '家谱主页', href: 'profile-family-home.html', context: true },
|
||||
{ label: '世系管理', items: [
|
||||
{ label: '世系图', href: 'profile-tree.html', context: true },
|
||||
{ label: '字辈谱', href: 'profile-generation.html', context: true },
|
||||
{ label: '资料提醒', href: 'profile-data-reminders.html', context: true }
|
||||
] },
|
||||
{ label: '内容发布', items: [
|
||||
{ label: '内容工作台', href: 'profile-content.html', context: true },
|
||||
{ label: '谱文', href: 'profile-article.html', pages: ['profile-article-edit.html'], context: true },
|
||||
{ label: '家族圈', href: 'profile-feed.html', pages: ['profile-feed-detail.html', 'profile-feed-edit.html'], context: true },
|
||||
{ label: '相册', href: 'profile-album.html', pages: ['profile-album-detail.html', 'profile-album-edit.html'], context: true },
|
||||
{ label: '视频', href: 'profile-video.html', pages: ['profile-video-edit.html'], context: true },
|
||||
{ label: '祭祀活动', href: 'profile-ceremony.html', pages: ['profile-ceremony-detail.html', 'profile-gift-edit.html'], context: true },
|
||||
{ label: '贺礼邀请', href: 'profile-gift.html', context: true },
|
||||
{ label: '成长记录', href: 'profile-growth.html', pages: ['profile-growth-edit.html'], context: true },
|
||||
{ label: '功德录', href: 'profile-merit.html', pages: ['profile-merit-edit.html'], context: true },
|
||||
{ label: '备忘录', href: 'profile-memo.html', pages: ['profile-memo-edit.html'], context: true },
|
||||
{ label: '亲友记录', href: 'profile-relative.html', pages: ['profile-relative-edit.html'], context: true },
|
||||
{ label: '重要证件', href: 'profile-documents.html', context: true }
|
||||
] },
|
||||
{ label: '家族管理', items: [
|
||||
{ label: '成员管理', href: 'profile-family-admin.html', context: true },
|
||||
{ label: '入谱审核', href: 'profile-join-review.html', context: true },
|
||||
{ label: '邀请家人', href: 'profile-invite.html', context: true },
|
||||
{ label: '家谱维护', href: 'profile-family-settings.html', context: true },
|
||||
{ label: '权限说明', href: 'profile-admin-permissions.html', context: true }
|
||||
] },
|
||||
{ label: '消息中心', href: 'profile-messages.html' }
|
||||
];
|
||||
var accountWorkspaceNavigation = [
|
||||
{ label: '个人中心', href: 'profile.html' },
|
||||
{ label: '我的家谱', items: [
|
||||
{ label: '家谱列表', href: 'profile-families.html' },
|
||||
{ label: '创建家谱', href: 'profile-create-family.html' },
|
||||
{ label: '加入家谱', href: 'join-genealogy.html' },
|
||||
{ label: '我的申请', href: 'profile-join-family.html' }
|
||||
] },
|
||||
{ label: '账号资料', items: [
|
||||
{ label: '个人资料', href: 'profile-data.html' },
|
||||
{ label: '账号安全', href: 'profile-security.html' }
|
||||
] },
|
||||
{ label: '服务与帮助', items: [
|
||||
{ label: '帮助与服务', href: 'profile-services.html' },
|
||||
{ label: '意见反馈', href: 'profile-feedback.html', pages: ['my-tickets.html', 'submit-ticket.html', 'ticket-detail.html'] },
|
||||
{ label: '推广收益', href: 'profile-earnings.html' },
|
||||
{ label: '分享与邀请', href: 'profile-share.html' }
|
||||
] },
|
||||
{ label: '消息中心', href: 'profile-messages.html' }
|
||||
];
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
@@ -33,8 +96,20 @@
|
||||
element.innerHTML = renderApiState(type, message);
|
||||
}
|
||||
|
||||
function revealDetail(element) {
|
||||
var reduceMotion;
|
||||
|
||||
if (!element) return;
|
||||
reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
if (typeof element.scrollIntoView === 'function') {
|
||||
element.scrollIntoView({ behavior: reduceMotion ? 'auto' : 'smooth', block: 'start' });
|
||||
}
|
||||
if (typeof element.focus === 'function') element.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
profileUI.renderApiState = renderApiState;
|
||||
profileUI.setApiState = setApiState;
|
||||
profileUI.revealDetail = revealDetail;
|
||||
window.ProfileUI = profileUI;
|
||||
|
||||
if (!$) return;
|
||||
@@ -105,21 +180,22 @@
|
||||
var genealogyId = getGenealogyId();
|
||||
var context = getStoredGenealogyContext();
|
||||
|
||||
return context && context.genealogyId === genealogyId ? context : null;
|
||||
if (!context) return null;
|
||||
return !genealogyId || context.genealogyId === genealogyId ? context : null;
|
||||
}
|
||||
|
||||
function openLegacyLogout() {
|
||||
var $modal = $('#logoutModal');
|
||||
|
||||
if (!$modal.length) return;
|
||||
$modal.addClass('show').attr('aria-hidden', 'false');
|
||||
$modal.prop('hidden', false).addClass('show').attr('aria-hidden', 'false');
|
||||
$('body').addClass('logout-modal-open');
|
||||
}
|
||||
|
||||
function closeLegacyLogout() {
|
||||
var $modal = $('#logoutModal');
|
||||
|
||||
$modal.removeClass('show').attr('aria-hidden', 'true');
|
||||
$modal.removeClass('show').attr('aria-hidden', 'true').prop('hidden', true);
|
||||
$('body').removeClass('logout-modal-open');
|
||||
}
|
||||
|
||||
@@ -129,7 +205,8 @@
|
||||
content: '确定继续吗?',
|
||||
confirmText: '确定',
|
||||
cancelText: '取消',
|
||||
onConfirm: $.noop
|
||||
onConfirm: $.noop,
|
||||
onCancel: $.noop
|
||||
}, options || {});
|
||||
|
||||
if (layerApi) {
|
||||
@@ -140,11 +217,26 @@
|
||||
}, function (index) {
|
||||
settings.onConfirm();
|
||||
layerApi.close(index);
|
||||
}, function () {
|
||||
settings.onCancel();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.confirm(settings.content)) settings.onConfirm();
|
||||
else settings.onCancel();
|
||||
}
|
||||
|
||||
function confirmAction(content, options) {
|
||||
var settings = typeof content === 'string'
|
||||
? $.extend({}, options || {}, { content: content })
|
||||
: $.extend({}, content || {});
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
settings.onConfirm = function () { resolve(true); };
|
||||
settings.onCancel = function () { resolve(false); };
|
||||
openConfirm(settings);
|
||||
});
|
||||
}
|
||||
|
||||
function initLayui() {
|
||||
@@ -274,7 +366,8 @@
|
||||
}
|
||||
|
||||
function syncGenealogyContextLinks() {
|
||||
var genealogyId = getGenealogyId();
|
||||
var context = getCurrentGenealogyContext();
|
||||
var genealogyId = getGenealogyId() || (context && context.genealogyId);
|
||||
|
||||
$('[data-genealogy-context-link], a[href]').filter(function () {
|
||||
var $link = $(this);
|
||||
@@ -315,7 +408,8 @@
|
||||
}
|
||||
|
||||
function initCurrentGenealogyContext() {
|
||||
var genealogyId = getGenealogyId();
|
||||
var context = getCurrentGenealogyContext();
|
||||
var genealogyId = getGenealogyId() || (context && context.genealogyId);
|
||||
var $nav;
|
||||
var $actions;
|
||||
var $context;
|
||||
@@ -332,6 +426,94 @@
|
||||
refreshCurrentGenealogyContext();
|
||||
}
|
||||
|
||||
function getCurrentPageName() {
|
||||
return (window.location.pathname.split('/').pop() || 'profile.html').split('?')[0];
|
||||
}
|
||||
|
||||
function isWorkspaceItemCurrent(item, currentPage) {
|
||||
return item.href === currentPage || (Array.isArray(item.pages) && item.pages.indexOf(currentPage) >= 0);
|
||||
}
|
||||
|
||||
function renderWorkspaceLink(item, currentPage) {
|
||||
var isCurrent = isWorkspaceItemCurrent(item, currentPage);
|
||||
|
||||
return '<a' + (isCurrent ? ' class="active" aria-current="page"' : '') +
|
||||
' href="' + escapeHtml(item.href) + '"' + (item.context ? ' data-genealogy-context-link' : '') + '>' +
|
||||
escapeHtml(item.label) + '</a>';
|
||||
}
|
||||
|
||||
function renderWorkspaceNavigation(items, currentPage) {
|
||||
return items.map(function (item) {
|
||||
var isOpen;
|
||||
|
||||
if (!item.items) return renderWorkspaceLink(item, currentPage);
|
||||
isOpen = item.items.some(function (child) { return isWorkspaceItemCurrent(child, currentPage); });
|
||||
return '<details class="workspace-nav-group' + (isOpen ? ' is-open' : '') +
|
||||
'" data-workspace-nav-group' + (isOpen ? ' open' : '') + '>' +
|
||||
'<summary>' + escapeHtml(item.label) + '</summary>' +
|
||||
'<div class="workspace-nav-items">' + item.items.map(function (child) {
|
||||
return renderWorkspaceLink(child, currentPage);
|
||||
}).join('') + '</div></details>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function getHeaderSection(currentPage) {
|
||||
if (['profile-families.html', 'profile-create-family.html', 'join-genealogy.html', 'profile-join-family.html', 'profile-family-home.html'].indexOf(currentPage) >= 0) {
|
||||
return 'profile-families.html';
|
||||
}
|
||||
if ([
|
||||
'profile-tree.html', 'profile-generation.html', 'profile-family-admin.html',
|
||||
'profile-admin-permissions.html', 'profile-join-review.html', 'profile-invite.html',
|
||||
'profile-data-reminders.html', 'profile-documents.html', 'profile-family-settings.html'
|
||||
].indexOf(currentPage) >= 0) return 'profile-family-admin.html';
|
||||
if (familyWorkspacePages.indexOf(currentPage) >= 0) return 'profile-content.html';
|
||||
if (['profile-data.html', 'profile-security.html'].indexOf(currentPage) >= 0) return 'profile-data.html';
|
||||
if (['profile-services.html', 'profile-feedback.html', 'my-tickets.html', 'submit-ticket.html', 'ticket-detail.html', 'profile-earnings.html', 'profile-share.html'].indexOf(currentPage) >= 0) {
|
||||
return 'profile-services.html';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function renderHeaderLink(item, currentSection) {
|
||||
var isCurrent = item.href === currentSection;
|
||||
|
||||
return '<a' + (isCurrent ? ' class="active" aria-current="page"' : '') +
|
||||
' href="' + escapeHtml(item.href) + '"' + (item.context ? ' data-genealogy-context-link' : '') + '>' +
|
||||
escapeHtml(item.label) + '</a>';
|
||||
}
|
||||
|
||||
function initWorkspaceNavigation() {
|
||||
var currentPage = getCurrentPageName();
|
||||
var isFamilyWorkspace = familyWorkspacePages.indexOf(currentPage) >= 0;
|
||||
var navigation = isFamilyWorkspace ? familyWorkspaceNavigation : accountWorkspaceNavigation;
|
||||
var $sidebar = $('.module-nav').first();
|
||||
var $header = $('.site-header').first();
|
||||
var headerItems = [
|
||||
{ label: '我的家谱', href: 'profile-families.html' },
|
||||
{ label: '家族管理', href: 'profile-family-admin.html', context: true },
|
||||
{ label: '内容发布', href: 'profile-content.html', context: true },
|
||||
{ label: '个人资料', href: 'profile-data.html' },
|
||||
{ label: '帮助与服务', href: 'profile-services.html' }
|
||||
];
|
||||
var currentSection = getHeaderSection(currentPage);
|
||||
|
||||
if ($header.length) {
|
||||
$header.find('.brand').first().attr('href', 'profile.html').html(
|
||||
'<img class="brand-logo brand-logo-mark" src="public/images/logo-mark.png" alt="" />' +
|
||||
'<span class="brand-text">个人中心</span>'
|
||||
);
|
||||
$header.find('.nav-links').first().attr('aria-label', '个人中心主导航').html(headerItems.map(function (item) {
|
||||
return renderHeaderLink(item, currentSection);
|
||||
}).join(''));
|
||||
}
|
||||
if (!$sidebar.length) return;
|
||||
$sidebar
|
||||
.attr('aria-label', isFamilyWorkspace ? '当前家谱功能导航' : '个人中心功能导航')
|
||||
.attr('data-workspace-navigation', isFamilyWorkspace ? 'family' : 'account')
|
||||
.html(renderWorkspaceNavigation(navigation, currentPage));
|
||||
$('body').addClass('has-workspace-navigation');
|
||||
}
|
||||
|
||||
function setMobileMenuOpen(isOpen) {
|
||||
var $menu = $('[data-profile-mobile-menu]').first();
|
||||
var $toggle = $('[data-profile-mobile-menu-toggle]').first();
|
||||
@@ -345,26 +527,28 @@
|
||||
var $header = $('.site-header').first();
|
||||
var $nav;
|
||||
var currentPage;
|
||||
var isFamilyWorkspace;
|
||||
var navigation;
|
||||
var workspaceLabel;
|
||||
|
||||
if (!$('body').is('.page-profile, .page-profile-module')) return;
|
||||
if (!$header.length || $header.find('[data-profile-mobile-menu]').length) return;
|
||||
$nav = $header.find('.nav').first();
|
||||
if (!$nav.length) return;
|
||||
$nav.append('<button class="profile-mobile-menu-toggle" type="button" data-profile-mobile-menu-toggle ' +
|
||||
'aria-expanded="false" aria-controls="' + mobileMenuId + '">菜单</button>');
|
||||
currentPage = getCurrentPageName();
|
||||
isFamilyWorkspace = familyWorkspacePages.indexOf(currentPage) >= 0;
|
||||
navigation = isFamilyWorkspace ? familyWorkspaceNavigation : accountWorkspaceNavigation;
|
||||
workspaceLabel = isFamilyWorkspace ? '当前家谱功能导航' : '个人中心功能导航';
|
||||
$header.append('<div class="profile-mobile-menu" id="' + mobileMenuId + '" data-profile-mobile-menu hidden aria-hidden="true">' +
|
||||
'<div class="container profile-mobile-menu-inner">' +
|
||||
'<a href="profile.html">个人中心</a>' +
|
||||
'<a href="profile-families.html">我的家谱</a>' +
|
||||
'<a href="profile-create-family.html">创建家谱</a>' +
|
||||
'<a href="profile-family-home.html" data-genealogy-context-link>家谱主页</a>' +
|
||||
'<a href="profile-content.html" data-genealogy-context-link>内容发布</a>' +
|
||||
'<a href="profile-family-admin.html" data-genealogy-context-link>家族管理</a>' +
|
||||
'<a href="profile-data.html">个人资料</a>' +
|
||||
'<a href="profile-messages.html">消息中心</a>' +
|
||||
'<a href="profile-services.html">帮助与服务</a>' +
|
||||
'<button type="button" data-logout-open data-logout-url="index.html">退出登录</button>' +
|
||||
'<nav class="profile-mobile-workspace" aria-label="' + workspaceLabel + '">' +
|
||||
renderWorkspaceNavigation(navigation, currentPage) + '</nav>' +
|
||||
'<div class="profile-mobile-account-actions">' +
|
||||
(isFamilyWorkspace ? '<a href="profile-families.html">切换家谱</a>' : '') +
|
||||
'<button type="button" data-logout-open data-logout-url="index.html">退出登录</button></div>' +
|
||||
'</div></div>');
|
||||
currentPage = (window.location.pathname.split('/').pop() || 'profile.html').split('?')[0];
|
||||
$header.find('[data-profile-mobile-menu] a').each(function () {
|
||||
var $link = $(this);
|
||||
var target = ($link.attr('href') || '').split('?')[0];
|
||||
@@ -390,7 +574,13 @@
|
||||
if (!$(event.target).closest('.site-header').length) setMobileMenuOpen(false);
|
||||
});
|
||||
$(document).on('keydown', function (event) {
|
||||
if (event.key === 'Escape') setMobileMenuOpen(false);
|
||||
var $menu;
|
||||
|
||||
if (event.key !== 'Escape') return;
|
||||
$menu = $('[data-profile-mobile-menu]').first();
|
||||
if (!$menu.length || $menu.prop('hidden')) return;
|
||||
setMobileMenuOpen(false);
|
||||
$('[data-profile-mobile-menu-toggle]').first().trigger('focus');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -411,6 +601,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
function promptAction(options) {
|
||||
var settings = $.extend({
|
||||
title: '填写内容',
|
||||
value: '',
|
||||
formType: 1,
|
||||
maxlength: 200
|
||||
}, options || {});
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
if (!layerApi) {
|
||||
resolve(window.prompt(settings.title, settings.value) || null);
|
||||
return;
|
||||
}
|
||||
|
||||
layerApi.prompt({
|
||||
title: settings.title,
|
||||
value: settings.value,
|
||||
formType: settings.formType,
|
||||
maxlength: settings.maxlength,
|
||||
cancel: function () { resolve(null); }
|
||||
}, function (value, index) {
|
||||
resolve(value);
|
||||
layerApi.close(index);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindPromptActions() {
|
||||
$(document).on('click', '[data-layer-prompt]', function (event) {
|
||||
var $item = $(this);
|
||||
@@ -493,6 +710,7 @@
|
||||
|
||||
$(function () {
|
||||
initLayui();
|
||||
initWorkspaceNavigation();
|
||||
bindLogout();
|
||||
bindConfirmActions();
|
||||
bindPromptActions();
|
||||
@@ -508,6 +726,8 @@
|
||||
|
||||
Object.assign(profileUI, {
|
||||
confirm: openConfirm,
|
||||
confirmAction: confirmAction,
|
||||
promptAction: promptAction,
|
||||
closeLogout: closeLegacyLogout,
|
||||
initLayui: initLayui,
|
||||
getGenealogyId: getGenealogyId,
|
||||
@@ -521,6 +741,7 @@
|
||||
refreshCurrentGenealogyContext: refreshCurrentGenealogyContext,
|
||||
syncGenealogyContextLinks: syncGenealogyContextLinks,
|
||||
renderApiState: renderApiState,
|
||||
setApiState: setApiState
|
||||
setApiState: setApiState,
|
||||
revealDetail: revealDetail
|
||||
});
|
||||
})(window, window.jQuery || (window.layui && window.layui.$));
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
'use strict';
|
||||
|
||||
var documentRef = root.document;
|
||||
var MediaDisplay = root.MediaDisplay || (typeof require === 'function' ? require('./media-display.js') : null);
|
||||
|
||||
function trimOrUndefined(value) {
|
||||
var text = value === undefined || value === null ? '' : String(value).trim();
|
||||
@@ -40,18 +41,23 @@
|
||||
}
|
||||
|
||||
function buildProfileView(profile) {
|
||||
var avatarFile = MediaDisplay && MediaDisplay.normalizeFileAccess(profile && profile.avatarFile);
|
||||
var view;
|
||||
|
||||
// 页面展示模型和接口返回分开,后续改文案不影响接口契约。
|
||||
return {
|
||||
view = {
|
||||
displayName: profileValue(profile, 'nickName', '未设置昵称'),
|
||||
realName: profileValue(profile, 'realName', '待填写'),
|
||||
avatarText: getAvatarText(profile),
|
||||
avatarStatus: profile && profile.avatar ? '已设置' : '未设置',
|
||||
avatarStatus: avatarFile ? '已设置' : '未设置',
|
||||
phone: profileValue(profile, 'phone', '未绑定'),
|
||||
sex: getSexText(profile && profile.sex),
|
||||
birthday: profileValue(profile, 'birthday', '待填写'),
|
||||
email: profileValue(profile, 'email', '待填写'),
|
||||
regionText: '当前资料接口未提供'
|
||||
};
|
||||
if (avatarFile) view.avatarFile = avatarFile;
|
||||
return view;
|
||||
}
|
||||
|
||||
function buildProfileUpdateBody(values) {
|
||||
@@ -159,10 +165,21 @@
|
||||
var key = node.getAttribute('data-profile-text');
|
||||
var value = view[key];
|
||||
|
||||
if (key === 'avatarText' && view.avatarFile) return;
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
node.textContent = value;
|
||||
}
|
||||
});
|
||||
queryAll('[data-profile-avatar]').forEach(function (node) {
|
||||
if (!MediaDisplay) {
|
||||
node.textContent = view.avatarText;
|
||||
return;
|
||||
}
|
||||
node.innerHTML = MediaDisplay.renderAvatar(view.avatarFile, {
|
||||
name: view.displayName,
|
||||
className: 'profile-avatar-media'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setProfileLoadStatus(type, message) {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
(function (root, factory) {
|
||||
if (typeof module === 'object' && module.exports) {
|
||||
module.exports = factory(root);
|
||||
return;
|
||||
}
|
||||
root.ProfileReminderPages = factory(root);
|
||||
if (root.document) root.document.addEventListener('DOMContentLoaded', root.ProfileReminderPages.init);
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function normalizeId(value) {
|
||||
var text = String(value === undefined || value === null ? '' : value).trim();
|
||||
return /^[1-9][0-9]*$/.test(text) ? text : '';
|
||||
}
|
||||
|
||||
function normalizeReminder(item) {
|
||||
var source = item || {};
|
||||
var personId = normalizeId(source.personId);
|
||||
var displayName = String(source.displayName || '').trim();
|
||||
var label = String(source.missingFieldLabel || source.missingField || '').trim();
|
||||
|
||||
if (!personId || !displayName || !label) return null;
|
||||
return {
|
||||
personId: personId,
|
||||
displayName: displayName,
|
||||
label: label,
|
||||
handlingStatus: String(source.handlingStatus || '').trim(),
|
||||
updatedAt: String(source.updatedAt || '').trim()
|
||||
};
|
||||
}
|
||||
|
||||
function renderReminders(data, genealogyId) {
|
||||
var items = Array.isArray(data) ? data.map(normalizeReminder).filter(Boolean) : [];
|
||||
var target = 'profile-tree.html?genealogyId=' + encodeURIComponent(genealogyId || '');
|
||||
|
||||
if (!items.length) return '<div class="api-empty">当前家谱没有待完善的成员资料</div>';
|
||||
return items.map(function (item) {
|
||||
return '<a class="module-row" href="' + target + '"><div><h3>' + escapeHtml(item.displayName) +
|
||||
'</h3><p>待完善:' + escapeHtml(item.label) +
|
||||
(item.updatedAt ? ' · 最近更新 ' + escapeHtml(item.updatedAt) : '') +
|
||||
'</p></div><span class="pill">去完善</span></a>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function init() {
|
||||
var page = root.document && root.document.querySelector('[data-profile-reminder-page]');
|
||||
var list = page && page.querySelector('[data-profile-reminder-list]');
|
||||
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
var genealogyId = root.ProfileUI && root.ProfileUI.getGenealogyId ? root.ProfileUI.getGenealogyId() : '';
|
||||
|
||||
if (!page) return;
|
||||
if (!api || !api.getToken || !api.getToken()) {
|
||||
if (root.location) root.location.href = 'login.html';
|
||||
return;
|
||||
}
|
||||
if (!genealogyId) {
|
||||
root.ProfileUI.setApiState(list, 'error', '缺少家谱上下文,请从“我的家谱”重新进入。');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
list.innerHTML = renderReminders(await api.genealogyProfileReminders(genealogyId), genealogyId);
|
||||
} catch (error) {
|
||||
root.ProfileUI.setApiState(list, 'error', error.message || '资料提醒读取失败,请稍后重试。');
|
||||
}
|
||||
}
|
||||
|
||||
return { normalizeReminder: normalizeReminder, renderReminders: renderReminders, init: init };
|
||||
});
|
||||
+28
-10
@@ -13,6 +13,15 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function confirmAction(message) {
|
||||
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
|
||||
return root.ProfileUI.confirmAction(message);
|
||||
}
|
||||
return Promise.resolve(!root['confirm'] || root['confirm'](message));
|
||||
}
|
||||
|
||||
var MediaDisplay = root.MediaDisplay || (typeof require === 'function' ? require('./media-display.js') : null);
|
||||
|
||||
var documentRef = root.document;
|
||||
var recordsById = Object.create(null);
|
||||
var canEditContent = false;
|
||||
@@ -213,7 +222,7 @@
|
||||
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 mediaFiles = MediaDisplay ? MediaDisplay.normalizeFileList(item && item.mediaFiles) : [];
|
||||
var giftAmount = normalizeAmount(item && item.giftAmount);
|
||||
var sortOrder = optionalSafeInteger(item && item.sortOrder);
|
||||
var record;
|
||||
@@ -221,7 +230,6 @@
|
||||
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,
|
||||
@@ -237,10 +245,10 @@
|
||||
eventTime: stringValue(item.eventTime),
|
||||
giftAmount: giftAmount,
|
||||
recordContent: stringValue(item.recordContent),
|
||||
mediaOssIds: mediaOssIds,
|
||||
status: status,
|
||||
remark: stringValue(item.remark)
|
||||
};
|
||||
if (mediaFiles.length) record.mediaFiles = mediaFiles;
|
||||
if (appUserId) record.appUserId = appUserId;
|
||||
if (sortOrder !== undefined) record.sortOrder = sortOrder;
|
||||
return record;
|
||||
@@ -265,8 +273,8 @@
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function attachmentCount(mediaOssIds) {
|
||||
return mediaOssIds ? mediaOssIds.split(',').filter(Boolean).length : 0;
|
||||
function attachmentCount(mediaFiles) {
|
||||
return Array.isArray(mediaFiles) ? mediaFiles.length : 0;
|
||||
}
|
||||
|
||||
function buildListUrl(genealogyId, relativeId) {
|
||||
@@ -312,14 +320,15 @@
|
||||
var actions = '';
|
||||
|
||||
if (!record) return stateHtml('empty', '请选择一条亲友记录查看详情');
|
||||
count = attachmentCount(record.mediaOssIds);
|
||||
count = attachmentCount(record.mediaFiles);
|
||||
if (canEditContent) {
|
||||
actions = '<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="' +
|
||||
'<button class="btn danger" type="button" data-relative-delete-id="' +
|
||||
escapeHtml(record.relativeId) + '">删除记录</button></div>';
|
||||
}
|
||||
return '<div class="form-like relative-detail">' +
|
||||
return (MediaDisplay ? MediaDisplay.renderGallery(record.mediaFiles, { label: '亲友记录附件' }) : '') +
|
||||
'<div class="form-like relative-detail">' +
|
||||
detailValue('亲友姓名', record.relativeName) +
|
||||
detailValue('关系名称', record.relationName) +
|
||||
detailValue('事件名称', record.eventName) +
|
||||
@@ -666,7 +675,7 @@
|
||||
var record = recordsById[relativeId];
|
||||
|
||||
if (writePending || !canEditContent || !genealogyId || !normalizeId(relativeId) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认删除“' + (record ? record.relativeName : '该亲友记录') + '”吗?删除后不可恢复,并会释放附件引用。')) return;
|
||||
if (!await confirmAction('确认删除“' + (record ? record.relativeName : '该亲友记录') + '”吗?删除后不可恢复,并会释放附件引用。')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
await api.deleteRelativeRecord(genealogyId, relativeId);
|
||||
@@ -696,10 +705,19 @@
|
||||
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]');
|
||||
var relativeId;
|
||||
var genealogyId;
|
||||
|
||||
if (detailButton) {
|
||||
event.preventDefault();
|
||||
loadRelativeDetail(detailButton.getAttribute('data-relative-detail-id'));
|
||||
relativeId = detailButton.getAttribute('data-relative-detail-id');
|
||||
genealogyId = requireGenealogyId();
|
||||
if (root.history && genealogyId) root.history.replaceState(null, '', buildListUrl(genealogyId, relativeId));
|
||||
loadRelativeDetail(relativeId).then(function (record) {
|
||||
if (record && root.ProfileUI && root.ProfileUI.revealDetail) {
|
||||
root.ProfileUI.revealDetail(query('[data-relative-detail-heading]'));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (deleteButton) {
|
||||
|
||||
@@ -31,6 +31,24 @@
|
||||
instance.textarea.value = fieldValue(instance.editor);
|
||||
}
|
||||
|
||||
function makeToolbarAccessible(toolbar) {
|
||||
var labelButtons = function () {
|
||||
Array.prototype.forEach.call(toolbar.querySelectorAll('button[data-tooltip]'), function (button) {
|
||||
if (!button.getAttribute('aria-label')) button.setAttribute('aria-label', button.getAttribute('data-tooltip'));
|
||||
});
|
||||
};
|
||||
|
||||
labelButtons();
|
||||
if (root.MutationObserver) {
|
||||
new root.MutationObserver(labelButtons).observe(toolbar, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['data-tooltip']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createRichEditor(target, options) {
|
||||
var textarea = findTextarea(target);
|
||||
var settings = options || {};
|
||||
@@ -79,6 +97,7 @@
|
||||
config: Object.assign({ toolbarKeys: toolbarKeys }, settings.toolbarConfig || {}),
|
||||
mode: settings.mode || 'default'
|
||||
});
|
||||
makeToolbarAccessible(toolbar);
|
||||
} catch (error) {
|
||||
host.remove();
|
||||
return null;
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function confirmAction(message) {
|
||||
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
|
||||
return root.ProfileUI.confirmAction(message);
|
||||
}
|
||||
return Promise.resolve(!root['confirm'] || root['confirm'](message));
|
||||
}
|
||||
|
||||
var documentRef = root.document;
|
||||
var CAPTCHA_OPERATIONS = {
|
||||
phone: 'phone-change',
|
||||
@@ -403,7 +410,7 @@
|
||||
showMessage('请完整输入“确认注销”');
|
||||
return;
|
||||
}
|
||||
if (root.confirm && !root.confirm('账号注销后将退出登录,确认继续?')) return;
|
||||
if (!await confirmAction('账号注销后将退出登录,确认继续?')) return;
|
||||
|
||||
try {
|
||||
setSubmitting(form, true);
|
||||
@@ -423,7 +430,7 @@
|
||||
async function logout() {
|
||||
var api = getApi();
|
||||
|
||||
if (root.confirm && !root.confirm('确认退出当前账号?')) return;
|
||||
if (!await confirmAction('确认退出当前账号?')) return;
|
||||
|
||||
try {
|
||||
if (api) await api.logout();
|
||||
|
||||
+21
-10
@@ -13,6 +13,15 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function confirmAction(message) {
|
||||
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
|
||||
return root.ProfileUI.confirmAction(message);
|
||||
}
|
||||
return Promise.resolve(!root['confirm'] || root['confirm'](message));
|
||||
}
|
||||
|
||||
var MediaDisplay = root.MediaDisplay || (typeof require === 'function' ? require('./media-display.js') : null);
|
||||
|
||||
var documentRef = root.document;
|
||||
var videosById = Object.create(null);
|
||||
var canEditContent = false;
|
||||
@@ -134,8 +143,8 @@
|
||||
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 videoFile = MediaDisplay && MediaDisplay.normalizeFileAccess(item && item.videoFile);
|
||||
var coverFile = MediaDisplay && MediaDisplay.normalizeFileAccess(item && item.coverFile);
|
||||
var publisherUserId = normalizeId(item && item.publisherUserId);
|
||||
var title = String(item && item.videoTitle || '').trim();
|
||||
var status = stringValue(item && item.status);
|
||||
@@ -144,8 +153,8 @@
|
||||
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 (!videoId || !genealogyId || !videoFile || !title || (status !== '0' && status !== '1')) return null;
|
||||
if (item.coverFile !== undefined && item.coverFile !== null && !coverFile) return null;
|
||||
if (item.publisherUserId !== undefined && item.publisherUserId !== null && item.publisherUserId !== '' && !publisherUserId) return null;
|
||||
video = {
|
||||
videoId: videoId,
|
||||
@@ -155,14 +164,14 @@
|
||||
surname: stringValue(item.surname),
|
||||
videoTitle: title,
|
||||
videoDesc: stringValue(item.videoDesc),
|
||||
videoOssId: videoOssId,
|
||||
videoFile: videoFile,
|
||||
publisherNickName: stringValue(item.publisherNickName),
|
||||
publisherPhone: stringValue(item.publisherPhone),
|
||||
publishTime: stringValue(item.publishTime),
|
||||
status: status,
|
||||
remark: stringValue(item.remark)
|
||||
};
|
||||
if (coverOssId) video.coverOssId = coverOssId;
|
||||
if (coverFile) video.coverFile = coverFile;
|
||||
if (publisherUserId) video.publisherUserId = publisherUserId;
|
||||
duration = optionalInteger(item.durationSeconds, 0);
|
||||
viewCount = optionalInteger(item.viewCount, 0);
|
||||
@@ -236,16 +245,18 @@
|
||||
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>';
|
||||
'<button class="btn danger" type="button" data-video-delete-id="' + escapeHtml(video.videoId) + '">删除视频</button></div>';
|
||||
}
|
||||
return '<div class="form-like video-detail">' +
|
||||
return (MediaDisplay ? MediaDisplay.renderVideo(video.videoFile, {
|
||||
posterFile: video.coverFile, title: video.videoTitle, className: 'video-detail-media'
|
||||
}) : '') + '<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('封面文件', video.coverFile ? '已上传' : '未上传') +
|
||||
detailValue('视频时长', formatDuration(video.durationSeconds)) +
|
||||
detailValue('浏览量', video.viewCount === undefined ? '' : String(video.viewCount)) +
|
||||
detailValue('状态', video.status === '0' ? '正常' : '停用') +
|
||||
@@ -594,7 +605,7 @@
|
||||
var video = videosById[videoId];
|
||||
|
||||
if (writePending || !canEditContent || !genealogyId || !normalizeId(videoId) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认删除“' + (video ? video.videoTitle : '该视频') + '”吗?删除会释放视频和封面文件引用。')) return;
|
||||
if (!await confirmAction('确认删除“' + (video ? video.videoTitle : '该视频') + '”吗?删除会释放视频和封面文件引用。')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
await api.deleteVideo(genealogyId, videoId);
|
||||
|
||||
+114
-16
@@ -13,6 +13,13 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function confirmAction(message) {
|
||||
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
|
||||
return root.ProfileUI.confirmAction(message);
|
||||
}
|
||||
return Promise.resolve(!root['confirm'] || root['confirm'](message));
|
||||
}
|
||||
|
||||
var PACKAGE_TYPES = {
|
||||
vip: '会员套餐',
|
||||
storage: '存储扩容'
|
||||
@@ -138,13 +145,11 @@
|
||||
var body = {};
|
||||
var packageId = normalizeId(source.packageId);
|
||||
var genealogyId = normalizeId(source.genealogyId);
|
||||
var payType = optionalText(source.payType);
|
||||
|
||||
if (packageId) body.packageId = packageId;
|
||||
else if (source.packageId !== undefined) body.invalidPackageId = true;
|
||||
if (genealogyId) body.genealogyId = genealogyId;
|
||||
else if (source.genealogyId) body.invalidGenealogyId = true;
|
||||
if (payType) body.payType = payType;
|
||||
return body;
|
||||
}
|
||||
|
||||
@@ -155,10 +160,20 @@
|
||||
String(body.genealogyId).trim() !== '' && !normalizeId(body.genealogyId))) {
|
||||
return '家谱选项无效';
|
||||
}
|
||||
if (body.payType && body.payType !== 'wechat') return '支付方式无效';
|
||||
return '';
|
||||
}
|
||||
|
||||
function normalizeVipCapability(value) {
|
||||
var source = value || {};
|
||||
var methods = Array.isArray(source.paymentMethods) ? source.paymentMethods : [];
|
||||
var wechat = methods.find(function (item) { return item && item.paymentMethod === 'WECHAT'; });
|
||||
|
||||
return {
|
||||
enabled: source.enabled === true && !!wechat && wechat.enabled === true,
|
||||
disabledReason: optionalText(source.disabledReason || wechat && wechat.disabledReason)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeVipOrder(item) {
|
||||
var source = item || {};
|
||||
var orderId = normalizeId(source.orderId);
|
||||
@@ -176,7 +191,8 @@
|
||||
(source.genealogyId !== undefined && source.genealogyId !== null && !genealogyId) ||
|
||||
orderAmount === null || payAmount === null ||
|
||||
!Object.prototype.hasOwnProperty.call(PAY_STATUS, payStatus) ||
|
||||
['0', '1'].indexOf(status) < 0) {
|
||||
['0', '1'].indexOf(status) < 0 ||
|
||||
(source.transactionId !== undefined && source.transactionId !== null && !normalizeId(source.transactionId))) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
@@ -194,10 +210,38 @@
|
||||
payTime: optionalText(source.payTime),
|
||||
expireTime: optionalText(source.expireTime),
|
||||
status: status,
|
||||
remark: optionalText(source.remark)
|
||||
remark: optionalText(source.remark),
|
||||
transactionId: source.transactionId === undefined || source.transactionId === null ? '' : normalizeId(source.transactionId),
|
||||
canRefreshPayment: source.canRefreshPayment === true,
|
||||
canClose: source.canClose === true
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePaymentOrder(value) {
|
||||
var source = value || {};
|
||||
var order = normalizeVipOrder(source.order);
|
||||
var transactionId = normalizeId(source.transactionId);
|
||||
var codeUrl = optionalText(source.codeUrl);
|
||||
|
||||
if (!order || !transactionId || !codeUrl || source.tradeType !== 'NATIVE' || source.paymentMethod !== 'WECHAT') return null;
|
||||
return {
|
||||
order: order,
|
||||
transactionId: transactionId,
|
||||
codeUrl: codeUrl,
|
||||
expiresAt: optionalText(source.expiresAt),
|
||||
completed: source.completed === true
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePaymentStatus(value) {
|
||||
var source = value || {};
|
||||
var transactionId = normalizeId(source.transactionId);
|
||||
var status = optionalText(source.status);
|
||||
|
||||
if (!transactionId || ['CREATED', 'PAYING', 'SUCCESS', 'CLOSED', 'REFUNDING', 'REFUNDED'].indexOf(status) < 0) return null;
|
||||
return { transactionId: transactionId, status: status, paidAt: optionalText(source.paidAt) };
|
||||
}
|
||||
|
||||
function normalizeVipOrders(data) {
|
||||
if (!Array.isArray(data)) return [];
|
||||
return data.map(normalizeVipOrder).filter(Boolean);
|
||||
@@ -255,7 +299,9 @@
|
||||
|
||||
return '<article class="module-row"><div><h3>' + escapeHtml(item.packageName) +
|
||||
'</h3><p>' + escapeHtml(details) + '</p></div><span class="pill">' +
|
||||
PAY_STATUS[item.payStatus] + '</span></article>';
|
||||
PAY_STATUS[item.payStatus] + '</span>' +
|
||||
(item.canRefreshPayment && item.transactionId ? '<button class="btn outline" type="button" data-vip-payment-refresh="' + escapeHtml(item.transactionId) + '">查询支付</button>' : '') +
|
||||
(item.canClose && item.transactionId ? '<button class="btn outline" type="button" data-vip-payment-close="' + escapeHtml(item.transactionId) + '">关闭支付</button>' : '') + '</article>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
@@ -282,6 +328,9 @@
|
||||
var status = root.document && root.document.querySelector('[data-vip-order-status]');
|
||||
var refresh = root.document && root.document.querySelector('[data-vip-order-refresh]');
|
||||
var orderList = root.document && root.document.querySelector('[data-vip-order-list]');
|
||||
var paymentPanel = root.document && root.document.querySelector('[data-vip-payment-panel]');
|
||||
var paymentQr = root.document && root.document.querySelector('[data-vip-payment-qr]');
|
||||
var paymentText = root.document && root.document.querySelector('[data-vip-payment-text]');
|
||||
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
var packages = [];
|
||||
var selectedPackageId = '';
|
||||
@@ -289,6 +338,7 @@
|
||||
? root.ProfileUI.getGenealogyId()
|
||||
: '';
|
||||
var writePending = false;
|
||||
var purchaseEnabled = false;
|
||||
|
||||
if (!page) return;
|
||||
if (shouldRedirectToLogin(api)) {
|
||||
@@ -296,7 +346,7 @@
|
||||
return;
|
||||
}
|
||||
if (!packageList || !selectedText || !genealogySelect || !form || !orderList ||
|
||||
!api.vipPackages || !api.genealogiesMine || !api.vipOrders || !api.createVipOrder) {
|
||||
!api.vipCapability || !api.vipPackages || !api.genealogiesMine || !api.vipOrders || !api.createVipOrder) {
|
||||
setListState(packageList, 'error', '会员套餐服务初始化失败,请刷新后重试。');
|
||||
setListState(orderList, 'error', '会员订单服务初始化失败,请刷新后重试。');
|
||||
setStatus('error', '会员服务接口初始化失败,请刷新后重试。');
|
||||
@@ -351,22 +401,38 @@
|
||||
return orders;
|
||||
}
|
||||
|
||||
function showPayment(payment) {
|
||||
if (!paymentPanel || !paymentQr || !paymentText) return;
|
||||
paymentPanel.hidden = false;
|
||||
paymentQr.innerHTML = '';
|
||||
if (typeof root.QRCode === 'function') {
|
||||
new root.QRCode(paymentQr, { text: payment.codeUrl, width: 220, height: 220 });
|
||||
} else {
|
||||
paymentQr.textContent = payment.codeUrl;
|
||||
}
|
||||
paymentText.textContent = '请使用微信扫码支付' + (payment.expiresAt ? ',支付单有效期至 ' + payment.expiresAt : '');
|
||||
paymentPanel.setAttribute('data-transaction-id', payment.transactionId);
|
||||
}
|
||||
|
||||
setListState(packageList, 'loading', '正在读取会员套餐…');
|
||||
setListState(orderList, 'loading', '正在读取会员订单…');
|
||||
setStatus('loading', '正在读取会员服务…');
|
||||
try {
|
||||
var initial = await Promise.all([api.vipPackages(), api.genealogiesMine(), api.vipOrders()]);
|
||||
var genealogyOptions = normalizeGenealogyOptions(initial[1]);
|
||||
var initial = await Promise.all([api.vipCapability(), api.vipPackages(), api.genealogiesMine(), api.vipOrders()]);
|
||||
var capability = normalizeVipCapability(initial[0]);
|
||||
var genealogyOptions = normalizeGenealogyOptions(initial[2]);
|
||||
var selectedContextGenealogyId = resolveContextGenealogyId(genealogyOptions, contextGenealogyId);
|
||||
|
||||
packages = normalizeVipPackages(initial[0]);
|
||||
purchaseEnabled = capability.enabled;
|
||||
packages = normalizeVipPackages(initial[1]);
|
||||
renderPackages();
|
||||
genealogySelect.innerHTML = renderGenealogyOptions(genealogyOptions);
|
||||
if (selectedContextGenealogyId) genealogySelect.value = selectedContextGenealogyId;
|
||||
renderOrders(initial[2]);
|
||||
setStatus('', packages.length
|
||||
renderOrders(initial[3]);
|
||||
submit.disabled = !purchaseEnabled;
|
||||
setStatus(purchaseEnabled ? '' : 'forbidden', purchaseEnabled && packages.length
|
||||
? (selectedContextGenealogyId ? '已关联当前家谱,请选择会员套餐' : '请选择会员套餐')
|
||||
: '当前没有可用会员套餐');
|
||||
: (capability.disabledReason || '当前暂不能使用微信扫码支付'));
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
@@ -416,12 +482,14 @@
|
||||
form.addEventListener('submit', async function (event) {
|
||||
var body;
|
||||
var validation;
|
||||
var payment;
|
||||
var created;
|
||||
var refreshed;
|
||||
var verified;
|
||||
|
||||
event.preventDefault();
|
||||
if (writePending) return;
|
||||
if (!purchaseEnabled) return setStatus('forbidden', '当前暂不能创建支付订单');
|
||||
body = buildVipOrderBody({
|
||||
packageId: selectedPackageId,
|
||||
genealogyId: genealogySelect.value
|
||||
@@ -436,13 +504,15 @@
|
||||
setPendingButton(submit, true, '正在创建订单…');
|
||||
setStatus('loading', '正在创建会员订单…');
|
||||
try {
|
||||
created = normalizeVipOrder(await api.createVipOrder(body));
|
||||
if (!created) throw new Error('创建响应缺少有效会员订单信息');
|
||||
payment = normalizePaymentOrder(await api.createVipOrder(body));
|
||||
if (!payment) throw new Error('创建响应缺少有效微信扫码支付信息');
|
||||
created = payment.order;
|
||||
showPayment(payment);
|
||||
refreshed = normalizeVipOrders(await api.vipOrders());
|
||||
verified = refreshed.find(function (item) { return item.orderId === created.orderId; });
|
||||
if (!verified) throw new Error('订单已提交,但重新读取未找到对应记录');
|
||||
renderOrders(refreshed);
|
||||
setStatus('', '订单已创建;当前 PC 暂未开放在线支付。');
|
||||
setStatus('', '支付单已创建,请使用微信扫码支付。');
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
@@ -454,6 +524,31 @@
|
||||
setPendingButton(submit, false, '正在创建订单…');
|
||||
}
|
||||
});
|
||||
|
||||
orderList.addEventListener('click', async function (event) {
|
||||
var refreshButton = event.target.closest('[data-vip-payment-refresh]');
|
||||
var closeButton = event.target.closest('[data-vip-payment-close]');
|
||||
var transactionId;
|
||||
var paymentStatus;
|
||||
|
||||
if (writePending || !refreshButton && !closeButton) return;
|
||||
transactionId = (refreshButton || closeButton).getAttribute(refreshButton ? 'data-vip-payment-refresh' : 'data-vip-payment-close');
|
||||
writePending = true;
|
||||
try {
|
||||
if (closeButton) {
|
||||
if (!await confirmAction('确认关闭这笔支付吗?')) return;
|
||||
await api.closeVipPayment(transactionId);
|
||||
setStatus('', '支付单已关闭');
|
||||
} else {
|
||||
paymentStatus = normalizePaymentStatus(await api.vipPaymentStatus(transactionId));
|
||||
if (!paymentStatus) throw new Error('支付状态响应无效');
|
||||
setStatus('', paymentStatus.status === 'SUCCESS' ? '支付成功' : '当前支付状态:' + paymentStatus.status);
|
||||
}
|
||||
await loadOrders();
|
||||
} catch (error) {
|
||||
setStatus('error', error.message || '支付状态操作失败');
|
||||
} finally { writePending = false; }
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -464,8 +559,11 @@
|
||||
resolveContextGenealogyId: resolveContextGenealogyId,
|
||||
buildVipOrderBody: buildVipOrderBody,
|
||||
validateVipOrderBody: validateVipOrderBody,
|
||||
normalizeVipCapability: normalizeVipCapability,
|
||||
normalizeVipOrder: normalizeVipOrder,
|
||||
normalizeVipOrders: normalizeVipOrders,
|
||||
normalizePaymentOrder: normalizePaymentOrder,
|
||||
normalizePaymentStatus: normalizePaymentStatus,
|
||||
renderVipPackages: renderVipPackages,
|
||||
renderGenealogyOptions: renderGenealogyOptions,
|
||||
renderVipOrders: renderVipOrders,
|
||||
|
||||
Reference in New Issue
Block a user