From 0e34eeb1df6f50b59dc04f9ad421476c371e8397 Mon Sep 17 00:00:00 2001 From: xiangsilian Date: Thu, 5 Nov 2026 10:43:01 +0800 Subject: [PATCH] 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. --- docs/TAOBAO-ADAPTATION-2026-11-02.md | 6 ++++++ taobao/pages/login/login.js | 2 +- taobao/pages/login/login.json | 2 +- taobao/scripts/sync-shared.cjs | 2 +- taobao/test/page-header.test.cjs | 20 +++++++++++++++++++- taobao/utils/page-header.js | 15 ++++++++++----- 6 files changed, 38 insertions(+), 9 deletions(-) diff --git a/docs/TAOBAO-ADAPTATION-2026-11-02.md b/docs/TAOBAO-ADAPTATION-2026-11-02.md index abf7e2a..963ed22 100644 --- a/docs/TAOBAO-ADAPTATION-2026-11-02.md +++ b/docs/TAOBAO-ADAPTATION-2026-11-02.md @@ -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 返回值与业务登录流程。回调用独立序号及可见性隔离,离页及旧回调不能更改现页标题状态。 diff --git a/taobao/pages/login/login.js b/taobao/pages/login/login.js index 9722234..f85e2bc 100644 --- a/taobao/pages/login/login.js +++ b/taobao/pages/login/login.js @@ -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 })); diff --git a/taobao/pages/login/login.json b/taobao/pages/login/login.json index 597251b..431957a 100644 --- a/taobao/pages/login/login.json +++ b/taobao/pages/login/login.json @@ -1,5 +1,5 @@ { - "defaultTitle": "​", + "defaultTitle": "1'me肌肤状态检测", "titleBarColor": "#FFFFFF", "backgroundColor": "#142632", "disableScroll": true, diff --git a/taobao/scripts/sync-shared.cjs b/taobao/scripts/sync-shared.cjs index 6f6e72c..9fe5c0c 100644 --- a/taobao/scripts/sync-shared.cjs +++ b/taobao/scripts/sync-shared.cjs @@ -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)'); diff --git a/taobao/test/page-header.test.cjs b/taobao/test/page-header.test.cjs index b9c21b9..1d1226a 100644 --- a/taobao/test/page-header.test.cjs +++ b/taobao/test/page-header.test.cjs @@ -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(); } diff --git a/taobao/utils/page-header.js b/taobao/utils/page-header.js index 35d16ec..cdc27e7 100644 --- a/taobao/utils/page-header.js +++ b/taobao/utils/page-header.js @@ -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); };