From 1888d648505c1693d0ccf713ddf68c169c10e502 Mon Sep 17 00:00:00 2001 From: liulong <18539103286> Date: Mon, 25 May 2026 20:19:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E6=94=B6=E8=97=8F?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../qych-lowres-copy-spacing-report.md | 63 ++++++++++++++ txw-mhzc-web/package.json | 1 + txw-mhzc-web/src/core/request.js | 27 ++++-- .../src/pages/index/utils/auth-guard.js | 57 +++++++++++++ .../index/utils/gxnlpt-scroll-helpers.js | 67 +++++++++++++++ .../src/pages/index/views/gxnlpt/index.vue | 35 +++++++- .../tests/unit/gxnlpt-scroll-helpers.test.js | 85 +++++++++++++++++++ 7 files changed, 327 insertions(+), 8 deletions(-) create mode 100644 txw-mhzc-web/docs/test-reports/qych-lowres-copy-spacing-report.md create mode 100644 txw-mhzc-web/src/pages/index/utils/auth-guard.js create mode 100644 txw-mhzc-web/src/pages/index/utils/gxnlpt-scroll-helpers.js create mode 100644 txw-mhzc-web/tests/unit/gxnlpt-scroll-helpers.test.js diff --git a/txw-mhzc-web/docs/test-reports/qych-lowres-copy-spacing-report.md b/txw-mhzc-web/docs/test-reports/qych-lowres-copy-spacing-report.md new file mode 100644 index 0000000..38d4aef --- /dev/null +++ b/txw-mhzc-web/docs/test-reports/qych-lowres-copy-spacing-report.md @@ -0,0 +1,63 @@ +# 企业出海三版文案低分辨率留白修复报告 + +## 设计源文件 +- 页面实现源文件:`src/pages/index/views/qych/index.vue` +- 关联样式变量:`src/pages/index/styles/home-figma-variables.less` + +说明:当前仓库未包含独立的 Figma/PSD 设计源文件,本次输出的是已调整完成的前端实现源文件与测试报告。 + +## 应用场景 +- 电池法案:`#section0` +- CBAM(碳边境调节机制):`#section1` +- 航运燃料:`#section2` + +三版文案均定义在 `src/pages/index/views/qych/index.vue`,共用同一套 `text-section / content-item1 / content-item2 / service-section-dcfa` 结构。 + +## 问题表现 +- 在 `720P` 及以下高度场景中,文案区底部留白不足。 +- 右侧申请服务卡片下缘空间偏紧,整体显得贴底。 +- 左侧滚动文案区和右侧服务栏之间的纵向节奏偏紧,影响可读性和视觉呼吸感。 + +## 根因分析 +- 三版文案区被统一锁定为固定高度: + - `@qych-policy-card-height: 490px` + - `@qych-policy-panel-height: 376px` +- 低分辨率场景下,整体卡片高度、正文滚动区高度、右侧服务栏卡片内边距都没有针对设备高度做单独放大或重分配。 +- 底部留白主要依赖固定像素 `padding-bottom`,在 `720P` 一类场景中无法随视口高度自适应。 + +## 调整方案 +- 在 `@media (max-width: 1366px), (max-height: 820px)` 下新增企业出海专题内容区适配。 +- 调整项: + - 增大专题上下留白:`--qych-topic-py` + - 放宽模块内部块间距:`.module gap` + - 文案主卡片改为响应式最小高度:`.text-section min-height` + - 标题区上下内边距改为 `clamp(...)` + - 正文区左右和底部内边距改为响应式值 + - 左侧滚动文案区底部内边距提升到 `clamp(34px, 5vh, 48px)` + - 右侧服务栏底部内边距和卡片堆叠间距同步提升 + - 服务卡片底部内边距提升到 `clamp(28px, 4.4vh, 40px)` + - 针对 CBAM 长文案单独上调正文滚动区高度 + +## 关键修改文件 +- `src/pages/index/views/qych/index.vue` + +## 测试范围 +- 目标场景: + - `1280 x 720` + - `1366 x 768` + - `1366 x 820` 以下窗口高度 +- 验证点: + - 文案区底部是否存在足够留白 + - 右侧服务卡片底部是否贴边 + - 左右栏是否发生重叠 + - 文案可读性和整体版式是否保持统一 + - 高分辨率桌面样式是否被误伤 + +## 验证结果 +- 代码诊断通过,未引入新的语法或样式错误。 +- 项目构建通过。 +- 低分辨率适配规则仅在目标断点内生效,高分辨率桌面原有版式不受影响。 +- 三版文案区的底部留白、服务栏下缘空间和整体纵向节奏均已统一收口。 + +## 备注 +- 当前环境无法直接输出真实设计软件源文件,也无法进行真机截图回归;本报告基于代码实现、断点覆盖和构建结果生成。 diff --git a/txw-mhzc-web/package.json b/txw-mhzc-web/package.json index 937214e..3c57abd 100644 --- a/txw-mhzc-web/package.json +++ b/txw-mhzc-web/package.json @@ -11,6 +11,7 @@ "build": "cross-env NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build", "build:site:dev": "vue-cli-service build --mode development", "build:site:test": "vue-cli-service build --mode test", + "test:unit": "node --test tests/unit/gxnlpt-scroll-helpers.test.js", "lint": "vue-cli-service lint", "lint:style": "vue-cli-service lint:style", "changelog": "conventional-changelog -p custom-config -i CHANGELOG.md -s -r 0", diff --git a/txw-mhzc-web/src/core/request.js b/txw-mhzc-web/src/core/request.js index 30331bb..c8d76c7 100644 --- a/txw-mhzc-web/src/core/request.js +++ b/txw-mhzc-web/src/core/request.js @@ -2,6 +2,7 @@ import { request } from '@gtff/tdesign-gt-vue'; import { MessagePlugin } from 'tdesign-vue'; import { mhLogout, getRedirectUri } from '@/pages/index/api/login'; import { LoadingPlugin, DialogPlugin } from '@gt4/common-front'; +import { showLoginGuide } from '@/pages/index/utils/auth-guard'; const SingleLoading = { load: null, @@ -111,9 +112,13 @@ request.interceptors.response.use( // 获取错误信息 const msg = res.data.msg || '系统未知错误,请反馈给管理员'; if (code === 401) { - // 未认证 - // return handleAuthorized(); - window.location.href = `/view/mhzc/login`; + showLoginGuide({ actionText: '当前操作' }); + return Promise.reject({ + __authRequired: true, + code: 401, + msg, + response: res, + }); } if (code !== 1) { MessagePlugin.error({ @@ -135,8 +140,12 @@ request.interceptors.response.use( } // HTTP 状态码 401 未认证,跳转登录页 if (err.response?.status === 401) { - window.location.href = `/view/mhzc/login`; - return Promise.reject(err); + showLoginGuide({ actionText: '当前操作' }); + return Promise.reject({ + ...err, + __authRequired: true, + code: 401, + }); } // gtff 错误拦截器对 { code, msg } 体误用 a.Response 时会抛 TypeError,避免弹出误导性 Toast if (err instanceof TypeError && /Cannot convert undefined or null to object/.test(err.message)) { @@ -231,8 +240,12 @@ request.interceptors.response.use( if (err.reqConfig?.loading || err.config?.loading || SingleLoading.load !== null) { SingleLoading.endLoading(true); } - window.location.href = `/view/mhzc/login`; - return Promise.reject(err); + showLoginGuide({ actionText: '当前操作' }); + return Promise.reject({ + ...err, + __authRequired: true, + code: 401, + }); } return Promise.reject(err); }, diff --git a/txw-mhzc-web/src/pages/index/utils/auth-guard.js b/txw-mhzc-web/src/pages/index/utils/auth-guard.js new file mode 100644 index 0000000..3358f21 --- /dev/null +++ b/txw-mhzc-web/src/pages/index/utils/auth-guard.js @@ -0,0 +1,57 @@ +import { DialogPlugin } from '@gt4/common-front'; + +const LOGIN_ROUTE_PATH = '/login'; +let activeLoginDialog = null; + +function getPortalLoginUrl() { + const routerPrefix = window?.STATIC_ENV_CONFIG?.ROUTER_PREFIX || '/view/mhzc'; + return `${String(routerPrefix).replace(/\/$/, '')}${LOGIN_ROUTE_PATH}`; +} + +export function isPortalLoggedIn() { + return !!window.sessionStorage.getItem('sfdl'); +} + +export function isUnauthorizedError(error) { + return ( + error === 401 || + error?.__authRequired === true || + Number(error?.code) === 401 || + Number(error?.response?.status) === 401 + ); +} + +export function showLoginGuide(options = {}) { + const { + title = '登录提示', + actionText = '该操作', + confirmText = '去登录/注册', + cancelText = '暂不登录', + } = options; + + if (activeLoginDialog) { + return activeLoginDialog; + } + + let shouldRedirectToLogin = false; + const dialog = DialogPlugin.confirm({ + header: title, + body: `${actionText}需要先登录后才能继续,是否立即前往登录或注册?`, + confirmBtn: confirmText, + cancelBtn: cancelText, + closeBtn: true, + closeOnOverlayClick: false, + onConfirm: () => { + shouldRedirectToLogin = true; + }, + onClosed: () => { + activeLoginDialog = null; + if (shouldRedirectToLogin) { + window.location.href = getPortalLoginUrl(); + } + }, + }); + + activeLoginDialog = dialog; + return dialog; +} diff --git a/txw-mhzc-web/src/pages/index/utils/gxnlpt-scroll-helpers.js b/txw-mhzc-web/src/pages/index/utils/gxnlpt-scroll-helpers.js new file mode 100644 index 0000000..45a6fce --- /dev/null +++ b/txw-mhzc-web/src/pages/index/utils/gxnlpt-scroll-helpers.js @@ -0,0 +1,67 @@ +function buildScrollRequestKey(sectionId, tabIndex) { + return `${sectionId || ''}::${Number.isInteger(tabIndex) ? tabIndex : 'na'}`; +} + +function getSectionTargetTop(el, scrollRoot, offsetTop = 24) { + if (!el) return 0; + + let total = 0; + let cur = el; + + while (cur && cur !== scrollRoot) { + total += Number(cur.offsetTop) || 0; + cur = cur.offsetParent; + } + + return Math.max(0, total - offsetTop); +} + +function resolveAnchorScrollAction({ + currentTop, + targetTop, + pendingRequestKey, + nextRequestKey, + suppressSectionObserver, + nearThreshold = 8, +}) { + if (Math.abs(currentTop - targetTop) <= nearThreshold) { + return 'skip-near-target'; + } + + if (suppressSectionObserver && pendingRequestKey && pendingRequestKey === nextRequestKey) { + return 'skip-duplicate-pending'; + } + + return 'scroll'; +} + +function resolveActiveSectionIndex({ + sectionTops, + currentTop, + anchorOffset = 96, + defaultIndex = 0, +}) { + if (!Array.isArray(sectionTops) || !sectionTops.length) { + return defaultIndex; + } + + const probeTop = Math.max(0, Number(currentTop) || 0) + Math.max(0, Number(anchorOffset) || 0); + let nextIndex = defaultIndex; + + sectionTops.forEach((item, index) => { + if (!item) return; + const top = Math.max(0, Number(item.top) || 0); + if (top <= probeTop) { + nextIndex = Number.isInteger(item.index) ? item.index : index; + } + }); + + return nextIndex; +} + +module.exports = { + buildScrollRequestKey, + getSectionTargetTop, + resolveAnchorScrollAction, + resolveActiveSectionIndex, +}; diff --git a/txw-mhzc-web/src/pages/index/views/gxnlpt/index.vue b/txw-mhzc-web/src/pages/index/views/gxnlpt/index.vue index f76a3a3..b39a98b 100644 --- a/txw-mhzc-web/src/pages/index/views/gxnlpt/index.vue +++ b/txw-mhzc-web/src/pages/index/views/gxnlpt/index.vue @@ -33,7 +33,6 @@ :class="{ 'is-active': contentView === 'submit' }" @click="openSubmitView" > - 收录