优化swup,优化清理事件处理逻辑,改进主题切换和搜索面板关闭功能
This commit is contained in:
parent
2c71dcdbd9
commit
20276f1a86
141
package-lock.json
generated
141
package-lock.json
generated
@ -21,6 +21,7 @@
|
|||||||
"@swup/head-plugin": "^2.3.1",
|
"@swup/head-plugin": "^2.3.1",
|
||||||
"@swup/preload-plugin": "^3.2.11",
|
"@swup/preload-plugin": "^3.2.11",
|
||||||
"@swup/progress-plugin": "^3.2.0",
|
"@swup/progress-plugin": "^3.2.0",
|
||||||
|
"@swup/scripts-plugin": "^2.1.0",
|
||||||
"@tailwindcss/vite": "^4.1.4",
|
"@tailwindcss/vite": "^4.1.4",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.1.2",
|
||||||
@ -310,23 +311,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.26.2",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.26.2.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||||
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
|
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-validator-identifier": "^7.25.9",
|
"@babel/helper-validator-identifier": "^7.27.1",
|
||||||
"js-tokens": "^4.0.0",
|
"js-tokens": "^4.0.0",
|
||||||
"picocolors": "^1.0.0"
|
"picocolors": "^1.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/compat-data": {
|
"node_modules/@babel/compat-data": {
|
||||||
"version": "7.26.8",
|
"version": "7.27.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.26.8.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.27.2.tgz",
|
||||||
"integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==",
|
"integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@ -372,13 +373,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/generator": {
|
"node_modules/@babel/generator": {
|
||||||
"version": "7.27.0",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.27.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.27.1.tgz",
|
||||||
"integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==",
|
"integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.27.0",
|
"@babel/parser": "^7.27.1",
|
||||||
"@babel/types": "^7.27.0",
|
"@babel/types": "^7.27.1",
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
"@jridgewell/trace-mapping": "^0.3.25",
|
"@jridgewell/trace-mapping": "^0.3.25",
|
||||||
"jsesc": "^3.0.2"
|
"jsesc": "^3.0.2"
|
||||||
@ -388,13 +389,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-compilation-targets": {
|
"node_modules/@babel/helper-compilation-targets": {
|
||||||
"version": "7.27.0",
|
"version": "7.27.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
|
||||||
"integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==",
|
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/compat-data": "^7.26.8",
|
"@babel/compat-data": "^7.27.2",
|
||||||
"@babel/helper-validator-option": "^7.25.9",
|
"@babel/helper-validator-option": "^7.27.1",
|
||||||
"browserslist": "^4.24.0",
|
"browserslist": "^4.24.0",
|
||||||
"lru-cache": "^5.1.1",
|
"lru-cache": "^5.1.1",
|
||||||
"semver": "^6.3.1"
|
"semver": "^6.3.1"
|
||||||
@ -413,27 +414,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-module-imports": {
|
"node_modules/@babel/helper-module-imports": {
|
||||||
"version": "7.25.9",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
|
||||||
"integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
|
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/traverse": "^7.25.9",
|
"@babel/traverse": "^7.27.1",
|
||||||
"@babel/types": "^7.25.9"
|
"@babel/types": "^7.27.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-module-transforms": {
|
"node_modules/@babel/helper-module-transforms": {
|
||||||
"version": "7.26.0",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz",
|
||||||
"integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
|
"integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-module-imports": "^7.25.9",
|
"@babel/helper-module-imports": "^7.27.1",
|
||||||
"@babel/helper-validator-identifier": "^7.25.9",
|
"@babel/helper-validator-identifier": "^7.27.1",
|
||||||
"@babel/traverse": "^7.25.9"
|
"@babel/traverse": "^7.27.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@ -443,36 +444,36 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-plugin-utils": {
|
"node_modules/@babel/helper-plugin-utils": {
|
||||||
"version": "7.26.5",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
|
||||||
"integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==",
|
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-string-parser": {
|
"node_modules/@babel/helper-string-parser": {
|
||||||
"version": "7.25.9",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||||
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
|
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-validator-identifier": {
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
"version": "7.25.9",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||||
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-validator-option": {
|
"node_modules/@babel/helper-validator-option": {
|
||||||
"version": "7.25.9",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
||||||
"integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
|
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@ -492,12 +493,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.27.0",
|
"version": "7.27.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.27.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.27.2.tgz",
|
||||||
"integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
|
"integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.27.0"
|
"@babel/types": "^7.27.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
@ -537,30 +538,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.27.0",
|
"version": "7.27.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz",
|
||||||
"integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
|
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.26.2",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/parser": "^7.27.0",
|
"@babel/parser": "^7.27.2",
|
||||||
"@babel/types": "^7.27.0"
|
"@babel/types": "^7.27.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/traverse": {
|
"node_modules/@babel/traverse": {
|
||||||
"version": "7.27.0",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.27.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.27.1.tgz",
|
||||||
"integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==",
|
"integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.26.2",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.27.0",
|
"@babel/generator": "^7.27.1",
|
||||||
"@babel/parser": "^7.27.0",
|
"@babel/parser": "^7.27.1",
|
||||||
"@babel/template": "^7.27.0",
|
"@babel/template": "^7.27.1",
|
||||||
"@babel/types": "^7.27.0",
|
"@babel/types": "^7.27.1",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
"globals": "^11.1.0"
|
"globals": "^11.1.0"
|
||||||
},
|
},
|
||||||
@ -569,13 +570,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.27.0",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.27.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.27.1.tgz",
|
||||||
"integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
|
"integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-string-parser": "^7.25.9",
|
"@babel/helper-string-parser": "^7.27.1",
|
||||||
"@babel/helper-validator-identifier": "^7.25.9"
|
"@babel/helper-validator-identifier": "^7.27.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@ -2708,6 +2709,18 @@
|
|||||||
"swup": "^4.0.0"
|
"swup": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@swup/scripts-plugin": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@swup/scripts-plugin/-/scripts-plugin-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-JSMFsFCN9gn4q3m1Ccv0gq3gwRoZl6UGALOQO3OeQ8wOIq9vPC5dcUD3CMBuaPanksjR4GC8ZoukIjHrlT52fg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@swup/plugin": "^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"swup": "^4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
"version": "4.1.4",
|
"version": "4.1.4",
|
||||||
"resolved": "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.1.4.tgz",
|
"resolved": "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.1.4.tgz",
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
"@swup/head-plugin": "^2.3.1",
|
"@swup/head-plugin": "^2.3.1",
|
||||||
"@swup/preload-plugin": "^3.2.11",
|
"@swup/preload-plugin": "^3.2.11",
|
||||||
"@swup/progress-plugin": "^3.2.0",
|
"@swup/progress-plugin": "^3.2.0",
|
||||||
|
"@swup/scripts-plugin": "^2.1.0",
|
||||||
"@tailwindcss/vite": "^4.1.4",
|
"@tailwindcss/vite": "^4.1.4",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.1.2",
|
||||||
|
@ -208,33 +208,6 @@ const breadcrumbs: Breadcrumb[] = pathSegments
|
|||||||
return handler;
|
return handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统一的清理函数,执行完整清理并自销毁
|
|
||||||
function selfDestruct() {
|
|
||||||
// 1. 移除所有普通事件监听器
|
|
||||||
allListeners.forEach(({ element, eventType, handler, options }) => {
|
|
||||||
try {
|
|
||||||
element.removeEventListener(eventType, handler, options);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`[面包屑]移除事件监听器出错:`, err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 清空监听器数组
|
|
||||||
allListeners.length = 0;
|
|
||||||
|
|
||||||
// 2. 最后移除清理事件监听器自身
|
|
||||||
cleanupListeners.forEach(({ element, eventType, handler, options }) => {
|
|
||||||
try {
|
|
||||||
element.removeEventListener(eventType, handler, options);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`[面包屑]移除清理监听器出错:`, err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 清空清理监听器数组
|
|
||||||
cleanupListeners.length = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前URL路径(与导航栏保持一致)
|
// 获取当前URL路径(与导航栏保持一致)
|
||||||
function getCurrentPath() {
|
function getCurrentPath() {
|
||||||
const path = window.location.pathname;
|
const path = window.location.pathname;
|
||||||
@ -556,9 +529,15 @@ const breadcrumbs: Breadcrumb[] = pathSegments
|
|||||||
// 添加路径变化检测和自动更新
|
// 添加路径变化检测和自动更新
|
||||||
function setupPathChangeDetection() {
|
function setupPathChangeDetection() {
|
||||||
let lastPathChecked = getCurrentPath();
|
let lastPathChecked = getCurrentPath();
|
||||||
|
let isCleaningUp = false; // 标记是否在清理中
|
||||||
|
|
||||||
// 统一的路径变化处理函数
|
// 统一的路径变化处理函数
|
||||||
function handlePathChange() {
|
function handlePathChange() {
|
||||||
|
// 如果正在清理中,不要继续更新
|
||||||
|
if (isCleaningUp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentPath = getCurrentPath();
|
const currentPath = getCurrentPath();
|
||||||
if (currentPath !== lastPathChecked) {
|
if (currentPath !== lastPathChecked) {
|
||||||
// 更新面包屑
|
// 更新面包屑
|
||||||
@ -571,81 +550,138 @@ const breadcrumbs: Breadcrumb[] = pathSegments
|
|||||||
|
|
||||||
// 监听hashchange事件 - 当URL的hash部分改变时触发
|
// 监听hashchange事件 - 当URL的hash部分改变时触发
|
||||||
addListener(window, 'hashchange', () => {
|
addListener(window, 'hashchange', () => {
|
||||||
|
if (isCleaningUp) return;
|
||||||
handlePathChange();
|
handlePathChange();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 为所有导航链接添加点击拦截
|
// 监听popstate事件
|
||||||
addListener(document, 'click', (e) => {
|
addListener(window, 'popstate', () => {
|
||||||
// 检查点击的是否为站内导航链接
|
if (isCleaningUp) return;
|
||||||
const link = e.target.closest('a');
|
handlePathChange();
|
||||||
if (link && link.host === window.location.host && !e.ctrlKey && !e.metaKey) {
|
|
||||||
// 延迟检查以确保导航已完成
|
|
||||||
setTimeout(handlePathChange, 50);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听history API的方法
|
// 监听页面转换事件,设置清理标志
|
||||||
const originalPushState = window.history.pushState;
|
addListener(document, 'page-transition', () => {
|
||||||
const originalReplaceState = window.history.replaceState;
|
// 设置清理标志,阻止后续的handlePathChange调用
|
||||||
|
isCleaningUp = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听页面内容替换完成后更新面包屑
|
||||||
|
addListener(document, 'swup:contentReplaced', () => {
|
||||||
|
if (isCleaningUp) return;
|
||||||
|
setTimeout(() => {
|
||||||
|
// 在DOM更新后,重新计算面包屑
|
||||||
|
handlePathChange();
|
||||||
|
}, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存原始history方法
|
||||||
|
originalPushState = window.history.pushState;
|
||||||
|
originalReplaceState = window.history.replaceState;
|
||||||
|
|
||||||
// 重写pushState
|
// 重写pushState
|
||||||
window.history.pushState = function() {
|
window.history.pushState = function() {
|
||||||
|
// 如果正在清理中,不要执行被重写的方法,直接调用原始方法
|
||||||
|
if (isCleaningUp) {
|
||||||
|
return originalPushState.apply(this, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
originalPushState.apply(this, arguments);
|
originalPushState.apply(this, arguments);
|
||||||
handlePathChange();
|
handlePathChange();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 重写replaceState
|
// 重写replaceState
|
||||||
window.history.replaceState = function() {
|
window.history.replaceState = function() {
|
||||||
|
// 如果正在清理中,不要执行被重写的方法,直接调用原始方法
|
||||||
|
if (isCleaningUp) {
|
||||||
|
return originalReplaceState.apply(this, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
originalReplaceState.apply(this, arguments);
|
originalReplaceState.apply(this, arguments);
|
||||||
handlePathChange();
|
handlePathChange();
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 添加到清理列表
|
// 统一的清理函数,执行完整清理并自销毁
|
||||||
addListener(window, 'beforeunload', () => {
|
function selfDestruct() {
|
||||||
// 恢复原始history方法
|
|
||||||
window.history.pushState = originalPushState;
|
|
||||||
window.history.replaceState = originalReplaceState;
|
|
||||||
}, { once: true });
|
|
||||||
|
|
||||||
// 监听popstate事件
|
// 1. 移除所有普通事件监听器
|
||||||
addListener(window, 'popstate', () => {
|
allListeners.forEach(({ element, eventType, handler, options }) => {
|
||||||
setTimeout(() => {
|
try {
|
||||||
handlePathChange();
|
element.removeEventListener(eventType, handler, options);
|
||||||
}, 50);
|
} catch (err) {
|
||||||
|
console.error(`[面包屑] 移除事件监听器出错:`, err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 清空监听器数组
|
||||||
|
allListeners.length = 0;
|
||||||
|
|
||||||
|
// 恢复原始的history方法
|
||||||
|
if (originalPushState && originalReplaceState) {
|
||||||
|
try {
|
||||||
|
// 立即恢复原始方法,确保后续pushState调用直接使用原始方法
|
||||||
|
const tempOriginalPushState = originalPushState;
|
||||||
|
const tempOriginalReplaceState = originalReplaceState;
|
||||||
|
|
||||||
|
window.history.pushState = tempOriginalPushState;
|
||||||
|
window.history.replaceState = tempOriginalReplaceState;
|
||||||
|
|
||||||
|
// 清空引用
|
||||||
|
originalPushState = null;
|
||||||
|
originalReplaceState = null;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[面包屑] 恢复History方法失败:', err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('[面包屑] 找不到原始History方法引用');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 最后移除清理事件监听器自身
|
||||||
|
cleanupListeners.forEach(({ element, eventType, handler, options }) => {
|
||||||
|
try {
|
||||||
|
element.removeEventListener(eventType, handler, options);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[面包屑] 移除清理监听器出错:`, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清空清理监听器数组
|
||||||
|
cleanupListeners.length = 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册清理事件,并保存引用
|
// 注册清理事件,并保存引用
|
||||||
function registerCleanupEvents() {
|
function registerCleanupEvents() {
|
||||||
// 创建一次性事件处理函数
|
// 创建事件处理函数
|
||||||
|
const pageTransitionHandler = (event) => {
|
||||||
|
selfDestruct();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Astro视图转换事件 - 保留这个作为后备
|
||||||
const beforeSwapHandler = () => {
|
const beforeSwapHandler = () => {
|
||||||
selfDestruct();
|
selfDestruct();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 页面卸载事件 - 保留这个作为后备
|
||||||
const beforeUnloadHandler = () => {
|
const beforeUnloadHandler = () => {
|
||||||
selfDestruct();
|
selfDestruct();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加清理事件监听器并保存引用
|
// 只监听统一的页面转换事件
|
||||||
|
document.addEventListener("page-transition", pageTransitionHandler);
|
||||||
|
|
||||||
|
// 保留Astro和页面卸载事件作为后备
|
||||||
document.addEventListener("astro:before-swap", beforeSwapHandler, { once: true });
|
document.addEventListener("astro:before-swap", beforeSwapHandler, { once: true });
|
||||||
window.addEventListener("beforeunload", beforeUnloadHandler, { once: true });
|
window.addEventListener("beforeunload", beforeUnloadHandler, { once: true });
|
||||||
|
|
||||||
// 如果页面使用swup,也注册swup相关的清理事件
|
|
||||||
if (typeof window.swup !== 'undefined') {
|
|
||||||
document.addEventListener("swup:willReplaceContent", beforeSwapHandler, { once: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存清理事件引用,用于完全销毁
|
// 保存清理事件引用,用于完全销毁
|
||||||
cleanupListeners.push(
|
cleanupListeners.push(
|
||||||
|
{ element: document, eventType: "page-transition", handler: pageTransitionHandler, options: null },
|
||||||
{ element: document, eventType: "astro:before-swap", handler: beforeSwapHandler, options: { once: true } },
|
{ element: document, eventType: "astro:before-swap", handler: beforeSwapHandler, options: { once: true } },
|
||||||
{ element: window, eventType: "beforeunload", handler: beforeUnloadHandler, options: { once: true } }
|
{ element: window, eventType: "beforeunload", handler: beforeUnloadHandler, options: { once: true } }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (typeof window.swup !== 'undefined') {
|
|
||||||
cleanupListeners.push(
|
|
||||||
{ element: document, eventType: "swup:willReplaceContent", handler: beforeSwapHandler, options: { once: true } }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置返回按钮功能
|
// 设置返回按钮功能
|
||||||
|
@ -153,7 +153,7 @@ const navSelectorClassName = "mr-4";
|
|||||||
|
|
||||||
<!-- 使用自定义主题切换组件 -->
|
<!-- 使用自定义主题切换组件 -->
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<ThemeToggle className="group" transitionDuration={700} />
|
<ThemeToggle transitionDuration={700} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -343,6 +343,15 @@ const navSelectorClassName = "mr-4";
|
|||||||
|
|
||||||
// 单独保存清理事件的监听器引用
|
// 单独保存清理事件的监听器引用
|
||||||
const cleanupListeners = [];
|
const cleanupListeners = [];
|
||||||
|
|
||||||
|
// 内部状态管理
|
||||||
|
const state = {
|
||||||
|
isCleaningUp: false,
|
||||||
|
lastPathLogged: '',
|
||||||
|
originalPushState: window.history.pushState,
|
||||||
|
originalReplaceState: window.history.replaceState
|
||||||
|
};
|
||||||
|
|
||||||
// 获取当前URL路径
|
// 获取当前URL路径
|
||||||
function getCurrentPath() {
|
function getCurrentPath() {
|
||||||
const path = window.location.pathname;
|
const path = window.location.pathname;
|
||||||
@ -361,8 +370,30 @@ const navSelectorClassName = "mr-4";
|
|||||||
return handler;
|
return handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新高亮背景的函数声明(提前声明)
|
||||||
|
let updateHighlights;
|
||||||
|
|
||||||
|
// 初始化当前页面的激活状态函数声明(提前声明)
|
||||||
|
let initActiveState;
|
||||||
|
|
||||||
|
// 统一的路径变化处理函数
|
||||||
|
function handlePathChange() {
|
||||||
|
if (state.isCleaningUp) return;
|
||||||
|
|
||||||
|
const currentPath = getCurrentPath();
|
||||||
|
if (currentPath !== state.lastPathLogged) {
|
||||||
|
// 主动调用初始化和更新高亮
|
||||||
|
initActiveState();
|
||||||
|
updateHighlights(true);
|
||||||
|
|
||||||
|
state.lastPathLogged = currentPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 统一的清理函数,执行完整清理并自销毁
|
// 统一的清理函数,执行完整清理并自销毁
|
||||||
function selfDestruct() {
|
function selfDestruct() {
|
||||||
|
// 标记清理状态
|
||||||
|
state.isCleaningUp = true;
|
||||||
|
|
||||||
// 1. 移除所有普通事件监听器
|
// 1. 移除所有普通事件监听器
|
||||||
allListeners.forEach(({ element, eventType, handler, options }) => {
|
allListeners.forEach(({ element, eventType, handler, options }) => {
|
||||||
@ -376,7 +407,17 @@ const navSelectorClassName = "mr-4";
|
|||||||
// 清空监听器数组
|
// 清空监听器数组
|
||||||
allListeners.length = 0;
|
allListeners.length = 0;
|
||||||
|
|
||||||
// 2. 最后移除清理事件监听器自身
|
// 2. 恢复原始history方法
|
||||||
|
if (state.originalPushState && state.originalReplaceState) {
|
||||||
|
try {
|
||||||
|
window.history.pushState = state.originalPushState;
|
||||||
|
window.history.replaceState = state.originalReplaceState;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('恢复History方法失败:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 最后移除清理事件监听器自身
|
||||||
cleanupListeners.forEach(({ element, eventType, handler, options }) => {
|
cleanupListeners.forEach(({ element, eventType, handler, options }) => {
|
||||||
try {
|
try {
|
||||||
element.removeEventListener(eventType, handler, options);
|
element.removeEventListener(eventType, handler, options);
|
||||||
@ -387,29 +428,30 @@ const navSelectorClassName = "mr-4";
|
|||||||
|
|
||||||
// 清空清理监听器数组
|
// 清空清理监听器数组
|
||||||
cleanupListeners.length = 0;
|
cleanupListeners.length = 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册清理事件,并保存引用
|
// 注册清理事件,并保存引用
|
||||||
function registerCleanupEvents() {
|
function registerCleanupEvents() {
|
||||||
// 创建一次性事件处理函数
|
// 所有可能触发清理的事件
|
||||||
const beforeSwapHandler = () => {
|
const cleanupEvents = [
|
||||||
selfDestruct();
|
{ element: document, eventType: "astro:before-swap", options: { once: true } },
|
||||||
};
|
{ element: window, eventType: "beforeunload", options: { once: true } },
|
||||||
|
{ element: document, eventType: "page-transition", options: null }
|
||||||
|
];
|
||||||
|
|
||||||
const beforeUnloadHandler = () => {
|
// 为每个事件添加监听器
|
||||||
selfDestruct();
|
cleanupEvents.forEach(({ element, eventType, options }) => {
|
||||||
};
|
// 添加监听器
|
||||||
|
element.addEventListener(eventType, selfDestruct, options);
|
||||||
|
|
||||||
// 添加清理事件监听器并保存引用
|
// 保存引用以便后续清理
|
||||||
document.addEventListener("astro:before-swap", beforeSwapHandler, { once: true });
|
cleanupListeners.push({
|
||||||
window.addEventListener("beforeunload", beforeUnloadHandler, { once: true });
|
element,
|
||||||
|
eventType,
|
||||||
// 保存清理事件引用,用于完全销毁
|
handler: selfDestruct,
|
||||||
cleanupListeners.push(
|
options
|
||||||
{ element: document, eventType: "astro:before-swap", handler: beforeSwapHandler, options: { once: true } },
|
});
|
||||||
{ element: window, eventType: "beforeunload", handler: beforeUnloadHandler, options: { once: true } }
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建一个共享的计算高亮位置的函数
|
// 创建一个共享的计算高亮位置的函数
|
||||||
@ -633,8 +675,8 @@ const navSelectorClassName = "mr-4";
|
|||||||
// 注册清理事件
|
// 注册清理事件
|
||||||
registerCleanupEvents();
|
registerCleanupEvents();
|
||||||
|
|
||||||
// 更新高亮背景 - 提升到全局作用域
|
// 更新高亮背景 - 提升到顶部作用域
|
||||||
function updateHighlights(immediate = false) {
|
updateHighlights = function(immediate = false) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const highlightPositions = calculateHighlightPositions({
|
const highlightPositions = calculateHighlightPositions({
|
||||||
navSelector,
|
navSelector,
|
||||||
@ -645,10 +687,10 @@ const navSelectorClassName = "mr-4";
|
|||||||
highlightPositions.applyPositions(immediate);
|
highlightPositions.applyPositions(immediate);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// 初始化当前页面的激活状态 - 提升到全局作用域
|
// 初始化当前页面的激活状态 - 提升到顶部作用域
|
||||||
function initActiveState() {
|
initActiveState = function() {
|
||||||
// 获取当前路径
|
// 获取当前路径
|
||||||
const currentPath = getCurrentPath();
|
const currentPath = getCurrentPath();
|
||||||
|
|
||||||
@ -738,7 +780,7 @@ const navSelectorClassName = "mr-4";
|
|||||||
|
|
||||||
// 计算正确高亮位置
|
// 计算正确高亮位置
|
||||||
updateHighlights(true);
|
updateHighlights(true);
|
||||||
}
|
};
|
||||||
|
|
||||||
// DOM加载完成后执行初始化
|
// DOM加载完成后执行初始化
|
||||||
function initNavigation() {
|
function initNavigation() {
|
||||||
@ -752,27 +794,15 @@ const navSelectorClassName = "mr-4";
|
|||||||
|
|
||||||
// 主要设置函数
|
// 主要设置函数
|
||||||
function setupNavigation() {
|
function setupNavigation() {
|
||||||
|
// 初始化路径记录
|
||||||
|
state.lastPathLogged = getCurrentPath();
|
||||||
|
|
||||||
// 设置桌面导航
|
// 设置桌面导航
|
||||||
setupNavSelector();
|
setupNavSelector();
|
||||||
|
|
||||||
// 设置移动端导航
|
// 设置移动端导航
|
||||||
setupMobileNav();
|
setupMobileNav();
|
||||||
|
|
||||||
// 记录最后一次路径值
|
|
||||||
let lastPathLogged = getCurrentPath();
|
|
||||||
|
|
||||||
// 统一的路径变化处理函数
|
|
||||||
function handlePathChange() {
|
|
||||||
const currentPath = getCurrentPath();
|
|
||||||
if (currentPath !== lastPathLogged) {
|
|
||||||
// 主动调用初始化和更新高亮
|
|
||||||
initActiveState();
|
|
||||||
updateHighlights(true);
|
|
||||||
|
|
||||||
lastPathLogged = currentPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听hashchange事件 - 当URL的hash部分改变时触发
|
// 监听hashchange事件 - 当URL的hash部分改变时触发
|
||||||
addListener(window, 'hashchange', () => {
|
addListener(window, 'hashchange', () => {
|
||||||
handlePathChange();
|
handlePathChange();
|
||||||
@ -788,28 +818,38 @@ const navSelectorClassName = "mr-4";
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 设置History API监控
|
||||||
|
setupHistoryMonitoring();
|
||||||
|
}
|
||||||
|
|
||||||
// 监听history API的方法
|
// 监听history API的方法
|
||||||
const originalPushState = window.history.pushState;
|
function setupHistoryMonitoring() {
|
||||||
const originalReplaceState = window.history.replaceState;
|
// 保存初始状态 - 确保引用正确
|
||||||
|
state.originalPushState = window.history.pushState;
|
||||||
|
state.originalReplaceState = window.history.replaceState;
|
||||||
|
|
||||||
|
// 监听页面转换事件来设置清理标记
|
||||||
|
addListener(document, 'page-transition', () => {
|
||||||
|
state.isCleaningUp = true;
|
||||||
|
});
|
||||||
|
|
||||||
// 重写pushState
|
// 重写pushState
|
||||||
window.history.pushState = function() {
|
window.history.pushState = function() {
|
||||||
originalPushState.apply(this, arguments);
|
if (state.isCleaningUp) {
|
||||||
|
return state.originalPushState.apply(this, arguments);
|
||||||
|
}
|
||||||
|
state.originalPushState.apply(this, arguments);
|
||||||
handlePathChange();
|
handlePathChange();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 重写replaceState
|
// 重写replaceState
|
||||||
window.history.replaceState = function() {
|
window.history.replaceState = function() {
|
||||||
originalReplaceState.apply(this, arguments);
|
if (state.isCleaningUp) {
|
||||||
|
return state.originalReplaceState.apply(this, arguments);
|
||||||
|
}
|
||||||
|
state.originalReplaceState.apply(this, arguments);
|
||||||
handlePathChange();
|
handlePathChange();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加到清理列表
|
|
||||||
addListener(window, 'beforeunload', () => {
|
|
||||||
// 恢复原始history方法
|
|
||||||
window.history.pushState = originalPushState;
|
|
||||||
window.history.replaceState = originalReplaceState;
|
|
||||||
}, { once: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化导航选择器
|
// 初始化导航选择器
|
||||||
@ -1459,54 +1499,32 @@ const navSelectorClassName = "mr-4";
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 为移动端菜单的所有链接添加点击事件,点击后关闭菜单
|
// 为主题切换容器添加点击事件
|
||||||
const mobileMenuLinks = document.querySelectorAll('#mobile-menu a');
|
const themeToggleContainer = document.getElementById('theme-toggle-container');
|
||||||
mobileMenuLinks.forEach(link => {
|
if (themeToggleContainer) {
|
||||||
addListener(link, 'click', (e) => {
|
addListener(themeToggleContainer, 'click', (e) => {
|
||||||
// 如果使用客户端路由(如swup或Astro View Transitions),阻止默认行为
|
// 阻止事件冒泡,防止多次触发
|
||||||
const hasSwup = typeof window.swup !== 'undefined';
|
e.stopPropagation();
|
||||||
const hasViewTransitions = typeof document.startViewTransition !== 'undefined';
|
|
||||||
|
|
||||||
if (hasSwup || hasViewTransitions) {
|
// 获取容器内的主题切换按钮
|
||||||
e.preventDefault();
|
const themeToggleButton = themeToggleContainer.querySelector('#theme-toggle-button');
|
||||||
|
|
||||||
// 获取链接地址
|
// 如果找到按钮,通过创建自定义事件来传递坐标
|
||||||
const href = link.getAttribute('href');
|
if (themeToggleButton) {
|
||||||
if (!href) return;
|
// 创建自定义点击事件并携带原始事件的坐标信息
|
||||||
|
const clickEvent = new MouseEvent('click', {
|
||||||
// 先关闭菜单
|
bubbles: true,
|
||||||
closeMobileMenu();
|
cancelable: true,
|
||||||
closeMobileSearch();
|
view: window,
|
||||||
|
clientX: e.clientX,
|
||||||
// 延迟导航,确保菜单关闭动画完成
|
clientY: e.clientY
|
||||||
setTimeout(() => {
|
|
||||||
// 使用适当的导航方法
|
|
||||||
if (hasSwup) {
|
|
||||||
try {
|
|
||||||
window.swup.navigate(href);
|
|
||||||
} catch (err) {
|
|
||||||
try {
|
|
||||||
window.swup.loadPage({ url: href });
|
|
||||||
} catch (err2) {
|
|
||||||
window.location.href = href;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (hasViewTransitions) {
|
|
||||||
document.startViewTransition(() => {
|
|
||||||
window.location.href = href;
|
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
window.location.href = href;
|
// 使用自定义事件触发按钮点击
|
||||||
}
|
themeToggleButton.dispatchEvent(clickEvent);
|
||||||
}, 50);
|
|
||||||
} else {
|
|
||||||
// 普通链接导航,浏览器会自动处理跳转
|
|
||||||
// 但仍然需要关闭菜单
|
|
||||||
closeMobileMenu();
|
|
||||||
closeMobileSearch();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
// 为Astro View Transitions添加事件处理
|
// 为Astro View Transitions添加事件处理
|
||||||
addListener(document, 'astro:page-load', () => {
|
addListener(document, 'astro:page-load', () => {
|
||||||
@ -1525,11 +1543,71 @@ const navSelectorClassName = "mr-4";
|
|||||||
}, 50);
|
}, 50);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 为移动端菜单链接添加点击事件,点击后关闭菜单
|
||||||
|
function setupMobileMenuLinks() {
|
||||||
|
const mobileMenuLinks = document.querySelectorAll('#mobile-menu a');
|
||||||
|
mobileMenuLinks.forEach(link => {
|
||||||
|
// 避免重复添加事件监听器
|
||||||
|
addListener(link, 'click', (e) => {
|
||||||
|
// 如果是站内链接且不是控制键或Meta键点击(新窗口打开)
|
||||||
|
if (link.host === window.location.host && !e.ctrlKey && !e.metaKey) {
|
||||||
|
// 延迟关闭导航栏,确保页面跳转感知和点击体验更流畅
|
||||||
|
setTimeout(() => {
|
||||||
|
closeMobileMenu();
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 初始调用一次设置子菜单切换按钮和更新高亮状态
|
// 初始调用一次设置子菜单切换按钮和更新高亮状态
|
||||||
setupMobileSubmenuToggles();
|
setupMobileSubmenuToggles();
|
||||||
updateMobileMenuHighlight();
|
updateMobileMenuHighlight();
|
||||||
|
setupMobileMenuLinks(); // 添加对移动端链接的点击监听
|
||||||
|
|
||||||
return updateMobileMenuHighlight;
|
// 当菜单显示状态变化时重新绑定链接点击事件
|
||||||
|
addListener(mobileMenuButton, 'click', () => {
|
||||||
|
// 确保每次打开菜单时重新设置链接点击事件
|
||||||
|
setTimeout(() => {
|
||||||
|
setupMobileMenuLinks();
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加点击空白区域关闭菜单和搜索面板的事件监听
|
||||||
|
addListener(document, 'click', (e) => {
|
||||||
|
// 获取事件目标
|
||||||
|
const target = e.target;
|
||||||
|
|
||||||
|
// 处理移动端菜单内的链接点击
|
||||||
|
const menuLink = target.closest('#mobile-menu a');
|
||||||
|
if (menuLink && !e.ctrlKey && !e.metaKey) {
|
||||||
|
// 延迟关闭导航栏
|
||||||
|
setTimeout(() => {
|
||||||
|
closeMobileMenu();
|
||||||
|
}, 50);
|
||||||
|
return; // 如果是菜单链接点击,处理完成后返回
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查移动端菜单是否打开,点击空白区域关闭
|
||||||
|
if (mobileMenu && !mobileMenu.classList.contains('hidden')) {
|
||||||
|
// 检查点击是否在菜单区域外
|
||||||
|
// 菜单按钮和菜单内容都应该排除
|
||||||
|
if (!mobileMenu.contains(target) &&
|
||||||
|
mobileMenuButton && !mobileMenuButton.contains(target)) {
|
||||||
|
closeMobileMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查移动端搜索面板是否打开,点击空白区域关闭
|
||||||
|
if (mobileSearch && !mobileSearch.classList.contains('hidden')) {
|
||||||
|
// 检查点击是否在搜索面板区域外
|
||||||
|
// 搜索按钮和搜索面板内容都应该排除
|
||||||
|
if (!mobileSearch.contains(target) &&
|
||||||
|
mobileSearchButton && !mobileSearchButton.contains(target)) {
|
||||||
|
closeMobileSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始初始化
|
// 开始初始化
|
||||||
|
@ -152,38 +152,42 @@ const {
|
|||||||
|
|
||||||
// 注册清理事件,并保存引用
|
// 注册清理事件,并保存引用
|
||||||
function registerCleanupEvents() {
|
function registerCleanupEvents() {
|
||||||
// 创建一次性事件处理函数
|
// 创建统一的清理处理函数
|
||||||
const beforeSwapHandler = () => {
|
const cleanup = () => {
|
||||||
selfDestruct();
|
selfDestruct();
|
||||||
};
|
};
|
||||||
|
|
||||||
const beforeUnloadHandler = () => {
|
// 定义需要监听的所有清理事件
|
||||||
selfDestruct();
|
const cleanupEventTypes = [
|
||||||
};
|
{ element: document, eventType: "astro:before-swap", options: { once: true } },
|
||||||
|
{ element: window, eventType: "beforeunload", options: { once: true } },
|
||||||
|
{ element: document, eventType: "astro:before-preparation", options: { once: true } },
|
||||||
|
{ element: document, eventType: "page-transition", options: { once: true } }
|
||||||
|
];
|
||||||
|
|
||||||
// 添加清理事件监听器并保存引用
|
// 注册所有清理事件
|
||||||
document.addEventListener("astro:before-swap", beforeSwapHandler, { once: true });
|
cleanupEventTypes.forEach(({ element, eventType, options }) => {
|
||||||
window.addEventListener("beforeunload", beforeUnloadHandler, { once: true });
|
// 添加事件监听
|
||||||
|
element.addEventListener(eventType, cleanup, options);
|
||||||
|
|
||||||
// Astro特有的页面准备事件
|
// 保存事件引用到清理列表
|
||||||
document.addEventListener("astro:before-preparation", beforeSwapHandler, { once: true });
|
cleanupListeners.push({
|
||||||
|
element,
|
||||||
|
eventType,
|
||||||
|
handler: cleanup,
|
||||||
|
options
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// SPA框架可能使用的事件
|
// SPA框架可能使用的事件 - 特殊处理
|
||||||
if (typeof document.addEventListener === 'function') {
|
if (typeof document.addEventListener === 'function') {
|
||||||
document.addEventListener("swup:willReplaceContent", beforeSwapHandler, { once: true });
|
document.addEventListener("swup:willReplaceContent", cleanup, { once: true });
|
||||||
}
|
cleanupListeners.push({
|
||||||
|
element: document,
|
||||||
// 保存清理事件引用,用于完全销毁
|
eventType: "swup:willReplaceContent",
|
||||||
cleanupListeners.push(
|
handler: cleanup,
|
||||||
{ element: document, eventType: "astro:before-swap", handler: beforeSwapHandler, options: { once: true } },
|
options: { once: true }
|
||||||
{ element: window, eventType: "beforeunload", handler: beforeUnloadHandler, options: { once: true } },
|
});
|
||||||
{ element: document, eventType: "astro:before-preparation", handler: beforeSwapHandler, options: { once: true } }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (typeof document.addEventListener === 'function') {
|
|
||||||
cleanupListeners.push(
|
|
||||||
{ element: document, eventType: "swup:willReplaceContent", handler: beforeSwapHandler, options: { once: true } }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -449,7 +449,50 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 修改处理键盘导航的函数,增加上下箭头键切换建议
|
// 添加关闭移动端搜索面板的函数
|
||||||
|
const closeMobileSearchPanel = useCallback(() => {
|
||||||
|
// 查找移动端搜索面板
|
||||||
|
const mobileSearch = document.getElementById('mobile-search');
|
||||||
|
if (mobileSearch && !mobileSearch.classList.contains('hidden')) {
|
||||||
|
// 关闭移动端搜索面板
|
||||||
|
mobileSearch.classList.add('hidden');
|
||||||
|
|
||||||
|
// 更新按钮状态
|
||||||
|
const searchButton = document.getElementById('mobile-search-button');
|
||||||
|
if (searchButton) {
|
||||||
|
searchButton.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 修改navigateToUrl函数,确保在跳转前关闭界面元素
|
||||||
|
const navigateToUrl = useCallback((url: string) => {
|
||||||
|
// 先关闭所有相关UI元素
|
||||||
|
setShowResults(false);
|
||||||
|
setInlineSuggestion(prev => ({ ...prev, visible: false }));
|
||||||
|
closeMobileSearchPanel();
|
||||||
|
|
||||||
|
// 使用短暂延迟确保UI状态先更新
|
||||||
|
setTimeout(() => {
|
||||||
|
// 创建一个临时链接元素
|
||||||
|
const linkEl = document.createElement('a');
|
||||||
|
linkEl.href = url;
|
||||||
|
|
||||||
|
// 设置导航同源属性,确保使用内部导航机制
|
||||||
|
linkEl.setAttribute('data-astro-prefetch', 'true');
|
||||||
|
|
||||||
|
// 添加到DOM中并触发点击
|
||||||
|
document.body.appendChild(linkEl);
|
||||||
|
linkEl.click();
|
||||||
|
|
||||||
|
// 清理临时元素
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(linkEl);
|
||||||
|
}, 100);
|
||||||
|
}, 10); // 很短的延迟,只是让UI状态更新
|
||||||
|
}, [closeMobileSearchPanel]);
|
||||||
|
|
||||||
|
// 修改handleKeyDown函数中的回车键处理逻辑
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
|
||||||
// Tab键处理内联建议补全
|
// Tab键处理内联建议补全
|
||||||
@ -515,6 +558,12 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
if (inlineSuggestion.visible && inlineSuggestion.text) {
|
if (inlineSuggestion.visible && inlineSuggestion.text) {
|
||||||
const suggestionText = inlineSuggestion.text;
|
const suggestionText = inlineSuggestion.text;
|
||||||
|
|
||||||
|
// 立即更新搜索框内容和状态
|
||||||
|
setQuery(suggestionText);
|
||||||
|
if (searchInputRef.current) {
|
||||||
|
searchInputRef.current.value = suggestionText;
|
||||||
|
}
|
||||||
|
|
||||||
// 先检查当前搜索结果中是否有完全匹配的结果
|
// 先检查当前搜索结果中是否有完全匹配的结果
|
||||||
const exactMatchForSuggestion = allItems.find(item =>
|
const exactMatchForSuggestion = allItems.find(item =>
|
||||||
item.title.replace(/<\/?mark>/g, '').toLowerCase() === suggestionText.toLowerCase()
|
item.title.replace(/<\/?mark>/g, '').toLowerCase() === suggestionText.toLowerCase()
|
||||||
@ -524,12 +573,14 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
// 如果有完全匹配的结果,关闭搜索结果面板并导航
|
// 如果有完全匹配的结果,关闭搜索结果面板并导航
|
||||||
setShowResults(false);
|
setShowResults(false);
|
||||||
setInlineSuggestion(prev => ({ ...prev, visible: false }));
|
setInlineSuggestion(prev => ({ ...prev, visible: false }));
|
||||||
window.location.href = exactMatchForSuggestion.url;
|
// 关闭移动端搜索面板
|
||||||
|
closeMobileSearchPanel();
|
||||||
|
// 使用新的导航函数替代直接修改location
|
||||||
|
navigateToUrl(exactMatchForSuggestion.url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 没有完全匹配,先补全建议并导航到第一个结果
|
// 没有完全匹配,先补全建议并导航到第一个结果
|
||||||
// completeInlineSuggestion会自动处理关闭搜索结果的逻辑
|
|
||||||
completeInlineSuggestion(true); // 传入true表示需要导航到第一个结果
|
completeInlineSuggestion(true); // 传入true表示需要导航到第一个结果
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -544,14 +595,20 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
// 找到完全匹配,关闭搜索结果面板并导航
|
// 找到完全匹配,关闭搜索结果面板并导航
|
||||||
setShowResults(false);
|
setShowResults(false);
|
||||||
setInlineSuggestion(prev => ({ ...prev, visible: false }));
|
setInlineSuggestion(prev => ({ ...prev, visible: false }));
|
||||||
window.location.href = exactMatch.url;
|
// 关闭移动端搜索面板
|
||||||
|
closeMobileSearchPanel();
|
||||||
|
// 使用新的导航函数替代直接修改location
|
||||||
|
navigateToUrl(exactMatch.url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有完全匹配,但有搜索结果,关闭搜索结果面板并进入第一个结果
|
// 如果没有完全匹配,但有搜索结果,关闭搜索结果面板并进入第一个结果
|
||||||
setShowResults(false);
|
setShowResults(false);
|
||||||
setInlineSuggestion(prev => ({ ...prev, visible: false }));
|
setInlineSuggestion(prev => ({ ...prev, visible: false }));
|
||||||
window.location.href = allItems[0].url;
|
// 关闭移动端搜索面板
|
||||||
|
closeMobileSearchPanel();
|
||||||
|
// 使用新的导航函数替代直接修改location
|
||||||
|
navigateToUrl(allItems[0].url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -688,7 +745,10 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
|
|
||||||
// 如果需要导航到第一个结果,并且有结果
|
// 如果需要导航到第一个结果,并且有结果
|
||||||
if (shouldNavigateToFirstResult && result.items.length > 0) {
|
if (shouldNavigateToFirstResult && result.items.length > 0) {
|
||||||
window.location.href = result.items[0].url;
|
// 关闭移动端搜索面板
|
||||||
|
closeMobileSearchPanel();
|
||||||
|
// 使用新的导航函数替代直接修改location
|
||||||
|
navigateToUrl(result.items[0].url);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 检查组件是否仍然挂载
|
// 检查组件是否仍然挂载
|
||||||
@ -708,9 +768,9 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
// 保存建议文本
|
// 保存建议文本
|
||||||
const textToComplete = inlineSuggestion.text;
|
const textToComplete = inlineSuggestion.text;
|
||||||
|
|
||||||
// 直接更新DOM和状态
|
// 立即更新搜索框内容和状态
|
||||||
|
setQuery(textToComplete);
|
||||||
if (searchInputRef.current) {
|
if (searchInputRef.current) {
|
||||||
// 立即更新输入框值
|
|
||||||
searchInputRef.current.value = textToComplete;
|
searchInputRef.current.value = textToComplete;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -725,11 +785,11 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
suggestionText: ""
|
suggestionText: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新React状态
|
|
||||||
setQuery(textToComplete);
|
|
||||||
|
|
||||||
// 如果需要导航到第一个结果,保持结果面板显示状态,立即执行搜索
|
// 如果需要导航到第一个结果,保持结果面板显示状态,立即执行搜索
|
||||||
if (shouldNavigateToFirstResult) {
|
if (shouldNavigateToFirstResult) {
|
||||||
|
// 立即关闭搜索面板,然后执行搜索
|
||||||
|
setShowResults(false);
|
||||||
|
closeMobileSearchPanel();
|
||||||
performSearch(textToComplete, false, true);
|
performSearch(textToComplete, false, true);
|
||||||
} else {
|
} else {
|
||||||
// 如果不需要导航,关闭搜索结果面板,但仍然执行搜索以更新结果
|
// 如果不需要导航,关闭搜索结果面板,但仍然执行搜索以更新结果
|
||||||
@ -1151,10 +1211,13 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
<a
|
<a
|
||||||
href={item.url}
|
href={item.url}
|
||||||
className="group block hover:bg-primary-200/80 dark:hover:bg-primary-800/20 hover:shadow-md rounded-lg transition-all duration-200 ease-in-out p-2 -m-2 border border-transparent hover:border-primary-300/60 dark:hover:border-primary-700/30"
|
className="group block hover:bg-primary-200/80 dark:hover:bg-primary-800/20 hover:shadow-md rounded-lg transition-all duration-200 ease-in-out p-2 -m-2 border border-transparent hover:border-primary-300/60 dark:hover:border-primary-700/30"
|
||||||
onClick={() => {
|
data-astro-prefetch="hover"
|
||||||
// 点击搜索结果项时关闭搜索结果面板
|
onClick={(e) => {
|
||||||
setShowResults(false);
|
// 防止默认行为,由我们自己处理导航
|
||||||
setInlineSuggestion(prev => ({ ...prev, visible: false }));
|
e.preventDefault();
|
||||||
|
|
||||||
|
// 使用导航函数处理跳转,它会关闭所有面板
|
||||||
|
navigateToUrl(item.url);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
@ -1235,6 +1298,25 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 为搜索组件添加视图切换事件监听
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePageChange = () => {
|
||||||
|
// 确保在页面切换时关闭所有搜索相关界面
|
||||||
|
setShowResults(false);
|
||||||
|
setInlineSuggestion(prev => ({ ...prev, visible: false }));
|
||||||
|
closeMobileSearchPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听Astro视图转换事件
|
||||||
|
document.addEventListener('astro:after-swap', handlePageChange);
|
||||||
|
document.addEventListener('astro:page-load', handlePageChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('astro:after-swap', handlePageChange);
|
||||||
|
document.removeEventListener('astro:page-load', handlePageChange);
|
||||||
|
};
|
||||||
|
}, [closeMobileSearchPanel]);
|
||||||
|
|
||||||
// 渲染结束
|
// 渲染结束
|
||||||
const returnBlock = (
|
const returnBlock = (
|
||||||
<div className="relative [&_mark]:bg-yellow-200 dark:[&_mark]:bg-yellow-800">
|
<div className="relative [&_mark]:bg-yellow-200 dark:[&_mark]:bg-yellow-800">
|
||||||
|
@ -31,6 +31,7 @@ import "../styles/theme-toggle.css";
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
data-transition-mode={transitionMode}
|
data-transition-mode={transitionMode}
|
||||||
data-transition-duration={transitionDuration}
|
data-transition-duration={transitionDuration}
|
||||||
|
data-theme-transitioning="false"
|
||||||
>
|
>
|
||||||
<!-- 月亮图标 (暗色模式) -->
|
<!-- 月亮图标 (暗色模式) -->
|
||||||
<svg
|
<svg
|
||||||
@ -78,6 +79,9 @@ import "../styles/theme-toggle.css";
|
|||||||
let transitionTimeout = null;
|
let transitionTimeout = null;
|
||||||
let rippleTimeout = null;
|
let rippleTimeout = null;
|
||||||
|
|
||||||
|
// 局部存储当前过渡的引用
|
||||||
|
let currentTransition = null;
|
||||||
|
|
||||||
// 主题过渡模式
|
// 主题过渡模式
|
||||||
const TRANSITION_MODES = {
|
const TRANSITION_MODES = {
|
||||||
EXPAND: 'expand', // 扩散模式
|
EXPAND: 'expand', // 扩散模式
|
||||||
@ -96,8 +100,35 @@ import "../styles/theme-toggle.css";
|
|||||||
// 动画配置(毫秒)
|
// 动画配置(毫秒)
|
||||||
const ANIMATION_BUFFER = 100; // 动画缓冲时间
|
const ANIMATION_BUFFER = 100; // 动画缓冲时间
|
||||||
const TOTAL_TRANSITION_TIME = ANIMATION_DURATION + ANIMATION_BUFFER; // 总过渡时间
|
const TOTAL_TRANSITION_TIME = ANIMATION_DURATION + ANIMATION_BUFFER; // 总过渡时间
|
||||||
// 防抖时间动态计算,始终比动画时间略长一些
|
// 简化的冷却时间追踪 - 使用单一的上次切换时间
|
||||||
const DEBOUNCE_TIME = TOTAL_TRANSITION_TIME + 200; // 防抖时间比总过渡时间多200ms
|
let lastThemeToggleTime = 0;
|
||||||
|
const COOLDOWN_TIME = TOTAL_TRANSITION_TIME + 200; // 防抖冷却时间
|
||||||
|
|
||||||
|
// 辅助函数:设置/获取按钮的过渡状态
|
||||||
|
function setTransitioning(button, isTransitioning) {
|
||||||
|
if (!button) return;
|
||||||
|
button.dataset.themeTransitioning = isTransitioning.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTransitioning(button) {
|
||||||
|
if (!button) return false;
|
||||||
|
return button.dataset.themeTransitioning === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否可以执行切换(冷却中检查)
|
||||||
|
function canToggleTheme() {
|
||||||
|
const now = Date.now();
|
||||||
|
// 如果距离上次切换时间小于冷却时间,不允许切换
|
||||||
|
if (now - lastThemeToggleTime < COOLDOWN_TIME) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录切换时间
|
||||||
|
function recordToggleTime() {
|
||||||
|
lastThemeToggleTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
// 从本地存储获取主题过渡模式
|
// 从本地存储获取主题过渡模式
|
||||||
function getThemeTransitionMode() {
|
function getThemeTransitionMode() {
|
||||||
@ -122,13 +153,13 @@ import "../styles/theme-toggle.css";
|
|||||||
// 统一的清理函数,执行完整清理并自销毁
|
// 统一的清理函数,执行完整清理并自销毁
|
||||||
function selfDestruct() {
|
function selfDestruct() {
|
||||||
// 0. 取消正在进行的transition
|
// 0. 取消正在进行的transition
|
||||||
if (window._themeTransition && typeof window._themeTransition.skipTransition === 'function') {
|
if (currentTransition && typeof currentTransition.skipTransition === 'function') {
|
||||||
try {
|
try {
|
||||||
window._themeTransition.skipTransition();
|
currentTransition.skipTransition();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 忽略AbortError,这是正常现象
|
// 忽略AbortError,这是正常现象
|
||||||
} finally {
|
} finally {
|
||||||
window._themeTransition = null;
|
currentTransition = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,7 +190,15 @@ import "../styles/theme-toggle.css";
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5. 移除普通事件监听器
|
// 5. 重置全局状态
|
||||||
|
lastThemeToggleTime = 0;
|
||||||
|
|
||||||
|
// 6. 重置所有主题切换按钮的状态
|
||||||
|
document.querySelectorAll("#theme-toggle-button").forEach(button => {
|
||||||
|
setTransitioning(button, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7. 移除普通事件监听器
|
||||||
allListeners.forEach(({ element, eventType, handler, options }) => {
|
allListeners.forEach(({ element, eventType, handler, options }) => {
|
||||||
try {
|
try {
|
||||||
element.removeEventListener(eventType, handler, options);
|
element.removeEventListener(eventType, handler, options);
|
||||||
@ -171,7 +210,7 @@ import "../styles/theme-toggle.css";
|
|||||||
// 清空监听器数组
|
// 清空监听器数组
|
||||||
allListeners.length = 0;
|
allListeners.length = 0;
|
||||||
|
|
||||||
// 6. 最后移除清理事件监听器自身
|
// 8. 最后移除清理事件监听器自身
|
||||||
cleanupListeners.forEach(({ element, eventType, handler, options }) => {
|
cleanupListeners.forEach(({ element, eventType, handler, options }) => {
|
||||||
try {
|
try {
|
||||||
element.removeEventListener(eventType, handler, options);
|
element.removeEventListener(eventType, handler, options);
|
||||||
@ -186,24 +225,31 @@ import "../styles/theme-toggle.css";
|
|||||||
|
|
||||||
// 注册清理事件,并保存引用
|
// 注册清理事件,并保存引用
|
||||||
function registerCleanupEvents() {
|
function registerCleanupEvents() {
|
||||||
// 创建一次性事件处理函数
|
// 创建统一的清理处理函数
|
||||||
const beforeSwapHandler = () => {
|
const cleanup = () => {
|
||||||
selfDestruct();
|
selfDestruct();
|
||||||
};
|
};
|
||||||
|
|
||||||
const beforeUnloadHandler = () => {
|
// 定义需要监听的所有清理事件
|
||||||
selfDestruct();
|
const cleanupEventTypes = [
|
||||||
};
|
{ element: document, eventType: "astro:before-swap", options: { once: true } },
|
||||||
|
{ element: window, eventType: "beforeunload", options: { once: true } },
|
||||||
|
{ element: document, eventType: "page-transition", options: { once: true } }
|
||||||
|
];
|
||||||
|
|
||||||
// 添加清理事件监听器并保存引用
|
// 注册所有清理事件
|
||||||
document.addEventListener("astro:before-swap", beforeSwapHandler, { once: true });
|
cleanupEventTypes.forEach(({ element, eventType, options }) => {
|
||||||
window.addEventListener("beforeunload", beforeUnloadHandler, { once: true });
|
// 添加事件监听
|
||||||
|
element.addEventListener(eventType, cleanup, options);
|
||||||
|
|
||||||
// 保存清理事件引用,用于完全销毁
|
// 保存事件引用到清理列表
|
||||||
cleanupListeners.push(
|
cleanupListeners.push({
|
||||||
{ element: document, eventType: "astro:before-swap", handler: beforeSwapHandler, options: { once: true } },
|
element,
|
||||||
{ element: window, eventType: "beforeunload", handler: beforeUnloadHandler, options: { once: true } }
|
eventType,
|
||||||
);
|
handler: cleanup,
|
||||||
|
options
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建波纹动画元素
|
// 创建波纹动画元素
|
||||||
@ -269,16 +315,16 @@ import "../styles/theme-toggle.css";
|
|||||||
// 使用View Transitions API创建全屏过渡效果
|
// 使用View Transitions API创建全屏过渡效果
|
||||||
function createViewTransition(callback, x, y, fromTheme, toTheme, transitionMode) {
|
function createViewTransition(callback, x, y, fromTheme, toTheme, transitionMode) {
|
||||||
// 如果已有正在进行的过渡,先取消它
|
// 如果已有正在进行的过渡,先取消它
|
||||||
if (window._themeTransition) {
|
if (currentTransition) {
|
||||||
try {
|
try {
|
||||||
if (typeof window._themeTransition.skipTransition === 'function') {
|
if (typeof currentTransition.skipTransition === 'function') {
|
||||||
window._themeTransition.skipTransition();
|
currentTransition.skipTransition();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 忽略取消先前过渡的错误
|
// 忽略取消先前过渡的错误
|
||||||
} finally {
|
} finally {
|
||||||
// 无论成功与否,都清除引用
|
// 无论成功与否,都清除引用
|
||||||
window._themeTransition = null;
|
currentTransition = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -416,7 +462,7 @@ import "../styles/theme-toggle.css";
|
|||||||
const transition = document.startViewTransition(safeCallback);
|
const transition = document.startViewTransition(safeCallback);
|
||||||
|
|
||||||
// 存储transition引用以便清理
|
// 存储transition引用以便清理
|
||||||
window._themeTransition = transition;
|
currentTransition = transition;
|
||||||
|
|
||||||
// 生成动画需要的SVG资源
|
// 生成动画需要的SVG资源
|
||||||
const gradientOffset = 0.75;
|
const gradientOffset = 0.75;
|
||||||
@ -703,7 +749,7 @@ import "../styles/theme-toggle.css";
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
// 移除主题过渡标记类
|
// 移除主题过渡标记类
|
||||||
document.documentElement.classList.remove('theme-transition-active');
|
document.documentElement.classList.remove('theme-transition-active');
|
||||||
window._themeTransition = null;
|
currentTransition = null;
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (error.name !== 'AbortError') {
|
if (error.name !== 'AbortError') {
|
||||||
@ -713,7 +759,7 @@ import "../styles/theme-toggle.css";
|
|||||||
document.documentElement.classList.remove('theme-transition-active');
|
document.documentElement.classList.remove('theme-transition-active');
|
||||||
|
|
||||||
// 清除引用
|
// 清除引用
|
||||||
window._themeTransition = null;
|
currentTransition = null;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`主题切换错误:`, error);
|
console.error(`主题切换错误:`, error);
|
||||||
@ -735,14 +781,6 @@ import "../styles/theme-toggle.css";
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 防抖配置 - 使用前面定义的动态计算值
|
|
||||||
let lastClickTime = 0;
|
|
||||||
let transitioning = false;
|
|
||||||
|
|
||||||
// 将transitioning状态暴露给全局,便于其他脚本检查
|
|
||||||
window._themeTransitioning = false;
|
|
||||||
window._lastThemeToggleClickTime = 0;
|
|
||||||
|
|
||||||
// 状态清理函数 - 在页面切换、错误和超时情况下调用
|
// 状态清理函数 - 在页面切换、错误和超时情况下调用
|
||||||
const resetThemeToggleState = () => {
|
const resetThemeToggleState = () => {
|
||||||
if (transitionTimeout) {
|
if (transitionTimeout) {
|
||||||
@ -750,8 +788,13 @@ import "../styles/theme-toggle.css";
|
|||||||
transitionTimeout = null;
|
transitionTimeout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
transitioning = false;
|
// 重置全局切换时间状态
|
||||||
window._themeTransitioning = false;
|
lastThemeToggleTime = 0;
|
||||||
|
|
||||||
|
// 重置所有按钮的状态
|
||||||
|
themeToggleButtons.forEach(button => {
|
||||||
|
setTransitioning(button, false);
|
||||||
|
});
|
||||||
|
|
||||||
// 移除所有残留的波纹效果
|
// 移除所有残留的波纹效果
|
||||||
document.querySelectorAll(".theme-ripple").forEach(ripple => {
|
document.querySelectorAll(".theme-ripple").forEach(ripple => {
|
||||||
@ -784,10 +827,11 @@ import "../styles/theme-toggle.css";
|
|||||||
// 不处理未冒泡到document的事件
|
// 不处理未冒泡到document的事件
|
||||||
if (!e || !e.target) return;
|
if (!e || !e.target) return;
|
||||||
|
|
||||||
// 如果点击事件来自于主题切换相关元素,且正在过渡中,拦截所有后续处理
|
// 获取点击的按钮元素
|
||||||
if (window._themeTransitioning &&
|
const button = e.target.closest('#theme-toggle-button');
|
||||||
(e.target.closest('#theme-toggle-button') ||
|
|
||||||
e.target.closest('#theme-toggle-container'))) {
|
// 如果点击按钮,且按钮正在过渡中或在冷却期内,拦截后续处理
|
||||||
|
if (button && (isTransitioning(button) || !canToggleTheme())) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
@ -825,17 +869,6 @@ import "../styles/theme-toggle.css";
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 防抖函数 - 确保在指定时间内只执行一次
|
|
||||||
const isThrottled = () => {
|
|
||||||
const now = Date.now();
|
|
||||||
if (transitioning || now - lastClickTime < DEBOUNCE_TIME) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
lastClickTime = now;
|
|
||||||
window._lastThemeToggleClickTime = now;
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 为所有触发点提供统一的处理函数
|
// 为所有触发点提供统一的处理函数
|
||||||
const handleThemeToggle = (e, targetButton) => {
|
const handleThemeToggle = (e, targetButton) => {
|
||||||
// 防止事件默认行为和冒泡,无论如何都要先阻止
|
// 防止事件默认行为和冒泡,无论如何都要先阻止
|
||||||
@ -846,23 +879,36 @@ import "../styles/theme-toggle.css";
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 第1层保护:全局事件防抖
|
// 确保有有效的按钮参数,必要时从事件中获取
|
||||||
if (isThrottled()) {
|
if (!targetButton && e) {
|
||||||
|
targetButton = e.target.closest('#theme-toggle-button');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保有按钮可以操作
|
||||||
|
if (!targetButton) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 第2层保护:transitioning状态检查
|
// 保护:全局冷却期检查
|
||||||
if (window._themeTransitioning) {
|
if (!canToggleTheme()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 第3层保护:视图过渡检查
|
// 保护:按钮过渡中检查
|
||||||
if (window._themeTransition) {
|
if (isTransitioning(targetButton)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 立即设置transitioning状态,阻止后续点击
|
// 保护:视图过渡检查
|
||||||
window._themeTransitioning = true;
|
if (currentTransition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录此次切换时间
|
||||||
|
recordToggleTime();
|
||||||
|
|
||||||
|
// 立即设置过渡状态,阻止后续点击
|
||||||
|
setTransitioning(targetButton, true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 计算点击坐标或使用按钮中心坐标
|
// 计算点击坐标或使用按钮中心坐标
|
||||||
@ -933,7 +979,7 @@ import "../styles/theme-toggle.css";
|
|||||||
).then(() => {
|
).then(() => {
|
||||||
// 过渡完成后恢复状态
|
// 过渡完成后恢复状态
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window._themeTransitioning = false;
|
setTransitioning(targetButton, false);
|
||||||
}, ANIMATION_BUFFER); // 添加缓冲时间
|
}, ANIMATION_BUFFER); // 添加缓冲时间
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
// 出现错误时强制执行主题切换以确保功能可用
|
// 出现错误时强制执行主题切换以确保功能可用
|
||||||
@ -944,13 +990,13 @@ import "../styles/theme-toggle.css";
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window._themeTransitioning = false;
|
setTransitioning(targetButton, false);
|
||||||
}, ANIMATION_BUFFER);
|
}, ANIMATION_BUFFER);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 设置防抖保底 - 防止transition.finished不触发导致状态卡死
|
// 设置防抖保底 - 防止transition.finished不触发导致状态卡死
|
||||||
transitionTimeout = setTimeout(() => {
|
transitionTimeout = setTimeout(() => {
|
||||||
window._themeTransitioning = false;
|
setTransitioning(targetButton, false);
|
||||||
}, TOTAL_TRANSITION_TIME + 200); // 总过渡时间加额外缓冲
|
}, TOTAL_TRANSITION_TIME + 200); // 总过渡时间加额外缓冲
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 即使发生错误,也要确保主题能切换
|
// 即使发生错误,也要确保主题能切换
|
||||||
@ -976,7 +1022,7 @@ import "../styles/theme-toggle.css";
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 确保状态被重置
|
// 确保状态被重置
|
||||||
window._themeTransitioning = false;
|
setTransitioning(targetButton, false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -994,6 +1040,9 @@ import "../styles/theme-toggle.css";
|
|||||||
|
|
||||||
// 为每个按钮添加事件
|
// 为每个按钮添加事件
|
||||||
themeToggleButtons.forEach(button => {
|
themeToggleButtons.forEach(button => {
|
||||||
|
// 确保初始化状态
|
||||||
|
setTransitioning(button, false);
|
||||||
|
|
||||||
// 点击事件 - 使用捕获模式
|
// 点击事件 - 使用捕获模式
|
||||||
addListener(button, "click", (e) => handleThemeToggle(e, button), { capture: true });
|
addListener(button, "click", (e) => handleThemeToggle(e, button), { capture: true });
|
||||||
|
|
||||||
@ -1007,39 +1056,6 @@ import "../styles/theme-toggle.css";
|
|||||||
addListener(button, "keydown", keydownHandler);
|
addListener(button, "keydown", keydownHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理移动端主题切换容器 - 使用同样的逻辑处理
|
|
||||||
const themeToggleContainer = document.getElementById("theme-toggle-container");
|
|
||||||
if (themeToggleContainer) {
|
|
||||||
const containerClickHandler = (e) => {
|
|
||||||
// 如果点击的是按钮内部,让按钮自己处理
|
|
||||||
if (e.target.closest("#theme-toggle-button")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 找到容器内的主题切换按钮
|
|
||||||
const button = themeToggleContainer.querySelector("#theme-toggle-button");
|
|
||||||
if (button) {
|
|
||||||
// 调用统一的处理函数
|
|
||||||
handleThemeToggle(e, button);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 使用capture模式确保事件在捕获阶段就被处理
|
|
||||||
addListener(themeToggleContainer, "click", containerClickHandler, { capture: true });
|
|
||||||
|
|
||||||
// 为移动端容器添加关闭移动端菜单的功能
|
|
||||||
const closeMenuHandler = (e) => {
|
|
||||||
// 检查是否有closeMobileMenu全局函数(来自Header.astro)
|
|
||||||
if (typeof window.closeMobileMenu === 'function') {
|
|
||||||
// 这个函数会在点击主题切换容器后被调用,无论是否真正触发主题切换
|
|
||||||
window.closeMobileMenu();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 使用冒泡阶段,确保在主题切换处理后执行
|
|
||||||
addListener(themeToggleContainer, "click", closeMenuHandler, { capture: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化主题
|
// 初始化主题
|
||||||
initializeTheme();
|
initializeTheme();
|
||||||
}
|
}
|
||||||
@ -1051,7 +1067,6 @@ import "../styles/theme-toggle.css";
|
|||||||
|
|
||||||
// 设置主题切换功能
|
// 设置主题切换功能
|
||||||
setupThemeToggle();
|
setupThemeToggle();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断DOM是否已加载
|
// 判断DOM是否已加载
|
||||||
|
@ -561,7 +561,6 @@ const tableOfContents = generateTableOfContents(headings);
|
|||||||
class="scrollbar-thin scrollbar-thumb-primary-200 dark:scrollbar-thumb-primary-800 scrollbar-track-transparent"
|
class="scrollbar-thin scrollbar-thumb-primary-200 dark:scrollbar-thumb-primary-800 scrollbar-track-transparent"
|
||||||
set:html={tableOfContents}
|
set:html={tableOfContents}
|
||||||
>
|
>
|
||||||
<!-- 目录内容在服务端生成 -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -628,38 +627,31 @@ const tableOfContents = generateTableOfContents(headings);
|
|||||||
|
|
||||||
// 注册清理事件,并保存引用
|
// 注册清理事件,并保存引用
|
||||||
function registerCleanupEvents() {
|
function registerCleanupEvents() {
|
||||||
// 创建一次性事件处理函数
|
// 创建统一的清理处理函数
|
||||||
const beforeSwapHandler = () => {
|
const cleanup = () => {
|
||||||
selfDestruct();
|
selfDestruct();
|
||||||
};
|
};
|
||||||
|
|
||||||
const beforeUnloadHandler = () => {
|
// 定义需要监听的所有清理事件
|
||||||
selfDestruct();
|
const cleanupEventTypes = [
|
||||||
};
|
{ element: document, eventType: "astro:before-swap", options: { once: true } },
|
||||||
|
{ element: window, eventType: "beforeunload", options: { once: true } },
|
||||||
|
{ element: document, eventType: "page-transition", options: { once: true } }
|
||||||
|
];
|
||||||
|
|
||||||
// 添加清理事件监听器并保存引用
|
// 注册所有清理事件
|
||||||
document.addEventListener("astro:before-swap", beforeSwapHandler, {
|
cleanupEventTypes.forEach(({ element, eventType, options }) => {
|
||||||
once: true,
|
// 添加事件监听
|
||||||
});
|
element.addEventListener(eventType, cleanup, options);
|
||||||
window.addEventListener("beforeunload", beforeUnloadHandler, {
|
|
||||||
once: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 保存清理事件引用,用于完全销毁
|
// 保存事件引用到清理列表
|
||||||
cleanupListeners.push(
|
cleanupListeners.push({
|
||||||
{
|
element,
|
||||||
element: document,
|
eventType,
|
||||||
eventType: "astro:before-swap",
|
handler: cleanup,
|
||||||
handler: beforeSwapHandler,
|
options
|
||||||
options: { once: true },
|
});
|
||||||
},
|
});
|
||||||
{
|
|
||||||
element: window,
|
|
||||||
eventType: "beforeunload",
|
|
||||||
handler: beforeUnloadHandler,
|
|
||||||
options: { once: true },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化所有功能
|
// 初始化所有功能
|
||||||
|
@ -5,6 +5,8 @@ import SwupFragmentPlugin from '@swup/fragment-plugin';
|
|||||||
import SwupHeadPlugin from '@swup/head-plugin';
|
import SwupHeadPlugin from '@swup/head-plugin';
|
||||||
// 添加预加载插件 - 优化导航体验
|
// 添加预加载插件 - 优化导航体验
|
||||||
import SwupPreloadPlugin from '@swup/preload-plugin';
|
import SwupPreloadPlugin from '@swup/preload-plugin';
|
||||||
|
// 添加Scripts插件 - 确保页面转场后脚本能重新执行
|
||||||
|
import SwupScriptsPlugin from '@swup/scripts-plugin';
|
||||||
|
|
||||||
// 检查是否是文章相关页面
|
// 检查是否是文章相关页面
|
||||||
function isArticlePage() {
|
function isArticlePage() {
|
||||||
@ -12,49 +14,38 @@ function isArticlePage() {
|
|||||||
return path.includes('/articles') || path.includes('/filtered');
|
return path.includes('/articles') || path.includes('/filtered');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 为元素应用动画样式
|
// 为元素设置过渡状态
|
||||||
function applyAnimationStyles(element, className, duration = 300) {
|
function setElementTransition(element) {
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
|
|
||||||
// 添加动画类
|
|
||||||
element.classList.add(className);
|
|
||||||
|
|
||||||
// 设置过渡属性
|
|
||||||
element.style.transition = 'opacity 0.3s ease';
|
|
||||||
element.style.animationDuration = '0.3s';
|
|
||||||
element.style.opacity = '1';
|
|
||||||
|
|
||||||
// 添加data-swup属性标记
|
// 添加data-swup属性标记
|
||||||
element.setAttribute('data-swup-transition', 'true');
|
element.setAttribute('data-swup-transition', 'true');
|
||||||
element.setAttribute('data-swup-animation-duration', duration.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置元素淡入/淡出效果
|
// 设置元素淡入/淡出效果
|
||||||
function setElementOpacity(element, opacity) {
|
function setElementOpacity(element, opacity) {
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
element.style.opacity = opacity.toString();
|
element.style.opacity = opacity.toString();
|
||||||
if (opacity === 0) {
|
|
||||||
element.style.transition = 'opacity 0.3s ease';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直接应用样式到元素上
|
// 应用过渡效果到相关元素
|
||||||
function applyStylesDirectly() {
|
function applyTransitions() {
|
||||||
// 应用到主容器 - 只在非文章页面
|
// 应用到主容器 - 只在非文章页面
|
||||||
const mainElement = document.querySelector('main');
|
const mainElement = document.querySelector('main');
|
||||||
if (mainElement) {
|
if (mainElement) {
|
||||||
mainElement.classList.add('transition-fade');
|
mainElement.classList.add('transition-fade');
|
||||||
|
|
||||||
// 只有在非文章页面时,才为main添加必要的动画样式
|
// 只有在非文章页面时,才为main添加必要的过渡标记
|
||||||
if (!isArticlePage()) {
|
if (!isArticlePage()) {
|
||||||
applyAnimationStyles(mainElement, 'transition-fade');
|
setElementTransition(mainElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用到文章内容 - 只在文章页面
|
// 应用到文章内容 - 只在文章页面
|
||||||
const articleContent = document.querySelector('#article-content');
|
const articleContent = document.querySelector('#article-content');
|
||||||
if (articleContent) {
|
if (articleContent) {
|
||||||
applyAnimationStyles(articleContent, 'swup-transition-article');
|
articleContent.classList.add('swup-transition-article');
|
||||||
|
setElementTransition(articleContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,13 +60,13 @@ function getActiveElement() {
|
|||||||
|
|
||||||
// 在DOM加载完成后初始化
|
// 在DOM加载完成后初始化
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// 直接应用样式
|
// 应用过渡效果
|
||||||
applyStylesDirectly();
|
applyTransitions();
|
||||||
|
|
||||||
// 创建Swup实例
|
// 创建Swup实例
|
||||||
const swup = new Swup({
|
const swup = new Swup({
|
||||||
// Swup的基本配置
|
// Swup的基本配置
|
||||||
animationSelector: '[class*="transition-"], #article-content, .swup-transition-article, main',
|
animationSelector: '[class*="transition-"], .swup-transition-article, #article-content',
|
||||||
cache: true,
|
cache: true,
|
||||||
containers: ['main'],
|
containers: ['main'],
|
||||||
animationScope: 'html', // 确保动画状态类添加到html元素
|
animationScope: 'html', // 确保动画状态类添加到html元素
|
||||||
@ -86,6 +77,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
plugins: [] // 手动添加插件以控制顺序
|
plugins: [] // 手动添加插件以控制顺序
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 发送页面转换事件 - 自定义全局事件
|
||||||
|
function sendPageTransitionEvent() {
|
||||||
|
// 创建自定义事件并触发
|
||||||
|
const event = new CustomEvent('page-transition', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: false,
|
||||||
|
detail: { timestamp: Date.now() }
|
||||||
|
});
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
// 添加预加载插件 - 代替原有的预加载功能
|
// 添加预加载插件 - 代替原有的预加载功能
|
||||||
const preloadPlugin = new SwupPreloadPlugin({
|
const preloadPlugin = new SwupPreloadPlugin({
|
||||||
// 最多同时预加载5个链接
|
// 最多同时预加载5个链接
|
||||||
@ -112,37 +114,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const headPlugin = new SwupHeadPlugin();
|
const headPlugin = new SwupHeadPlugin();
|
||||||
swup.use(headPlugin);
|
swup.use(headPlugin);
|
||||||
|
|
||||||
|
// 添加Scripts插件 - 确保页面转场后脚本能重新执行
|
||||||
|
const scriptsPlugin = new SwupScriptsPlugin({
|
||||||
|
// 以下选项确定哪些脚本会被重新执行
|
||||||
|
head: true, // 重新执行head中的脚本
|
||||||
|
body: true, // 重新执行body中的脚本
|
||||||
|
optin: false, // 是否只执行带有[data-swup-reload-script]属性的脚本
|
||||||
|
oprout: false, // 是否排除带有[data-no-swup]属性的脚本
|
||||||
|
once: true // 是否每个脚本只执行一次
|
||||||
|
});
|
||||||
|
swup.use(scriptsPlugin);
|
||||||
|
|
||||||
// 创建Fragment插件 - 简化规则避免匹配问题
|
// 创建Fragment插件 - 简化规则避免匹配问题
|
||||||
const fragmentPlugin = new SwupFragmentPlugin({
|
const fragmentPlugin = new SwupFragmentPlugin({
|
||||||
debug: false, // 关闭调试模式
|
debug: false, // 关闭调试模式
|
||||||
// 简化规则,确保基本匹配
|
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
// 文章页面之间的导航
|
|
||||||
name: 'article-pages',
|
name: 'article-pages',
|
||||||
from: '/articles', // 简化匹配规则
|
from: ['/articles', '/filtered'],
|
||||||
to: '/articles',
|
to: ['/articles', '/filtered'],
|
||||||
containers: ['#article-content']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 从文章到筛选页面
|
|
||||||
name: 'article-to-filter',
|
|
||||||
from: '/articles',
|
|
||||||
to: '/filtered',
|
|
||||||
containers: ['#article-content']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 从筛选到文章页面
|
|
||||||
name: 'filter-to-article',
|
|
||||||
from: '/filtered',
|
|
||||||
to: '/articles',
|
|
||||||
containers: ['#article-content']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 筛选页面内部导航
|
|
||||||
name: 'filter-pages',
|
|
||||||
from: '/filtered',
|
|
||||||
to: '/filtered',
|
|
||||||
containers: ['#article-content']
|
containers: ['#article-content']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -156,10 +146,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
swup.preloadLinks();
|
swup.preloadLinks();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
// 强制应用动画样式到特定元素
|
// 重新设置过渡元素
|
||||||
function setupTransition() {
|
function setupTransition() {
|
||||||
// 直接应用样式 - 会根据页面类型自动选择正确的元素
|
// 应用过渡效果
|
||||||
applyStylesDirectly();
|
applyTransitions();
|
||||||
|
|
||||||
// 确保初始状态正确
|
// 确保初始状态正确
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -184,6 +174,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// 监听动画开始和结束
|
// 监听动画开始和结束
|
||||||
swup.hooks.on('animation:out:start', () => {
|
swup.hooks.on('animation:out:start', () => {
|
||||||
|
// 发送页面切换事件
|
||||||
|
sendPageTransitionEvent();
|
||||||
|
|
||||||
// 获取并淡出当前活跃元素
|
// 获取并淡出当前活跃元素
|
||||||
const activeElement = getActiveElement();
|
const activeElement = getActiveElement();
|
||||||
setElementOpacity(activeElement, 0);
|
setElementOpacity(activeElement, 0);
|
||||||
@ -200,6 +193,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// 添加手动强制动画事件
|
// 添加手动强制动画事件
|
||||||
document.addEventListener('swup:willReplaceContent', () => {
|
document.addEventListener('swup:willReplaceContent', () => {
|
||||||
|
// 发送页面切换事件
|
||||||
|
sendPageTransitionEvent();
|
||||||
|
|
||||||
// 获取并淡出当前活跃元素
|
// 获取并淡出当前活跃元素
|
||||||
const activeElement = getActiveElement();
|
const activeElement = getActiveElement();
|
||||||
setElementOpacity(activeElement, 0);
|
setElementOpacity(activeElement, 0);
|
||||||
@ -214,11 +210,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// 先设置透明
|
// 先设置透明
|
||||||
setElementOpacity(activeElement, 0);
|
setElementOpacity(activeElement, 0);
|
||||||
|
|
||||||
// 重新应用适当的类和属性
|
// 重新应用适当的类
|
||||||
if (isArticlePage() && activeElement.id === 'article-content') {
|
if (isArticlePage() && activeElement.id === 'article-content') {
|
||||||
applyAnimationStyles(activeElement, 'swup-transition-article');
|
activeElement.classList.add('swup-transition-article');
|
||||||
|
setElementTransition(activeElement);
|
||||||
} else if (!isArticlePage() && activeElement.tagName.toLowerCase() === 'main') {
|
} else if (!isArticlePage() && activeElement.tagName.toLowerCase() === 'main') {
|
||||||
applyAnimationStyles(activeElement, 'transition-fade');
|
activeElement.classList.add('transition-fade');
|
||||||
|
setElementTransition(activeElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 延迟后淡入
|
// 延迟后淡入
|
||||||
@ -229,6 +227,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// 监听URL变化以更新动画行为
|
// 监听URL变化以更新动画行为
|
||||||
swup.hooks.on('visit:start', (visit) => {
|
swup.hooks.on('visit:start', (visit) => {
|
||||||
|
// 发送页面切换事件
|
||||||
|
sendPageTransitionEvent();
|
||||||
|
|
||||||
// 检查目标URL是否为文章相关页面
|
// 检查目标URL是否为文章相关页面
|
||||||
const isTargetArticlePage = visit.to.url.includes('/articles') || visit.to.url.includes('/filtered');
|
const isTargetArticlePage = visit.to.url.includes('/articles') || visit.to.url.includes('/filtered');
|
||||||
const isCurrentArticlePage = isArticlePage();
|
const isCurrentArticlePage = isArticlePage();
|
||||||
@ -244,62 +245,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
else if (!isCurrentArticlePage && isTargetArticlePage) {
|
else if (!isCurrentArticlePage && isTargetArticlePage) {
|
||||||
const mainElement = document.querySelector('main');
|
const mainElement = document.querySelector('main');
|
||||||
if (mainElement) {
|
if (mainElement) {
|
||||||
// 移除main的动画效果
|
// 移除main的过渡效果
|
||||||
mainElement.style.transition = '';
|
mainElement.style.transition = '';
|
||||||
mainElement.style.opacity = '1';
|
mainElement.style.opacity = '1';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fragment导航后手动更新面包屑
|
|
||||||
function updateBreadcrumb(url) {
|
|
||||||
// 1. 获取新页面的HTML以提取面包屑
|
|
||||||
fetch(url)
|
|
||||||
.then(response => response.text())
|
|
||||||
.then(html => {
|
|
||||||
// 创建一个临时的DOM解析新页面
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const newDoc = parser.parseFromString(html, 'text/html');
|
|
||||||
|
|
||||||
// 获取新页面的面包屑容器 - 使用更精确的选择器
|
|
||||||
const newBreadcrumbContainer = newDoc.querySelector('.bg-white.dark\\:bg-gray-800.rounded-xl.mb-4, .bg-white.dark\\:bg-gray-800.rounded-xl.p-4');
|
|
||||||
|
|
||||||
// 获取当前页面的面包屑容器
|
|
||||||
const currentBreadcrumbContainer = document.querySelector('.bg-white.dark\\:bg-gray-800.rounded-xl.mb-4, .bg-white.dark\\:bg-gray-800.rounded-xl.p-4');
|
|
||||||
|
|
||||||
if (newBreadcrumbContainer && currentBreadcrumbContainer) {
|
|
||||||
// 更新面包屑内容
|
|
||||||
currentBreadcrumbContainer.innerHTML = newBreadcrumbContainer.innerHTML;
|
|
||||||
|
|
||||||
// 重新初始化面包屑相关脚本
|
|
||||||
const breadcrumbScript = currentBreadcrumbContainer.querySelector('script');
|
|
||||||
if (breadcrumbScript) {
|
|
||||||
const newScript = document.createElement('script');
|
|
||||||
newScript.textContent = breadcrumbScript.textContent;
|
|
||||||
breadcrumbScript.parentNode.replaceChild(newScript, breadcrumbScript);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
// 出错时静默处理
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在每次页面转换结束后更新面包屑
|
|
||||||
swup.hooks.on('visit:end', (visit) => {
|
|
||||||
// 所有导航都更新面包屑
|
|
||||||
updateBreadcrumb(visit.to.url);
|
|
||||||
|
|
||||||
// 确保在页面加载完成后元素有正确样式
|
|
||||||
setTimeout(() => {
|
|
||||||
setupTransition();
|
|
||||||
|
|
||||||
// 加载完成后重新扫描预加载链接
|
|
||||||
setTimeout(() => {
|
|
||||||
swup.preloadLinks();
|
|
||||||
}, 500);
|
|
||||||
}, 50);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听Fragment插件是否成功应用
|
// 监听Fragment插件是否成功应用
|
||||||
document.addEventListener('swup:fragmentReplaced', () => {
|
document.addEventListener('swup:fragmentReplaced', () => {
|
||||||
@ -311,10 +263,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// 在页面卸载和Astro视图转换时清理资源
|
// 在页面卸载和Astro视图转换时清理资源
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
|
// 发送页面切换事件
|
||||||
|
sendPageTransitionEvent();
|
||||||
|
|
||||||
if (swup) {
|
if (swup) {
|
||||||
swup.unuse(fragmentPlugin);
|
swup.unuse(fragmentPlugin);
|
||||||
swup.unuse(headPlugin);
|
swup.unuse(headPlugin);
|
||||||
swup.unuse(preloadPlugin);
|
swup.unuse(preloadPlugin);
|
||||||
|
swup.unuse(scriptsPlugin); // 也需要卸载Scripts插件
|
||||||
swup.destroy();
|
swup.destroy();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@import "./swup-transitions.css";
|
||||||
|
|
||||||
/* 定义深色模式选择器 */
|
/* 定义深色模式选择器 */
|
||||||
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
||||||
|
36
src/styles/swup-transitions.css
Normal file
36
src/styles/swup-transitions.css
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/* Swup页面过渡动画样式 */
|
||||||
|
|
||||||
|
/* 基础过渡效果 */
|
||||||
|
.transition-fade {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
animation-duration: 0.3s;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文章内容过渡效果 */
|
||||||
|
.swup-transition-article {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
animation-duration: 0.3s;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 直接为article-content元素设置动画 */
|
||||||
|
#article-content {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
animation-duration: 0.3s;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 淡出状态 */
|
||||||
|
html.is-animating .transition-fade,
|
||||||
|
html.is-animating .swup-transition-article,
|
||||||
|
html.is-animating #article-content {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 淡入状态 */
|
||||||
|
html.is-changing .transition-fade,
|
||||||
|
html.is-changing .swup-transition-article,
|
||||||
|
html.is-changing #article-content {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user