feat:修复部分问题

This commit is contained in:
liulong 2026-06-02 23:28:48 +08:00
parent 60abcddfdf
commit ff5bcae58f
31 changed files with 26478 additions and 762 deletions

6
.codex/config.toml Normal file
View File

@ -0,0 +1,6 @@
[mcp_servers.code-review-graph]
command = "uvx"
args = [
"code-review-graph",
"serve",
]

28
.codex/hooks.json Normal file
View File

@ -0,0 +1,28 @@
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|Bash",
"hooks": [
{
"type": "command",
"command": "code-review-graph update --skip-flows",
"timeout": 30
}
]
}
],
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "code-review-graph status",
"timeout": 10
}
]
}
]
}
}

38
AGENTS.md Normal file
View File

@ -0,0 +1,38 @@
<!-- code-review-graph MCP tools -->
## MCP Tools: code-review-graph
**IMPORTANT: This project has a knowledge graph. ALWAYS use the
code-review-graph MCP tools BEFORE using Grep/Glob/Read to explore
the codebase.** The graph is faster, cheaper (fewer tokens), and gives
you structural context (callers, dependents, test coverage) that file
scanning cannot.
### When to use graph tools FIRST
- **Exploring code**: `semantic_search_nodes` or `query_graph` instead of Grep
- **Understanding impact**: `get_impact_radius` instead of manually tracing imports
- **Code review**: `detect_changes` + `get_review_context` instead of reading entire files
- **Finding relationships**: `query_graph` with callers_of/callees_of/imports_of/tests_for
- **Architecture questions**: `get_architecture_overview` + `list_communities`
Fall back to Grep/Glob/Read **only** when the graph doesn't cover what you need.
### Key Tools
| Tool | Use when |
|------|----------|
| `detect_changes` | Reviewing code changes — gives risk-scored analysis |
| `get_review_context` | Need source snippets for review — token-efficient |
| `get_impact_radius` | Understanding blast radius of a change |
| `get_affected_flows` | Finding which execution paths are impacted |
| `query_graph` | Tracing callers, callees, imports, tests, dependencies |
| `semantic_search_nodes` | Finding functions/classes by name or keyword |
| `get_architecture_overview` | Understanding high-level codebase structure |
| `refactor_tool` | Planning renames, finding dead code |
### Workflow
1. The graph auto-updates on file changes (via hooks).
2. Use `detect_changes` for code review.
3. Use `get_affected_flows` to understand impact.
4. Use `query_graph` pattern="tests_for" to check coverage.

View File

@ -0,0 +1,54 @@
// 截图验证
import WebSocket from 'ws';
import { writeFileSync } from 'fs';
const res = await fetch('http://127.0.0.1:9222/json');
const targets = await res.json();
const target = targets.find((t) => t.type === 'page') || targets[0];
const ws = new WebSocket(target.webSocketDebuggerUrl);
let msgId = 0;
const pending = new Map();
function send(method, params = {}) {
return new Promise((resolve, reject) => {
const id = ++msgId;
pending.set(id, { resolve, reject });
ws.send(JSON.stringify({ id, method, params }));
});
}
ws.on('open', async () => {
try {
await send('Page.enable');
await send('Runtime.enable');
await send('Emulation.setDeviceMetricsOverride', { width: 1280, height: 800, deviceScaleFactor: 1, mobile: false });
await send('Page.navigate', { url: 'http://localhost:9002/gxnlpt' });
await new Promise((r) => setTimeout(r, 6000));
const state = await send('Runtime.evaluate', {
expression: `JSON.stringify({
dialogs: document.querySelectorAll('.t-dialog').length,
sections: document.querySelectorAll('.gxnlpt-block').length,
cards: document.querySelectorAll('.gxnlpt-card').length,
})`,
returnByValue: true,
});
console.log('state:', state.result?.value);
const shot = await send('Page.captureScreenshot', { format: 'png' });
writeFileSync('E:/develop/code/txw/txw-mhzc-web/scripts/final-fix.png', Buffer.from(shot.data, 'base64'));
} catch (e) {
console.error(e);
} finally {
ws.close();
}
});
ws.on('message', (data) => {
const msg = JSON.parse(data);
if (msg.id && pending.has(msg.id)) {
const { resolve, reject } = pending.get(msg.id);
pending.delete(msg.id);
if (msg.error) reject(new Error(msg.error.message));
else resolve(msg.result);
}
});
ws.on('error', console.error);

View File

@ -1,5 +1,8 @@
module.exports = {
root: true,
// babel-eslint 能正确解析 ES module 的 import/export 语法
// 默认的 espree parser 不支持,会报 "export is reserved"
parser: 'babel-eslint',
extends: [],
rules: {
// 关闭所有校验

24256
txw-mhzc-web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@
"@vue/babel-preset-jsx": "1.4.0"
},
"dependencies": {
"@cssyq/ggzc-web": "file:../local-nodemodules/@cssyq/ggzc-web",
"@fortawesome/fontawesome-free": "^7.0.1",
"@gt4/common-front": "2.0.113",
"@gtff/tdesign-gt-vue": "1.4.0",
@ -102,9 +103,9 @@
"author": "wangjianxin@css.com.cn <wangjianxin@css.com.cn>",
"license": "MIT",
"resolutions": {
"ggzc-web": "link:D:\\shanghai\\txw2\\local-nodemodules\\@cssyq\\ggzc-web",
"common-front": "link:D:\\shanghai\\txw2\\local-nodemodules\\@gt4\\common-front",
"tdesign-gt-vue": "link:D:\\shanghai\\txw2\\local-nodemodules\\@gtff\\tdesign-gt-vue"
"ggzc-web": "file:../local-nodemodules/@cssyq/ggzc-web",
"common-front": "file:../local-nodemodules/@gt4/common-front",
"tdesign-gt-vue": "file:../local-nodemodules/@gtff/tdesign-gt-vue"
},
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
}

View File

@ -0,0 +1,6 @@
# 前端外部配置占位
`config.json` 用于消除开发环境对 `/config/config.json` 的 404 请求。
- 业务默认配置在 `src/settings/index.js`,通过 `main.js``extSettings` 注入。
- 本文件保持 `{}` 即可;仅在需要**不改代码、热覆盖**默认配置时,再在此写入键值(与 `settings/index.js` 同结构)。

View File

@ -0,0 +1,3 @@
{
"code": 200
}

View File

@ -1,146 +0,0 @@
import fs from 'fs';
const path = 'e:/develop/code/txw/txw-mhzc-web/src/pages/index/views/home2/index.vue';
let s = fs.readFileSync(path, 'utf8');
const start = s.indexOf('<style lang="less" scoped>');
const end = s.indexOf('</style>', start);
if (start < 0 || end < 0) throw new Error('style block not found');
let css = s.slice(start, end);
const reps = [
[/#f5f7fa\b/g, '@home-color-page-bg'],
[/#003b1a\b/gi, '@home-color-primary-dark'],
[/#556659\b/gi, '@home-color-text-body'],
[/#1a1a2e\b/gi, '@home-color-text-title'],
[/#999999\b/g, '@home-color-text-muted'],
[/\b#999\b/g, '@home-color-text-muted'],
[/#333333\b/g, '@home-color-text-dark'],
[/\b#333\b/g, '@home-color-text-dark'],
[/#666666\b/g, '@home-color-text-secondary'],
[/\b#666\b/g, '@home-color-text-secondary'],
[/#222222\b/g, '@home-color-text-partner'],
[/\b#222\b/g, '@home-color-text-partner'],
[/#000000\b/g, '@home-color-black'],
[/\b#000\b/g, '@home-color-black'],
[/#ffffff\b/gi, '@home-color-white'],
[/\b#fff\b/gi, '@home-color-white'],
[/#00b96b\b/gi, '@home-color-primary-green'],
[/#00a25d\b/gi, '@home-color-primary-green-dark'],
[/#2e7d32\b/gi, '@home-color-secondary-green'],
[/#e6efe6\b/gi, '@home-color-tab-bg'],
[/#008cff\b/gi, '@home-color-accent-blue'],
[/#2196f3\b/gi, '@home-color-btn-blue-to'],
[/#99d2fe\b/gi, '@home-color-btn-blue-from'],
[/#007242\b/gi, '@home-color-cta-btn-from'],
[/#00d87d\b/gi, '@home-color-cta-btn-to'],
[/#f0f7f2\b/gi, '@home-color-footer-bg'],
[/'PingFang SC', 'Microsoft YaHei', sans-serif/g, '@home-font-family'],
[/'PingFang SC', sans-serif/g, '@home-font-family'],
[/font-family: PingFang SC;/g, 'font-family: @home-font-family;'],
[/font-family: 'PingFang SC';/g, 'font-family: @home-font-family;'],
[/linear-gradient\(180deg, #00b96b 0%, #00a25d 100%\)/g, '@home-gradient-search-btn'],
[/linear-gradient\(180deg, rgba\(245, 247, 250, 0\) 0%, rgba\(245, 247, 250\) 100%\)/g, '@home-gradient-hero-fade'],
[/linear-gradient\(180deg, rgba\(245, 247, 250, 0\) 0%, rgba\(245, 247, 250, 1\) 100%\)/g, '@home-gradient-hero-fade'],
[/linear-gradient\(90deg, #ffffff 0%, #e6efff 36\.06%, #e6efff 62\.5%, #ffffff 100%\)/g, '@home-gradient-core-tag'],
[/linear-gradient\(0deg, rgba\(255, 255, 255, 0\.9\) 0%, rgba\(255, 255, 255, 0\.4\) 100%\)/g, '@home-gradient-ability-card'],
[/linear-gradient\(180deg, #99d2fe 0%, #2196f3 100%\)/g, '@home-gradient-export-btn'],
[/linear-gradient\(90deg, #007242 0%, #00d87d 100%\)/g, '@home-gradient-cta-btn'],
[/0 8px 12px rgba\(0, 0, 0, 0\.08\)/g, '@home-shadow-search'],
[/0 8px 12px rgba\(127, 179, 213, 0\.2\)/g, '@home-shadow-card-blue'],
[/0 8px 12px rgba\(0, 185, 107, 0\.1\)/g, '@home-shadow-card-green'],
[/0 8px 20px rgba\(0, 209, 121, 0\.3\)/g, '@home-shadow-card-green-strong'],
[/0 2px 5px rgba\(0, 0, 0, 0\.1\)/g, '@home-shadow-news-item'],
[/0 2px 10px rgba\(0, 0, 0, 0\.1\)/g, '@home-shadow-news-item'],
[/0px 4px 8px 0px rgba\(0, 185, 107, 0\.06\)/g, '@home-shadow-partner-logo'],
[/0 4px 12px rgba\(255, 255, 255, 1\)/g, '@home-shadow-title-sub'],
[/0 0 8px rgba\(255, 255, 255, 1\)/g, '@home-shadow-card-title'],
[/0 0 8px #ffffff/g, '@home-shadow-card-title'],
[/0 4px 12px #ffffff/g, '@home-shadow-title-sub'],
[/rgba\(255, 255, 255, 0\.7\)/g, '@home-color-white-70'],
[/rgba\(255, 255, 255, 0\.6\)/g, '@home-color-white-60'],
[/background: rgba\(245, 247, 250, 1\)/g, 'background: @home-color-page-bg'],
[/background: rgba\(222, 243, 255, 0\.75\)/g, '@home-color-card-blue-btn-primary-bg'],
[/background: rgba\(222, 243, 255, 0\.95\)/g, 'fade(@home-color-card-blue-btn-primary-bg, 95%)'],
[/background: rgba\(222, 243, 255, 0\.5\)/g, '@home-color-card-blue-btn-secondary-bg'],
[/border: 1px solid rgba\(0, 141, 213, 0\.4\)/g, 'border: 1px solid @home-color-card-blue-btn-secondary-border'],
[/background: rgba\(255, 255, 255, 0\.35\)/g, '@home-color-card-chain-bg'],
[/background: rgba\(255, 255, 255, 0\.4\)/g, '@home-color-card-chain-btn-secondary-bg'],
[/border: 1px solid rgba\(0, 0, 0, 0\.2\)/g, 'border: 1px solid @home-color-card-chain-btn-secondary-border'],
[/background: rgba\(232, 255, 234, 0\.5\)/g, '@home-color-card-green-btn-secondary-bg'],
[/border: 1px solid rgba\(0, 180, 42, 0\.4\)/g, 'border: 1px solid @home-color-card-green-btn-secondary-border'],
];
for (const [re, rep] of reps) css = css.replace(re, rep);
const numReps = [
['max-width: 867px', 'max-width: @home-hero-title-width'],
['max-width: 600px', 'max-width: @home-search-box-width'],
['width: 92px', 'width: @home-search-btn-width'],
['height: 300px', 'height: @home-hero-fade-height'],
['padding: 8px 8px 8px 16px', 'padding: @home-search-box-padding'],
['padding: 2px 8px', 'padding: @home-search-tag-padding'],
['padding: 0 0 24px', 'padding: 0 0 @home-space-24'],
['gap: 36px', 'gap: @home-feature-cards-gap'],
['gap: 32px', 'gap: @home-space-32'],
['gap: 16px', 'gap: @home-space-16'],
['gap: 12px', 'gap: @home-search-tag-gap'],
['gap: 28px', 'gap: @home-export-card-gap'],
['gap: 14px', 'gap: @home-core-col-gap'],
['gap: 42px', 'gap: @home-section-gap'],
['gap: 52px', 'gap: @home-space-52'],
['gap: 56px', 'gap: @home-space-56'],
['font-size: 56px', 'font-size: @home-font-size-56'],
['font-size: 40px', 'font-size: @home-font-size-40'],
['font-size: 32px', 'font-size: @home-font-size-32'],
['font-size: 28px', 'font-size: @home-font-size-28'],
['font-size: 24px', 'font-size: @home-font-size-24'],
['font-size: 22px', 'font-size: @home-font-size-22'],
['font-size: 20px', 'font-size: @home-font-size-20'],
['font-size: 16px', 'font-size: @home-font-size-16'],
['font-size: 14px', 'font-size: @home-font-size-14'],
['font-size: 12px', 'font-size: @home-font-size-12'],
['line-height: 38px', 'line-height: @home-line-height-38'],
['line-height: 28px', 'line-height: @home-line-height-28'],
['line-height: 26px', 'line-height: @home-line-height-26'],
['line-height: 22px', 'line-height: @home-line-height-22'],
['line-height: 20px', 'line-height: @home-line-height-20'],
['font-weight: 600', 'font-weight: @home-font-weight-semibold'],
['font-weight: 500', 'font-weight: @home-font-weight-medium'],
['font-weight: 400', 'font-weight: @home-font-weight-regular'],
['border-radius: 16px', 'border-radius: @home-radius-xl'],
['border-radius: 12px', 'border-radius: @home-radius-lg'],
['border-radius: 10px', 'border-radius: @home-radius-md'],
['border-radius: 8px', 'border-radius: @home-radius-sm'],
['border-radius: 4px', 'border-radius: @home-radius-xs'],
['border-radius: 24px', 'border-radius: @home-radius-pill'],
['backdrop-filter: blur(20px)', 'backdrop-filter: blur(@home-blur-tag)'],
['backdrop-filter: blur(10px)', 'backdrop-filter: blur(@home-blur-md)'],
['backdrop-filter: blur(8px)', 'backdrop-filter: blur(@home-blur-sm)'],
['backdrop-filter: blur(4px)', 'backdrop-filter: blur(@home-blur-ability)'],
['width: 145px', 'width: @home-news-thumb-width'],
['width: 282px', 'width: @home-partner-logo-card-width'],
['height: 89px', 'height: @home-partner-logo-card-height'],
['height: 66px', 'height: @home-cta-btn-height'],
['width: 360px', 'width: @home-cta-btn-width'],
['width: 77px', 'width: @home-export-btn-size'],
['height: 77px', 'height: @home-export-btn-size'],
['min-height: 119px', 'min-height: @home-export-card-height'],
['max-width: 628px', 'max-width: @home-export-list-width'],
['padding: 0 30px', 'padding: 0 @home-core-grid-padding-x'],
['column-gap: 13px', 'column-gap: @home-ability-grid-col-gap'],
['row-gap: 70px', 'row-gap: @home-ability-grid-row-gap'],
['min-height: 330px', 'min-height: @home-ability-grid-min-height'],
['padding: 16px 16px 16px 32px', 'padding: @home-ability-card-padding'],
];
for (const [from, to] of numReps) css = css.replaceAll(from, to);
// Fix double-replaced height on search btn line-height
css = css.replace(
/line-height: @home-search-btn-height;\s*\n\s*font-family: @home-font-family;/g,
'line-height: @home-search-btn-height;\n font-family: @home-font-family;'
);
s = s.slice(0, start) + css + s.slice(end);
fs.writeFileSync(path, s);
console.log('migration complete');

View File

@ -44,7 +44,10 @@ request.interceptors.request.use(
if (newConf.loading) {
SingleLoading.startLoading();
}
const { url } = newConf;
const url = newConf && newConf.url ? newConf.url : '';
if (!url) {
return newConf;
}
if (newConf.method === 'get' && newConf.params) {
// 先加时间戳(如果 URL 还没参数)
if (url.indexOf('?') === -1) {
@ -112,15 +115,21 @@ request.interceptors.response.use(
// 获取错误信息
const msg = res.data.msg || '系统未知错误,请反馈给管理员';
if (code === 401) {
showLoginGuide({ actionText: '当前操作' });
// 调用方显式静默 401如只读列表的公开接口不弹登录提示
// 让业务层自行 fallback / 提示 / 跳转
const silent = res.config?.__silent401 || res.reqConfig?.__silent401;
if (!silent) {
showLoginGuide({ actionText: '当前操作' });
}
return Promise.reject({
__authRequired: true,
code: 401,
msg,
response: res,
__silent401: !!silent,
});
}
if (code !== 1) {
if (code !== 200 && code !== 1) {
MessagePlugin.error({
content: msg,
duration: 2000,
@ -140,7 +149,12 @@ request.interceptors.response.use(
}
// HTTP 状态码 401 未认证,跳转登录页
if (err.response?.status === 401) {
showLoginGuide({ actionText: '当前操作' });
// 调用方显式静默 401如只读列表的公开接口不弹登录提示
// 让业务层自行 fallback / 提示 / 跳转
const silent = err.config?.__silent401 || err.reqConfig?.__silent401;
if (!silent) {
showLoginGuide({ actionText: '当前操作' });
}
return Promise.reject({
...err,
__authRequired: true,
@ -240,11 +254,16 @@ request.interceptors.response.use(
if (err.reqConfig?.loading || err.config?.loading || SingleLoading.load !== null) {
SingleLoading.endLoading(true);
}
showLoginGuide({ actionText: '当前操作' });
// 调用方显式静默 401如只读列表的公开接口不弹登录提示
const silent = err.config?.__silent401 || err.reqConfig?.__silent401;
if (!silent) {
showLoginGuide({ actionText: '当前操作' });
}
return Promise.reject({
...err,
__authRequired: true,
code: 401,
__silent401: !!silent,
});
}
return Promise.reject(err);

View File

@ -3,12 +3,13 @@ import { fetchSso } from '@/core/request';
const basurl = '/mhzc/gxnl';
export default {
/** 共性能力网站列表 */
/** 共性能力网站列表(公开浏览接口,未登录时静默 401由 gxnlpt fallback 到 demo */
wzxxList(params = {}) {
return fetchSso({
url: `${basurl}/wzxx/list`,
method: 'post',
data: JSON.stringify(params),
__silent401: true,
});
},

View File

@ -4,16 +4,17 @@ import { fetchSso } from '@/core/request';
* 门户用户相关 API
*/
export const getProfile = () => {
return fetchSso('/mhzc/user/profile', { method: 'GET' });
return fetchSso({ url: '/mhzc/user/profile', method: 'GET' });
};
export const updateProfile = (data) => {
return fetchSso('/mhzc/user/profile', {
return fetchSso({
url: '/mhzc/user/profile',
method: 'PUT',
data: JSON.stringify(data)
});
};
export const getSsoBind = () => {
return fetchSso('/mhzc/user/sso-bind', { method: 'GET' });
return fetchSso({ url: '/mhzc/user/sso-bind', method: 'GET' });
};

View File

@ -1,24 +1,24 @@
function login() {
return import(/* webpackChunkName: "sbfdemo" */ '@/pages/index/views/login/login.vue');
return import(/* webpackChunkName: "page-login" */ '@/pages/index/views/login/login.vue');
}
function home() {
return import(/* webpackChunkName: "sbfdemo" */ '@/pages/index/views/home2/index.vue');
return import(/* webpackChunkName: "page-home" */ '@/pages/index/views/home2/index.vue');
}
// function home2() {
// return import(/* webpackChunkName: "sbfdemo" */ '@/pages/index/views/home2/index.vue');
// return import(/* webpackChunkName: "page-home" */ '@/pages/index/views/home2/index.vue');
// }
function yhzx() {
return import(/* webpackChunkName: "sbfdemo" */ '@/pages/index/views/glxtSy/glxtSy.vue');
return import(/* webpackChunkName: "page-yhzx" */ '@/pages/index/views/glxtSy/glxtSy.vue');
}
function mhNewMain() {
return import(/* webpackChunkName: "sbfdemo" */ '@/pages/index/views/home/mhNewMain.vue');
return import(/* webpackChunkName: "page-mh-new-main" */ '@/pages/index/views/home/mhNewMain.vue');
}
function zxym() {
return import(/* webpackChunkName: "sbfdemo" */ '@/pages/index/views/zx/index.vue');
return import(/* webpackChunkName: "page-zxym" */ '@/pages/index/views/zx/index.vue');
}
function authorize() {
return import(/* webpackChunkName: "sbfdemo" */ '@/pages/index/views/dddl/authorize.vue');
return import(/* webpackChunkName: "page-authorize" */ '@/pages/index/views/dddl/authorize.vue');
}
// 企业出海
function qych() {
@ -34,22 +34,22 @@ function hydt() {
}
function gxnlpt() {
return import(/* webpackChunkName: "sbfdemo" */ '@/pages/index/views/gxnlpt/index.vue');
return import(/* webpackChunkName: "page-gxnlpt" */ '@/pages/index/views/gxnlpt/index.vue');
}
//消息中心
function newsCenter() {
return import(/* webpackChunkName: "sbfdemo" */ '@/pages/index/views/ggwhglHtgl/index.vue');
return import(/* webpackChunkName: "page-news-center" */ '@/pages/index/views/ggwhglHtgl/index.vue');
}
//碳服务市场
function fwsc() {
return import(/* webpackChunkName: "sbfdemo" */ '@/pages/index/views/fwsc/fwsc.vue');
return import(/* webpackChunkName: "page-tfwsc" */ '@/pages/index/views/fwsc/fwsc.vue');
}
//碳金融市场
function jrsc() {
return import(/* webpackChunkName: "sbfdemo" */ '@/pages/index/views/fwsc/jrsc.vue');
return import(/* webpackChunkName: "page-tjrsc" */ '@/pages/index/views/fwsc/jrsc.vue');
}
//碳需求市场

View File

@ -965,6 +965,7 @@ html.portal-figma-scale-active.portal-market-figma-scale-active {
.portal-market-figma-page .fwsc-main,
.portal-market-figma-page .xqsc-main,
.portal-market-figma-page .sjsc-main,
.portal-market-figma-page .jrsc-main,
.portal-market-figma-page .page-body,
.portal-market-figma-page .content-wrapper {

View File

@ -0,0 +1,13 @@
export default {
methods: {
showComingSoon() {
if (this._comingSoonLock) return
this._comingSoonLock = true
this.$message.info(String.fromCharCode(25964,35831,26399,24453))
setTimeout(function() { this._comingSoonLock = false }, 2000)
}
},
beforeDestroy() {
this._comingSoonLock = false
}
};

View File

@ -0,0 +1,75 @@
import { createFullPageScroller } from './fullpage-scroll';
/**
* 整屏滚动(FullPage.js 风格)Vue mixin
*
* 用法:
* import fullpageScrollMixin from '@/pages/index/utils/fullpage-scroll-mixin';
* export default { mixins: [fullpageScrollMixin] }
*
* 行为:
* - mounted / activated , .content-wrap 作为滚动根,绑定 wheel/touch/keydown,
* 每次只跳一屏( element.scrollIntoView({ behavior: 'smooth' }) 触发浏览器原生平滑动画)
* - 监听 page 内的 .snap-section / .qych-snap-section 等以 -snap-section 结尾的 class
* - 同一时刻最多一个动画在进行,新动画会被忽略,直到 scrollend 600ms 自动解锁
* - 滚动期间 50ms 内的 wheel 脉冲会被 debounce
*
* 暴露给组件的方法:
* - fullPageScrollTo(index) 跳到第 N
* - fullPageRefresh() 重新收集 sections(用于动态内容)
* - onFullPageSectionChange(i, from, el) 钩子:section 切换后触发
* - onBeforeFullPageSectionChange(i, from, el) 钩子:section 切换前,return false 可阻止
*/
export default {
data() {
return {
_fullPageScroller: null,
fullPageActiveIndex: 0,
};
},
mounted() {
this.$nextTick(() => this._initFullPageScroller());
},
activated() {
this.$nextTick(() => this._initFullPageScroller());
},
beforeDestroy() {
this._destroyFullPageScroller();
},
deactivated() {
this._destroyFullPageScroller();
},
methods: {
_initFullPageScroller() {
if (this._fullPageScroller) return;
const rootEl = document.querySelector('.content-wrap');
if (!rootEl) return;
this._fullPageScroller = createFullPageScroller(rootEl, {
onSectionChange: (newIndex, fromIndex, sectionEl) => {
this.fullPageActiveIndex = newIndex;
if (typeof this.onFullPageSectionChange === 'function') {
this.onFullPageSectionChange(newIndex, fromIndex, sectionEl);
}
},
onBeforeSectionChange: (newIndex, fromIndex, sectionEl) => {
if (typeof this.onBeforeFullPageSectionChange === 'function') {
return this.onBeforeFullPageSectionChange(newIndex, fromIndex, sectionEl);
}
return true;
},
});
},
_destroyFullPageScroller() {
if (this._fullPageScroller) {
this._fullPageScroller.destroy();
this._fullPageScroller = null;
}
},
fullPageScrollTo(index) {
if (this._fullPageScroller) this._fullPageScroller.scrollTo(index, true);
},
fullPageRefresh() {
if (this._fullPageScroller) this._fullPageScroller.update();
},
},
};

View File

@ -0,0 +1,290 @@
/**
* 整屏滚动工具(FullPage.js 风格,业界最主流的"分屏翻页"实现)
*
* 设计原则:
* 1. 不依赖 CSS Scroll Snap(因为有 .content-wrap 多层 wrapper,snap 算法不跨层)
* 2. 不劫持 wheel 后做 deltaY 累加(之前的实现,会导致"反向""卡顿")
* 3. 监听 wheel / touchstart / touchmove / keydown 事件
* 每次只跳一屏:根据 direction 计算目标 section 索引
* element.scrollIntoView({ behavior: 'smooth' }) 触发原生平滑滚动
* 浏览器原生处理动画;我们只决定"跳到第几屏"
* 4. 滚动期间锁:使用 scrollend 事件(Safari 15.4+)作为解锁信号,
* fallback debounce,不使用硬编码 setTimeout(800)
* 5. 支持 keyboard 上下方向键 / PageUp / PageDown / Home / End
* 6. 尊重 prefers-reduced-motion:无障碍用户跳过动画
* 7. 不影响子元素自身的滚动: wheel 事件来自带 overflow 的子元素,事件在
* 子元素中自然滚动,不触发整屏切换
* 重要这里要排除 transform: scale() 缩放的元素 portal-figma-scale
* 把内部内容 transform 缩放 0.85,导致内部 scrollHeight > clientHeight,
* 但视觉上并不可滚,会让本函数误判"用户在内部滚动"而吞掉整屏切换
*/
const SCROLL_END_FALLBACK_MS = 600;
const TOUCH_THRESHOLD = 30;
const TOUCH_MAX_DURATION = 800;
const WHEEL_DEBOUNCE_MS = 50;
const WHEEL_MIN_DELTA = 2;
function isMobile() {
if (typeof window === 'undefined') return false;
return window.matchMedia('(max-width: 767px), (pointer: coarse)').matches;
}
function prefersReducedMotion() {
if (typeof window === 'undefined') return false;
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
function collectSnapSections(rootEl) {
if (!rootEl) return [];
// 匹配规则:任何 class 名以 "snap-section" 结尾的元素,或显式 [data-snap-section]
// - home2 用 class="... snap-section"
// - qych 用 class="qych-snap-section"
// 用属性选择器 [class*="snap-section"] 一把抓,但要排除只命中 "snap-section-stop" 之类的子串
// 改用 querySelectorAll + filter 做精确包含匹配
const all = Array.from(rootEl.querySelectorAll('[class*="snap-section"], [data-snap-section]'));
return all.filter((el) => {
const cls = (el.className || '').toString();
// class 里 token 包含 "snap-section"(用空格分词判断)
return cls.split(/\s+/).some((c) => c === 'snap-section' || c.endsWith('-snap-section'));
});
}
/**
* 判断节点是否被 transform 缩放过transform: scale() 缩放后,
* 内部元素的 scrollHeight/clientHeight 不变,但视觉高度变化,
* getComputedStyle 不会暴露 transform 矩阵,所以直接看 inline style / computedStyle transform
*/
function hasTransform(node) {
if (!node || !(node instanceof HTMLElement)) return false;
const style = getComputedStyle(node);
const t = style.transform;
if (!t || t === 'none') return false;
// 任何 matrix(...) 或 translate3d(...) 都算
return /matrix\(|matrix3d\(|translate3d\(/.test(t);
}
function isEventInLocalScrollable(e, rootEl) {
let node = e.target;
// 【重要修复】从 target 向上找,跳过 rootEl 自己(rootEl 就是滚动容器,我们就是它,
// 不能因为 rootEl overflow-y:auto 就阻止穿透 — 那会永远 return true,整屏永远不切换)
while (node && node !== document.body) {
if (rootEl && node === rootEl) break;
if (node instanceof HTMLElement) {
// 跳过 transform 缩放的元素(portal-figma-scale 的 .stage):
// 它"看起来"可滚,但事件应该穿透到外层 scrollRoot 触发整屏切换
if (hasTransform(node)) {
node = node.parentElement;
continue;
}
const style = getComputedStyle(node);
const overflowY = style.overflowY;
if ((overflowY === 'auto' || overflowY === 'scroll') && node.scrollHeight > node.clientHeight + 1) {
const atTop = node.scrollTop <= 0;
const atBottom = node.scrollTop + node.clientHeight >= node.scrollHeight - 1;
if (e.deltaY > 0 && !atBottom) return true;
if (e.deltaY < 0 && !atTop) return true;
}
}
node = node.parentElement;
}
return false;
}
function debounce(fn, ms) {
let t = null;
return function debounced(...args) {
if (t) clearTimeout(t);
t = setTimeout(() => fn.apply(this, args), ms);
};
}
export function createFullPageScroller(rootEl, options = {}) {
if (!rootEl) {
return {
update: () => {},
destroy: () => {},
scrollTo: () => false,
getCurrentIndex: () => 0,
};
}
const { onSectionChange, onBeforeSectionChange } = options;
let sections = collectSnapSections(rootEl);
let currentIndex = 0;
let isAnimating = false;
let lastWheelTime = 0;
let touchStartY = 0;
let touchStartX = 0;
let touchStartTime = 0;
let touchActive = false;
let fallbackUnlockTimer = null;
const reducedMotion = prefersReducedMotion();
function emitChange(newIndex) {
if (typeof onSectionChange === 'function' && newIndex !== currentIndex) {
onSectionChange(newIndex, currentIndex, sections[newIndex]);
}
currentIndex = newIndex;
}
function emitBefore(newIndex) {
if (typeof onBeforeSectionChange === 'function' && newIndex !== currentIndex) {
return onBeforeSectionChange(newIndex, currentIndex, sections[newIndex]);
}
return true;
}
function scheduleUnlock() {
if (fallbackUnlockTimer) clearTimeout(fallbackUnlockTimer);
if ('onscrollend' in window) {
const handler = () => {
rootEl.removeEventListener('scrollend', handler);
isAnimating = false;
};
rootEl.addEventListener('scrollend', handler, { once: true, passive: true });
}
fallbackUnlockTimer = setTimeout(() => {
isAnimating = false;
}, SCROLL_END_FALLBACK_MS);
}
function scrollTo(index, smooth = true) {
if (index < 0 || index >= sections.length) return false;
if (!sections[index]) return false;
if (isAnimating) return false;
if (!emitBefore(index)) return false;
const el = sections[index];
isAnimating = true;
const behavior = smooth && !reducedMotion ? 'smooth' : 'auto';
el.scrollIntoView({ behavior, block: 'start', inline: 'nearest' });
emitChange(index);
scheduleUnlock();
return true;
}
function onWheel(e) {
if (isMobile()) return;
if (isEventInLocalScrollable(e, rootEl)) return;
if (isAnimating) {
e.preventDefault();
return;
}
const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
if (now - lastWheelTime < WHEEL_DEBOUNCE_MS) return;
if (Math.abs(e.deltaY) < WHEEL_MIN_DELTA) return;
lastWheelTime = now;
e.preventDefault();
const direction = e.deltaY > 0 ? 1 : -1;
const targetIndex = currentIndex + direction;
if (targetIndex < 0 || targetIndex >= sections.length) return;
scrollTo(targetIndex, true);
}
function onKeydown(e) {
if (isMobile()) return;
if (!rootEl.contains(document.activeElement) && document.activeElement !== document.body) {
return;
}
let dir = 0;
switch (e.key) {
case 'ArrowDown':
case 'PageDown':
case ' ':
dir = 1;
break;
case 'ArrowUp':
case 'PageUp':
dir = -1;
break;
case 'Home':
e.preventDefault();
scrollTo(0, true);
return;
case 'End':
e.preventDefault();
scrollTo(sections.length - 1, true);
return;
default:
return;
}
if (dir !== 0) {
e.preventDefault();
if (isAnimating) return;
const targetIndex = currentIndex + dir;
if (targetIndex < 0 || targetIndex >= sections.length) return;
scrollTo(targetIndex, true);
}
}
function onTouchStart(e) {
if (!isMobile()) return;
const t = e.touches && e.touches[0];
if (!t) return;
touchStartY = t.clientY;
touchStartX = t.clientX;
touchStartTime = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
touchActive = true;
}
function onTouchMove(e) {
if (!isMobile() || !touchActive || isAnimating) return;
const t = e.touches && e.touches[0];
if (!t) return;
const dy = touchStartY - t.clientY;
const dx = Math.abs(touchStartX - t.clientX);
const duration = ((typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now()) - touchStartTime;
if (dx > Math.abs(dy)) return;
if (Math.abs(dy) < TOUCH_THRESHOLD) return;
if (duration > TOUCH_MAX_DURATION) {
touchActive = false;
return;
}
e.preventDefault();
const direction = dy > 0 ? 1 : -1;
const targetIndex = currentIndex + direction;
if (targetIndex < 0 || targetIndex >= sections.length) {
touchActive = false;
return;
}
touchActive = false;
scrollTo(targetIndex, true);
}
function onTouchEnd() {
touchActive = false;
}
const onResize = debounce(() => {
sections = collectSnapSections(rootEl);
}, 200);
rootEl.addEventListener('wheel', onWheel, { passive: false });
rootEl.addEventListener('touchstart', onTouchStart, { passive: true });
rootEl.addEventListener('touchmove', onTouchMove, { passive: false });
rootEl.addEventListener('touchend', onTouchEnd, { passive: true });
rootEl.addEventListener('keydown', onKeydown);
window.addEventListener('resize', onResize);
return {
scrollTo,
getCurrentIndex: () => currentIndex,
update: () => { sections = collectSnapSections(rootEl); },
destroy: () => {
rootEl.removeEventListener('wheel', onWheel);
rootEl.removeEventListener('touchstart', onTouchStart);
rootEl.removeEventListener('touchmove', onTouchMove);
rootEl.removeEventListener('touchend', onTouchEnd);
rootEl.removeEventListener('keydown', onKeydown);
window.removeEventListener('resize', onResize);
if (fallbackUnlockTimer) {
clearTimeout(fallbackUnlockTimer);
fallbackUnlockTimer = null;
}
},
};
}

View File

@ -1,24 +1,96 @@
import { syncPortalFigmaStageLayout } from './portal-figma-scale';
/** 门户 Figma 等比缩放页mounted/resize 时同步 stage 实测高度 */
/**
* 门户 Figma 等比缩放页
*
* 实现要点参考 Vue 3 / Nuxt 3 SPA 路由切换最佳实践
* 1. ResizeObserver 替代 nextTick 一次性计算 stage 高度
* 任何尺寸变化异步数据填充窗口缩放字体加载都会重算
* 不会因为单次算偏小而锁住 --home-figma-visual-height
* 2. 路由进入前清掉全局 clamp 变量由新页面 mount 后自己算
* 避免旧页面残留的 visualHeight 把新页面裁切
* 3. keep-alive 场景下 activated 重新建立观察者并立即算一次
*/
export default {
mounted() {
this._onPortalFigmaResize = () => {
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => this.syncPortalFigmaStageLayout());
});
};
window.addEventListener('resize', this._onPortalFigmaResize);
this.$nextTick(() => this.syncPortalFigmaStageLayout());
this._setupPortalFigmaScaleObserver();
},
activated() {
// keep-alive 场景下,组件从缓存中恢复时不会触发 mounted
// 必须重新挂载观察者并立即算一次。
this._setupPortalFigmaScaleObserver();
},
beforeRouteUpdate() {
// 路由参数变化时清掉旧视觉高度,避免 stale 值 clamp 新内容
this._resetPortalFigmaVisualHeight();
},
beforeRouteEnter(to, from, next) {
// 进入路由前清掉全局 clamp 变量
if (typeof document !== 'undefined') {
const root = document.documentElement;
root.style.removeProperty('--home-figma-visual-height');
root.style.removeProperty('--portal-shell-min-design-height');
}
next();
},
beforeDestroy() {
if (this._onPortalFigmaResize) {
window.removeEventListener('resize', this._onPortalFigmaResize);
this._onPortalFigmaResize = null;
}
this._teardownPortalFigmaScaleObserver();
},
deactivated() {
this._teardownPortalFigmaScaleObserver();
},
methods: {
_setupPortalFigmaScaleObserver() {
if (typeof window === 'undefined') return;
if (window.innerWidth < 768) return;
this._teardownPortalFigmaScaleObserver();
this._onPortalFigmaResize = () => {
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => this.syncPortalFigmaStageLayout());
});
};
window.addEventListener('resize', this._onPortalFigmaResize);
const stage = this.$refs.figmaStage;
if (stage && typeof ResizeObserver !== 'undefined') {
this._portalFigmaResizeObserver = new ResizeObserver(() => {
// 防抖:短时间内多次变化只算一次
if (this._portalFigmaResizeTimer) {
clearTimeout(this._portalFigmaResizeTimer);
}
this._portalFigmaResizeTimer = setTimeout(() => {
this.syncPortalFigmaStageLayout();
}, 50);
});
this._portalFigmaResizeObserver.observe(stage);
}
// 立即算一次cover 首次渲染
this.$nextTick(() => this.syncPortalFigmaStageLayout());
},
_teardownPortalFigmaScaleObserver() {
if (this._onPortalFigmaResize && typeof window !== 'undefined') {
window.removeEventListener('resize', this._onPortalFigmaResize);
this._onPortalFigmaResize = null;
}
if (this._portalFigmaResizeObserver) {
this._portalFigmaResizeObserver.disconnect();
this._portalFigmaResizeObserver = null;
}
if (this._portalFigmaResizeTimer) {
clearTimeout(this._portalFigmaResizeTimer);
this._portalFigmaResizeTimer = null;
}
},
_resetPortalFigmaVisualHeight() {
if (typeof document === 'undefined') return;
const root = document.documentElement;
root.style.removeProperty('--home-figma-visual-height');
},
syncPortalFigmaStageLayout() {
if (typeof window === 'undefined') return;
if (window.innerWidth < 768) return;
const stage = this.$refs.figmaStage;
if (!stage) return;

View File

@ -1,5 +1,22 @@
/**
* 桌面端全屏分屏滚轮移动端/触控设备使用原生滚动
* 门户滚动工具集
*
* 设计原则参考 Material Design 3 / iOS Safari 滚动模型 / Nuxt 3 page transition
* 1. **绝不劫持 wheel 事件** 浏览器原生滚动在 GPU 合成滚动惯性
* 触控板缩放辅助滚动屏幕阅读器等场景下都是最优的手写 wheel handler
* 累加 deltaY + setTimeout 锁会导致"滑动不稳 / 向下滑却向上滚"
* 2. **分屏落点用 scrollIntoView + scrollend 判定** 让浏览器原生动画
* 只在 scrollend 时解锁 / 计算下一段
* 3. **CSS overscroll-behavior: contain 隔离滚动冒泡** 子容器滚动到边界
* 时不再触发父级连锁滚动
* 4. **deltaMode 区分鼠标/触控板** 阈值判断用 pixel 模式
*/
const SCROLL_END_TIMEOUT_MS = 120;
/**
* 桌面端全屏分屏滚轮兼容旧调用方新实现改为原生滚动 + scrollIntoView
* 现仅保留接口签名供外部 import不主动 bind wheel
*/
export function shouldUseSectionWheelScroll() {
if (typeof window === 'undefined') return true;
@ -7,38 +24,19 @@ export function shouldUseSectionWheelScroll() {
}
/**
* 按视口动态绑定/解绑 wheel 分屏逻辑
* @param {HTMLElement} root
* @param {(e: WheelEvent) => void} handler
* @returns {() => void} cleanup
* @deprecated 老接口不再劫持 wheel 事件新逻辑请直接用 scrollIntoView + scrollend
* 此函数保留仅为不破坏外部 import运行时不绑定任何监听
*/
export function bindSectionWheelScroll(root, handler) {
if (!root || !handler) return () => {};
const mq = window.matchMedia('(max-width: 767px), (pointer: coarse)');
const sync = () => {
root.removeEventListener('wheel', handler);
if (shouldUseSectionWheelScroll()) {
root.addEventListener('wheel', handler, { passive: false });
}
};
sync();
if (typeof mq.addEventListener === 'function') {
mq.addEventListener('change', sync);
} else if (typeof mq.addListener === 'function') {
mq.addListener(sync);
// 静默 no-op提示调用方迁移
if (typeof console !== 'undefined') {
// eslint-disable-next-line no-console
console.warn(
'[portal-scroll-mode] bindSectionWheelScroll 已废弃:不再劫持 wheel 事件。'
+ ' 请改用 scrollIntoView({ behavior: "smooth" }) + scrollend 判定。',
);
}
return () => {
root.removeEventListener('wheel', handler);
if (typeof mq.removeEventListener === 'function') {
mq.removeEventListener('change', sync);
} else if (typeof mq.removeListener === 'function') {
mq.removeListener(sync);
}
};
return () => {};
}
/**
@ -56,3 +54,69 @@ export function scrollPortalContentToTop(options = {}) {
}
window.scrollTo({ top: 0, behavior });
}
/**
* Promise 化的 scrollend 等到滚动真正结束再 resolve
* 兼容不支持 scrollend 的浏览器Safari < 15.4fallback debounced scroll
* @param {HTMLElement | Window} target
* @param {number} timeoutMs
* @returns {Promise<void>}
*/
export function waitForScrollEnd(target = window, timeoutMs = SCROLL_END_TIMEOUT_MS) {
return new Promise((resolve) => {
if (typeof window === 'undefined') {
resolve();
return;
}
let settled = false;
const done = () => {
if (settled) return;
settled = true;
target.removeEventListener && target.removeEventListener('scrollend', done);
target.removeEventListener && target.removeEventListener('scroll', onScroll);
resolve();
};
const onScroll = () => {
if (settled) return;
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(done, timeoutMs);
};
let debounceTimer = setTimeout(done, timeoutMs);
if ('onscrollend' in window) {
target.addEventListener('scrollend', done, { once: true, passive: true });
} else {
target.addEventListener('scroll', onScroll, { passive: true });
}
});
}
/**
* 平滑滚动到指定 section 元素等待滚动结束
* @param {HTMLElement} el 目标元素
* @param {{ block?: ScrollLogicalPosition, offset?: number, scrollRoot?: HTMLElement | Window }} [options]
* @returns {Promise<void>}
*/
export async function smoothScrollTo(el, options = {}) {
if (!el) return;
const { block = 'start', offset = 0, scrollRoot = window } = options;
const behavior = 'smooth';
// 处理 offsetscrollIntoView 不支持 offset临时用 transform 占位再回滚
if (offset !== 0) {
const orig = el.style.scrollMarginTop;
el.style.scrollMarginTop = `${offset}px`;
try {
el.scrollIntoView({ behavior, block, inline: 'nearest' });
} finally {
// 还原nextTick 后给浏览器时间生效)
setTimeout(() => {
el.style.scrollMarginTop = orig;
}, 0);
}
} else {
el.scrollIntoView({ behavior, block, inline: 'nearest' });
}
await waitForScrollEnd(scrollRoot);
}

View File

@ -0,0 +1,242 @@
<template>
<!-- 发布数据抽屉 -->
<t-drawer
:visible.sync="drawerVisible"
header="发布碳数据"
size="600px"
:closeOnOverlayClick="false"
:onClose="onClose"
class="global-drawer"
>
<div class="publish-form">
<t-form
:rules="rules"
:data="formData"
ref="form"
labelAlign="top"
@submit="submitForm"
>
<t-row :gutter="16">
<t-col :span="12">
<t-form-item label="数据标题" name="bt1">
<t-input v-model="formData.bt1" placeholder="请输入数据标题" clearable />
</t-form-item>
</t-col>
<t-col :span="12">
<t-form-item label="数据类型" name="fwlxjh">
<t-select
v-model="formData.fwlxjh"
:options="sjlxOptions"
placeholder="请选择数据类型"
clearable
/>
</t-form-item>
</t-col>
</t-row>
<t-row :gutter="16">
<t-col :span="12">
<t-form-item label="所属行业" name="sshy">
<t-select v-model="formData.sshy" :options="sshyDmOptions" placeholder="请选择所属行业" clearable />
</t-form-item>
</t-col>
<t-col :span="12">
<t-form-item label="服务地区" name="fwfw">
<t-select v-model="formData.fwfw" :options="fwfwOptions" placeholder="请选择服务地区" clearable />
</t-form-item>
</t-col>
</t-row>
<t-form-item label="数据详细描述" name="fwnr">
<t-textarea v-model="formData.fwnr" placeholder="请填写数据详细描述" :maxlength="100" />
</t-form-item>
<t-form-item label="标签" name="bqjh">
<t-select
v-model="formData.bqjh"
:max="4"
:options="bqOptions"
placeholder="请选择标签"
multiple
clearable
/>
</t-form-item>
<t-row :gutter="16">
<t-col :span="8">
<t-form-item label="联系人" name="lxr">
<t-input v-model="formData.lxr" placeholder="请输入联系人" />
</t-form-item>
</t-col>
<t-col :span="8">
<t-form-item label="联系电话" name="lxdh">
<t-input v-model="formData.lxdh" placeholder="请输入联系电话" />
</t-form-item>
</t-col>
<t-col :span="8">
<t-form-item label="电子邮箱" name="email">
<t-input v-model="formData.email" placeholder="请输入电子邮箱" />
</t-form-item>
</t-col>
</t-row>
</t-form>
<div class="form-actions">
<t-button theme="primary" type="submit" @click="handleSubmit">提交发布</t-button>
</div>
</div>
</t-drawer>
</template>
<script>
import api from '@/pages/index/api/fwsc/index.js';
export default {
name: 'SjscPublish',
props: {
visible: {
type: Boolean,
default: false,
},
},
data() {
return {
drawerVisible: false,
formData: {
bt1: '',
fwlxjh: '',
sshy: '',
fwfw: '',
fwnr: '',
bqjh: [],
lxr: '',
lxdh: '',
email: '',
},
rules: {
bt1: [{ required: true, message: '必填', type: 'error' }],
fwlxjh: [{ required: true, message: '必填', type: 'error' }],
sshy: [{ required: true, message: '必填', type: 'error' }],
fwfw: [{ required: true, message: '必填', type: 'error' }],
fwnr: [{ required: true, message: '必填', type: 'error' }],
lxr: [{ required: true, message: '必填', type: 'error' }],
lxdh: [{ required: true, message: '必填', type: 'error' }],
},
// dataTypeList value fwlxjh
sjlxOptions: [
{ value: 'public', label: '公共数据' },
{ value: 'factor', label: '因子库' },
{ value: 'social', label: '社会性数据' },
],
sshyDmOptions: [],
fwfwOptions: [],
bqOptions: [],
};
},
watch: {
visible(val) {
this.drawerVisible = val;
if (val) {
this.loadDmList();
}
},
drawerVisible(val) {
this.$emit('update:visible', val);
},
},
methods: {
loadDmList() {
this.sshyoptionsSearch();
this.fwfwoptionsSearch();
this.bqoptionsSearch();
},
async sshyoptionsSearch() {
try {
const res = await api.dms2mc('sshy', {});
this.sshyDmOptions = res.data || [];
} catch (error) {
this.sshyDmOptions = [];
}
},
async fwfwoptionsSearch() {
try {
const res = await api.dms2mc('xzqh', {});
this.fwfwOptions = res.data || [];
} catch (error) {
this.fwfwOptions = [];
}
},
async bqoptionsSearch() {
try {
const res = await api.dms2mc('bq', {});
this.bqOptions = res.data || [];
} catch (error) {
this.bqOptions = [];
}
},
handleSubmit() {
this.$refs.form.validate().then((result) => {
if (result === true) {
this.gxfb();
} else {
this.$message.warning('请填写全部必填信息');
}
});
},
async gxfb() {
try {
let prame = {
ywlxDm: '03',
...this.formData,
};
if (prame.bqjh && prame.bqjh.length) {
prame.bqjh = prame.bqjh.toString();
}
await api.gxfb(prame);
this.$message.success('发布成功,请等待审核');
this.drawerVisible = false;
this.resetForm();
this.$emit('success');
} catch (error) {
console.error('发布失败', error);
}
},
resetForm() {
this.formData = {
bt1: '',
fwlxjh: '',
sshy: '',
fwfw: '',
fwnr: '',
bqjh: [],
lxr: '',
lxdh: '',
email: '',
};
},
onClose() {
this.drawerVisible = false;
this.resetForm();
},
submitForm() {
// handled by handleSubmit
},
},
};
</script>
<style lang="less" scoped>
.publish-form {
padding: 0 8px;
}
.form-actions {
display: flex;
justify-content: center;
margin-top: 30px;
.t-button {
width: 200px;
}
}
</style>

View File

@ -183,10 +183,11 @@ import api from '@/pages/index/api/fwsc/index.js';
import { hasLogin } from '@/pages/index/api/login';
import { scrollPortalContentToTop } from '@/pages/index/utils/portal-scroll-mode';
import portalFigmaScaleMixin from '@/pages/index/utils/portal-figma-scale-mixin';
import comingSoonMixin from '@/pages/index/utils/coming-soon-mixin';
export default {
name: 'FwscPage',
mixins: [portalFigmaScaleMixin],
mixins: [portalFigmaScaleMixin, comingSoonMixin],
components: {
NewNav,
BreadcrumbNav,
@ -453,7 +454,7 @@ export default {
},
goToTab({path, disable}) {
if (disable) {
this.$message.info('敬请期待');
this.showComingSoon();
return
}
this.$router.push(path);

View File

@ -98,10 +98,11 @@
<script>
import portalFigmaScaleMixin from '@/pages/index/utils/portal-figma-scale-mixin';
import comingSoonMixin from '@/pages/index/utils/coming-soon-mixin';
export default {
name: 'FwscIndex',
mixins: [portalFigmaScaleMixin],
mixins: [portalFigmaScaleMixin, comingSoonMixin],
methods: {
goHome() {
this.$router.push('/view/mhzc/home');
@ -110,7 +111,7 @@ export default {
if (path) {
this.$router.push(path);
} else {
this.$message.info('敬请期待');
this.showComingSoon();
}
}
}
@ -125,6 +126,18 @@ export default {
.portal-figma-scale-page();
}
/* gxnlpt portal-figma-scale-page() mounted nextTick
时拿到的 stage 高度偏小卡片/v-for 还没装填导致
--home-figma-visual-height 被算小整页被 clamp 裁切
表现就是切换到 /fwsc 时只看到一片背景内容没渲染出来
这里直接重写 .fwsc-page 的高度为 auto不依赖 visualHeight */
html.portal-figma-scale-active.portal-landing-figma-scale-active .fwsc-page,
html.portal-figma-scale-active .fwsc-page {
height: auto !important;
max-height: none !important;
overflow: visible !important;
}
// Banner 350px
.banner-section {
position: relative;

File diff suppressed because it is too large Load Diff

View File

@ -212,10 +212,11 @@ import XqscPublish from './components/XqscPublish.vue';
import api from '@/pages/index/api/fwsc/index.js';
import { scrollPortalContentToTop } from '@/pages/index/utils/portal-scroll-mode';
import portalFigmaScaleMixin from '@/pages/index/utils/portal-figma-scale-mixin';
import comingSoonMixin from '@/pages/index/utils/coming-soon-mixin';
export default {
name: 'XqscPage',
mixins: [portalFigmaScaleMixin],
mixins: [portalFigmaScaleMixin, comingSoonMixin],
components: {
NewNav,
BreadcrumbNav,
@ -419,7 +420,7 @@ export default {
},
goToTab({path, disable}) {
if (disable) {
this.$message.info('敬请期待');
this.showComingSoon();
return
}
this.$router.push(path);

View File

@ -147,7 +147,8 @@
<span class="gxnlpt-favorites-star" aria-hidden="true"></span>
<h2 class="gxnlpt-block-title">我的收藏</h2>
</header>
<div v-if="!favoriteCards.length" class="gxnlpt-state">暂无收藏</div>
<div v-if="favoritesLoading" class="gxnlpt-state">加载中...</div>
<div v-else-if="!favoriteCards.length" class="gxnlpt-state">暂无收藏</div>
<div v-else class="gxnlpt-card-grid">
<article
v-for="card in favoriteCards"
@ -239,7 +240,7 @@
<script>
import GxnlptCardTags from '@/pages/index/views/gxnlpt/components/GxnlptCardTags.vue';
import api from '@/pages/index/api/fwsc/index.js';
import api from '@/pages/index/api/gxnl/index.js';
import {
isPortalLoggedIn,
isUnauthorizedError,
@ -247,6 +248,7 @@ import {
} from '@/pages/index/utils/auth-guard';
import { BP } from '@/pages/index/utils/breakpoint.js';
import portalFigmaScaleMixin from '@/pages/index/utils/portal-figma-scale-mixin';
import comingSoonMixin from '@/pages/index/utils/coming-soon-mixin';
const starOutline = require('@/pages/index/assets/fwsc/wsc.svg');
const starFilled = require('@/pages/index/assets/fwsc/ysc.svg');
@ -531,11 +533,11 @@ const SIDE_ICON_WHITE = {
};
const CATEGORY_META = [
{ id: 'content-1', title: '碳核算平台', icon: 'thspt.svg', keywords: ['核算', '足迹', 'LCA', 'CBAM', '碳管理', '碳盘查', '标准化'] },
{ id: 'content-2', title: '碳认证机构', icon: 'trzjg.svg', keywords: ['认证', '核查', '审定', '验证'] },
{ id: 'content-3', title: '碳交易平台', icon: 'tjypt.svg', keywords: ['交易', '交易所', '配额', 'CCER'] },
{ id: 'content-4', title: '碳金融服务', icon: 'tjrfw.svg', keywords: ['金融', '融资', '信贷', '保险', '基金', '资产'] },
{ id: 'content-5', title: '碳技术咨询', icon: 'tjszx.svg', keywords: ['咨询', '技术', '研究', '规划', '方案'] },
{ id: 'content-1', title: '碳核算平台', flDm: '01', icon: 'thspt.svg', keywords: ['核算', '足迹', 'LCA', 'CBAM', '碳管理', '碳盘查', '标准化'] },
{ id: 'content-2', title: '碳认证机构', flDm: '02', icon: 'trzjg.svg', keywords: ['认证', '核查', '审定', '验证'] },
{ id: 'content-3', title: '碳交易平台', flDm: '03', icon: 'tjypt.svg', keywords: ['交易', '交易所', '配额', 'CCER'] },
{ id: 'content-4', title: '碳金融服务', flDm: '04', icon: 'tjrfw.svg', keywords: ['金融', '融资', '信贷', '保险', '基金', '资产'] },
{ id: 'content-5', title: '碳技术咨询', flDm: '05', icon: 'tjszx.svg', keywords: ['咨询', '技术', '研究', '规划', '方案'] },
];
function buildCategoryList() {
@ -549,6 +551,18 @@ function buildCategoryList() {
}));
}
const FL_DM_MAP = {
'碳核算平台': '01',
'碳认证机构': '02',
'碳交易平台': '03',
'碳金融服务': '04',
'碳技术咨询': '05',
};
function flTitleToDm(title) {
return FL_DM_MAP[title] || '';
}
const SUBMIT_FORM_EMPTY = () => ({
bt1: '',
lj: '',
@ -559,7 +573,7 @@ const SUBMIT_FORM_EMPTY = () => ({
export default {
name: 'GxnlptIndex',
mixins: [portalFigmaScaleMixin],
mixins: [portalFigmaScaleMixin, comingSoonMixin],
components: { GxnlptCardTags },
data() {
return {
@ -578,6 +592,8 @@ export default {
submitAttempted: false,
stackedNavLayout: readStackedNavLayout(),
_stackedNavMq: null,
favoriteList: [],
favoritesLoading: false,
};
},
computed: {
@ -593,6 +609,9 @@ export default {
return this.categoryList.map((item, index) => ({ item, index }));
},
favoriteCards() {
if (this.favoriteList && this.favoriteList.length) {
return this.favoriteList;
}
const list = [];
this.categoryList.forEach((cat) => {
cat.cardList.forEach((card) => {
@ -610,17 +629,22 @@ export default {
mounted() {
this.bindStackedNavMedia();
this.bootstrap();
this.$nextTick(() => this.initSidebarSticky());
},
activated() {
this.syncStackedNavLayout();
if (this.contentView === 'list' && !this.stackedNavLayout) {
this.$nextTick(() => this.initScrollSpy());
this.$nextTick(() => {
this.initScrollSpy();
this.initSidebarSticky();
});
}
},
beforeDestroy() {
this.unbindStackedNavMedia();
this.clearScrollUnlock();
this.destroyScrollSpy();
this.destroySidebarSticky();
},
methods: {
/** 收录 / 我的收藏 / 收藏操作前校验登录(弹窗引导,不直接跳登录页) */
@ -647,11 +671,13 @@ export default {
this.activeTabIndex = tabIndex;
} else {
this.initScrollSpy();
this.initSidebarSticky();
this.scrollToSection(anchor, tabIndex);
}
return;
}
this.initScrollSpy();
this.initSidebarSticky();
});
},
getIconUrl(iconName) {
@ -777,6 +803,8 @@ export default {
/** 解析 fwlxjh / bqjh含 JSON并生成 tagList */
normalizeRecord(item) {
const record = { ...item };
record.gxUuid = record.wzUuid;
record.fwnr = record.jj;
const typeTags = this.splitTagText(record.fwlxjh);
const labelTags = this.splitTagText(record.bqjh);
record.fwlxbqList = [...new Set([...typeTags, ...labelTags])];
@ -800,7 +828,12 @@ export default {
card.fwlxbqList = inferred;
},
matchCategory(record, category) {
const text = `${record.bt1 || ''}${record.fwnr || ''}${record.fwlxjh || ''}${record.qymc || ''}`;
// gxnlFlDm: 01~05
if (record.gxnlFlDm && category.flDm && record.gxnlFlDm === category.flDm) {
return true;
}
//
const text = `${record.bt1 || ''}${record.jj || ''}${record.fwlxjh || ''}${record.qymc || ''}`;
return category.keywords.some((kw) => text.includes(kw));
},
assignCategoryCards(allRecords) {
@ -855,12 +888,9 @@ export default {
return;
}
try {
const res = await api.gxxxList({
ywlxDm: '01',
const res = await api.wzxxList({
pageNo: 1,
pageSize: 200,
scbz: 'N',
nr: '',
});
const data = res && res.data;
const records = ((data && data.records) || []).map((r) => this.normalizeRecord(r));
@ -881,6 +911,10 @@ export default {
}
},
getScrollRoot() {
// .content-wrapmain.vue
// .gxnlpt-side-nav-wrap overflow-y:auto
const portalRoot = document.querySelector('.content-wrap');
if (portalRoot) return portalRoot;
let el = this.$el.parentElement;
while (el) {
const style = window.getComputedStyle(el);
@ -947,6 +981,7 @@ export default {
return;
}
this.initScrollSpy();
this.initSidebarSticky();
this.scrollToSection(sectionId, tabIndex);
},
openSubmitView() {
@ -954,6 +989,7 @@ export default {
return;
}
this.destroyScrollSpy();
this.destroySidebarSticky();
this.contentView = 'submit';
this.resetSubmitForm();
},
@ -962,7 +998,29 @@ export default {
return;
}
this.destroyScrollSpy();
this.destroySidebarSticky();
this.contentView = 'favorites';
this.loadFavorites();
},
async loadFavorites() {
this.favoritesLoading = true;
try {
const res = await api.myFavoriteList({ pageNo: 1, pageSize: 200 });
const data = res && res.data;
const records = ((data && data.records) || []).map((r) => this.normalizeRecord(r));
this.favoriteList = records.map((card) => {
card.scbz = 'Y';
return card;
});
} catch (e) {
if (isUnauthorizedError(e)) {
showLoginGuide({ actionText: '查看我的收藏' });
return;
}
this.$message.warning('我的收藏加载失败');
} finally {
this.favoritesLoading = false;
}
},
resetSubmitForm() {
this.submitForm = SUBMIT_FORM_EMPTY();
@ -1023,10 +1081,11 @@ export default {
return;
}
try {
await api.gxfb({
ywlxDm: '01',
bt1: this.submitForm.bt1,
fwnr: this.submitForm.jj,
await api.submitSlxx({
bt: this.submitForm.bt1,
wzLj: this.submitForm.lj,
jj: this.submitForm.jj,
gxnlFlDm: flTitleToDm(this.submitForm.fl),
bqjh: this.submitForm.bq,
});
this.$message.success('提交成功,请等待审核');
@ -1041,70 +1100,310 @@ export default {
this.$message.warning('提交失败,请稍后重试');
}
},
/**
* 平滑滚动到指定分类区块
* 通过 getBoundingClientRect 获取目标元素在视口内的视觉位置
* 反算至滚动容器布局坐标后 scrollTo不受 scale 变换影响
*/
scrollToSection(sectionId, tabIndex) {
const el = document.getElementById(sectionId);
if (!el) return;
const scrollRoot = this.getScrollRoot();
const offsetTop = 24;
if (!el || !scrollRoot) return;
if (scrollRoot) {
const rootRect = scrollRoot.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
const targetTop = scrollRoot.scrollTop + (elRect.top - rootRect.top) - offsetTop;
scrollRoot.scrollTo({ top: Math.max(0, targetTop), behavior: 'smooth' });
} else {
const y = window.pageYOffset + el.getBoundingClientRect().top - offsetTop;
window.scrollTo({ top: Math.max(0, y), behavior: 'smooth' });
}
const scale = this._readScale();
if (scale <= 0) return;
const rootVisualTop = scrollRoot.getBoundingClientRect().top;
const elVisualTop = el.getBoundingClientRect().top;
//
const visualDist = elVisualTop - rootVisualTop;
const layoutDist = visualDist / scale;
const layoutTarget = scrollRoot.scrollTop + layoutDist;
// nav (64dp) + gap (24dp)
const OFFSET_DP = 88; // 稿
const target = Math.max(0, layoutTarget - OFFSET_DP);
scrollRoot.scrollTo({ top: target, behavior: 'smooth' });
this.scheduleScrollUnlock(tabIndex);
},
destroyScrollSpy() {
if (this.scrollRootEl && this._onScrollSpy) {
this.scrollRootEl.removeEventListener('scroll', this._onScrollSpy);
}
this.scrollRootEl = null;
this._onScrollSpy = null;
if (this._scrollSpyRaf) {
cancelAnimationFrame(this._scrollSpyRaf);
this._scrollSpyRaf = null;
}
},
/* ========== IntersectionObserver 驱动的 ScrollSpy ========== */
/**
* IntersectionObserver 监听每个分类区块与滚动容器顶部的交叉比例
* 选择最接近顶部且露出面积最大的区块为当前激活项
* 相比手写 scroll 事件更精准更省性能且不受 scale 变换影响
*/
initScrollSpy() {
this.destroyScrollSpy();
if (this.isStackedNavMode || this.contentView !== 'list') return;
const scrollRoot = this.getScrollRoot();
if (!scrollRoot) return;
this.scrollRootEl = scrollRoot;
const anchorOffset = 96;
this._onScrollSpy = () => {
if (this.suppressSectionObserver) return;
if (this._scrollSpyRaf) return;
this._scrollSpyRaf = requestAnimationFrame(() => {
this._scrollSpyRaf = null;
const line = scrollRoot.getBoundingClientRect().top + anchorOffset;
let nextIndex = 0;
this.categoryList.forEach((item, index) => {
const el = document.getElementById(item.id);
if (!el) return;
if (el.getBoundingClientRect().top <= line) {
nextIndex = index;
}
});
if (this.activeTabIndex !== nextIndex) {
this.activeTabIndex = nextIndex;
// { index, ratio }
this._spyEntries = [];
this._spyRootRect = null;
const obsOptions = {
root: scrollRoot,
// rootMargin nav(64dp)+gap(24dp)=88dp
// 线""
rootMargin: `-88px 0px 0px 0px`,
threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
};
this._spyObserver = new IntersectionObserver((entries) => {
const rootRect = scrollRoot.getBoundingClientRect();
this._spyRootRect = rootRect;
entries.forEach((entry) => {
const index = Number(entry.target.dataset.sectionIndex);
if (Number.isNaN(index)) return;
const existing = this._spyEntries.find((e) => e.index === index);
if (existing) {
existing.ratio = entry.intersectionRatio;
} else {
this._spyEntries.push({ index, ratio: entry.intersectionRatio });
}
});
this._applySpyResult();
}, obsOptions);
// gxnlpt-block
const blocks = this.$el.querySelectorAll('.gxnlpt-block[data-section-index]');
blocks.forEach((block) => this._spyObserver.observe(block));
// cover
this.$nextTick(() => {
this._spyEntries = [];
const rootRect = scrollRoot.getBoundingClientRect();
blocks.forEach((block, index) => {
const sectionEl = document.getElementById(this.categoryList[index]?.id);
if (!sectionEl) return;
// section 线
const sectionTop = sectionEl.getBoundingClientRect().top;
const line = rootRect.top + 88;
if (sectionTop <= line) {
this._spyEntries.push({ index, ratio: sectionTop <= rootRect.top ? 1 : 0.5 });
}
});
this._applySpyResult();
});
},
_applySpyResult() {
if (this.suppressSectionObserver) return;
if (!this._spyEntries.length) return;
// ratio > 0
const visible = this._spyEntries.filter((e) => e.ratio > 0);
if (visible.length) {
// ratio
const best = visible.reduce((a, b) => (a.ratio >= b.ratio ? a : b));
if (this.activeTabIndex !== best.index) {
this.activeTabIndex = best.index;
}
return;
}
// "section 线"
const rootRect = this._spyRootRect || this.getScrollRoot()?.getBoundingClientRect();
if (!rootRect) return;
const line = rootRect.top + 88;
let lastAbove = 0;
this.categoryList.forEach((item, index) => {
const el = document.getElementById(item.id);
if (!el) return;
if (el.getBoundingClientRect().top <= line) {
lastAbove = index;
}
});
if (this.activeTabIndex !== lastAbove) {
this.activeTabIndex = lastAbove;
}
},
destroyScrollSpy() {
if (this._spyObserver) {
this._spyObserver.disconnect();
this._spyObserver = null;
}
this._spyEntries = [];
this._spyRootRect = null;
this.scrollRootEl = null;
},
/* ========== 侧边导航 sticky 模拟(替代 CSS position:sticky ========== */
/**
* 原因.portal-figma-scale-stage transform:scale()
* 会创建新的 containing block导致 CSS position:sticky 完全失效
*
* 解决方案
* - 每帧滚动时实时测量侧边栏"自然视觉位置" transform 时的 getBoundingClientRect
* - 与导航栏上方 88dp (64dp 导航栏 + 24dp 间距) 比较
* - 若自然位置已被滚出视口上方 translateY 推回来
* - translateY 值不超过 sidebar 容器高度防止侧边栏脱出
*
* 坐标系
* - getBoundingClientRect 视觉像素scale
* - translateY 布局像素scale 容器内部空间
*/
_readScale() {
if (typeof document === 'undefined') return 1;
const val = parseFloat(
getComputedStyle(document.documentElement).getPropertyValue('--home-figma-scale'),
);
return val > 0 ? val : (window.innerWidth >= 768 ? window.innerWidth / 1440 : 1);
},
/**
* 测量 sticky 元素当前的自然视觉 top清除 transform 后测量再恢复
* 返回 { naturalVisualTop, stickyHeight, sidebarHeight } 三个视觉像素值
* 若元素不存在或 scale 无效则返回 null
*/
_measureStickyNatural() {
const stickyEl = this.$el?.querySelector('.gxnlpt-sidebar-sticky');
const sidebarEl = stickyEl?.parentElement;
const scrollRoot = this.getScrollRoot();
const scale = this._readScale();
if (!stickyEl || !sidebarEl || !scrollRoot || scale <= 0) return null;
// transform
const saved = stickyEl.style.transform;
stickyEl.style.transform = '';
const rect = stickyEl.getBoundingClientRect();
const rootRect = scrollRoot.getBoundingClientRect();
const sidebarRect = sidebarEl.getBoundingClientRect();
// transform
stickyEl.style.transform = saved;
return {
// top visual top
naturalVisualTop: rect.top - rootRect.top,
// sticky
stickyHeight: rect.height,
//
sidebarHeight: sidebarRect.height,
// top visual top
sidebarVisualTop: sidebarRect.top - rootRect.top,
};
},
initSidebarSticky() {
this.destroySidebarSticky();
if (this.isStackedNavMode || this.contentView !== 'list') return;
const scrollRoot = this.getScrollRoot();
if (!scrollRoot) return;
this._sidebarStickyScrollEl = scrollRoot;
const handler = () => {
if (this._sidebarStickyRaf) return;
this._sidebarStickyRaf = requestAnimationFrame(() => {
this._sidebarStickyRaf = null;
this.updateSidebarSticky();
});
};
scrollRoot.addEventListener('scroll', this._onScrollSpy, { passive: true });
this._onScrollSpy();
scrollRoot.addEventListener('scroll', handler, { passive: true });
this._onSidebarStickyScroll = handler;
// resize scale
this._onSidebarResize = () => {
if (this._sidebarResizeTimer) clearTimeout(this._sidebarResizeTimer);
this._sidebarResizeTimer = setTimeout(() => {
this.updateSidebarSticky();
}, 60);
};
window.addEventListener('resize', this._onSidebarResize);
// figmaStage
const stage = this.$refs.figmaStage;
if (stage && typeof ResizeObserver !== 'undefined') {
this._sidebarStageObserver = new ResizeObserver(() => {
if (this._sidebarStageTimer) clearTimeout(this._sidebarStageTimer);
this._sidebarStageTimer = setTimeout(() => {
this.updateSidebarSticky();
}, 80);
});
this._sidebarStageObserver.observe(stage);
}
this.$nextTick(() => this.updateSidebarSticky());
},
destroySidebarSticky() {
if (this._sidebarStickyScrollEl && this._onSidebarStickyScroll) {
this._sidebarStickyScrollEl.removeEventListener('scroll', this._onSidebarStickyScroll);
}
this._sidebarStickyScrollEl = null;
this._onSidebarStickyScroll = null;
if (this._sidebarStickyRaf) {
cancelAnimationFrame(this._sidebarStickyRaf);
this._sidebarStickyRaf = null;
}
if (this._onSidebarResize) {
window.removeEventListener('resize', this._onSidebarResize);
this._onSidebarResize = null;
}
if (this._sidebarResizeTimer) {
clearTimeout(this._sidebarResizeTimer);
this._sidebarResizeTimer = null;
}
if (this._sidebarStageObserver) {
this._sidebarStageObserver.disconnect();
this._sidebarStageObserver = null;
}
if (this._sidebarStageTimer) {
clearTimeout(this._sidebarStageTimer);
this._sidebarStageTimer = null;
}
const stickyEl = this.$el?.querySelector('.gxnlpt-sidebar-sticky');
if (stickyEl) stickyEl.style.transform = '';
},
updateSidebarSticky() {
const stickyEl = this.$el?.querySelector('.gxnlpt-sidebar-sticky');
if (!stickyEl) return;
const info = this._measureStickyNatural();
if (!info) return;
const scale = this._readScale();
// CSS position:sticky top:24px 24dp
// naturalVisualTop 24
const DESIRED_DP = 24;
const desiredVisualTop = DESIRED_DP * scale;
const naturalVisualTop = info.naturalVisualTop;
// 线
if (naturalVisualTop >= desiredVisualTop) {
stickyEl.style.transform = '';
return;
}
//
const visualOffset = desiredVisualTop - naturalVisualTop;
// translateY scale
const layoutOffset = visualOffset / scale;
//
// sticky sidebar padding sidebar sticky
// sidebarHeight - stickyHeight
const sidebarBottom = info.sidebarVisualTop + info.sidebarHeight;
const stickyBottom = info.naturalVisualTop + info.stickyHeight;
// sidebar - sticky
const availableVisual = Math.max(0, sidebarBottom - stickyBottom);
// naturalVisualTop padding availableVisual
// sticky sidebar
const maxLayoutOffset = availableVisual / scale;
const clamped = Math.min(layoutOffset, maxLayoutOffset);
stickyEl.style.transform = clamped > 0
? `translateY(${clamped}px)`
: '';
},
handleCardClick(card) {
if (this.useDemoData || !card.gxUuid) {
this.$message.info('敬请期待');
this.showComingSoon();
return;
}
this.$router.push({ path: '/tfwsc', query: { id: card.gxUuid } });
@ -1120,8 +1419,10 @@ export default {
}
try {
const type = card.scbz === 'Y' ? 'remove' : 'add';
await api.gxsc({ gxUuid: card.gxUuid, type });
card.scbz = card.scbz === 'Y' ? 'N' : 'Y';
await api.toggleGxsc({ wzUuid: card.gxUuid, type });
const next = card.scbz === 'Y' ? 'N' : 'Y';
card.scbz = next;
this.syncFavoriteListAfterToggle(card, next);
} catch (e) {
if (isUnauthorizedError(e)) {
showLoginGuide({ actionText: '收藏' });
@ -1130,6 +1431,21 @@ export default {
this.$message.warning('收藏操作失败');
}
},
/** 收藏切换后同步本地 favoriteList避免再次进入「我的收藏」时与服务端不一致 */
syncFavoriteListAfterToggle(card, nextScbz) {
const list = this.favoriteList || [];
const idx = list.findIndex((c) => c.gxUuid && c.gxUuid === card.gxUuid);
if (nextScbz === 'Y') {
if (idx < 0) {
list.unshift({ ...card, scbz: 'Y' });
} else {
list.splice(idx, 1, { ...card, scbz: 'Y' });
}
} else if (idx >= 0) {
list.splice(idx, 1);
}
this.favoriteList = list;
},
},
};
</script>
@ -1151,6 +1467,39 @@ export default {
.portal-figma-scale-page();
}
/* gxnlpt 5 +
- 整页走 Figma 缩放时stage 内的 .gxnlpt-shell (max-width: 1200px) 会被整页 scale 缩放
到视口宽度stage 自身 layout 高度 = 内容自然高度transform 不影响 layout 高度
这会远大于 .content-wrap 的固定高度理论上 .content-wrap 会出现滚动条
- 关键陷阱home-figma-variables.less
html.portal-figma-scale-active.portal-landing-figma-scale-active
.portal-landing-figma-page { height: var(--home-figma-visual-height) !important; ... }
会把 page 高度 clamp --home-figma-visual-height
--home-figma-visual-height syncHomeFigmaStageLayout mounted nextTick 写入
visualHeight = stage.offsetHeight * scale
gxnlpt stage 此时还是初始高度demo 数据/卡片未装填算出来的值比真实
stage 高度小几百到上千像素于是 page 永远被 clamp 裁切
--home-figma-visual-height 也不会被重算除了 resize
表现F12 关闭视口时 height=616 裁切剩 1 F12 打开后视口高度变化
resize 重算 visualHeight 变成一个不同的值 整页能塞下
- 解法 .gxnlpt-page 不依赖 --home-figma-visual-height 决定高度直接重写为
height:auto / max-height:none / overflow:visible */
html.portal-figma-scale-active.portal-landing-figma-scale-active .gxnlpt-page,
html.portal-figma-scale-active .gxnlpt-page {
/* 不再走 landing page 的 height clamp否则 page 高度被卡死,外层不会滚动 */
height: auto !important;
max-height: none !important;
overflow: visible !important;
.portal-figma-scale-viewport {
/* viewport stage clamp --home-figma-visual-height
这样 .content-wrap 内的 .gxnlpt-page 能被 5 个分类的内容自然撑高 */
height: auto !important;
/* overflow:hidden stage transform:scale
layout 高度 = 缩放前高度viewport 高度 = 同一个 layout 高度stage 不会被裁切 */
}
}
.gxnlpt-shell.page-content-wrap {
display: flex;
flex: 1 0 auto;
@ -1171,13 +1520,15 @@ export default {
@gxnlpt-side-actions-block-h: 114px;
@gxnlpt-side-actions-gap-top: 20px;
/* Figma Frame_left 602px白卡片与右侧同高向下铺满 */
/* Figma Frame_left 602px
sticky 侧边导航提供足够的垂直滚动空间确保导航始终可见
空白区域由 gxnlpt-sidebar-tail 填充符合设计稿"白底向下铺满"的意图 */
.gxnlpt-sidebar {
display: flex;
flex-direction: column;
align-self: stretch;
width: @gxnlpt-sidebar-width;
min-height: @gxnlpt-content-min-height;
min-height: 0;
padding: @gxnlpt-sidebar-padding;
background: #fff;
border-radius: @gxnlpt-sidebar-radius;

View File

@ -5,7 +5,7 @@
<!-- 主页面 -->
<div class="container">
<!-- 顶部背景轮播 -->
<div class="top-box" id="section-hero">
<div class="top-box snap-section" id="section-hero">
<t-swiper class="top-banner-swiper" animation="fade" :height="topBannerHeight" :interval="10000" :duration="500"
:loop="true" :autoplay="true" theme="dark" :navigation="{ showSlideBtn: 'never' }">
<t-swiper-item v-for="(video, idx) in topBannerVideos" :key="idx">
@ -92,7 +92,7 @@
</div>
<!-- 中间核心驱动 -->
<section class="core-section" id="section-core">
<section class="core-section snap-section" id="section-core">
<div class="home-shelf home-shelf--core">
<div class="section-title core-title">
<div class="home-block-title-content">
@ -130,7 +130,7 @@
</section>
<!-- 共性能力模块 -->
<section class="capability-section" id="section-capability">
<section class="capability-section snap-section" id="section-capability">
<div class="home-shelf home-shelf--capability">
<div class="capability-header">
<div class="capability-title-group">
@ -156,7 +156,7 @@
</section>
<!-- 企业出海模块 -->
<section class="overseas2-section" id="section-overseas">
<section class="overseas2-section snap-section" id="section-overseas">
<div class="home-shelf home-shelf--overseas">
<div class="overseas2-header">
<div class="overseas2-title-group">
@ -195,7 +195,7 @@
</section>
<!-- 行业动态布局与 overseas2 同构section-container + header + grid -->
<section class="news-section" id="section-news">
<section class="news-section snap-section" id="section-news">
<div class="home-shelf home-shelf--news">
<div class="news-header">
<div class="news-title-group">
@ -262,7 +262,7 @@
</section>
<!-- 合作伙伴模块 -->
<section class="partner-section" id="section-partner">
<section class="partner-section snap-section" id="section-partner">
<div class="home-shelf home-shelf--partner">
<div class="partner-title-area">
<div class="partner-title-content">
@ -309,7 +309,7 @@
</section>
<!-- CTA独占一屏Figma 150645:1019 -->
<div class="bottom-box" id="section-bottom">
<div class="bottom-box snap-section" id="section-bottom">
<img
class="bottom-box-bg"
:src="homeCtaBg"
@ -340,25 +340,27 @@ import Footer from '@/pages/index/components/footer/index.vue';
import hydtApi from '@/pages/index/api/hydt';
import gxzxApi from '@/pages/index/api/gxzx/index.js';
import searchApi from '@/pages/index/api/search.js';
import { bindSectionWheelScroll } from '@/pages/index/utils/portal-scroll-mode';
import { smoothScrollTo, waitForScrollEnd } from '@/pages/index/utils/portal-scroll-mode';
import { applyPortalFigmaScaleToRoot } from '@/pages/index/utils/portal-figma-scale';
import { calcHomeFigmaScale } from '@/pages/index/utils/home-figma-scale';
import portalFigmaScaleMixin from '@/pages/index/utils/portal-figma-scale-mixin';
import fullpageScrollMixin from '@/pages/index/utils/fullpage-scroll-mixin';
import comingSoonMixin from '@/pages/index/utils/coming-soon-mixin';
export default {
mixins: [portalFigmaScaleMixin],
name: 'Home2Index',
mixins: [portalFigmaScaleMixin, fullpageScrollMixin, comingSoonMixin],
data() {
return {
inputValue: '',
activeTab: 0,
topBannerHeight: 686,
newsLoading: true,
isScrolling: false,
scrollThreshold: 1,
scrollRoot: null,
unbindSectionWheel: null,
sectionOffsets: [],
sectionIds: ['section-hero', 'section-core', 'section-capability', 'section-overseas', 'section-news', 'section-partner', 'section-bottom'],
// section IntersectionObserver
activeSectionIndex: 0,
coreSelectedIndex: 3,
newsListByType: {
gjzc: [], //
@ -491,6 +493,16 @@ export default {
components: {
Footer,
},
activated() {
// keep-alive IntersectionObserver section
this.$nextTick(() => {
this.scrollRoot = document.querySelector('.content-wrap');
if (this.scrollRoot) {
this.refreshSectionOffsets();
this.setupSectionObserver();
}
});
},
watch: {
'$route.query.section'(section) {
this.$nextTick(() => {
@ -526,11 +538,15 @@ export default {
}
this.syncBannerHeight();
if (this.scrollRoot) {
this.unbindSectionWheel = bindSectionWheelScroll(this.scrollRoot, this.handleWheel);
this.refreshSectionOffsets();
// IntersectionObserver wheel
//
// " section "
this.setupSectionObserver();
if (typeof ResizeObserver !== 'undefined') {
this._scrollRootResizeObserver = new ResizeObserver(() => {
this.onViewportResize();
this.refreshSectionOffsets();
});
this._scrollRootResizeObserver.observe(this.scrollRoot);
}
@ -550,10 +566,7 @@ export default {
this._scrollRootResizeObserver.disconnect();
this._scrollRootResizeObserver = null;
}
if (this.unbindSectionWheel) {
this.unbindSectionWheel();
this.unbindSectionWheel = null;
}
this.teardownSectionObserver();
},
methods: {
syncNewsTabFromRoute(type) {
@ -586,6 +599,7 @@ export default {
return scrollRoot.scrollTop + (rect.top - rootRect.top);
},
getCurrentSectionIndex(scrollRoot) {
// IntersectionObserver 使
if (!this.sectionOffsets.length) {
this.refreshSectionOffsets();
}
@ -599,87 +613,74 @@ export default {
}
return currentIndex;
},
scrollRootToSection(sectionIndex, smooth = true) {
const scrollRoot = this.getScrollRoot();
if (!scrollRoot) return;
if (!this.sectionOffsets.length) {
this.refreshSectionOffsets();
}
const top = this.sectionOffsets[sectionIndex] ?? 0;
scrollRoot.scrollTo({ top, behavior: smooth ? 'smooth' : 'auto' });
/**
* 滚动到指定 section 走浏览器原生 scrollIntoView不再劫持 wheel 事件
* 不再使用 setTimeout waitForScrollEnd 在真正结束时通知
*/
async scrollRootToSection(sectionIndex, smooth = true) {
// fullpageScrollMixin, fullpage-scroll.js
this.fullPageScrollTo(sectionIndex);
},
trySectionPageScroll(sectionEl, scrollRoot, direction, e, edgeGap = 8) {
if (!sectionEl || !scrollRoot) return false;
if (sectionEl.offsetHeight <= scrollRoot.clientHeight + edgeGap) {
return false;
}
const sectionIndex = this.sectionIds.indexOf(sectionEl.id);
const sectionTop = sectionIndex >= 0 && this.sectionOffsets.length
? (this.sectionOffsets[sectionIndex] ?? 0)
: this.getSectionScrollTop(sectionEl, scrollRoot);
const sectionBottom = sectionTop + sectionEl.offsetHeight;
const viewTop = scrollRoot.scrollTop;
const viewBottom = viewTop + scrollRoot.clientHeight;
if (direction > 0 && viewBottom < sectionBottom - edgeGap) {
e.preventDefault();
scrollRoot.scrollTop += e.deltaY;
return true;
}
if (direction < 0 && viewTop > sectionTop + edgeGap) {
e.preventDefault();
scrollRoot.scrollTop += e.deltaY;
return true;
}
return false;
onFullPageSectionChange(newIndex) {
this.activeSectionIndex = newIndex;
},
handleWheel(e) {
if (this.isScrolling) return;
const delta = Math.abs(e.deltaY);
if (delta < this.scrollThreshold) return;
/**
* 替代原 bindSectionWheelScroll IntersectionObserver 监听每个 section
* 与视口的相交状态自动算"当前激活 section"不再干扰原生滚动
*/
setupSectionObserver() {
if (typeof window === 'undefined' || typeof IntersectionObserver === 'undefined') {
return;
}
this.teardownSectionObserver();
const scrollRoot = this.getScrollRoot();
if (!scrollRoot) return;
const direction = e.deltaY > 0 ? 1 : -1;
const currentIndex = this.getCurrentSectionIndex(scrollRoot);
const currentEl = document.getElementById(this.sectionIds[currentIndex]);
// rootMargin " 40% / 40%"
// section viewport active
this._sectionObserver = new IntersectionObserver(
(entries) => {
// isIntersecting section
let bestIndex = this.activeSectionIndex;
let bestRatio = -1;
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const idx = this.sectionIds.indexOf(entry.target.id);
if (idx < 0) return;
if (entry.intersectionRatio > bestRatio) {
bestRatio = entry.intersectionRatio;
bestIndex = idx;
}
});
if (bestRatio >= 0) {
this.activeSectionIndex = bestIndex;
}
},
{
root: scrollRoot,
// 40% 20%
rootMargin: '-40% 0px -40% 0px',
threshold: [0, 0.25, 0.5, 0.75, 1],
},
);
if (currentEl && this.trySectionPageScroll(currentEl, scrollRoot, direction, e)) {
return;
this.sectionIds.forEach((id) => {
const el = document.getElementById(id);
if (el) this._sectionObserver.observe(el);
});
},
teardownSectionObserver() {
if (this._sectionObserver) {
this._sectionObserver.disconnect();
this._sectionObserver = null;
}
let targetIndex;
if (direction > 0) {
targetIndex = Math.min(currentIndex + 1, this.sectionIds.length - 1);
} else {
targetIndex = Math.max(currentIndex - 1, 0);
}
if (targetIndex === currentIndex) {
const top = this.sectionOffsets[currentIndex] ?? 0;
if (Math.abs(scrollRoot.scrollTop - top) > 6) {
e.preventDefault();
this.isScrolling = true;
this.scrollRootToSection(currentIndex, true);
setTimeout(() => {
this.isScrolling = false;
}, 800);
}
return;
}
e.preventDefault();
this.isScrolling = true;
this.scrollRootToSection(targetIndex, true);
setTimeout(() => {
this.isScrolling = false;
}, 800);
},
openNewTab(url) {
if (url) {
window.open(url);
} else {
this.$message.info('敬请期待');
this.showComingSoon();
}
},
openService() {
@ -744,7 +745,7 @@ export default {
// -
handleNavigate(link) {
if (!link) {
this.$message.info('敬请期待');
this.showComingSoon();
return;
}
if (link.startsWith('http://') || link.startsWith('https://')) {
@ -756,7 +757,7 @@ export default {
// iframe - parentmain.vueiframe
handleIframeNavigate(url) {
if (!url) {
this.$message.info('敬请期待');
this.showComingSoon();
return;
}
const u = `${this.buttonLinkHost}${url}`;
@ -821,25 +822,29 @@ export default {
},
async fetchNewsData() {
this.newsLoading = true;
try {
//
const [hydtRes, ptggRes] = await Promise.all([
hydtApi.getHydtGroupedList(),
hydtApi.getPtggList()
]);
//
const [hydtResult, ptggResult] = await Promise.allSettled([
hydtApi.getHydtGroupedList(),
hydtApi.getPtggList()
]);
if (hydtRes.data) {
this.newsListByType.gjzc = hydtRes.data.gjzc || [];
this.newsListByType.hyzx = hydtRes.data.hyzx || [];
}
if (ptggRes.data) {
this.newsListByType.ptgg = ptggRes.data || [];
}
} catch (e) {
console.error('获取新闻数据失败', e);
} finally {
this.newsLoading = false;
if (hydtResult.status === 'fulfilled' && hydtResult.value && hydtResult.value.data) {
this.newsListByType.gjzc = hydtResult.value.data.gjzc || [];
this.newsListByType.hyzx = hydtResult.value.data.hyzx || [];
} else {
this.newsListByType.gjzc = [];
this.newsListByType.hyzx = [];
console.error('获取行业动态失败', hydtResult.status === 'rejected' ? hydtResult.reason : null);
}
if (ptggResult.status === 'fulfilled' && ptggResult.value && ptggResult.value.data) {
this.newsListByType.ptgg = ptggResult.value.data || [];
} else {
this.newsListByType.ptgg = [];
console.error('获取平台公告失败', ptggResult.status === 'rejected' ? ptggResult.reason : null);
}
this.newsLoading = false;
},
getDefaultPic(type) {
const defaultPics = {
@ -884,6 +889,24 @@ export default {
background: @home-color-page-bg;
}
/* === CSS Scroll Snap ===
1. .content-wrap 是滚动容器main.vue已设置 overflow-y: auto
2. 滚动容器的 snap-type main.vue 里统一声明
3. 每个 .snap-section 声明 scroll-snap-align: start对齐到容器顶部
4. proximity 而非 mandatory用户在 section 内部可自由滚动
只有接近边界时才 snap 到下一屏避免"卡顿 / 反向""无法停在中间"
5. scroll-snap-stop: normal允许用户快速滑动跨多屏
6. min-height: 100% section 至少占满一个视口高度
这样 snap 才有"分屏"的几何基础
7. 移动端关闭 snap移动端用原生滚动体验更好
整屏 snap 在小屏上会显得非常卡 */
.snap-section {
scroll-snap-align: start;
scroll-snap-stop: normal;
min-height: 100%;
box-sizing: border-box;
}
/* Figma 首页1920/1440token 见 home-figma-variables.less */
.portal-page {
.home-portal-page-vars();
@ -1044,7 +1067,8 @@ export default {
min-width: 0;
margin-top: auto;
margin-bottom: 0;
gap: var(--home-hero-content-gap-fluid, clamp(24px, 4vh, @home-hero-content-gap));
// / : 12px, Figma
gap: var(--home-hero-content-gap-fluid, clamp(8px, 1.2vh, 12px));
box-sizing: border-box;
}
@ -1129,6 +1153,8 @@ export default {
gap: @home-space-16;
width: 100%;
max-width: @home-hero-title-width;
// line-height ,沿
margin-bottom: -14px;
}
.top-hero-actions {
@ -2833,7 +2859,7 @@ export default {
.portal-page .home-hero-content {
margin-top: auto;
gap: clamp(24px, 4vh, @home-hero-content-gap);
// 28px ,
}
.portal-page .top-hero-actions {
@ -2969,7 +2995,8 @@ export default {
.portal-page .home-hero-content {
margin-top: 0;
gap: @home-space-32;
// / : 32px 12px, Figma
gap: 12px;
max-width: 100%;
}

View File

@ -168,24 +168,33 @@ export default {
},
async fetchNewsData() {
this.pageLoading = true;
try {
const [hydtRes, ptggRes] = await Promise.all([
hydtApi.getHydtGroupedList(),
hydtApi.getPtggList(),
]);
if (hydtRes.data) {
this.newsListByType.gjzc = hydtRes.data.gjzc || [];
this.newsListByType.hyzx = hydtRes.data.hyzx || [];
}
if (ptggRes.data) {
this.newsListByType.ptgg = ptggRes.data || [];
}
} catch (e) {
console.error('获取行业动态失败', e);
this.$message.warning('加载失败,请稍后重试');
} finally {
this.pageLoading = false;
const [hydtResult, ptggResult] = await Promise.allSettled([
hydtApi.getHydtGroupedList(),
hydtApi.getPtggList(),
]);
if (hydtResult.status === 'fulfilled' && hydtResult.value && hydtResult.value.data) {
this.newsListByType.gjzc = hydtResult.value.data.gjzc || [];
this.newsListByType.hyzx = hydtResult.value.data.hyzx || [];
} else {
this.newsListByType.gjzc = [];
this.newsListByType.hyzx = [];
console.error('获取行业动态失败', hydtResult.status === 'rejected' ? hydtResult.reason : null);
}
if (ptggResult.status === 'fulfilled' && ptggResult.value && ptggResult.value.data) {
this.newsListByType.ptgg = ptggResult.value.data || [];
} else {
this.newsListByType.ptgg = [];
console.error('获取平台公告失败', ptggResult.status === 'rejected' ? ptggResult.reason : null);
}
const allFailed = hydtResult.status === 'rejected' && ptggResult.status === 'rejected';
if (allFailed) {
this.$message.warning('加载失败,请稍后重试');
}
this.pageLoading = false;
},
getDefaultPic(type) {
const defaultPics = {

View File

@ -44,10 +44,11 @@
<script>
import { mapState } from 'vuex';
import portalFigmaScaleMixin from '@/pages/index/utils/portal-figma-scale-mixin';
import comingSoonMixin from '@/pages/index/utils/coming-soon-mixin';
export default {
name: 'hyzt',
mixins: [portalFigmaScaleMixin],
mixins: [portalFigmaScaleMixin, comingSoonMixin],
components: {},
data() {
return {
@ -65,7 +66,7 @@ export default {
if (href) {
window.open(href, '_blank');
} else {
this.$message.info('敬请期待');
this.showComingSoon();
}
},
},

View File

@ -275,18 +275,17 @@
import serviceCardIcon1 from '../../assets/qych/service-card-icon-1.png'
import serviceCardIcon2 from '../../assets/qych/service-card-icon-2.png'
import portalFigmaScaleMixin from '@/pages/index/utils/portal-figma-scale-mixin';
import fullpageScrollMixin from '@/pages/index/utils/fullpage-scroll-mixin';
import comingSoonMixin from '@/pages/index/utils/coming-soon-mixin';
export default {
name: 'CompliancePortal',
mixins: [portalFigmaScaleMixin],
mixins: [portalFigmaScaleMixin, fullpageScrollMixin, comingSoonMixin],
components: {},
data() {
return {
serviceCardIcon1,
serviceCardIcon2,
isScrolling: false,
scrollThreshold: 1,
scrollRoot: null,
sectionIds: ['section-landing', 'section0', 'section1', 'section2'],
navList: [
{
@ -322,24 +321,15 @@ export default {
},
mounted() {
this.$nextTick(() => {
this.scrollRoot = document.querySelector('.content-wrap');
if (this.scrollRoot) {
this.scrollRoot.addEventListener('wheel', this.handleWheel, { passive: false });
}
this.scrollToSectionFromQuery();
});
},
beforeDestroy() {
if (this.scrollRoot) {
this.scrollRoot.removeEventListener('wheel', this.handleWheel);
}
},
methods: {
goPage(url) {
if (url) {
window.open(url);
} else {
this.$message.info('敬请期待')
this.showComingSoon()
}
},
handleCardBtnClick(item) {
@ -390,108 +380,23 @@ export default {
return currentIndex;
},
scrollRootToSection(sectionIndex, smooth = true) {
const scrollRoot = this.getScrollRoot();
const el = document.getElementById(this.sectionIds[sectionIndex]);
if (!scrollRoot || !el) return;
const top = this.getSectionScrollTop(el, scrollRoot);
scrollRoot.scrollTo({ top, behavior: smooth ? 'smooth' : 'auto' });
this.fullPageScrollTo(sectionIndex);
this.syncNavBySectionIndex(sectionIndex);
},
/** 专题区 .module 内部先滚完,再翻页 */
/** 当前屏内容高于视口时,先在 .content-wrap 内滚完该屏 */
trySectionPageScroll(sectionEl, scrollRoot, direction, e, edgeGap = 8) {
if (!sectionEl || !scrollRoot) return false;
if (sectionEl.offsetHeight <= scrollRoot.clientHeight + edgeGap) {
return false;
}
const sectionTop = this.getSectionScrollTop(sectionEl, scrollRoot);
const sectionBottom = sectionTop + sectionEl.offsetHeight;
const viewTop = scrollRoot.scrollTop;
const viewBottom = viewTop + scrollRoot.clientHeight;
if (direction > 0 && viewBottom < sectionBottom - edgeGap) {
e.preventDefault();
scrollRoot.scrollTop += e.deltaY;
return true;
}
if (direction < 0 && viewTop > sectionTop + edgeGap) {
e.preventDefault();
scrollRoot.scrollTop += e.deltaY;
return true;
}
return false;
},
/** 导航点跳到指定 section使用 fullpageScrollMixin 提供的 fullPageScrollTo */
scrollToSection(item) {
const sectionIndex = this.sectionIds.indexOf(item.sectionId);
if (sectionIndex < 0) return;
this.setNavActive(item.index);
this.isScrolling = true;
this.scrollRootToSection(sectionIndex, true);
setTimeout(() => {
this.isScrolling = false;
}, 800);
this.fullPageScrollTo(sectionIndex);
},
syncNavBySectionIndex(sectionIndex) {
if (sectionIndex >= 1 && sectionIndex <= 3) {
this.setNavActive(sectionIndex - 1);
}
},
handleWheel(e) {
if (this.isScrolling) return;
const delta = Math.abs(e.deltaY);
if (delta < this.scrollThreshold) return;
const scrollRoot = this.getScrollRoot();
if (!scrollRoot) return;
const direction = e.deltaY > 0 ? 1 : -1;
const maxScrollTop = scrollRoot.scrollHeight - scrollRoot.clientHeight;
const atBottom = maxScrollTop <= 0 || scrollRoot.scrollTop >= maxScrollTop - 12;
const atTop = scrollRoot.scrollTop <= 12;
//
if ((direction > 0 && atBottom) || (direction < 0 && atTop)) {
return;
}
const lastSectionEl = document.getElementById('section2');
if (lastSectionEl) {
const lastBottom = this.getSectionScrollTop(lastSectionEl, scrollRoot) + lastSectionEl.offsetHeight;
if (scrollRoot.scrollTop + scrollRoot.clientHeight * 0.35 > lastBottom) {
return;
}
}
const currentIndex = this.getCurrentSectionIndex(scrollRoot);
const currentEl = document.getElementById(this.sectionIds[currentIndex]);
if (currentEl && this.trySectionPageScroll(currentEl, scrollRoot, direction, e)) {
return;
}
let targetIndex;
if (direction > 0) {
targetIndex = Math.min(currentIndex + 1, this.sectionIds.length - 1);
} else {
targetIndex = Math.max(currentIndex - 1, 0);
}
if (targetIndex === currentIndex) {
const top = this.getSectionScrollTop(currentEl, scrollRoot);
if (Math.abs(scrollRoot.scrollTop - top) > 6) {
e.preventDefault();
this.isScrolling = true;
this.scrollRootToSection(currentIndex, true);
setTimeout(() => {
this.isScrolling = false;
}, 800);
}
return;
}
e.preventDefault();
this.isScrolling = true;
this.scrollRootToSection(targetIndex, true);
setTimeout(() => {
this.isScrolling = false;
}, 800);
onFullPageSectionChange(newIndex /*, fromIndex, sectionEl */) {
this.syncNavBySectionIndex(newIndex);
},
},
};
@ -788,10 +693,12 @@ body {
.text-section-content {
display: flex;
flex: 1 1 auto;
flex-direction: row;
align-items: flex-start;
gap: @qych-policy-content-gap;
padding: 0 @qych-policy-content-px;
min-height: 0;
background: transparent;
}
@ -805,11 +712,8 @@ body {
display: flex;
flex: 1 1 auto;
flex-direction: column;
align-self: flex-start;
min-width: 0;
height: @qych-policy-panel-height;
min-height: @qych-policy-panel-height;
max-height: @qych-policy-panel-height;
max-height: 328px;
padding: @qych-policy-panel-padding;
overflow-x: hidden;
overflow-y: auto;
@ -904,7 +808,7 @@ body {
align-items: flex-start;
justify-content: flex-start;
gap: 6px;
min-height: 132px;
min-height: 160px;
height: auto;
padding: 20px @qych-policy-card-padding-x 16px;
border-radius: @qych-policy-card-radius-inner;

View File

@ -163,7 +163,9 @@ if (LOCAL_DEV && process.env.VUE_APP_IE === 'true') {
transpileDependencies.push('sockjs-client');
}
module.exports = {
lintOnSave: LOCAL_DEV,
// 关闭 eslint-loader默认的 espree parser 不能解析 ES module 语法,
// 会导致 "import is reserved" 编译错误。lint 用 IDE / CI 单独做。
lintOnSave: false,
transpileDependencies,
publicPath: process.env.NODE_ENV === 'production' ? '/' : process.env.VUE_APP_CDN_PATH,
assetsDir: 'assets_res',
@ -294,10 +296,7 @@ module.exports = {
changeOrigin: true,
},
'^/mhzc': {
// target: 'http://localhost:9300',
// target: 'http://carbon.liantu.tech',
target: 'https://www.cciw.com.cn',
// target: 'http://10.23.20.13:94/',
target: process.env.VUE_APP_MHZC_PROXY || 'https://www.cciw.com.cn',
changeOrigin: true,
},
'^/gxzx': {