fix: prevent duplicate titles on Taobao login page

Keep a single native title on the entry page even when the host ignores title clearing. Preserve centered headers elsewhere and normalize manifest line endings during shared-code checks.
This commit is contained in:
xsl
2026-11-05 10:43:01 +08:00
parent 873b45d3a5
commit 0e34eeb1df
6 changed files with 38 additions and 9 deletions
+6
View File
@@ -1,3 +1,9 @@
## 2026-11-04 登录页固定只显示原生标题
用户真机再次确认未登录页仍出现两个标题,说明不能根据 setNavigationBar 的成功回调认定原生首页标题已经清空。本轮登录页明确启用 nativeTitleOnly,首次 Page 数据即 showTitle=false,进入、返回、旋转和成功/失败回调都不再启用自绘标题。登录页 JSON 和运行时都保留 1'me肌肤状态检测 原生标题,公共白色页眉背景、安全区及页面高度不变。其余五个业务页仍使用居中标题,登录页采用淘宝原生对齐方式,不承诺居中。
143 项相关回归通过,新增“平台返回成功但实际未隐藏标题”、API 不存在、失败和生命周期重入场景,保证登录页不再次绘制第二个标题。IDE 重新编译后实际登录页只看到一个左侧原生标题;手机效果仍待新预览确认。共享逻辑检查同时发现 Git 在 Windows 检出 manifest.json 为 CRLF 后误报,已仅对清单文本比较归一化换行;实际五份共享 JS 仍逐字节校验,不放宽业务代码一致性。
## 2026-11-04 未登录首页重复标题的动态清理
用户反馈未登录登录页出现两个标题。此前仅 JSON defaultTitle=零宽空格,尚未在生命周期调用 setNavigationBar,不能证明首页平台标题已在真机隐藏。本轮在公共页眉包装器的 onLoad/onShow/onReady 调用 my.setNavigationBar({title:零宽空格}),不修改导航背景及返回/胶囊;保留原页面生命周期、Promise 返回值与业务登录流程。回调用独立序号及可见性隔离,离页及旧回调不能更改现页标题状态。
+1 -1
View File
@@ -47,4 +47,4 @@ Page(require('../../utils/page-header').page({
onTaobaoLogin: function () { this.enter(false); },
onGuest: function () { this.enter(true); },
noop: function () {}
}, typeof my === 'undefined' ? {} : my, { nativeTitleFallback: true }));
}, typeof my === 'undefined' ? {} : my, { nativeTitleOnly: true }));
+1 -1
View File
@@ -1,5 +1,5 @@
{
"defaultTitle": "​",
"defaultTitle": "1'me肌肤状态检测",
"titleBarColor": "#FFFFFF",
"backgroundColor": "#142632",
"disableScroll": true,
+1 -1
View File
@@ -13,6 +13,6 @@ for (const name of files) {
} else { fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.writeFileSync(dest, source); }
}
const dest = path.join(root, 'vendor/manifest.json'), expected = JSON.stringify(manifest, null, 2) + '\n';
if (check) { if (fs.readFileSync(dest, 'utf8') !== expected) throw Error('Shared manifest drift'); }
if (check) { if (fs.readFileSync(dest, 'utf8').replace(/\r\n/g, '\n') !== expected) throw Error('Shared manifest drift'); }
else fs.writeFileSync(dest, expected);
console.log(check ? 'PASS: scoring, report UI, copy and capture gates match H5 byte for byte' : 'Synced shared logic from H5 (source files unchanged)');
+19 -1
View File
@@ -39,6 +39,23 @@ test('native errors cannot block page loading and rejected promises are observed
assert.equal(loaded, 2);
});
test('native-only entry never draws its own title even when the host reports success without changing the native title', () => {
for (const mode of ['ignored-success', 'failure', 'missing-api']) {
const calls = [];
const platform = mode === 'missing-api' ? {} : { setNavigationBar(options) {
calls.push(options.title);
if (mode === 'ignored-success') options.success({}); else options.fail({});
} };
const p = header.page({ data: {} }, platform, { nativeTitleOnly: true });
p.setData = patch => Object.assign(p.data, patch);
assert.equal(p.data.brandHeader.showTitle, false, 'no duplicate on the first frame');
for (const lifecycle of ['onLoad', 'onShow', 'onReady', 'onResize', 'onHide', 'onShow']) {
p[lifecycle](); assert.equal(p.data.brandHeader.showTitle, false, lifecycle);
}
assert(calls.every(title => title === "1'me肌肤状态检测"), 'entry keeps the native title instead of trying to clear it');
}
});
test('actual header template centers on the full screen and keeps clear of status and native buttons', async () => {
const fs = require('node:fs'), path = require('node:path'), { chromium } = require('playwright');
const root = path.resolve(__dirname, '..');
@@ -63,7 +80,8 @@ test('actual header template centers on the full screen and keeps clear of statu
}
for (const route of JSON.parse(fs.readFileSync(path.join(root, 'app.json'), 'utf8')).pages) {
const config = JSON.parse(fs.readFileSync(path.join(root, route + '.json'), 'utf8'));
assert.equal(config.transparentTitle, 'always'); assert.equal(config.defaultTitle, '\u200b');
assert.equal(config.transparentTitle, 'always');
assert.equal(config.defaultTitle, route === 'pages/login/login' ? header.layout().title : '\u200b');
assert.match(fs.readFileSync(path.join(root, route + '.axml'), 'utf8'), /template is="brand-header"/);
}
} finally { await browser.close(); }
+10 -5
View File
@@ -11,32 +11,37 @@ function layout(platform) {
function page(definition, platform, options) {
options = options || {};
definition.data = Object.assign({ brandHeader: layout() }, definition.data);
// Taobao may report success yet keep its entry-page app title visible.
// Login must never draw a second title, including its very first frame.
if (options.nativeTitleOnly) definition.data.brandHeader.showTitle = false;
var load = definition.onLoad, resize = definition.onResize, show = definition.onShow,
ready = definition.onReady, hide = definition.onHide, unload = definition.onUnload;
function refresh(instance) {
var next = layout(platform);
if (instance.data.brandHeader) next.showTitle = instance.data.brandHeader.showTitle;
if (options.nativeTitleOnly) next.showTitle = false;
instance.setData({ brandHeader: next });
}
function clearNativeTitle(instance) {
function syncNativeTitle(instance) {
if (!platform || typeof platform.setNavigationBar !== 'function') return;
var seq = instance.brandHeaderSeq = (instance.brandHeaderSeq || 0) + 1, settled = false;
function result(ok) {
if (settled) return; settled = true;
if (!instance.brandHeaderVisible || instance.brandHeaderSeq !== seq) return;
if (options.nativeTitleOnly) return;
// The entry page can retain a platform-owned app name. If clearing is
// explicitly rejected, show one native title rather than two titles.
if (!ok && !options.nativeTitleFallback) return;
instance.setData({ brandHeader: Object.assign({}, instance.data.brandHeader, { showTitle: ok }) });
}
try {
var pending = platform.setNavigationBar({ title: '\u200b', success: function () { result(true); }, fail: function () { result(false); } });
var pending = platform.setNavigationBar({ title: options.nativeTitleOnly ? TITLE : '\u200b', success: function () { result(true); }, fail: function () { result(false); } });
if (pending && typeof pending.then === 'function') Promise.resolve(pending).then(function () { result(true); }, function () { result(false); });
} catch (_) { result(false); }
}
definition.onLoad = function () { this.brandHeaderVisible = true; refresh(this); clearNativeTitle(this); if (load) return load.apply(this, arguments); };
definition.onShow = function () { this.brandHeaderVisible = true; clearNativeTitle(this); if (show) return show.apply(this, arguments); };
definition.onReady = function () { clearNativeTitle(this); if (ready) return ready.apply(this, arguments); };
definition.onLoad = function () { this.brandHeaderVisible = true; refresh(this); syncNativeTitle(this); if (load) return load.apply(this, arguments); };
definition.onShow = function () { this.brandHeaderVisible = true; syncNativeTitle(this); if (show) return show.apply(this, arguments); };
definition.onReady = function () { syncNativeTitle(this); if (ready) return ready.apply(this, arguments); };
definition.onHide = function () { this.brandHeaderVisible = false; this.brandHeaderSeq = (this.brandHeaderSeq || 0) + 1; if (hide) return hide.apply(this, arguments); };
definition.onUnload = function () { this.brandHeaderVisible = false; this.brandHeaderSeq = (this.brandHeaderSeq || 0) + 1; if (unload) return unload.apply(this, arguments); };
definition.onResize = function () { refresh(this); if (resize) return resize.apply(this, arguments); };