-
+
+
退
确认退出登录?
退出后需要重新登录,才能继续管理家谱、审核消息和编辑资料。
diff --git a/public/css/profile-module.css b/public/css/profile-module.css
index a2497b7..7e66343 100644
--- a/public/css/profile-module.css
+++ b/public/css/profile-module.css
@@ -543,6 +543,68 @@
box-shadow: 0 24px 60px rgba(57, 48, 36, .11);
}
+.module-card.is-unavailable {
+ cursor: not-allowed;
+ opacity: .68;
+}
+
+.module-card.is-unavailable:after {
+ display: none;
+}
+
+.module-card.is-unavailable .pill {
+ display: inline-flex;
+ margin-top: 12px;
+ color: #6b542d;
+ background: #fff0cf;
+}
+
+.module-nav-unavailable {
+ padding: 14px 16px;
+ border-radius: 14px;
+ color: var(--muted);
+ background: #f4f1eb;
+ font-weight: 900;
+}
+
+.profile-path {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin: 0 0 18px;
+ color: #6f766f;
+ font-size: 14px;
+ font-weight: 800;
+}
+
+.profile-path a {
+ color: var(--red);
+}
+
+.profile-path a:hover {
+ text-decoration: underline;
+}
+
+.profile-path span {
+ color: #a89f91;
+}
+
+.profile-path strong {
+ min-width: 0;
+ overflow: hidden;
+ color: var(--ink);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.module-nav-unavailable small {
+ display: block;
+ margin-top: 4px;
+ color: #8a5a3b;
+ font-size: 12px;
+ font-weight: 700;
+}
+
.module-card.is-selected {
border-color: rgba(200, 59, 50, .5);
background: #fff6f1;
@@ -825,6 +887,10 @@
}
@media (max-width: 720px) {
+ .page-profile-module .module-nav {
+ display: none;
+ }
+
.module-title-row {
display: grid;
}
@@ -833,10 +899,6 @@
font-size: 38px;
}
- .module-nav {
- grid-template-columns: 1fr;
- }
-
.lineage-filter-toolbar {
grid-template-columns: 1fr;
}
diff --git a/public/css/profile.css b/public/css/profile.css
index bd360ca..128fa44 100644
--- a/public/css/profile.css
+++ b/public/css/profile.css
@@ -33,6 +33,7 @@
.profile-card,
.profile-stats,
+.profile-next-card,
.panel,
.side-card {
border: 1px solid rgba(224, 211, 189, .84);
@@ -94,6 +95,13 @@
line-height: 1.9;
}
+.profile-info .profile-load-status {
+ margin: 14px 0 0;
+ color: var(--muted);
+ font-size: 14px;
+ font-weight: 800;
+}
+
.profile-tags,
.mini-actions {
display: flex;
@@ -118,6 +126,29 @@
gap: 14px;
}
+.profile-next-card {
+ padding: 32px;
+}
+
+.profile-next-card .eyebrow {
+ margin-bottom: 14px;
+}
+
+.profile-next-card h2 {
+ margin-bottom: 12px;
+ font-size: 30px;
+}
+
+.profile-next-card p {
+ margin-bottom: 22px;
+ color: var(--muted);
+ line-height: 1.8;
+}
+
+.profile-hero-actions {
+ margin-top: 22px;
+}
+
.profile-stats div {
min-height: 116px;
padding: 20px;
@@ -610,4 +641,4 @@
.logout-actions {
grid-template-columns: 1fr;
}
-}
\ No newline at end of file
+}
diff --git a/public/css/public.css b/public/css/public.css
index 0e58463..18b1bcb 100644
--- a/public/css/public.css
+++ b/public/css/public.css
@@ -186,6 +186,39 @@ img {
white-space: nowrap;
}
+.profile-current-genealogy {
+ min-width: 0;
+ max-width: 240px;
+ display: grid;
+ gap: 2px;
+ margin-left: auto;
+ padding: 7px 12px;
+ border: 1px solid rgba(255, 248, 230, .28);
+ border-radius: 12px;
+ color: #fff8e6;
+ background: rgba(79, 29, 26, .18);
+ line-height: 1.2;
+}
+
+.profile-current-genealogy span {
+ color: rgba(255, 248, 230, .72);
+ font-size: 11px;
+ font-weight: 700;
+}
+
+.profile-current-genealogy strong {
+ overflow: hidden;
+ color: #fff8e6;
+ font-size: 14px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.profile-mobile-menu-toggle,
+.profile-mobile-menu {
+ display: none;
+}
+
.btn {
min-width: 118px;
height: 44px;
@@ -790,6 +823,78 @@ textarea {
background: #fff4ef;
}
+@media (max-width: 1320px) {
+ .page-profile .nav-links,
+ .page-profile-module .nav-links {
+ display: none;
+ }
+
+ .profile-mobile-menu-toggle {
+ min-width: 58px;
+ height: 38px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 12px;
+ border: 1px solid rgba(255, 248, 230, .38);
+ border-radius: 10px;
+ color: #fff8e6;
+ background: rgba(79, 29, 26, .2);
+ font: inherit;
+ font-size: 13px;
+ font-weight: 800;
+ }
+
+ .page-profile .nav-actions,
+ .page-profile-module .nav-actions {
+ display: none;
+ }
+
+ .profile-mobile-menu.is-open {
+ display: block;
+ position: absolute;
+ top: 100%;
+ right: 0;
+ left: 0;
+ padding: 14px 0 18px;
+ border-bottom: 1px solid rgba(255, 253, 248, .24);
+ background: rgba(121, 42, 35, .99);
+ box-shadow: 0 22px 38px rgba(83, 32, 26, .28);
+ }
+
+ .profile-mobile-menu-inner {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 8px;
+ }
+
+ .profile-mobile-menu a,
+ .profile-mobile-menu button {
+ min-height: 46px;
+ display: flex;
+ align-items: center;
+ padding: 10px 14px;
+ border: 1px solid rgba(255, 248, 230, .16);
+ border-radius: 10px;
+ color: #fff8e6;
+ background: rgba(255, 253, 248, .08);
+ font: inherit;
+ font-weight: 800;
+ text-align: left;
+ }
+
+ .profile-mobile-menu button {
+ width: 100%;
+ cursor: pointer;
+ }
+
+ .profile-mobile-menu a.is-current {
+ border-color: rgba(255, 241, 202, .76);
+ color: #fff7d8;
+ background: rgba(255, 253, 248, .2);
+ }
+}
+
@media (max-width: 1120px) {
.nav-links {
display: none;
@@ -819,6 +924,57 @@ textarea {
display: none;
}
+ .page-profile-module .nav,
+ .page-profile .nav {
+ height: 70px;
+ gap: 10px;
+ }
+
+ .page-profile-module .brand,
+ .page-profile .brand {
+ padding: 7px 10px;
+ }
+
+ .page-profile-module .brand-logo,
+ .page-profile .brand-logo {
+ width: 114px;
+ }
+
+ .page-profile-module .brand-logo-mark {
+ width: 38px;
+ height: 38px;
+ }
+
+ .page-profile-module .brand-text {
+ display: none;
+ }
+
+ .page-profile-module .nav-actions {
+ display: none;
+ }
+
+ .profile-current-genealogy {
+ max-width: 112px;
+ padding: 7px 9px;
+ }
+
+ .profile-current-genealogy span {
+ display: none;
+ }
+
+ .profile-current-genealogy strong {
+ font-size: 13px;
+ }
+
+ .profile-mobile-menu-toggle {
+ min-width: 50px;
+ padding: 0 9px;
+ }
+
+ .profile-mobile-menu-inner {
+ grid-template-columns: 1fr;
+ }
+
.inner-hero .container {
padding: 46px 0 54px;
}
diff --git a/public/js/auth-pages.js b/public/js/auth-pages.js
index 79cf2c8..c19b477 100644
--- a/public/js/auth-pages.js
+++ b/public/js/auth-pages.js
@@ -104,6 +104,39 @@
return config.successUrl || '';
}
+ function normalizeGenealogyId(value) {
+ var id = value === undefined || value === null ? '' : String(value).trim();
+
+ if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
+ return /^[1-9][0-9]*$/.test(id) ? id : '';
+ }
+
+ function getPostLoginUrl(genealogies) {
+ var ids = (Array.isArray(genealogies) ? genealogies : []).map(function (item) {
+ return normalizeGenealogyId(item && item.genealogyId);
+ }).filter(Boolean).filter(function (id, index, values) {
+ return values.indexOf(id) === index;
+ });
+
+ if (ids.length === 1) {
+ return 'profile-family-home.html?genealogyId=' + encodeURIComponent(ids[0]);
+ }
+
+ return 'profile-families.html';
+ }
+
+ async function resolvePostLoginUrl(api, formType) {
+ if ((formType !== 'login' && formType !== 'sms-login') || !api || !api.genealogiesMine) {
+ return getSuccessUrl(formType);
+ }
+
+ try {
+ return getPostLoginUrl(await api.genealogiesMine());
+ } catch (error) {
+ return getSuccessUrl(formType);
+ }
+ }
+
function getFormCaptchaOperation(form, formType) {
// 业务动作由当前脚本统一管理,避免散落在 HTML 属性中。
var operationCode = getCaptchaOperation(formType);
@@ -327,7 +360,7 @@
setBusy(button, true);
try {
await api.login(buildLoginBody(values));
- root.location.href = getSuccessUrl('login');
+ root.location.href = await resolvePostLoginUrl(api, 'login');
} catch (error) {
showMessage(error.message || '登录失败');
} finally {
@@ -348,7 +381,7 @@
setBusy(button, true);
try {
await api.loginBySms(buildSmsLoginBody(values));
- root.location.href = getSuccessUrl('sms-login');
+ root.location.href = await resolvePostLoginUrl(api, 'sms-login');
} catch (error) {
showMessage(error.message || '短信登录失败');
} finally {
@@ -489,6 +522,8 @@
getCaptchaOperation: getCaptchaOperation,
getFormCaptchaOperation: getFormCaptchaOperation,
getSuccessUrl: getSuccessUrl,
+ getPostLoginUrl: getPostLoginUrl,
+ resolvePostLoginUrl: resolvePostLoginUrl,
validateAuthValues: validateAuthValues,
init: init
};
diff --git a/public/js/family-home-pages.js b/public/js/family-home-pages.js
index 5914090..2e0be9b 100644
--- a/public/js/family-home-pages.js
+++ b/public/js/family-home-pages.js
@@ -163,7 +163,11 @@
try {
result = await loadFamilyHome(api, genealogyId);
+ if (root.ProfileUI && root.ProfileUI.setCurrentGenealogyContext) {
+ root.ProfileUI.setCurrentGenealogyContext(result.overview.genealogyId, result.overview.genealogyName);
+ }
setText('[data-family-home-title]', result.overview.genealogyName);
+ setText('[data-family-home-context]', result.overview.genealogyName);
query('[data-family-home-overview]').innerHTML = renderFamilyOverview(result.overview);
setText('[data-family-home-member-count]', String(result.overview.memberCount));
setText('[data-family-home-person-count]', String(result.overview.personCount));
diff --git a/public/js/genealogy-entry-pages.js b/public/js/genealogy-entry-pages.js
index 2462492..db77257 100644
--- a/public/js/genealogy-entry-pages.js
+++ b/public/js/genealogy-entry-pages.js
@@ -209,6 +209,9 @@
if (!created) throw new Error('创建响应缺少有效家谱信息');
verified = normalizeCreatedGenealogy(await api.genealogyDetail(created.genealogyId));
if (!verified || verified.genealogyId !== created.genealogyId) throw new Error('创建后无法读取同一家谱');
+ if (root.ProfileUI && root.ProfileUI.setCurrentGenealogyContext) {
+ root.ProfileUI.setCurrentGenealogyContext(verified.genealogyId, verified.genealogyName);
+ }
if (root.location) root.location.href = buildCreatedGenealogyUrl(verified);
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
@@ -266,6 +269,7 @@
].filter(Boolean).join(' · ');
return '
' +
'' + escapeHtml(item.genealogyName) + '
' + escapeHtml(meta) +
'
' + (item.canManage ? '管理并进入' : '进入家谱') + '';
@@ -284,6 +288,10 @@
var result;
if (!status) return;
+ if (shouldRedirectToLogin(api)) {
+ redirectToLogin(api);
+ return;
+ }
if (!api || !api.genealogyQuota || !api.genealogiesMine) {
status.textContent = '家谱入口接口初始化失败,请刷新后重试。';
return;
@@ -294,6 +302,10 @@
renderGenealogies(container, result[0], target);
status.textContent = buildStatus(result[1]);
} catch (error) {
+ if (shouldRedirectToLogin(api, error)) {
+ redirectToLogin(api);
+ return;
+ }
status.textContent = '无法读取我的家谱,请稍后重试。';
}
}
diff --git a/public/js/profile-common.js b/public/js/profile-common.js
index 74925b7..5cc514b 100644
--- a/public/js/profile-common.js
+++ b/public/js/profile-common.js
@@ -5,6 +5,77 @@
var layuiReady = false;
var layerApi = null;
+ var genealogyContextStorageKey = 'genealogy_current_context_v1';
+ var mobileMenuId = 'profile-mobile-menu';
+
+ function getSessionStorage() {
+ try {
+ return window.sessionStorage;
+ } catch (error) {
+ return null;
+ }
+ }
+
+ function normalizeGenealogyName(value) {
+ var name = String(value === undefined || value === null ? '' : value).trim();
+
+ return name && name.length <= 80 ? name : '';
+ }
+
+ function getStoredGenealogyContext() {
+ var storage = getSessionStorage();
+ var stored;
+ var context;
+ var genealogyId;
+ var genealogyName;
+
+ if (!storage) return null;
+ try {
+ stored = storage.getItem(genealogyContextStorageKey);
+ context = stored ? JSON.parse(stored) : null;
+ } catch (error) {
+ return null;
+ }
+ genealogyId = normalizeGenealogyId(context && context.genealogyId);
+ genealogyName = normalizeGenealogyName(context && context.genealogyName);
+ return genealogyId && genealogyName ? {
+ genealogyId: genealogyId,
+ genealogyName: genealogyName
+ } : null;
+ }
+
+ function setCurrentGenealogyContext(genealogyId, genealogyName) {
+ var storage = getSessionStorage();
+ var context = {
+ genealogyId: normalizeGenealogyId(genealogyId),
+ genealogyName: normalizeGenealogyName(genealogyName)
+ };
+
+ if (!context.genealogyId || !context.genealogyName) return null;
+ if (storage) {
+ try {
+ storage.setItem(genealogyContextStorageKey, JSON.stringify(context));
+ } catch (error) {}
+ }
+ renderCurrentGenealogyContext();
+ return context;
+ }
+
+ function clearCurrentGenealogyContext() {
+ var storage = getSessionStorage();
+
+ if (!storage) return;
+ try {
+ storage.removeItem(genealogyContextStorageKey);
+ } catch (error) {}
+ }
+
+ function getCurrentGenealogyContext() {
+ var genealogyId = getGenealogyId();
+ var context = getStoredGenealogyContext();
+
+ return context && context.genealogyId === genealogyId ? context : null;
+ }
function openLegacyLogout() {
var $modal = $('#logoutModal');
@@ -58,6 +129,7 @@
function performLogout(targetUrl) {
var api = window.GenealogyApi && window.GenealogyApi.defaultClient;
var redirect = function () {
+ clearCurrentGenealogyContext();
window.location.href = targetUrl || 'index.html';
};
@@ -172,6 +244,113 @@
});
}
+ function renderCurrentGenealogyContext() {
+ var context = getCurrentGenealogyContext();
+ var name = context ? context.genealogyName : '正在识别家谱';
+
+ $('[data-current-genealogy-name]').text(name);
+ $('[data-current-genealogy-link]').attr('aria-label', context ?
+ '当前家谱:' + context.genealogyName + ',点击切换家谱' :
+ '当前家谱,点击切换家谱');
+ }
+
+ function refreshCurrentGenealogyContext() {
+ var genealogyId = getGenealogyId();
+ var api = window.GenealogyApi && window.GenealogyApi.defaultClient;
+
+ if (!genealogyId || getCurrentGenealogyContext() || !api || !api.genealogyDetail) return;
+ api.genealogyDetail(genealogyId).then(function (detail) {
+ var detailId = normalizeGenealogyId(detail && detail.genealogyId);
+ var detailName = normalizeGenealogyName(detail && detail.genealogyName);
+
+ if (detailId === genealogyId && detailName) {
+ setCurrentGenealogyContext(detailId, detailName);
+ }
+ }).catch(function () {
+ $('[data-current-genealogy-name]').text('当前家谱');
+ });
+ }
+
+ function initCurrentGenealogyContext() {
+ var genealogyId = getGenealogyId();
+ var $nav;
+ var $actions;
+ var $context;
+
+ if (!genealogyId) return;
+ $nav = $('.site-header .nav').first();
+ if (!$nav.length || $nav.find('[data-current-genealogy-link]').length) return;
+ $context = $('
' +
+ '当前家谱正在识别家谱');
+ $actions = $nav.find('.nav-actions').first();
+ if ($actions.length) $context.insertBefore($actions);
+ else $nav.append($context);
+ renderCurrentGenealogyContext();
+ refreshCurrentGenealogyContext();
+ }
+
+ function setMobileMenuOpen(isOpen) {
+ var $menu = $('[data-profile-mobile-menu]').first();
+ var $toggle = $('[data-profile-mobile-menu-toggle]').first();
+
+ if (!$menu.length || !$toggle.length) return;
+ $menu.prop('hidden', !isOpen).toggleClass('is-open', !!isOpen).attr('aria-hidden', isOpen ? 'false' : 'true');
+ $toggle.attr('aria-expanded', isOpen ? 'true' : 'false');
+ }
+
+ function initMobileMenu() {
+ var $header = $('.site-header').first();
+ var $nav;
+ var currentPage;
+
+ if (!$header.length || $header.find('[data-profile-mobile-menu]').length) return;
+ $nav = $header.find('.nav').first();
+ if (!$nav.length) return;
+ $nav.append('');
+ $header.append('');
+ 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];
+
+ if (target === currentPage) $link.addClass('is-current').attr('aria-current', 'page');
+ });
+ }
+
+ function bindGenealogySelection() {
+ $(document).on('click', '[data-genealogy-id][data-genealogy-name]', function () {
+ setCurrentGenealogyContext($(this).attr('data-genealogy-id'), $(this).attr('data-genealogy-name'));
+ });
+ }
+
+ function bindMobileMenu() {
+ $(document).on('click', '[data-profile-mobile-menu-toggle]', function () {
+ setMobileMenuOpen($(this).attr('aria-expanded') !== 'true');
+ });
+ $(document).on('click', '[data-profile-mobile-menu] a, [data-profile-mobile-menu] button', function () {
+ setMobileMenuOpen(false);
+ });
+ $(document).on('click', function (event) {
+ if (!$(event.target).closest('.site-header').length) setMobileMenuOpen(false);
+ });
+ $(document).on('keydown', function (event) {
+ if (event.key === 'Escape') setMobileMenuOpen(false);
+ });
+ }
+
function updateDisplay($trigger, value) {
var target = $trigger.attr('data-update-target');
var $target = target ? $(target) : $trigger.closest('p').find('b').first();
@@ -277,6 +456,10 @@
bindSelectActions();
bindMessageActions();
bindSelectableCards();
+ bindGenealogySelection();
+ bindMobileMenu();
+ initCurrentGenealogyContext();
+ initMobileMenu();
syncGenealogyContextLinks();
});
@@ -288,6 +471,10 @@
normalizeGenealogyId: normalizeGenealogyId,
withGenealogyId: withGenealogyId,
getGenealogyEntryUrl: getGenealogyEntryUrl,
+ getCurrentGenealogyContext: getCurrentGenealogyContext,
+ setCurrentGenealogyContext: setCurrentGenealogyContext,
+ clearCurrentGenealogyContext: clearCurrentGenealogyContext,
+ refreshCurrentGenealogyContext: refreshCurrentGenealogyContext,
syncGenealogyContextLinks: syncGenealogyContextLinks
};
})(window, window.jQuery || (window.layui && window.layui.$));
diff --git a/public/js/profile-pages.js b/public/js/profile-pages.js
index 0071996..2b4e983 100644
--- a/public/js/profile-pages.js
+++ b/public/js/profile-pages.js
@@ -155,6 +155,22 @@
});
}
+ function setProfileLoadStatus(message) {
+ var statusNode = query('[data-profile-load-status]');
+
+ if (statusNode) statusNode.textContent = message || '';
+ }
+
+ function buildUnavailableProfileView() {
+ return {
+ displayName: '账户资料暂时无法读取',
+ avatarText: '!',
+ phone: '暂无数据',
+ birthday: '暂无数据',
+ regionText: '暂无数据'
+ };
+ }
+
function getFormValues(form) {
var values = {};
@@ -236,6 +252,7 @@
if (!api || !documentRef || !documentRef.querySelector('[data-profile-page]')) return;
if (redirectUnauthorized(api)) return;
+ setProfileLoadStatus('正在读取账户资料…');
try {
profile = await api.currentProfile();
@@ -244,8 +261,11 @@
fillProfileForm(profile);
fillRegionPicker(profile);
renderRegionText(profile, requestId);
+ setProfileLoadStatus('');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
+ renderProfile(buildUnavailableProfileView());
+ setProfileLoadStatus('资料读取失败,请刷新页面后重试。');
showMessage(error.message || '个人资料加载失败');
}
}
@@ -293,6 +313,7 @@
getFinalRegionCode: getFinalRegionCode,
buildRegionText: buildRegionText,
buildProfileView: buildProfileView,
+ buildUnavailableProfileView: buildUnavailableProfileView,
buildProfileUpdateBody: buildProfileUpdateBody,
shouldRedirectToLogin: shouldRedirectToLogin,
reloadProfile: loadProfile,
diff --git a/tests/auth-pages.test.js b/tests/auth-pages.test.js
index 44759e2..030f219 100644
--- a/tests/auth-pages.test.js
+++ b/tests/auth-pages.test.js
@@ -118,3 +118,27 @@ test('authentication captcha ticket is removed from the form when consumed', ()
assert.equal(field.value, '');
assert.equal(AuthPages.takeCaptchaToken(form), '');
});
+
+test('successful login enters one real genealogy directly and otherwise opens the chooser', async () => {
+ assert.equal(
+ AuthPages.getPostLoginUrl([{ genealogyId: '2062179707935264769' }]),
+ 'profile-family-home.html?genealogyId=2062179707935264769'
+ );
+ assert.equal(AuthPages.getPostLoginUrl([]), 'profile-families.html');
+ assert.equal(
+ AuthPages.getPostLoginUrl([{ genealogyId: '1' }, { genealogyId: '2' }]),
+ 'profile-families.html'
+ );
+ assert.equal(
+ AuthPages.getPostLoginUrl([{ genealogyId: Number.MAX_SAFE_INTEGER + 1 }]),
+ 'profile-families.html'
+ );
+ assert.equal(
+ await AuthPages.resolvePostLoginUrl({
+ genealogiesMine: async () => {
+ throw new Error('temporary failure');
+ }
+ }, 'login'),
+ 'profile.html'
+ );
+});
diff --git a/tests/genealogy-entry-pages.test.js b/tests/genealogy-entry-pages.test.js
index 643c0f1..6cacc86 100644
--- a/tests/genealogy-entry-pages.test.js
+++ b/tests/genealogy-entry-pages.test.js
@@ -213,19 +213,48 @@ test('家谱业务模块运行时统一委托 ProfileUI 读取和传播上下文
});
});
-test('个人中心和内容发布在缺少家谱上下文时统一进入家谱入口', () => {
+test('个人中心只保留真实家谱入口,内容发布仍会透传家谱上下文', () => {
const profile = read('profile.html');
const content = read('profile-content.html');
const familyHome = read('profile-family-home.html');
const familyEntry = read('profile-families.html');
const profileCommon = read('public/js/profile-common.js');
+ const profileMain = profile.slice(0, profile.indexOf(''));
- assert.match(profile, /进入我的家谱/);
- assert.doesNotMatch(profile, /四川武胜汤氏族|四川达州刘氏族/);
- assert.match(profile, /href="profile-family-admin\.html" data-genealogy-context-link/);
+ assert.match(profileMain, /进入我的家谱/);
+ assert.match(profileMain, /href="profile-families\.html"/);
+ assert.doesNotMatch(profileMain, /鹿野 Loyel|待处理|已发布内容|资料完善度/);
+ assert.doesNotMatch(profileMain, /href="profile-(?:article|album|video|merit|gift|join-review)\.html"/);
assert.match(content, /href="profile-feed\.html" data-feature-link="available" data-genealogy-context-link/);
assert.match(familyHome, /href="profile-family-admin\.html" data-genealogy-context-link/);
assert.match(familyEntry, /href="profile-family-admin\.html" data-genealogy-context-link/);
assert.match(profileCommon, /function getGenealogyEntryUrl\(href\)/);
assert.match(profileCommon, /genealogyId \? withGenealogyId\([^\n]+\) : getGenealogyEntryUrl\(/);
+ assert.match(read('public/js/genealogy-entry-pages.js'), /if \(shouldRedirectToLogin\(api\)\) \{\s+redirectToLogin\(api\);/);
+});
+
+test('当前家谱名称从真实家谱响应写入会话上下文,并在页头提供切换入口', () => {
+ const common = read('public/js/profile-common.js');
+ const entry = read('public/js/genealogy-entry-pages.js');
+ const familyHome = read('public/js/family-home-pages.js');
+
+ assert.match(common, /genealogyContextStorageKey/);
+ assert.match(common, /function setCurrentGenealogyContext\(genealogyId, genealogyName\)/);
+ assert.match(common, /class="profile-current-genealogy"/);
+ assert.match(common, /href="profile-families\.html"/);
+ assert.match(entry, /data-genealogy-name=/);
+ assert.match(entry, /root\.ProfileUI\.setCurrentGenealogyContext\(verified\.genealogyId, verified\.genealogyName\)/);
+ assert.match(familyHome, /root\.ProfileUI\.setCurrentGenealogyContext\(result\.overview\.genealogyId, result\.overview\.genealogyName\)/);
+});
+
+test('小屏导航提供统一入口,并且所有家谱内链接继续透传当前上下文', () => {
+ const common = read('public/js/profile-common.js');
+ const css = read('public/css/public.css');
+
+ assert.match(common, /data-profile-mobile-menu-toggle/);
+ assert.match(common, /data-profile-mobile-menu/);
+ assert.match(common, /profile-family-home\.html" data-genealogy-context-link/);
+ assert.match(common, /profile-family-admin\.html" data-genealogy-context-link/);
+ assert.match(css, /\.profile-mobile-menu\.is-open/);
+ assert.match(css, /\.profile-current-genealogy/);
});
diff --git a/tests/layout-responsiveness.test.js b/tests/layout-responsiveness.test.js
new file mode 100644
index 0000000..b6a0fe8
--- /dev/null
+++ b/tests/layout-responsiveness.test.js
@@ -0,0 +1,36 @@
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const test = require('node:test');
+
+const root = path.join(__dirname, '..');
+
+function read(file) {
+ return fs.readFileSync(path.join(root, file), 'utf8');
+}
+
+test('profile navigation has a medium-desktop compact mode and no mobile duplicate sidebar', () => {
+ const publicCss = read('public/css/public.css');
+ const moduleCss = read('public/css/profile-module.css');
+
+ assert.match(
+ publicCss,
+ /@media \(max-width: 1320px\) \{[\s\S]*?\.page-profile \.nav-links,[\s\S]*?\.page-profile-module \.nav-links \{\s*display: none;/
+ );
+ assert.match(
+ moduleCss,
+ /@media \(max-width: 720px\) \{[\s\S]*?\.page-profile-module \.module-nav \{\s*display: none;/
+ );
+});
+
+test('my genealogies keeps one primary create action and the compact menu marks the current page', () => {
+ const page = read('profile-families.html');
+ const header = page.slice(0, page.indexOf(''));
+ const common = read('public/js/profile-common.js');
+ const publicCss = read('public/css/public.css');
+
+ assert.doesNotMatch(header, /href="profile-create-family\.html"/);
+ assert.match(page, /module-title-row[\s\S]*?href="profile-create-family\.html"/);
+ assert.match(common, /addClass\('is-current'\)\.attr\('aria-current', 'page'\)/);
+ assert.match(publicCss, /\.profile-mobile-menu a\.is-current/);
+});
diff --git a/tests/profile-pages.test.js b/tests/profile-pages.test.js
index eee7eed..76415e8 100644
--- a/tests/profile-pages.test.js
+++ b/tests/profile-pages.test.js
@@ -38,6 +38,16 @@ test('profile view reads only the documented profile fields', () => {
});
});
+test('profile loading failure has an explicit non-demo view', () => {
+ assert.deepEqual(ProfilePages.buildUnavailableProfileView(), {
+ displayName: '账户资料暂时无法读取',
+ avatarText: '!',
+ phone: '暂无数据',
+ birthday: '暂无数据',
+ regionText: '暂无数据'
+ });
+});
+
test('profile update emits only the six fields defined by ProfileUpdateBody', () => {
assert.deepEqual(ProfilePages.buildProfileUpdateBody({
nickName: '小林',
diff --git a/tests/stage6-navigation.test.js b/tests/stage6-navigation.test.js
index 88fb0e3..a93e2f3 100644
--- a/tests/stage6-navigation.test.js
+++ b/tests/stage6-navigation.test.js
@@ -26,12 +26,12 @@ test('家谱主页公开谱文、相册、视频、功德和祭祀活动入口
});
});
-test('个人中心功能入口没有把家谱业务页当作无上下文普通链接', () => {
+test('个人中心先引导进入真实家谱列表,不展示无上下文的业务入口', () => {
const page = read('profile.html');
+ const main = page.slice(0, page.indexOf(''));
- ['profile-article.html', 'profile-album.html', 'profile-video.html', 'profile-merit.html', 'profile-gift.html'].forEach((href) => {
- assert.match(page, new RegExp('href="' + href.replace('.', '\\.') + '"[^>]*data-genealogy-context-link'), `${href} 缺少个人中心上下文入口`);
- });
+ assert.match(main, /href="profile-families\.html"/);
+ assert.doesNotMatch(main, /href="profile-(?:article|album|video|merit|gift|join-review)\.html"/);
});
test('加入申请与管理员审核入口使用真实页面和家谱上下文', () => {
@@ -42,7 +42,7 @@ test('加入申请与管理员审核入口使用真实页面和家谱上下文',
assert.match(families, /href="profile-join-family\.html"/);
assert.doesNotMatch(families, /邀请码|分享码|手填.*ID/);
assert.match(familyAdmin, /href="profile-join-review\.html"[^>]*data-genealogy-context-link/);
- assert.match(profile, /href="profile-join-review\.html"[^>]*data-genealogy-context-link/);
+ assert.match(profile, /href="profile-families\.html"/);
});
test('帮助中心和服务中心开放同一反馈记录闭环', () => {
@@ -54,3 +54,18 @@ test('帮助中心和服务中心开放同一反馈记录闭环', () => {
assert.match(services, /href="profile-feedback\.html"[^>]*data-feature-link="available"/);
assert.doesNotMatch(help + services, /手填.*feedbackId|独立.*ticket.*接口/i);
});
+
+test('个人中心、我的家谱和当前家谱主页使用明确且可返回的三级路径', () => {
+ const account = read('profile.html');
+ const families = read('profile-families.html');
+ const familyHome = read('profile-family-home.html');
+ const familyHomeRuntime = read('public/js/family-home-pages.js');
+ const coreHeaders = [account, families, familyHome].map((page) => page.slice(0, page.indexOf(''))).join('');
+
+ assert.match(account, /
个人中心<\/a>/);
+ assert.match(account, /我的家谱<\/a>/);
+ assert.match(families, /