Merge branch 'refactor/clear' of https://git.liantu.tech/xiaoyu/txw into refactor/clear
This commit is contained in:
commit
d3e938c14e
6
.codex/config.toml
Normal file
6
.codex/config.toml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[mcp_servers.code-review-graph]
|
||||||
|
command = "uvx"
|
||||||
|
args = [
|
||||||
|
"code-review-graph",
|
||||||
|
"serve",
|
||||||
|
]
|
||||||
28
.codex/hooks.json
Normal file
28
.codex/hooks.json
Normal 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
38
AGENTS.md
Normal 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.
|
||||||
54
txw-mhzc-web scripts/shot-fix.mjs
Normal file
54
txw-mhzc-web scripts/shot-fix.mjs
Normal 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);
|
||||||
@ -1,5 +1,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
|
// babel-eslint 能正确解析 ES module 的 import/export 语法
|
||||||
|
// 默认的 espree parser 不支持,会报 "export is reserved"
|
||||||
|
parser: 'babel-eslint',
|
||||||
extends: [],
|
extends: [],
|
||||||
rules: {
|
rules: {
|
||||||
// 关闭所有校验
|
// 关闭所有校验
|
||||||
|
|||||||
24256
txw-mhzc-web/package-lock.json
generated
Normal file
24256
txw-mhzc-web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -20,6 +20,7 @@
|
|||||||
"@vue/babel-preset-jsx": "1.4.0"
|
"@vue/babel-preset-jsx": "1.4.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@cssyq/ggzc-web": "file:../local-nodemodules/@cssyq/ggzc-web",
|
||||||
"@fortawesome/fontawesome-free": "^7.0.1",
|
"@fortawesome/fontawesome-free": "^7.0.1",
|
||||||
"@gt4/common-front": "2.0.113",
|
"@gt4/common-front": "2.0.113",
|
||||||
"@gtff/tdesign-gt-vue": "1.4.0",
|
"@gtff/tdesign-gt-vue": "1.4.0",
|
||||||
@ -102,9 +103,9 @@
|
|||||||
"author": "wangjianxin@css.com.cn <wangjianxin@css.com.cn>",
|
"author": "wangjianxin@css.com.cn <wangjianxin@css.com.cn>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"ggzc-web": "link:D:\\shanghai\\txw2\\local-nodemodules\\@cssyq\\ggzc-web",
|
"ggzc-web": "file:../local-nodemodules/@cssyq/ggzc-web",
|
||||||
"common-front": "link:D:\\shanghai\\txw2\\local-nodemodules\\@gt4\\common-front",
|
"common-front": "file:../local-nodemodules/@gt4/common-front",
|
||||||
"tdesign-gt-vue": "link:D:\\shanghai\\txw2\\local-nodemodules\\@gtff\\tdesign-gt-vue"
|
"tdesign-gt-vue": "file:../local-nodemodules/@gtff/tdesign-gt-vue"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
|
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
|
||||||
}
|
}
|
||||||
|
|||||||
6
txw-mhzc-web/public/config/README.md
Normal file
6
txw-mhzc-web/public/config/README.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# 前端外部配置占位
|
||||||
|
|
||||||
|
`config.json` 用于消除开发环境对 `/config/config.json` 的 404 请求。
|
||||||
|
|
||||||
|
- 业务默认配置在 `src/settings/index.js`,通过 `main.js` 的 `extSettings` 注入。
|
||||||
|
- 本文件保持 `{}` 即可;仅在需要**不改代码、热覆盖**默认配置时,再在此写入键值(与 `settings/index.js` 同结构)。
|
||||||
3
txw-mhzc-web/public/config/config.json
Normal file
3
txw-mhzc-web/public/config/config.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"code": 200
|
||||||
|
}
|
||||||
@ -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');
|
|
||||||
@ -44,7 +44,10 @@ request.interceptors.request.use(
|
|||||||
if (newConf.loading) {
|
if (newConf.loading) {
|
||||||
SingleLoading.startLoading();
|
SingleLoading.startLoading();
|
||||||
}
|
}
|
||||||
const { url } = newConf;
|
const url = newConf && newConf.url ? newConf.url : '';
|
||||||
|
if (!url) {
|
||||||
|
return newConf;
|
||||||
|
}
|
||||||
if (newConf.method === 'get' && newConf.params) {
|
if (newConf.method === 'get' && newConf.params) {
|
||||||
// 先加时间戳(如果 URL 还没参数)
|
// 先加时间戳(如果 URL 还没参数)
|
||||||
if (url.indexOf('?') === -1) {
|
if (url.indexOf('?') === -1) {
|
||||||
@ -112,15 +115,21 @@ request.interceptors.response.use(
|
|||||||
// 获取错误信息
|
// 获取错误信息
|
||||||
const msg = res.data.msg || '系统未知错误,请反馈给管理员';
|
const msg = res.data.msg || '系统未知错误,请反馈给管理员';
|
||||||
if (code === 401) {
|
if (code === 401) {
|
||||||
showLoginGuide({ actionText: '当前操作' });
|
// 调用方显式静默 401(如只读列表的公开接口):不弹登录提示,
|
||||||
|
// 让业务层自行 fallback / 提示 / 跳转
|
||||||
|
const silent = res.config?.__silent401 || res.reqConfig?.__silent401;
|
||||||
|
if (!silent) {
|
||||||
|
showLoginGuide({ actionText: '当前操作' });
|
||||||
|
}
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
__authRequired: true,
|
__authRequired: true,
|
||||||
code: 401,
|
code: 401,
|
||||||
msg,
|
msg,
|
||||||
response: res,
|
response: res,
|
||||||
|
__silent401: !!silent,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (code !== 1) {
|
if (code !== 200 && code !== 1) {
|
||||||
MessagePlugin.error({
|
MessagePlugin.error({
|
||||||
content: msg,
|
content: msg,
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
@ -140,7 +149,12 @@ request.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
// HTTP 状态码 401 未认证,跳转登录页
|
// HTTP 状态码 401 未认证,跳转登录页
|
||||||
if (err.response?.status === 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({
|
return Promise.reject({
|
||||||
...err,
|
...err,
|
||||||
__authRequired: true,
|
__authRequired: true,
|
||||||
@ -240,11 +254,16 @@ request.interceptors.response.use(
|
|||||||
if (err.reqConfig?.loading || err.config?.loading || SingleLoading.load !== null) {
|
if (err.reqConfig?.loading || err.config?.loading || SingleLoading.load !== null) {
|
||||||
SingleLoading.endLoading(true);
|
SingleLoading.endLoading(true);
|
||||||
}
|
}
|
||||||
showLoginGuide({ actionText: '当前操作' });
|
// 调用方显式静默 401(如只读列表的公开接口):不弹登录提示
|
||||||
|
const silent = err.config?.__silent401 || err.reqConfig?.__silent401;
|
||||||
|
if (!silent) {
|
||||||
|
showLoginGuide({ actionText: '当前操作' });
|
||||||
|
}
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
...err,
|
...err,
|
||||||
__authRequired: true,
|
__authRequired: true,
|
||||||
code: 401,
|
code: 401,
|
||||||
|
__silent401: !!silent,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
|
|||||||
@ -3,12 +3,13 @@ import { fetchSso } from '@/core/request';
|
|||||||
const basurl = '/mhzc/gxnl';
|
const basurl = '/mhzc/gxnl';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
/** 共性能力网站列表 */
|
/** 共性能力网站列表(公开浏览接口,未登录时静默 401,由 gxnlpt fallback 到 demo) */
|
||||||
wzxxList(params = {}) {
|
wzxxList(params = {}) {
|
||||||
return fetchSso({
|
return fetchSso({
|
||||||
url: `${basurl}/wzxx/list`,
|
url: `${basurl}/wzxx/list`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: JSON.stringify(params),
|
data: JSON.stringify(params),
|
||||||
|
__silent401: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
20
txw-mhzc-web/src/pages/index/api/mhzc-user/index.js
Normal file
20
txw-mhzc-web/src/pages/index/api/mhzc-user/index.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { fetchSso } from '@/core/request';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 门户用户相关 API
|
||||||
|
*/
|
||||||
|
export const getProfile = () => {
|
||||||
|
return fetchSso({ url: '/mhzc/user/profile', method: 'GET' });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateProfile = (data) => {
|
||||||
|
return fetchSso({
|
||||||
|
url: '/mhzc/user/profile',
|
||||||
|
method: 'PUT',
|
||||||
|
data: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSsoBind = () => {
|
||||||
|
return fetchSso({ url: '/mhzc/user/sso-bind', method: 'GET' });
|
||||||
|
};
|
||||||
@ -1,9 +1,11 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import VueRouter from 'vue-router';
|
import VueRouter from 'vue-router';
|
||||||
import { hasLogin } from '@/pages/index/api/login';
|
import { hasLogin } from '@/pages/index/api/login';
|
||||||
|
import { getProfile } from '@/pages/index/api/mhzc-user';
|
||||||
import routes from './routes';
|
import routes from './routes';
|
||||||
|
|
||||||
import Main from '@/pages/index/views/main.vue'
|
import Main from '@/pages/index/views/main.vue'
|
||||||
|
|
||||||
|
let profileLoaded = false;
|
||||||
// import Home2 from '@/pages/index/views/home2/index.vue'
|
// import Home2 from '@/pages/index/views/home2/index.vue'
|
||||||
// import login from '@/pages/index/views/login/login.vue'
|
// import login from '@/pages/index/views/login/login.vue'
|
||||||
// import yhzx from '@/pages/index/views/glxtSy/glxtSy.vue'
|
// import yhzx from '@/pages/index/views/glxtSy/glxtSy.vue'
|
||||||
@ -63,12 +65,41 @@ const router = new VueRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
|
// 路由切换时清掉全局 figma 视觉高度 clamp 变量,
|
||||||
|
// 避免旧页面残留的 --home-figma-visual-height 把新页面裁切掉。
|
||||||
|
// 新页面的 portal-figma-scale-mixin 会在 mounted/activated 时基于真实高度重新写入。
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.style.removeProperty('--home-figma-visual-height');
|
||||||
|
root.style.removeProperty('--portal-shell-min-design-height');
|
||||||
|
root.style.removeProperty('--portal-market-shell-min-height');
|
||||||
|
}
|
||||||
// 检查是否需要登录且未登录
|
// 检查是否需要登录且未登录
|
||||||
if (to.meta.needLogin && !hasLogin()) {
|
if (to.meta.needLogin && !hasLogin()) {
|
||||||
// 使用路由跳转至登录页面
|
// 使用路由跳转至登录页面
|
||||||
next('/login');
|
next('/login');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 已登录时,懒加载门户用户信息。401 走清理 token + 跳登录
|
||||||
|
if (hasLogin() && to.path !== '/login' && !profileLoaded) {
|
||||||
|
getProfile()
|
||||||
|
.then((res) => {
|
||||||
|
if (Number(res?.code) === 401) {
|
||||||
|
window.sessionStorage.removeItem('sfdl');
|
||||||
|
profileLoaded = false;
|
||||||
|
next('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
profileLoaded = true;
|
||||||
|
next();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 网络异常不阻塞路由
|
||||||
|
profileLoaded = true;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 允许路由继续跳转
|
// 允许路由继续跳转
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,24 +1,24 @@
|
|||||||
function login() {
|
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() {
|
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() {
|
// 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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
function qych() {
|
||||||
@ -34,22 +34,22 @@ function hydt() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function gxnlpt() {
|
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() {
|
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() {
|
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() {
|
function jrsc() {
|
||||||
return import(/* webpackChunkName: "sbfdemo" */ '@/pages/index/views/fwsc/jrsc.vue');
|
return import(/* webpackChunkName: "page-tjrsc" */ '@/pages/index/views/fwsc/jrsc.vue');
|
||||||
}
|
}
|
||||||
|
|
||||||
//碳需求市场
|
//碳需求市场
|
||||||
|
|||||||
@ -965,6 +965,7 @@ html.portal-figma-scale-active.portal-market-figma-scale-active {
|
|||||||
|
|
||||||
.portal-market-figma-page .fwsc-main,
|
.portal-market-figma-page .fwsc-main,
|
||||||
.portal-market-figma-page .xqsc-main,
|
.portal-market-figma-page .xqsc-main,
|
||||||
|
.portal-market-figma-page .sjsc-main,
|
||||||
.portal-market-figma-page .jrsc-main,
|
.portal-market-figma-page .jrsc-main,
|
||||||
.portal-market-figma-page .page-body,
|
.portal-market-figma-page .page-body,
|
||||||
.portal-market-figma-page .content-wrapper {
|
.portal-market-figma-page .content-wrapper {
|
||||||
|
|||||||
13
txw-mhzc-web/src/pages/index/utils/coming-soon-mixin.js
Normal file
13
txw-mhzc-web/src/pages/index/utils/coming-soon-mixin.js
Normal 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
|
||||||
|
}
|
||||||
|
};
|
||||||
75
txw-mhzc-web/src/pages/index/utils/fullpage-scroll-mixin.js
Normal file
75
txw-mhzc-web/src/pages/index/utils/fullpage-scroll-mixin.js
Normal 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();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
290
txw-mhzc-web/src/pages/index/utils/fullpage-scroll.js
Normal file
290
txw-mhzc-web/src/pages/index/utils/fullpage-scroll.js
Normal 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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,24 +1,96 @@
|
|||||||
import { syncPortalFigmaStageLayout } from './portal-figma-scale';
|
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 {
|
export default {
|
||||||
mounted() {
|
mounted() {
|
||||||
this._onPortalFigmaResize = () => {
|
this._setupPortalFigmaScaleObserver();
|
||||||
window.requestAnimationFrame(() => {
|
},
|
||||||
window.requestAnimationFrame(() => this.syncPortalFigmaStageLayout());
|
activated() {
|
||||||
});
|
// keep-alive 场景下,组件从缓存中恢复时不会触发 mounted,
|
||||||
};
|
// 必须重新挂载观察者并立即算一次。
|
||||||
window.addEventListener('resize', this._onPortalFigmaResize);
|
this._setupPortalFigmaScaleObserver();
|
||||||
this.$nextTick(() => this.syncPortalFigmaStageLayout());
|
},
|
||||||
|
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() {
|
beforeDestroy() {
|
||||||
if (this._onPortalFigmaResize) {
|
this._teardownPortalFigmaScaleObserver();
|
||||||
window.removeEventListener('resize', this._onPortalFigmaResize);
|
},
|
||||||
this._onPortalFigmaResize = null;
|
deactivated() {
|
||||||
}
|
this._teardownPortalFigmaScaleObserver();
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
syncPortalFigmaStageLayout() {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
if (window.innerWidth < 768) return;
|
if (window.innerWidth < 768) return;
|
||||||
const stage = this.$refs.figmaStage;
|
const stage = this.$refs.figmaStage;
|
||||||
if (!stage) return;
|
if (!stage) return;
|
||||||
|
|||||||
@ -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() {
|
export function shouldUseSectionWheelScroll() {
|
||||||
if (typeof window === 'undefined') return true;
|
if (typeof window === 'undefined') return true;
|
||||||
@ -7,38 +24,19 @@ export function shouldUseSectionWheelScroll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按视口动态绑定/解绑 wheel 分屏逻辑
|
* @deprecated 老接口:不再劫持 wheel 事件。新逻辑请直接用 scrollIntoView + scrollend。
|
||||||
* @param {HTMLElement} root
|
* 此函数保留仅为不破坏外部 import,运行时不绑定任何监听。
|
||||||
* @param {(e: WheelEvent) => void} handler
|
|
||||||
* @returns {() => void} cleanup
|
|
||||||
*/
|
*/
|
||||||
export function bindSectionWheelScroll(root, handler) {
|
export function bindSectionWheelScroll(root, handler) {
|
||||||
if (!root || !handler) return () => {};
|
// 静默 no-op,提示调用方迁移
|
||||||
|
if (typeof console !== 'undefined') {
|
||||||
const mq = window.matchMedia('(max-width: 767px), (pointer: coarse)');
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn(
|
||||||
const sync = () => {
|
'[portal-scroll-mode] bindSectionWheelScroll 已废弃:不再劫持 wheel 事件。'
|
||||||
root.removeEventListener('wheel', handler);
|
+ ' 请改用 scrollIntoView({ behavior: "smooth" }) + scrollend 判定。',
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
return () => {};
|
||||||
return () => {
|
|
||||||
root.removeEventListener('wheel', handler);
|
|
||||||
if (typeof mq.removeEventListener === 'function') {
|
|
||||||
mq.removeEventListener('change', sync);
|
|
||||||
} else if (typeof mq.removeListener === 'function') {
|
|
||||||
mq.removeListener(sync);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,3 +54,69 @@ export function scrollPortalContentToTop(options = {}) {
|
|||||||
}
|
}
|
||||||
window.scrollTo({ top: 0, behavior });
|
window.scrollTo({ top: 0, behavior });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promise 化的 scrollend —— 等到滚动真正结束再 resolve。
|
||||||
|
* 兼容不支持 scrollend 的浏览器(Safari < 15.4),fallback 到 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';
|
||||||
|
// 处理 offset:scrollIntoView 不支持 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);
|
||||||
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
@ -183,10 +183,11 @@ import api from '@/pages/index/api/fwsc/index.js';
|
|||||||
import { hasLogin } from '@/pages/index/api/login';
|
import { hasLogin } from '@/pages/index/api/login';
|
||||||
import { scrollPortalContentToTop } from '@/pages/index/utils/portal-scroll-mode';
|
import { scrollPortalContentToTop } from '@/pages/index/utils/portal-scroll-mode';
|
||||||
import portalFigmaScaleMixin from '@/pages/index/utils/portal-figma-scale-mixin';
|
import portalFigmaScaleMixin from '@/pages/index/utils/portal-figma-scale-mixin';
|
||||||
|
import comingSoonMixin from '@/pages/index/utils/coming-soon-mixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FwscPage',
|
name: 'FwscPage',
|
||||||
mixins: [portalFigmaScaleMixin],
|
mixins: [portalFigmaScaleMixin, comingSoonMixin],
|
||||||
components: {
|
components: {
|
||||||
NewNav,
|
NewNav,
|
||||||
BreadcrumbNav,
|
BreadcrumbNav,
|
||||||
@ -453,7 +454,7 @@ export default {
|
|||||||
},
|
},
|
||||||
goToTab({path, disable}) {
|
goToTab({path, disable}) {
|
||||||
if (disable) {
|
if (disable) {
|
||||||
this.$message.info('敬请期待');
|
this.showComingSoon();
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$router.push(path);
|
this.$router.push(path);
|
||||||
|
|||||||
@ -98,10 +98,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import portalFigmaScaleMixin from '@/pages/index/utils/portal-figma-scale-mixin';
|
import portalFigmaScaleMixin from '@/pages/index/utils/portal-figma-scale-mixin';
|
||||||
|
import comingSoonMixin from '@/pages/index/utils/coming-soon-mixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FwscIndex',
|
name: 'FwscIndex',
|
||||||
mixins: [portalFigmaScaleMixin],
|
mixins: [portalFigmaScaleMixin, comingSoonMixin],
|
||||||
methods: {
|
methods: {
|
||||||
goHome() {
|
goHome() {
|
||||||
this.$router.push('/view/mhzc/home');
|
this.$router.push('/view/mhzc/home');
|
||||||
@ -110,7 +111,7 @@ export default {
|
|||||||
if (path) {
|
if (path) {
|
||||||
this.$router.push(path);
|
this.$router.push(path);
|
||||||
} else {
|
} else {
|
||||||
this.$message.info('敬请期待');
|
this.showComingSoon();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -125,6 +126,18 @@ export default {
|
|||||||
.portal-figma-scale-page();
|
.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 区 — 背景限定在 350px 标题区内
|
||||||
.banner-section {
|
.banner-section {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -212,10 +212,11 @@ import XqscPublish from './components/XqscPublish.vue';
|
|||||||
import api from '@/pages/index/api/fwsc/index.js';
|
import api from '@/pages/index/api/fwsc/index.js';
|
||||||
import { scrollPortalContentToTop } from '@/pages/index/utils/portal-scroll-mode';
|
import { scrollPortalContentToTop } from '@/pages/index/utils/portal-scroll-mode';
|
||||||
import portalFigmaScaleMixin from '@/pages/index/utils/portal-figma-scale-mixin';
|
import portalFigmaScaleMixin from '@/pages/index/utils/portal-figma-scale-mixin';
|
||||||
|
import comingSoonMixin from '@/pages/index/utils/coming-soon-mixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'XqscPage',
|
name: 'XqscPage',
|
||||||
mixins: [portalFigmaScaleMixin],
|
mixins: [portalFigmaScaleMixin, comingSoonMixin],
|
||||||
components: {
|
components: {
|
||||||
NewNav,
|
NewNav,
|
||||||
BreadcrumbNav,
|
BreadcrumbNav,
|
||||||
@ -419,7 +420,7 @@ export default {
|
|||||||
},
|
},
|
||||||
goToTab({path, disable}) {
|
goToTab({path, disable}) {
|
||||||
if (disable) {
|
if (disable) {
|
||||||
this.$message.info('敬请期待');
|
this.showComingSoon();
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$router.push(path);
|
this.$router.push(path);
|
||||||
|
|||||||
@ -147,7 +147,8 @@
|
|||||||
<span class="gxnlpt-favorites-star" aria-hidden="true"></span>
|
<span class="gxnlpt-favorites-star" aria-hidden="true"></span>
|
||||||
<h2 class="gxnlpt-block-title">我的收藏</h2>
|
<h2 class="gxnlpt-block-title">我的收藏</h2>
|
||||||
</header>
|
</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">
|
<div v-else class="gxnlpt-card-grid">
|
||||||
<article
|
<article
|
||||||
v-for="card in favoriteCards"
|
v-for="card in favoriteCards"
|
||||||
@ -239,7 +240,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import GxnlptCardTags from '@/pages/index/views/gxnlpt/components/GxnlptCardTags.vue';
|
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 {
|
import {
|
||||||
isPortalLoggedIn,
|
isPortalLoggedIn,
|
||||||
isUnauthorizedError,
|
isUnauthorizedError,
|
||||||
@ -247,6 +248,7 @@ import {
|
|||||||
} from '@/pages/index/utils/auth-guard';
|
} from '@/pages/index/utils/auth-guard';
|
||||||
import { BP } from '@/pages/index/utils/breakpoint.js';
|
import { BP } from '@/pages/index/utils/breakpoint.js';
|
||||||
import portalFigmaScaleMixin from '@/pages/index/utils/portal-figma-scale-mixin';
|
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 starOutline = require('@/pages/index/assets/fwsc/wsc.svg');
|
||||||
const starFilled = require('@/pages/index/assets/fwsc/ysc.svg');
|
const starFilled = require('@/pages/index/assets/fwsc/ysc.svg');
|
||||||
@ -531,11 +533,11 @@ const SIDE_ICON_WHITE = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CATEGORY_META = [
|
const CATEGORY_META = [
|
||||||
{ id: 'content-1', title: '碳核算平台', icon: 'thspt.svg', keywords: ['核算', '足迹', 'LCA', 'CBAM', '碳管理', '碳盘查', '标准化'] },
|
{ id: 'content-1', title: '碳核算平台', flDm: '01', icon: 'thspt.svg', keywords: ['核算', '足迹', 'LCA', 'CBAM', '碳管理', '碳盘查', '标准化'] },
|
||||||
{ id: 'content-2', title: '碳认证机构', icon: 'trzjg.svg', keywords: ['认证', '核查', '审定', '验证'] },
|
{ id: 'content-2', title: '碳认证机构', flDm: '02', icon: 'trzjg.svg', keywords: ['认证', '核查', '审定', '验证'] },
|
||||||
{ id: 'content-3', title: '碳交易平台', icon: 'tjypt.svg', keywords: ['交易', '交易所', '配额', 'CCER'] },
|
{ id: 'content-3', title: '碳交易平台', flDm: '03', icon: 'tjypt.svg', keywords: ['交易', '交易所', '配额', 'CCER'] },
|
||||||
{ id: 'content-4', title: '碳金融服务', icon: 'tjrfw.svg', keywords: ['金融', '融资', '信贷', '保险', '基金', '资产'] },
|
{ id: 'content-4', title: '碳金融服务', flDm: '04', icon: 'tjrfw.svg', keywords: ['金融', '融资', '信贷', '保险', '基金', '资产'] },
|
||||||
{ id: 'content-5', title: '碳技术咨询', icon: 'tjszx.svg', keywords: ['咨询', '技术', '研究', '规划', '方案'] },
|
{ id: 'content-5', title: '碳技术咨询', flDm: '05', icon: 'tjszx.svg', keywords: ['咨询', '技术', '研究', '规划', '方案'] },
|
||||||
];
|
];
|
||||||
|
|
||||||
function buildCategoryList() {
|
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 = () => ({
|
const SUBMIT_FORM_EMPTY = () => ({
|
||||||
bt1: '',
|
bt1: '',
|
||||||
lj: '',
|
lj: '',
|
||||||
@ -559,7 +573,7 @@ const SUBMIT_FORM_EMPTY = () => ({
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'GxnlptIndex',
|
name: 'GxnlptIndex',
|
||||||
mixins: [portalFigmaScaleMixin],
|
mixins: [portalFigmaScaleMixin, comingSoonMixin],
|
||||||
components: { GxnlptCardTags },
|
components: { GxnlptCardTags },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -578,6 +592,8 @@ export default {
|
|||||||
submitAttempted: false,
|
submitAttempted: false,
|
||||||
stackedNavLayout: readStackedNavLayout(),
|
stackedNavLayout: readStackedNavLayout(),
|
||||||
_stackedNavMq: null,
|
_stackedNavMq: null,
|
||||||
|
favoriteList: [],
|
||||||
|
favoritesLoading: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -593,6 +609,9 @@ export default {
|
|||||||
return this.categoryList.map((item, index) => ({ item, index }));
|
return this.categoryList.map((item, index) => ({ item, index }));
|
||||||
},
|
},
|
||||||
favoriteCards() {
|
favoriteCards() {
|
||||||
|
if (this.favoriteList && this.favoriteList.length) {
|
||||||
|
return this.favoriteList;
|
||||||
|
}
|
||||||
const list = [];
|
const list = [];
|
||||||
this.categoryList.forEach((cat) => {
|
this.categoryList.forEach((cat) => {
|
||||||
cat.cardList.forEach((card) => {
|
cat.cardList.forEach((card) => {
|
||||||
@ -610,17 +629,22 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.bindStackedNavMedia();
|
this.bindStackedNavMedia();
|
||||||
this.bootstrap();
|
this.bootstrap();
|
||||||
|
this.$nextTick(() => this.initSidebarSticky());
|
||||||
},
|
},
|
||||||
activated() {
|
activated() {
|
||||||
this.syncStackedNavLayout();
|
this.syncStackedNavLayout();
|
||||||
if (this.contentView === 'list' && !this.stackedNavLayout) {
|
if (this.contentView === 'list' && !this.stackedNavLayout) {
|
||||||
this.$nextTick(() => this.initScrollSpy());
|
this.$nextTick(() => {
|
||||||
|
this.initScrollSpy();
|
||||||
|
this.initSidebarSticky();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.unbindStackedNavMedia();
|
this.unbindStackedNavMedia();
|
||||||
this.clearScrollUnlock();
|
this.clearScrollUnlock();
|
||||||
this.destroyScrollSpy();
|
this.destroyScrollSpy();
|
||||||
|
this.destroySidebarSticky();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
/** 收录 / 我的收藏 / 收藏操作前校验登录(弹窗引导,不直接跳登录页) */
|
/** 收录 / 我的收藏 / 收藏操作前校验登录(弹窗引导,不直接跳登录页) */
|
||||||
@ -647,11 +671,13 @@ export default {
|
|||||||
this.activeTabIndex = tabIndex;
|
this.activeTabIndex = tabIndex;
|
||||||
} else {
|
} else {
|
||||||
this.initScrollSpy();
|
this.initScrollSpy();
|
||||||
|
this.initSidebarSticky();
|
||||||
this.scrollToSection(anchor, tabIndex);
|
this.scrollToSection(anchor, tabIndex);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.initScrollSpy();
|
this.initScrollSpy();
|
||||||
|
this.initSidebarSticky();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getIconUrl(iconName) {
|
getIconUrl(iconName) {
|
||||||
@ -777,6 +803,8 @@ export default {
|
|||||||
/** 解析 fwlxjh / bqjh(含 JSON)并生成 tagList */
|
/** 解析 fwlxjh / bqjh(含 JSON)并生成 tagList */
|
||||||
normalizeRecord(item) {
|
normalizeRecord(item) {
|
||||||
const record = { ...item };
|
const record = { ...item };
|
||||||
|
record.gxUuid = record.wzUuid;
|
||||||
|
record.fwnr = record.jj;
|
||||||
const typeTags = this.splitTagText(record.fwlxjh);
|
const typeTags = this.splitTagText(record.fwlxjh);
|
||||||
const labelTags = this.splitTagText(record.bqjh);
|
const labelTags = this.splitTagText(record.bqjh);
|
||||||
record.fwlxbqList = [...new Set([...typeTags, ...labelTags])];
|
record.fwlxbqList = [...new Set([...typeTags, ...labelTags])];
|
||||||
@ -800,7 +828,12 @@ export default {
|
|||||||
card.fwlxbqList = inferred;
|
card.fwlxbqList = inferred;
|
||||||
},
|
},
|
||||||
matchCategory(record, category) {
|
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));
|
return category.keywords.some((kw) => text.includes(kw));
|
||||||
},
|
},
|
||||||
assignCategoryCards(allRecords) {
|
assignCategoryCards(allRecords) {
|
||||||
@ -855,12 +888,9 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await api.gxxxList({
|
const res = await api.wzxxList({
|
||||||
ywlxDm: '01',
|
|
||||||
pageNo: 1,
|
pageNo: 1,
|
||||||
pageSize: 200,
|
pageSize: 200,
|
||||||
scbz: 'N',
|
|
||||||
nr: '',
|
|
||||||
});
|
});
|
||||||
const data = res && res.data;
|
const data = res && res.data;
|
||||||
const records = ((data && data.records) || []).map((r) => this.normalizeRecord(r));
|
const records = ((data && data.records) || []).map((r) => this.normalizeRecord(r));
|
||||||
@ -881,6 +911,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
getScrollRoot() {
|
getScrollRoot() {
|
||||||
|
// 门户落地页共用滚动容器为 .content-wrap(main.vue 唯一滚动区),
|
||||||
|
// 必须优先锁定它,避免被内部 .gxnlpt-side-nav-wrap 等 overflow-y:auto 抢走
|
||||||
|
const portalRoot = document.querySelector('.content-wrap');
|
||||||
|
if (portalRoot) return portalRoot;
|
||||||
let el = this.$el.parentElement;
|
let el = this.$el.parentElement;
|
||||||
while (el) {
|
while (el) {
|
||||||
const style = window.getComputedStyle(el);
|
const style = window.getComputedStyle(el);
|
||||||
@ -947,6 +981,7 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.initScrollSpy();
|
this.initScrollSpy();
|
||||||
|
this.initSidebarSticky();
|
||||||
this.scrollToSection(sectionId, tabIndex);
|
this.scrollToSection(sectionId, tabIndex);
|
||||||
},
|
},
|
||||||
openSubmitView() {
|
openSubmitView() {
|
||||||
@ -954,6 +989,7 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.destroyScrollSpy();
|
this.destroyScrollSpy();
|
||||||
|
this.destroySidebarSticky();
|
||||||
this.contentView = 'submit';
|
this.contentView = 'submit';
|
||||||
this.resetSubmitForm();
|
this.resetSubmitForm();
|
||||||
},
|
},
|
||||||
@ -962,7 +998,29 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.destroyScrollSpy();
|
this.destroyScrollSpy();
|
||||||
|
this.destroySidebarSticky();
|
||||||
this.contentView = 'favorites';
|
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() {
|
resetSubmitForm() {
|
||||||
this.submitForm = SUBMIT_FORM_EMPTY();
|
this.submitForm = SUBMIT_FORM_EMPTY();
|
||||||
@ -1023,10 +1081,11 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await api.gxfb({
|
await api.submitSlxx({
|
||||||
ywlxDm: '01',
|
bt: this.submitForm.bt1,
|
||||||
bt1: this.submitForm.bt1,
|
wzLj: this.submitForm.lj,
|
||||||
fwnr: this.submitForm.jj,
|
jj: this.submitForm.jj,
|
||||||
|
gxnlFlDm: flTitleToDm(this.submitForm.fl),
|
||||||
bqjh: this.submitForm.bq,
|
bqjh: this.submitForm.bq,
|
||||||
});
|
});
|
||||||
this.$message.success('提交成功,请等待审核');
|
this.$message.success('提交成功,请等待审核');
|
||||||
@ -1041,70 +1100,310 @@ export default {
|
|||||||
this.$message.warning('提交失败,请稍后重试');
|
this.$message.warning('提交失败,请稍后重试');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 平滑滚动到指定分类区块。
|
||||||
|
* 通过 getBoundingClientRect 获取目标元素在视口内的视觉位置,
|
||||||
|
* 反算至滚动容器布局坐标后 scrollTo,不受 scale 变换影响。
|
||||||
|
*/
|
||||||
scrollToSection(sectionId, tabIndex) {
|
scrollToSection(sectionId, tabIndex) {
|
||||||
const el = document.getElementById(sectionId);
|
const el = document.getElementById(sectionId);
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
const scrollRoot = this.getScrollRoot();
|
const scrollRoot = this.getScrollRoot();
|
||||||
const offsetTop = 24;
|
if (!el || !scrollRoot) return;
|
||||||
|
|
||||||
if (scrollRoot) {
|
const scale = this._readScale();
|
||||||
const rootRect = scrollRoot.getBoundingClientRect();
|
if (scale <= 0) return;
|
||||||
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 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);
|
this.scheduleScrollUnlock(tabIndex);
|
||||||
},
|
},
|
||||||
destroyScrollSpy() {
|
/* ========== IntersectionObserver 驱动的 ScrollSpy ========== */
|
||||||
if (this.scrollRootEl && this._onScrollSpy) {
|
/**
|
||||||
this.scrollRootEl.removeEventListener('scroll', this._onScrollSpy);
|
* 用 IntersectionObserver 监听每个分类区块与滚动容器顶部的交叉比例,
|
||||||
}
|
* 选择最接近顶部且露出面积最大的区块为当前激活项。
|
||||||
this.scrollRootEl = null;
|
* 相比手写 scroll 事件更精准、更省性能,且不受 scale 变换影响。
|
||||||
this._onScrollSpy = null;
|
*/
|
||||||
if (this._scrollSpyRaf) {
|
|
||||||
cancelAnimationFrame(this._scrollSpyRaf);
|
|
||||||
this._scrollSpyRaf = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
initScrollSpy() {
|
initScrollSpy() {
|
||||||
this.destroyScrollSpy();
|
this.destroyScrollSpy();
|
||||||
if (this.isStackedNavMode || this.contentView !== 'list') return;
|
if (this.isStackedNavMode || this.contentView !== 'list') return;
|
||||||
|
|
||||||
const scrollRoot = this.getScrollRoot();
|
const scrollRoot = this.getScrollRoot();
|
||||||
if (!scrollRoot) return;
|
if (!scrollRoot) return;
|
||||||
this.scrollRootEl = scrollRoot;
|
this.scrollRootEl = scrollRoot;
|
||||||
const anchorOffset = 96;
|
|
||||||
|
|
||||||
this._onScrollSpy = () => {
|
// 区块交叉记录:{ index, ratio }
|
||||||
if (this.suppressSectionObserver) return;
|
this._spyEntries = [];
|
||||||
if (this._scrollSpyRaf) return;
|
this._spyRootRect = null;
|
||||||
this._scrollSpyRaf = requestAnimationFrame(() => {
|
|
||||||
this._scrollSpyRaf = null;
|
const obsOptions = {
|
||||||
const line = scrollRoot.getBoundingClientRect().top + anchorOffset;
|
root: scrollRoot,
|
||||||
let nextIndex = 0;
|
// rootMargin 在顶部留出 nav(64dp)+gap(24dp)=88dp 的缓冲区,
|
||||||
this.categoryList.forEach((item, index) => {
|
// 让区块顶部触及该线时才算"进入视口"。
|
||||||
const el = document.getElementById(item.id);
|
rootMargin: `-88px 0px 0px 0px`,
|
||||||
if (!el) return;
|
threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
|
||||||
if (el.getBoundingClientRect().top <= line) {
|
};
|
||||||
nextIndex = index;
|
|
||||||
}
|
this._spyObserver = new IntersectionObserver((entries) => {
|
||||||
});
|
const rootRect = scrollRoot.getBoundingClientRect();
|
||||||
if (this.activeTabIndex !== nextIndex) {
|
this._spyRootRect = rootRect;
|
||||||
this.activeTabIndex = nextIndex;
|
|
||||||
|
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 });
|
scrollRoot.addEventListener('scroll', handler, { passive: true });
|
||||||
this._onScrollSpy();
|
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) {
|
handleCardClick(card) {
|
||||||
if (this.useDemoData || !card.gxUuid) {
|
if (this.useDemoData || !card.gxUuid) {
|
||||||
this.$message.info('敬请期待');
|
this.showComingSoon();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.$router.push({ path: '/tfwsc', query: { id: card.gxUuid } });
|
this.$router.push({ path: '/tfwsc', query: { id: card.gxUuid } });
|
||||||
@ -1120,8 +1419,10 @@ export default {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const type = card.scbz === 'Y' ? 'remove' : 'add';
|
const type = card.scbz === 'Y' ? 'remove' : 'add';
|
||||||
await api.gxsc({ gxUuid: card.gxUuid, type });
|
await api.toggleGxsc({ wzUuid: card.gxUuid, type });
|
||||||
card.scbz = card.scbz === 'Y' ? 'N' : 'Y';
|
const next = card.scbz === 'Y' ? 'N' : 'Y';
|
||||||
|
card.scbz = next;
|
||||||
|
this.syncFavoriteListAfterToggle(card, next);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (isUnauthorizedError(e)) {
|
if (isUnauthorizedError(e)) {
|
||||||
showLoginGuide({ actionText: '收藏' });
|
showLoginGuide({ actionText: '收藏' });
|
||||||
@ -1130,6 +1431,21 @@ export default {
|
|||||||
this.$message.warning('收藏操作失败');
|
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>
|
</script>
|
||||||
@ -1151,6 +1467,39 @@ export default {
|
|||||||
.portal-figma-scale-page();
|
.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 {
|
.gxnlpt-shell.page-content-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
@ -1171,13 +1520,15 @@ export default {
|
|||||||
@gxnlpt-side-actions-block-h: 114px;
|
@gxnlpt-side-actions-block-h: 114px;
|
||||||
@gxnlpt-side-actions-gap-top: 20px;
|
@gxnlpt-side-actions-gap-top: 20px;
|
||||||
|
|
||||||
/* Figma Frame_left 602px:白卡片与右侧同高向下铺满 */
|
/* Figma Frame_left 602px:白卡片,高度拉伸至与右侧内容列同高,
|
||||||
|
为 sticky 侧边导航提供足够的垂直滚动空间,确保导航始终可见。
|
||||||
|
空白区域由 gxnlpt-sidebar-tail 填充,符合设计稿"白底向下铺满"的意图。 */
|
||||||
.gxnlpt-sidebar {
|
.gxnlpt-sidebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
width: @gxnlpt-sidebar-width;
|
width: @gxnlpt-sidebar-width;
|
||||||
min-height: @gxnlpt-content-min-height;
|
min-height: 0;
|
||||||
padding: @gxnlpt-sidebar-padding;
|
padding: @gxnlpt-sidebar-padding;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: @gxnlpt-sidebar-radius;
|
border-radius: @gxnlpt-sidebar-radius;
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<!-- 主页面 -->
|
<!-- 主页面 -->
|
||||||
<div class="container">
|
<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"
|
<t-swiper class="top-banner-swiper" animation="fade" :height="topBannerHeight" :interval="10000" :duration="500"
|
||||||
:loop="true" :autoplay="true" theme="dark" :navigation="{ showSlideBtn: 'never' }">
|
:loop="true" :autoplay="true" theme="dark" :navigation="{ showSlideBtn: 'never' }">
|
||||||
<t-swiper-item v-for="(video, idx) in topBannerVideos" :key="idx">
|
<t-swiper-item v-for="(video, idx) in topBannerVideos" :key="idx">
|
||||||
@ -92,7 +92,7 @@
|
|||||||
</div>
|
</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="home-shelf home-shelf--core">
|
||||||
<div class="section-title core-title">
|
<div class="section-title core-title">
|
||||||
<div class="home-block-title-content">
|
<div class="home-block-title-content">
|
||||||
@ -130,7 +130,7 @@
|
|||||||
</section>
|
</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="home-shelf home-shelf--capability">
|
||||||
<div class="capability-header">
|
<div class="capability-header">
|
||||||
<div class="capability-title-group">
|
<div class="capability-title-group">
|
||||||
@ -156,7 +156,7 @@
|
|||||||
</section>
|
</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="home-shelf home-shelf--overseas">
|
||||||
<div class="overseas2-header">
|
<div class="overseas2-header">
|
||||||
<div class="overseas2-title-group">
|
<div class="overseas2-title-group">
|
||||||
@ -195,7 +195,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 行业动态(布局与 overseas2 同构:section-container + header + grid) -->
|
<!-- 行业动态(布局与 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="home-shelf home-shelf--news">
|
||||||
<div class="news-header">
|
<div class="news-header">
|
||||||
<div class="news-title-group">
|
<div class="news-title-group">
|
||||||
@ -262,7 +262,7 @@
|
|||||||
</section>
|
</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="home-shelf home-shelf--partner">
|
||||||
<div class="partner-title-area">
|
<div class="partner-title-area">
|
||||||
<div class="partner-title-content">
|
<div class="partner-title-content">
|
||||||
@ -309,7 +309,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- CTA:独占一屏(Figma 150645:1019) -->
|
<!-- CTA:独占一屏(Figma 150645:1019) -->
|
||||||
<div class="bottom-box" id="section-bottom">
|
<div class="bottom-box snap-section" id="section-bottom">
|
||||||
<img
|
<img
|
||||||
class="bottom-box-bg"
|
class="bottom-box-bg"
|
||||||
:src="homeCtaBg"
|
:src="homeCtaBg"
|
||||||
@ -340,25 +340,27 @@ import Footer from '@/pages/index/components/footer/index.vue';
|
|||||||
import hydtApi from '@/pages/index/api/hydt';
|
import hydtApi from '@/pages/index/api/hydt';
|
||||||
import gxzxApi from '@/pages/index/api/gxzx/index.js';
|
import gxzxApi from '@/pages/index/api/gxzx/index.js';
|
||||||
import searchApi from '@/pages/index/api/search.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 { applyPortalFigmaScaleToRoot } from '@/pages/index/utils/portal-figma-scale';
|
||||||
import { calcHomeFigmaScale } from '@/pages/index/utils/home-figma-scale';
|
import { calcHomeFigmaScale } from '@/pages/index/utils/home-figma-scale';
|
||||||
import portalFigmaScaleMixin from '@/pages/index/utils/portal-figma-scale-mixin';
|
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 {
|
export default {
|
||||||
mixins: [portalFigmaScaleMixin],
|
name: 'Home2Index',
|
||||||
|
mixins: [portalFigmaScaleMixin, fullpageScrollMixin, comingSoonMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
inputValue: '',
|
inputValue: '',
|
||||||
activeTab: 0,
|
activeTab: 0,
|
||||||
topBannerHeight: 686,
|
topBannerHeight: 686,
|
||||||
newsLoading: true,
|
newsLoading: true,
|
||||||
isScrolling: false,
|
|
||||||
scrollThreshold: 1,
|
|
||||||
scrollRoot: null,
|
scrollRoot: null,
|
||||||
unbindSectionWheel: null,
|
|
||||||
sectionOffsets: [],
|
sectionOffsets: [],
|
||||||
sectionIds: ['section-hero', 'section-core', 'section-capability', 'section-overseas', 'section-news', 'section-partner', 'section-bottom'],
|
sectionIds: ['section-hero', 'section-core', 'section-capability', 'section-overseas', 'section-news', 'section-partner', 'section-bottom'],
|
||||||
|
// 当前激活的 section(用于导航高亮),由 IntersectionObserver 驱动
|
||||||
|
activeSectionIndex: 0,
|
||||||
coreSelectedIndex: 3,
|
coreSelectedIndex: 3,
|
||||||
newsListByType: {
|
newsListByType: {
|
||||||
gjzc: [], // 国家政策
|
gjzc: [], // 国家政策
|
||||||
@ -491,6 +493,16 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
Footer,
|
Footer,
|
||||||
},
|
},
|
||||||
|
activated() {
|
||||||
|
// keep-alive 恢复时重新挂 IntersectionObserver 并刷新 section 偏移
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.scrollRoot = document.querySelector('.content-wrap');
|
||||||
|
if (this.scrollRoot) {
|
||||||
|
this.refreshSectionOffsets();
|
||||||
|
this.setupSectionObserver();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'$route.query.section'(section) {
|
'$route.query.section'(section) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@ -526,11 +538,15 @@ export default {
|
|||||||
}
|
}
|
||||||
this.syncBannerHeight();
|
this.syncBannerHeight();
|
||||||
if (this.scrollRoot) {
|
if (this.scrollRoot) {
|
||||||
this.unbindSectionWheel = bindSectionWheelScroll(this.scrollRoot, this.handleWheel);
|
|
||||||
this.refreshSectionOffsets();
|
this.refreshSectionOffsets();
|
||||||
|
// 用 IntersectionObserver 替代原来的 wheel 劫持:
|
||||||
|
// 不再拦截浏览器原生滚动,由浏览器处理滚动手势和惯性,
|
||||||
|
// 我们只负责"哪个 section 当前最接近视口中心"以高亮导航。
|
||||||
|
this.setupSectionObserver();
|
||||||
if (typeof ResizeObserver !== 'undefined') {
|
if (typeof ResizeObserver !== 'undefined') {
|
||||||
this._scrollRootResizeObserver = new ResizeObserver(() => {
|
this._scrollRootResizeObserver = new ResizeObserver(() => {
|
||||||
this.onViewportResize();
|
this.onViewportResize();
|
||||||
|
this.refreshSectionOffsets();
|
||||||
});
|
});
|
||||||
this._scrollRootResizeObserver.observe(this.scrollRoot);
|
this._scrollRootResizeObserver.observe(this.scrollRoot);
|
||||||
}
|
}
|
||||||
@ -550,10 +566,7 @@ export default {
|
|||||||
this._scrollRootResizeObserver.disconnect();
|
this._scrollRootResizeObserver.disconnect();
|
||||||
this._scrollRootResizeObserver = null;
|
this._scrollRootResizeObserver = null;
|
||||||
}
|
}
|
||||||
if (this.unbindSectionWheel) {
|
this.teardownSectionObserver();
|
||||||
this.unbindSectionWheel();
|
|
||||||
this.unbindSectionWheel = null;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
syncNewsTabFromRoute(type) {
|
syncNewsTabFromRoute(type) {
|
||||||
@ -586,6 +599,7 @@ export default {
|
|||||||
return scrollRoot.scrollTop + (rect.top - rootRect.top);
|
return scrollRoot.scrollTop + (rect.top - rootRect.top);
|
||||||
},
|
},
|
||||||
getCurrentSectionIndex(scrollRoot) {
|
getCurrentSectionIndex(scrollRoot) {
|
||||||
|
// 兜底方法:在 IntersectionObserver 还没回报时(如首屏)使用。
|
||||||
if (!this.sectionOffsets.length) {
|
if (!this.sectionOffsets.length) {
|
||||||
this.refreshSectionOffsets();
|
this.refreshSectionOffsets();
|
||||||
}
|
}
|
||||||
@ -599,87 +613,74 @@ export default {
|
|||||||
}
|
}
|
||||||
return currentIndex;
|
return currentIndex;
|
||||||
},
|
},
|
||||||
scrollRootToSection(sectionIndex, smooth = true) {
|
/**
|
||||||
const scrollRoot = this.getScrollRoot();
|
* 滚动到指定 section —— 走浏览器原生 scrollIntoView,不再劫持 wheel 事件。
|
||||||
if (!scrollRoot) return;
|
* 不再使用 setTimeout 锁,由 waitForScrollEnd 在真正结束时通知。
|
||||||
if (!this.sectionOffsets.length) {
|
*/
|
||||||
this.refreshSectionOffsets();
|
async scrollRootToSection(sectionIndex, smooth = true) {
|
||||||
}
|
// 整屏切换交给 fullpageScrollMixin,统一在 fullpage-scroll.js 中处理动画
|
||||||
const top = this.sectionOffsets[sectionIndex] ?? 0;
|
this.fullPageScrollTo(sectionIndex);
|
||||||
scrollRoot.scrollTo({ top, behavior: smooth ? 'smooth' : 'auto' });
|
|
||||||
},
|
},
|
||||||
trySectionPageScroll(sectionEl, scrollRoot, direction, e, edgeGap = 8) {
|
onFullPageSectionChange(newIndex) {
|
||||||
if (!sectionEl || !scrollRoot) return false;
|
this.activeSectionIndex = newIndex;
|
||||||
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;
|
|
||||||
},
|
},
|
||||||
handleWheel(e) {
|
/**
|
||||||
if (this.isScrolling) return;
|
* 替代原 bindSectionWheelScroll:用 IntersectionObserver 监听每个 section
|
||||||
const delta = Math.abs(e.deltaY);
|
* 与视口的相交状态,自动算"当前激活 section",不再干扰原生滚动。
|
||||||
if (delta < this.scrollThreshold) return;
|
*/
|
||||||
|
setupSectionObserver() {
|
||||||
|
if (typeof window === 'undefined' || typeof IntersectionObserver === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.teardownSectionObserver();
|
||||||
|
|
||||||
const scrollRoot = this.getScrollRoot();
|
const scrollRoot = this.getScrollRoot();
|
||||||
if (!scrollRoot) return;
|
if (!scrollRoot) return;
|
||||||
|
|
||||||
const direction = e.deltaY > 0 ? 1 : -1;
|
// rootMargin 让"上 40% / 下 40%"的区域作为命中带,
|
||||||
const currentIndex = this.getCurrentSectionIndex(scrollRoot);
|
// 哪个 section 中心更靠近 viewport 中心,谁就是 active。
|
||||||
const currentEl = document.getElementById(this.sectionIds[currentIndex]);
|
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)) {
|
this.sectionIds.forEach((id) => {
|
||||||
return;
|
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) {
|
openNewTab(url) {
|
||||||
if (url) {
|
if (url) {
|
||||||
window.open(url);
|
window.open(url);
|
||||||
} else {
|
} else {
|
||||||
this.$message.info('敬请期待');
|
this.showComingSoon();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openService() {
|
openService() {
|
||||||
@ -744,7 +745,7 @@ export default {
|
|||||||
// 封装跳转方法 - 外部链接新窗口,内部链接当前窗口
|
// 封装跳转方法 - 外部链接新窗口,内部链接当前窗口
|
||||||
handleNavigate(link) {
|
handleNavigate(link) {
|
||||||
if (!link) {
|
if (!link) {
|
||||||
this.$message.info('敬请期待');
|
this.showComingSoon();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (link.startsWith('http://') || link.startsWith('https://')) {
|
if (link.startsWith('http://') || link.startsWith('https://')) {
|
||||||
@ -756,7 +757,7 @@ export default {
|
|||||||
// iframe页面跳转 - 通过parent事件触发main.vue的iframe显示
|
// iframe页面跳转 - 通过parent事件触发main.vue的iframe显示
|
||||||
handleIframeNavigate(url) {
|
handleIframeNavigate(url) {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
this.$message.info('敬请期待');
|
this.showComingSoon();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const u = `${this.buttonLinkHost}${url}`;
|
const u = `${this.buttonLinkHost}${url}`;
|
||||||
@ -821,25 +822,29 @@ export default {
|
|||||||
},
|
},
|
||||||
async fetchNewsData() {
|
async fetchNewsData() {
|
||||||
this.newsLoading = true;
|
this.newsLoading = true;
|
||||||
try {
|
// 并行请求,任一失败不影响另一块数据展示
|
||||||
// 并行请求行业动态和平台公告
|
const [hydtResult, ptggResult] = await Promise.allSettled([
|
||||||
const [hydtRes, ptggRes] = await Promise.all([
|
hydtApi.getHydtGroupedList(),
|
||||||
hydtApi.getHydtGroupedList(),
|
hydtApi.getPtggList()
|
||||||
hydtApi.getPtggList()
|
]);
|
||||||
]);
|
|
||||||
|
|
||||||
if (hydtRes.data) {
|
if (hydtResult.status === 'fulfilled' && hydtResult.value && hydtResult.value.data) {
|
||||||
this.newsListByType.gjzc = hydtRes.data.gjzc || [];
|
this.newsListByType.gjzc = hydtResult.value.data.gjzc || [];
|
||||||
this.newsListByType.hyzx = hydtRes.data.hyzx || [];
|
this.newsListByType.hyzx = hydtResult.value.data.hyzx || [];
|
||||||
}
|
} else {
|
||||||
if (ptggRes.data) {
|
this.newsListByType.gjzc = [];
|
||||||
this.newsListByType.ptgg = ptggRes.data || [];
|
this.newsListByType.hyzx = [];
|
||||||
}
|
console.error('获取行业动态失败', hydtResult.status === 'rejected' ? hydtResult.reason : null);
|
||||||
} catch (e) {
|
|
||||||
console.error('获取新闻数据失败', e);
|
|
||||||
} finally {
|
|
||||||
this.newsLoading = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
getDefaultPic(type) {
|
||||||
const defaultPics = {
|
const defaultPics = {
|
||||||
@ -884,6 +889,24 @@ export default {
|
|||||||
background: @home-color-page-bg;
|
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/1440:token 见 home-figma-variables.less */
|
/* Figma 首页1920/1440:token 见 home-figma-variables.less */
|
||||||
.portal-page {
|
.portal-page {
|
||||||
.home-portal-page-vars();
|
.home-portal-page-vars();
|
||||||
@ -1044,7 +1067,8 @@ export default {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
margin-bottom: 0;
|
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;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1129,6 +1153,8 @@ export default {
|
|||||||
gap: @home-space-16;
|
gap: @home-space-16;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: @home-hero-title-width;
|
max-width: @home-hero-title-width;
|
||||||
|
// 抵消副标题 line-height 撑出的下方留白,让视觉底边贴近文字下沿
|
||||||
|
margin-bottom: -14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-hero-actions {
|
.top-hero-actions {
|
||||||
@ -2833,7 +2859,7 @@ export default {
|
|||||||
|
|
||||||
.portal-page .home-hero-content {
|
.portal-page .home-hero-content {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
gap: clamp(24px, 4vh, @home-hero-content-gap);
|
// 默认 28px 已统一,这里不再覆盖
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-page .top-hero-actions {
|
.portal-page .top-hero-actions {
|
||||||
@ -2969,7 +2995,8 @@ export default {
|
|||||||
|
|
||||||
.portal-page .home-hero-content {
|
.portal-page .home-hero-content {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
gap: @home-space-32;
|
// 标题/副标题 与 搜索框 的间距:从 32px 进一步收窄为 12px,符合 Figma 设计
|
||||||
|
gap: 12px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -168,24 +168,33 @@ export default {
|
|||||||
},
|
},
|
||||||
async fetchNewsData() {
|
async fetchNewsData() {
|
||||||
this.pageLoading = true;
|
this.pageLoading = true;
|
||||||
try {
|
const [hydtResult, ptggResult] = await Promise.allSettled([
|
||||||
const [hydtRes, ptggRes] = await Promise.all([
|
hydtApi.getHydtGroupedList(),
|
||||||
hydtApi.getHydtGroupedList(),
|
hydtApi.getPtggList(),
|
||||||
hydtApi.getPtggList(),
|
]);
|
||||||
]);
|
|
||||||
if (hydtRes.data) {
|
if (hydtResult.status === 'fulfilled' && hydtResult.value && hydtResult.value.data) {
|
||||||
this.newsListByType.gjzc = hydtRes.data.gjzc || [];
|
this.newsListByType.gjzc = hydtResult.value.data.gjzc || [];
|
||||||
this.newsListByType.hyzx = hydtRes.data.hyzx || [];
|
this.newsListByType.hyzx = hydtResult.value.data.hyzx || [];
|
||||||
}
|
} else {
|
||||||
if (ptggRes.data) {
|
this.newsListByType.gjzc = [];
|
||||||
this.newsListByType.ptgg = ptggRes.data || [];
|
this.newsListByType.hyzx = [];
|
||||||
}
|
console.error('获取行业动态失败', hydtResult.status === 'rejected' ? hydtResult.reason : null);
|
||||||
} catch (e) {
|
|
||||||
console.error('获取行业动态失败', e);
|
|
||||||
this.$message.warning('加载失败,请稍后重试');
|
|
||||||
} finally {
|
|
||||||
this.pageLoading = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
getDefaultPic(type) {
|
||||||
const defaultPics = {
|
const defaultPics = {
|
||||||
|
|||||||
@ -44,10 +44,11 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
import portalFigmaScaleMixin from '@/pages/index/utils/portal-figma-scale-mixin';
|
import portalFigmaScaleMixin from '@/pages/index/utils/portal-figma-scale-mixin';
|
||||||
|
import comingSoonMixin from '@/pages/index/utils/coming-soon-mixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'hyzt',
|
name: 'hyzt',
|
||||||
mixins: [portalFigmaScaleMixin],
|
mixins: [portalFigmaScaleMixin, comingSoonMixin],
|
||||||
components: {},
|
components: {},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -65,7 +66,7 @@ export default {
|
|||||||
if (href) {
|
if (href) {
|
||||||
window.open(href, '_blank');
|
window.open(href, '_blank');
|
||||||
} else {
|
} else {
|
||||||
this.$message.info('敬请期待');
|
this.showComingSoon();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -275,18 +275,17 @@
|
|||||||
import serviceCardIcon1 from '../../assets/qych/service-card-icon-1.png'
|
import serviceCardIcon1 from '../../assets/qych/service-card-icon-1.png'
|
||||||
import serviceCardIcon2 from '../../assets/qych/service-card-icon-2.png'
|
import serviceCardIcon2 from '../../assets/qych/service-card-icon-2.png'
|
||||||
import portalFigmaScaleMixin from '@/pages/index/utils/portal-figma-scale-mixin';
|
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 {
|
export default {
|
||||||
name: 'CompliancePortal',
|
name: 'CompliancePortal',
|
||||||
mixins: [portalFigmaScaleMixin],
|
mixins: [portalFigmaScaleMixin, fullpageScrollMixin, comingSoonMixin],
|
||||||
components: {},
|
components: {},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
serviceCardIcon1,
|
serviceCardIcon1,
|
||||||
serviceCardIcon2,
|
serviceCardIcon2,
|
||||||
isScrolling: false,
|
|
||||||
scrollThreshold: 1,
|
|
||||||
scrollRoot: null,
|
|
||||||
sectionIds: ['section-landing', 'section0', 'section1', 'section2'],
|
sectionIds: ['section-landing', 'section0', 'section1', 'section2'],
|
||||||
navList: [
|
navList: [
|
||||||
{
|
{
|
||||||
@ -322,24 +321,15 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.scrollRoot = document.querySelector('.content-wrap');
|
|
||||||
if (this.scrollRoot) {
|
|
||||||
this.scrollRoot.addEventListener('wheel', this.handleWheel, { passive: false });
|
|
||||||
}
|
|
||||||
this.scrollToSectionFromQuery();
|
this.scrollToSectionFromQuery();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
|
||||||
if (this.scrollRoot) {
|
|
||||||
this.scrollRoot.removeEventListener('wheel', this.handleWheel);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
goPage(url) {
|
goPage(url) {
|
||||||
if (url) {
|
if (url) {
|
||||||
window.open(url);
|
window.open(url);
|
||||||
} else {
|
} else {
|
||||||
this.$message.info('敬请期待')
|
this.showComingSoon()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleCardBtnClick(item) {
|
handleCardBtnClick(item) {
|
||||||
@ -390,108 +380,23 @@ export default {
|
|||||||
return currentIndex;
|
return currentIndex;
|
||||||
},
|
},
|
||||||
scrollRootToSection(sectionIndex, smooth = true) {
|
scrollRootToSection(sectionIndex, smooth = true) {
|
||||||
const scrollRoot = this.getScrollRoot();
|
this.fullPageScrollTo(sectionIndex);
|
||||||
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.syncNavBySectionIndex(sectionIndex);
|
this.syncNavBySectionIndex(sectionIndex);
|
||||||
},
|
},
|
||||||
/** 专题区 .module 内部先滚完,再翻页 */
|
/** 导航点跳到指定 section:使用 fullpageScrollMixin 提供的 fullPageScrollTo */
|
||||||
/** 当前屏内容高于视口时,先在 .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;
|
|
||||||
},
|
|
||||||
scrollToSection(item) {
|
scrollToSection(item) {
|
||||||
const sectionIndex = this.sectionIds.indexOf(item.sectionId);
|
const sectionIndex = this.sectionIds.indexOf(item.sectionId);
|
||||||
if (sectionIndex < 0) return;
|
if (sectionIndex < 0) return;
|
||||||
this.setNavActive(item.index);
|
this.setNavActive(item.index);
|
||||||
this.isScrolling = true;
|
this.fullPageScrollTo(sectionIndex);
|
||||||
this.scrollRootToSection(sectionIndex, true);
|
|
||||||
setTimeout(() => {
|
|
||||||
this.isScrolling = false;
|
|
||||||
}, 800);
|
|
||||||
},
|
},
|
||||||
syncNavBySectionIndex(sectionIndex) {
|
syncNavBySectionIndex(sectionIndex) {
|
||||||
if (sectionIndex >= 1 && sectionIndex <= 3) {
|
if (sectionIndex >= 1 && sectionIndex <= 3) {
|
||||||
this.setNavActive(sectionIndex - 1);
|
this.setNavActive(sectionIndex - 1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleWheel(e) {
|
onFullPageSectionChange(newIndex /*, fromIndex, sectionEl */) {
|
||||||
if (this.isScrolling) return;
|
this.syncNavBySectionIndex(newIndex);
|
||||||
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);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -788,10 +693,12 @@ body {
|
|||||||
|
|
||||||
.text-section-content {
|
.text-section-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: @qych-policy-content-gap;
|
gap: @qych-policy-content-gap;
|
||||||
padding: 0 @qych-policy-content-px;
|
padding: 0 @qych-policy-content-px;
|
||||||
|
min-height: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -805,11 +712,8 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-self: flex-start;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: @qych-policy-panel-height;
|
max-height: 328px;
|
||||||
min-height: @qych-policy-panel-height;
|
|
||||||
max-height: @qych-policy-panel-height;
|
|
||||||
padding: @qych-policy-panel-padding;
|
padding: @qych-policy-panel-padding;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@ -904,7 +808,7 @@ body {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
min-height: 132px;
|
min-height: 160px;
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 20px @qych-policy-card-padding-x 16px;
|
padding: 20px @qych-policy-card-padding-x 16px;
|
||||||
border-radius: @qych-policy-card-radius-inner;
|
border-radius: @qych-policy-card-radius-inner;
|
||||||
|
|||||||
@ -163,7 +163,9 @@ if (LOCAL_DEV && process.env.VUE_APP_IE === 'true') {
|
|||||||
transpileDependencies.push('sockjs-client');
|
transpileDependencies.push('sockjs-client');
|
||||||
}
|
}
|
||||||
module.exports = {
|
module.exports = {
|
||||||
lintOnSave: LOCAL_DEV,
|
// 关闭 eslint-loader:默认的 espree parser 不能解析 ES module 语法,
|
||||||
|
// 会导致 "import is reserved" 编译错误。lint 用 IDE / CI 单独做。
|
||||||
|
lintOnSave: false,
|
||||||
transpileDependencies,
|
transpileDependencies,
|
||||||
publicPath: process.env.NODE_ENV === 'production' ? '/' : process.env.VUE_APP_CDN_PATH,
|
publicPath: process.env.NODE_ENV === 'production' ? '/' : process.env.VUE_APP_CDN_PATH,
|
||||||
assetsDir: 'assets_res',
|
assetsDir: 'assets_res',
|
||||||
@ -294,10 +296,7 @@ module.exports = {
|
|||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
'^/mhzc': {
|
'^/mhzc': {
|
||||||
// target: 'http://localhost:9300',
|
target: process.env.VUE_APP_MHZC_PROXY || 'https://www.cciw.com.cn',
|
||||||
// target: 'http://carbon.liantu.tech',
|
|
||||||
target: 'https://www.cciw.com.cn',
|
|
||||||
// target: 'http://10.23.20.13:94/',
|
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
'^/gxzx': {
|
'^/gxzx': {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user