feat: 添加引导页、视频播放和触摸交互功能
- 新增引导页组件(Guide.tsx)及相关资源 - 添加视频播放功能和相关资源文件 - 实现useTouch自定义hook支持触摸交互 - 添加store状态管理 - 更新多个页面组件(Content, Menu, Publicity) - 优化图片组件和背景图片组件 - 更新项目依赖
No files matched your search
@@ -19,9 +19,11 @@
|
|||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.545.0",
|
"lucide-react": "^0.545.0",
|
||||||
|
"mammoth": "^1.11.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-router-dom": "^7.9.4"
|
"react-router-dom": "^7.9.4",
|
||||||
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.36.0",
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ importers:
|
|||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.545.0
|
specifier: ^0.545.0
|
||||||
version: 0.545.0(react@19.2.0)
|
version: 0.545.0(react@19.2.0)
|
||||||
|
mammoth:
|
||||||
|
specifier: ^1.11.0
|
||||||
|
version: 1.11.0
|
||||||
react:
|
react:
|
||||||
specifier: ^19.1.1
|
specifier: ^19.1.1
|
||||||
version: 19.2.0
|
version: 19.2.0
|
||||||
@@ -26,6 +29,9 @@ importers:
|
|||||||
react-router-dom:
|
react-router-dom:
|
||||||
specifier: ^7.9.4
|
specifier: ^7.9.4
|
||||||
version: 7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
|
zustand:
|
||||||
|
specifier: ^5.0.8
|
||||||
|
version: 5.0.8(@types/react@19.2.2)(react@19.2.0)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@eslint/js':
|
'@eslint/js':
|
||||||
specifier: ^9.36.0
|
specifier: ^9.36.0
|
||||||
@@ -723,6 +729,10 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||||
|
|
||||||
|
'@xmldom/xmldom@0.8.11':
|
||||||
|
resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
acorn-jsx@5.3.2:
|
acorn-jsx@5.3.2:
|
||||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -740,6 +750,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
argparse@1.0.10:
|
||||||
|
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
||||||
|
|
||||||
argparse@2.0.1:
|
argparse@2.0.1:
|
||||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
|
|
||||||
@@ -759,10 +772,16 @@ packages:
|
|||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
|
base64-js@1.5.1:
|
||||||
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
|
|
||||||
baseline-browser-mapping@2.8.16:
|
baseline-browser-mapping@2.8.16:
|
||||||
resolution: {integrity: sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==}
|
resolution: {integrity: sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
bluebird@3.4.7:
|
||||||
|
resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==}
|
||||||
|
|
||||||
brace-expansion@1.1.12:
|
brace-expansion@1.1.12:
|
||||||
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
||||||
|
|
||||||
@@ -822,6 +841,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
|
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
core-util-is@1.0.3:
|
||||||
|
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -849,6 +871,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
dingbat-to-unicode@1.0.1:
|
||||||
|
resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==}
|
||||||
|
|
||||||
|
duck@0.1.12:
|
||||||
|
resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1072,6 +1100,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
immediate@3.0.6:
|
||||||
|
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
|
||||||
|
|
||||||
import-fresh@3.3.1:
|
import-fresh@3.3.1:
|
||||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -1080,6 +1111,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
||||||
engines: {node: '>=0.8.19'}
|
engines: {node: '>=0.8.19'}
|
||||||
|
|
||||||
|
inherits@2.0.4:
|
||||||
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
|
|
||||||
is-extglob@2.1.1:
|
is-extglob@2.1.1:
|
||||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -1092,6 +1126,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||||
engines: {node: '>=0.12.0'}
|
engines: {node: '>=0.12.0'}
|
||||||
|
|
||||||
|
isarray@1.0.0:
|
||||||
|
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||||
|
|
||||||
isexe@2.0.0:
|
isexe@2.0.0:
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
@@ -1125,6 +1162,9 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jszip@3.10.1:
|
||||||
|
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||||
|
|
||||||
@@ -1132,6 +1172,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
lie@3.3.0:
|
||||||
|
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
|
||||||
|
|
||||||
lightningcss-darwin-arm64@1.30.1:
|
lightningcss-darwin-arm64@1.30.1:
|
||||||
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
|
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
@@ -1207,6 +1250,9 @@ packages:
|
|||||||
lodash.merge@4.6.2:
|
lodash.merge@4.6.2:
|
||||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||||
|
|
||||||
|
lop@0.4.2:
|
||||||
|
resolution: {integrity: sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==}
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||||
|
|
||||||
@@ -1218,6 +1264,11 @@ packages:
|
|||||||
magic-string@0.30.19:
|
magic-string@0.30.19:
|
||||||
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
|
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
|
||||||
|
|
||||||
|
mammoth@1.11.0:
|
||||||
|
resolution: {integrity: sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
math-intrinsics@1.1.0:
|
math-intrinsics@1.1.0:
|
||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1271,6 +1322,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
|
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
option@0.2.4:
|
||||||
|
resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==}
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -1283,6 +1337,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
pako@1.0.11:
|
||||||
|
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -1291,6 +1348,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
path-is-absolute@1.0.1:
|
||||||
|
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
path-key@3.1.1:
|
path-key@3.1.1:
|
||||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1317,6 +1378,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
process-nextick-args@2.0.1:
|
||||||
|
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||||
|
|
||||||
proxy-from-env@1.1.0:
|
proxy-from-env@1.1.0:
|
||||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||||
|
|
||||||
@@ -1357,6 +1421,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
|
resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
readable-stream@2.3.8:
|
||||||
|
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||||
|
|
||||||
resolve-from@4.0.0:
|
resolve-from@4.0.0:
|
||||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -1373,6 +1440,9 @@ packages:
|
|||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||||
|
|
||||||
|
safe-buffer@5.1.2:
|
||||||
|
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
||||||
|
|
||||||
scheduler@0.27.0:
|
scheduler@0.27.0:
|
||||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||||
|
|
||||||
@@ -1388,6 +1458,9 @@ packages:
|
|||||||
set-cookie-parser@2.7.1:
|
set-cookie-parser@2.7.1:
|
||||||
resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
|
resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
|
||||||
|
|
||||||
|
setimmediate@1.0.5:
|
||||||
|
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1400,6 +1473,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
sprintf-js@1.0.3:
|
||||||
|
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||||
|
|
||||||
|
string_decoder@1.1.1:
|
||||||
|
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||||
|
|
||||||
strip-json-comments@3.1.1:
|
strip-json-comments@3.1.1:
|
||||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1449,6 +1528,9 @@ packages:
|
|||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
underscore@1.13.7:
|
||||||
|
resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==}
|
||||||
|
|
||||||
undici-types@7.14.0:
|
undici-types@7.14.0:
|
||||||
resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==}
|
resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==}
|
||||||
|
|
||||||
@@ -1461,6 +1543,9 @@ packages:
|
|||||||
uri-js@4.4.1:
|
uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
|
|
||||||
|
util-deprecate@1.0.2:
|
||||||
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
vite@7.1.10:
|
vite@7.1.10:
|
||||||
resolution: {integrity: sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==}
|
resolution: {integrity: sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -1510,6 +1595,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
xmlbuilder@10.1.1:
|
||||||
|
resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
|
||||||
yallist@3.1.1:
|
yallist@3.1.1:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||||
|
|
||||||
@@ -1521,6 +1610,24 @@ packages:
|
|||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
zustand@5.0.8:
|
||||||
|
resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==}
|
||||||
|
engines: {node: '>=12.20.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '>=18.0.0'
|
||||||
|
immer: '>=9.0.6'
|
||||||
|
react: '>=18.0.0'
|
||||||
|
use-sync-external-store: '>=1.2.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
immer:
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
use-sync-external-store:
|
||||||
|
optional: true
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
@@ -2089,6 +2196,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@xmldom/xmldom@0.8.11': {}
|
||||||
|
|
||||||
acorn-jsx@5.3.2(acorn@8.15.0):
|
acorn-jsx@5.3.2(acorn@8.15.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.15.0
|
acorn: 8.15.0
|
||||||
@@ -2106,6 +2215,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
color-convert: 2.0.1
|
color-convert: 2.0.1
|
||||||
|
|
||||||
|
argparse@1.0.10:
|
||||||
|
dependencies:
|
||||||
|
sprintf-js: 1.0.3
|
||||||
|
|
||||||
argparse@2.0.1: {}
|
argparse@2.0.1: {}
|
||||||
|
|
||||||
asynckit@0.4.0: {}
|
asynckit@0.4.0: {}
|
||||||
@@ -2130,8 +2243,12 @@ snapshots:
|
|||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
|
base64-js@1.5.1: {}
|
||||||
|
|
||||||
baseline-browser-mapping@2.8.16: {}
|
baseline-browser-mapping@2.8.16: {}
|
||||||
|
|
||||||
|
bluebird@3.4.7: {}
|
||||||
|
|
||||||
brace-expansion@1.1.12:
|
brace-expansion@1.1.12:
|
||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 1.0.2
|
balanced-match: 1.0.2
|
||||||
@@ -2187,6 +2304,8 @@ snapshots:
|
|||||||
|
|
||||||
cookie@1.0.2: {}
|
cookie@1.0.2: {}
|
||||||
|
|
||||||
|
core-util-is@1.0.3: {}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
@@ -2205,6 +2324,12 @@ snapshots:
|
|||||||
|
|
||||||
detect-libc@2.1.2: {}
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
|
dingbat-to-unicode@1.0.1: {}
|
||||||
|
|
||||||
|
duck@0.1.12:
|
||||||
|
dependencies:
|
||||||
|
underscore: 1.13.7
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind-apply-helpers: 1.0.2
|
call-bind-apply-helpers: 1.0.2
|
||||||
@@ -2456,6 +2581,8 @@ snapshots:
|
|||||||
|
|
||||||
ignore@7.0.5: {}
|
ignore@7.0.5: {}
|
||||||
|
|
||||||
|
immediate@3.0.6: {}
|
||||||
|
|
||||||
import-fresh@3.3.1:
|
import-fresh@3.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
parent-module: 1.0.1
|
parent-module: 1.0.1
|
||||||
@@ -2463,6 +2590,8 @@ snapshots:
|
|||||||
|
|
||||||
imurmurhash@0.1.4: {}
|
imurmurhash@0.1.4: {}
|
||||||
|
|
||||||
|
inherits@2.0.4: {}
|
||||||
|
|
||||||
is-extglob@2.1.1: {}
|
is-extglob@2.1.1: {}
|
||||||
|
|
||||||
is-glob@4.0.3:
|
is-glob@4.0.3:
|
||||||
@@ -2471,6 +2600,8 @@ snapshots:
|
|||||||
|
|
||||||
is-number@7.0.0: {}
|
is-number@7.0.0: {}
|
||||||
|
|
||||||
|
isarray@1.0.0: {}
|
||||||
|
|
||||||
isexe@2.0.0: {}
|
isexe@2.0.0: {}
|
||||||
|
|
||||||
jiti@2.6.1: {}
|
jiti@2.6.1: {}
|
||||||
@@ -2491,6 +2622,13 @@ snapshots:
|
|||||||
|
|
||||||
json5@2.2.3: {}
|
json5@2.2.3: {}
|
||||||
|
|
||||||
|
jszip@3.10.1:
|
||||||
|
dependencies:
|
||||||
|
lie: 3.3.0
|
||||||
|
pako: 1.0.11
|
||||||
|
readable-stream: 2.3.8
|
||||||
|
setimmediate: 1.0.5
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
json-buffer: 3.0.1
|
json-buffer: 3.0.1
|
||||||
@@ -2500,6 +2638,10 @@ snapshots:
|
|||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
type-check: 0.4.0
|
type-check: 0.4.0
|
||||||
|
|
||||||
|
lie@3.3.0:
|
||||||
|
dependencies:
|
||||||
|
immediate: 3.0.6
|
||||||
|
|
||||||
lightningcss-darwin-arm64@1.30.1:
|
lightningcss-darwin-arm64@1.30.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -2551,6 +2693,12 @@ snapshots:
|
|||||||
|
|
||||||
lodash.merge@4.6.2: {}
|
lodash.merge@4.6.2: {}
|
||||||
|
|
||||||
|
lop@0.4.2:
|
||||||
|
dependencies:
|
||||||
|
duck: 0.1.12
|
||||||
|
option: 0.2.4
|
||||||
|
underscore: 1.13.7
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist: 3.1.1
|
yallist: 3.1.1
|
||||||
@@ -2563,6 +2711,19 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
mammoth@1.11.0:
|
||||||
|
dependencies:
|
||||||
|
'@xmldom/xmldom': 0.8.11
|
||||||
|
argparse: 1.0.10
|
||||||
|
base64-js: 1.5.1
|
||||||
|
bluebird: 3.4.7
|
||||||
|
dingbat-to-unicode: 1.0.1
|
||||||
|
jszip: 3.10.1
|
||||||
|
lop: 0.4.2
|
||||||
|
path-is-absolute: 1.0.1
|
||||||
|
underscore: 1.13.7
|
||||||
|
xmlbuilder: 10.1.1
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
merge2@1.4.1: {}
|
merge2@1.4.1: {}
|
||||||
@@ -2602,6 +2763,8 @@ snapshots:
|
|||||||
|
|
||||||
normalize-range@0.1.2: {}
|
normalize-range@0.1.2: {}
|
||||||
|
|
||||||
|
option@0.2.4: {}
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
deep-is: 0.1.4
|
deep-is: 0.1.4
|
||||||
@@ -2619,12 +2782,16 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-limit: 3.1.0
|
p-limit: 3.1.0
|
||||||
|
|
||||||
|
pako@1.0.11: {}
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
callsites: 3.1.0
|
callsites: 3.1.0
|
||||||
|
|
||||||
path-exists@4.0.0: {}
|
path-exists@4.0.0: {}
|
||||||
|
|
||||||
|
path-is-absolute@1.0.1: {}
|
||||||
|
|
||||||
path-key@3.1.1: {}
|
path-key@3.1.1: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
@@ -2643,6 +2810,8 @@ snapshots:
|
|||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
|
process-nextick-args@2.0.1: {}
|
||||||
|
|
||||||
proxy-from-env@1.1.0: {}
|
proxy-from-env@1.1.0: {}
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
@@ -2672,6 +2841,16 @@ snapshots:
|
|||||||
|
|
||||||
react@19.2.0: {}
|
react@19.2.0: {}
|
||||||
|
|
||||||
|
readable-stream@2.3.8:
|
||||||
|
dependencies:
|
||||||
|
core-util-is: 1.0.3
|
||||||
|
inherits: 2.0.4
|
||||||
|
isarray: 1.0.0
|
||||||
|
process-nextick-args: 2.0.1
|
||||||
|
safe-buffer: 5.1.2
|
||||||
|
string_decoder: 1.1.1
|
||||||
|
util-deprecate: 1.0.2
|
||||||
|
|
||||||
resolve-from@4.0.0: {}
|
resolve-from@4.0.0: {}
|
||||||
|
|
||||||
reusify@1.1.0: {}
|
reusify@1.1.0: {}
|
||||||
@@ -2708,6 +2887,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask: 1.2.3
|
queue-microtask: 1.2.3
|
||||||
|
|
||||||
|
safe-buffer@5.1.2: {}
|
||||||
|
|
||||||
scheduler@0.27.0: {}
|
scheduler@0.27.0: {}
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
@@ -2716,6 +2897,8 @@ snapshots:
|
|||||||
|
|
||||||
set-cookie-parser@2.7.1: {}
|
set-cookie-parser@2.7.1: {}
|
||||||
|
|
||||||
|
setimmediate@1.0.5: {}
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
shebang-regex: 3.0.0
|
shebang-regex: 3.0.0
|
||||||
@@ -2724,6 +2907,12 @@ snapshots:
|
|||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
sprintf-js@1.0.3: {}
|
||||||
|
|
||||||
|
string_decoder@1.1.1:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.1.2
|
||||||
|
|
||||||
strip-json-comments@3.1.1: {}
|
strip-json-comments@3.1.1: {}
|
||||||
|
|
||||||
supports-color@7.2.0:
|
supports-color@7.2.0:
|
||||||
@@ -2772,6 +2961,8 @@ snapshots:
|
|||||||
|
|
||||||
typescript@5.9.3: {}
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
|
underscore@1.13.7: {}
|
||||||
|
|
||||||
undici-types@7.14.0: {}
|
undici-types@7.14.0: {}
|
||||||
|
|
||||||
update-browserslist-db@1.1.3(browserslist@4.26.3):
|
update-browserslist-db@1.1.3(browserslist@4.26.3):
|
||||||
@@ -2784,6 +2975,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|
||||||
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
vite@7.1.10(@types/node@24.8.0)(jiti@2.6.1)(lightningcss@1.30.1):
|
vite@7.1.10(@types/node@24.8.0)(jiti@2.6.1)(lightningcss@1.30.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.11
|
esbuild: 0.25.11
|
||||||
@@ -2804,8 +2997,15 @@ snapshots:
|
|||||||
|
|
||||||
word-wrap@1.2.5: {}
|
word-wrap@1.2.5: {}
|
||||||
|
|
||||||
|
xmlbuilder@10.1.1: {}
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|
||||||
yallist@5.0.0: {}
|
yallist@5.0.0: {}
|
||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
|
zustand@5.0.8(@types/react@19.2.2)(react@19.2.0):
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.2
|
||||||
|
react: 19.2.0
|
||||||
|
After Width: | Height: | Size: 467 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 467 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 198 KiB |
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
|
|
||||||
interface BackgroundImageProps {
|
interface BackgroundImageProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
src: string;
|
src: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -14,12 +14,12 @@ interface BackgroundImageProps {
|
|||||||
fallback?: string;
|
fallback?: string;
|
||||||
onLoad?: () => void;
|
onLoad?: () => void;
|
||||||
onError?: () => void;
|
onError?: () => void;
|
||||||
onClick?: () => void;
|
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
overlay?: boolean;
|
overlay?: boolean;
|
||||||
overlayColor?: string;
|
overlayColor?: string;
|
||||||
overlayOpacity?: number;
|
overlayOpacity?: number;
|
||||||
overflowHidden?: boolean;
|
overflowHidden?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BackgroundImage({
|
export default function BackgroundImage({
|
||||||
@@ -35,12 +35,13 @@ export default function BackgroundImage({
|
|||||||
fallback,
|
fallback,
|
||||||
onLoad,
|
onLoad,
|
||||||
onError,
|
onError,
|
||||||
onClick,
|
|
||||||
children,
|
children,
|
||||||
overlay = false,
|
overlay = false,
|
||||||
overlayColor = "rgba(0, 0, 0, 0.5)",
|
overlayColor = "rgba(0, 0, 0, 0.5)",
|
||||||
overlayOpacity = 0.5,
|
overlayOpacity = 0.5,
|
||||||
overflowHidden = true,
|
overflowHidden = true,
|
||||||
|
style,
|
||||||
|
...restProps
|
||||||
}: BackgroundImageProps) {
|
}: BackgroundImageProps) {
|
||||||
const [imageSrc, setImageSrc] = useState(src);
|
const [imageSrc, setImageSrc] = useState(src);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@@ -125,6 +126,7 @@ export default function BackgroundImage({
|
|||||||
backgroundPosition,
|
backgroundPosition,
|
||||||
backgroundRepeat,
|
backgroundRepeat,
|
||||||
position: "relative" as const,
|
position: "relative" as const,
|
||||||
|
...style,
|
||||||
};
|
};
|
||||||
|
|
||||||
const overlayStyle = {
|
const overlayStyle = {
|
||||||
@@ -141,7 +143,7 @@ export default function BackgroundImage({
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
style={backgroundStyle}
|
style={backgroundStyle}
|
||||||
onClick={onClick}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{/* 加载占位符 */}
|
{/* 加载占位符 */}
|
||||||
{isLoading && !placeholder && (
|
{isLoading && !placeholder && (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
|
|
||||||
interface ImageProps {
|
interface ImageProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
src: string;
|
src: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@@ -16,7 +16,6 @@ interface ImageProps {
|
|||||||
fallback?: string;
|
fallback?: string;
|
||||||
onLoad?: () => void;
|
onLoad?: () => void;
|
||||||
onError?: () => void;
|
onError?: () => void;
|
||||||
onClick?: () => void;
|
|
||||||
draggable?: boolean;
|
draggable?: boolean;
|
||||||
quality?: number;
|
quality?: number;
|
||||||
blur?: boolean;
|
blur?: boolean;
|
||||||
@@ -38,11 +37,11 @@ export default function Image({
|
|||||||
fallback,
|
fallback,
|
||||||
onLoad,
|
onLoad,
|
||||||
onError,
|
onError,
|
||||||
onClick,
|
|
||||||
draggable = false,
|
draggable = false,
|
||||||
quality: _quality = 80,
|
quality: _quality = 80,
|
||||||
blur = false,
|
blur = false,
|
||||||
button = false,
|
button = false,
|
||||||
|
...restProps
|
||||||
}: ImageProps) {
|
}: ImageProps) {
|
||||||
const [imageSrc, setImageSrc] = useState(src);
|
const [imageSrc, setImageSrc] = useState(src);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@@ -116,7 +115,7 @@ export default function Image({
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
style={{ width, height, ...style }}
|
style={{ width, height, ...style }}
|
||||||
onClick={onClick}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{/* 加载占位符 */}
|
{/* 加载占位符 */}
|
||||||
{isLoading && placeholder && (
|
{isLoading && placeholder && (
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const skillTrainingActivities: Activity[] = [
|
|||||||
date: "2024年11月14日",
|
date: "2024年11月14日",
|
||||||
docPath: "/word/【11月15日】工作室活动情况记录.docx",
|
docPath: "/word/【11月15日】工作室活动情况记录.docx",
|
||||||
},
|
},
|
||||||
];
|
].reverse();
|
||||||
|
|
||||||
//交流与展示活动
|
//交流与展示活动
|
||||||
export const exchangeAndShowActivities: Activity[] = [
|
export const exchangeAndShowActivities: Activity[] = [
|
||||||
@@ -69,7 +69,7 @@ export const exchangeAndShowActivities: Activity[] = [
|
|||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
name: "与桂同心育新农,山海携手促共富",
|
name: "与桂同心育新农,山海携手促共富",
|
||||||
date: "2025年",
|
date: "2025年08月23日",
|
||||||
docPath: "/word/与桂同心育新农,山海携手促共富.docx",
|
docPath: "/word/与桂同心育新农,山海携手促共富.docx",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -86,4 +86,4 @@ export const exchangeAndShowActivities: Activity[] = [
|
|||||||
docPath:
|
docPath:
|
||||||
"/word/政校携手,口岸直播助侗乡——南湖学子浦江口岸助农实践出新彩.docx",
|
"/word/政校携手,口岸直播助侗乡——南湖学子浦江口岸助农实践出新彩.docx",
|
||||||
},
|
},
|
||||||
];
|
].reverse();
|
||||||
@@ -0,0 +1,428 @@
|
|||||||
|
# useTouch Hook 使用文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
`useTouch` 是一个灵活强大的触摸手势处理 Hook,支持多种手势识别,包括:
|
||||||
|
|
||||||
|
- 点击 (Tap)
|
||||||
|
- 双击 (Double Tap)
|
||||||
|
- 长按 (Long Press)
|
||||||
|
- 滑动 (Swipe) - 支持四个方向
|
||||||
|
- 触摸移动追踪
|
||||||
|
|
||||||
|
同时支持触摸屏和鼠标操作,适用于移动端和桌面端。
|
||||||
|
|
||||||
|
## 基础用法
|
||||||
|
|
||||||
|
### 1. 简单点击
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useTouch } from "@/hooks";
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const { handlers } = useTouch({
|
||||||
|
onTap: () => {
|
||||||
|
console.log("点击了");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div {...handlers}>点击我</div>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 滑动手势
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const { handlers } = useTouch({
|
||||||
|
onSwipeLeft: () => console.log("向左滑动"),
|
||||||
|
onSwipeRight: () => console.log("向右滑动"),
|
||||||
|
onSwipeUp: () => console.log("向上滑动"),
|
||||||
|
onSwipeDown: () => console.log("向下滑动"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div {...handlers}>滑动区域</div>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 双击
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const { handlers } = useTouch({
|
||||||
|
onDoubleTap: () => {
|
||||||
|
console.log("双击触发");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div {...handlers}>双击我</div>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 长按
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const { handlers } = useTouch({
|
||||||
|
onLongPress: () => {
|
||||||
|
console.log("长按触发");
|
||||||
|
},
|
||||||
|
longPressDelay: 500, // 500ms 触发长按
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div {...handlers}>长按我</div>;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高级用法
|
||||||
|
|
||||||
|
### 获取触摸数据
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const { handlers, touchData, isTouching } = useTouch({
|
||||||
|
onTouchMove: (data) => {
|
||||||
|
console.log("移动距离:", data.deltaX, data.deltaY);
|
||||||
|
console.log("移动速度:", data.velocity);
|
||||||
|
console.log("移动方向:", data.direction);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...handlers}>
|
||||||
|
{isTouching && touchData && (
|
||||||
|
<div>
|
||||||
|
移动: {touchData.deltaX.toFixed(0)}px, {touchData.deltaY.toFixed(0)}px
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 图片查看器示例
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function ImageViewer() {
|
||||||
|
const [scale, setScale] = useState(1);
|
||||||
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
const { handlers } = useTouch({
|
||||||
|
onDoubleTap: () => {
|
||||||
|
// 双击放大/还原
|
||||||
|
setScale(scale === 1 ? 2 : 1);
|
||||||
|
},
|
||||||
|
onSwipe: (data) => {
|
||||||
|
// 滑动移动图片
|
||||||
|
if (scale > 1) {
|
||||||
|
setPosition((prev) => ({
|
||||||
|
x: prev.x + data.deltaX,
|
||||||
|
y: prev.y + data.deltaY,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
swipeThreshold: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...handlers} style={{ overflow: "hidden" }}>
|
||||||
|
<img
|
||||||
|
src="image.jpg"
|
||||||
|
style={{
|
||||||
|
transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`,
|
||||||
|
transition: "transform 0.3s",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 卡片滑动删除
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function SwipeableCard() {
|
||||||
|
const [offset, setOffset] = useState(0);
|
||||||
|
|
||||||
|
const { handlers } = useTouch({
|
||||||
|
onTouchMove: (data) => {
|
||||||
|
if (data.direction === "left" || data.direction === "right") {
|
||||||
|
setOffset(data.deltaX);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTouchEnd: (data) => {
|
||||||
|
if (Math.abs(data.deltaX) > 100) {
|
||||||
|
// 滑动超过 100px,执行删除
|
||||||
|
onDelete();
|
||||||
|
} else {
|
||||||
|
// 弹回原位
|
||||||
|
setOffset(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...handlers}
|
||||||
|
style={{
|
||||||
|
transform: `translateX(${offset}px)`,
|
||||||
|
transition: offset === 0 ? "transform 0.3s" : "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>卡片内容</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 下拉刷新
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function PullToRefresh() {
|
||||||
|
const [pullDistance, setPullDistance] = useState(0);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
|
const { handlers } = useTouch({
|
||||||
|
onTouchMove: (data) => {
|
||||||
|
// 只在向下滑动且在顶部时触发
|
||||||
|
if (data.direction === "down" && window.scrollY === 0) {
|
||||||
|
setPullDistance(Math.min(data.deltaY, 80));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTouchEnd: (data) => {
|
||||||
|
if (pullDistance > 50) {
|
||||||
|
setIsRefreshing(true);
|
||||||
|
// 执行刷新逻辑
|
||||||
|
refresh().then(() => {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
setPullDistance(0);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setPullDistance(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...handlers}>
|
||||||
|
<div style={{ height: pullDistance, transition: "height 0.3s" }}>
|
||||||
|
{isRefreshing
|
||||||
|
? "刷新中..."
|
||||||
|
: pullDistance > 50
|
||||||
|
? "松开刷新"
|
||||||
|
: "下拉刷新"}
|
||||||
|
</div>
|
||||||
|
<div>内容区域</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 轮播图
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Carousel({ images }: { images: string[] }) {
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
|
||||||
|
const { handlers } = useTouch({
|
||||||
|
onSwipeLeft: () => {
|
||||||
|
setCurrentIndex((prev) => Math.min(prev + 1, images.length - 1));
|
||||||
|
},
|
||||||
|
onSwipeRight: () => {
|
||||||
|
setCurrentIndex((prev) => Math.max(prev - 1, 0));
|
||||||
|
},
|
||||||
|
swipeThreshold: 50,
|
||||||
|
swipeVelocityThreshold: 0.3,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...handlers} style={{ overflow: "hidden" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
transform: `translateX(-${currentIndex * 100}%)`,
|
||||||
|
transition: "transform 0.3s",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{images.map((img, i) => (
|
||||||
|
<img key={i} src={img} style={{ width: "100%", flexShrink: 0 }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置选项
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface UseTouchOptions {
|
||||||
|
// 基础配置
|
||||||
|
enabled?: boolean; // 是否启用,默认 true
|
||||||
|
preventDefault?: boolean; // 是否阻止默认行为,默认 false
|
||||||
|
stopPropagation?: boolean; // 是否阻止事件冒泡,默认 false
|
||||||
|
|
||||||
|
// 滑动配置
|
||||||
|
swipeThreshold?: number; // 滑动阈值(px),默认 10
|
||||||
|
swipeVelocityThreshold?: number; // 滑动速度阈值(px/ms),默认 0.5
|
||||||
|
|
||||||
|
// 长按配置
|
||||||
|
longPressDelay?: number; // 长按延迟(ms),默认 500
|
||||||
|
longPressMoveThreshold?: number; // 长按允许移动距离(px),默认 10
|
||||||
|
|
||||||
|
// 双击配置
|
||||||
|
doubleTapDelay?: number; // 双击间隔(ms),默认 300
|
||||||
|
doubleTapMoveThreshold?: number; // 双击允许移动距离(px),默认 10
|
||||||
|
|
||||||
|
// 事件回调
|
||||||
|
onTouchStart?: (data: TouchEventData) => void;
|
||||||
|
onTouchMove?: (data: TouchEventData) => void;
|
||||||
|
onTouchEnd?: (data: TouchEventData) => void;
|
||||||
|
onTap?: (data: TouchEventData) => void;
|
||||||
|
onDoubleTap?: (data: TouchEventData) => void;
|
||||||
|
onLongPress?: (data: TouchEventData) => void;
|
||||||
|
onSwipe?: (data: TouchEventData) => void;
|
||||||
|
onSwipeLeft?: (data: TouchEventData) => void;
|
||||||
|
onSwipeRight?: (data: TouchEventData) => void;
|
||||||
|
onSwipeUp?: (data: TouchEventData) => void;
|
||||||
|
onSwipeDown?: (data: TouchEventData) => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 触摸数据
|
||||||
|
|
||||||
|
所有回调函数都会接收到 `TouchEventData` 对象:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface TouchEventData {
|
||||||
|
startPoint: TouchPoint; // 起始位置 { x, y, timestamp }
|
||||||
|
currentPoint: TouchPoint; // 当前位置 { x, y, timestamp }
|
||||||
|
deltaX: number; // X 轴移动距离
|
||||||
|
deltaY: number; // Y 轴移动距离
|
||||||
|
distance: number; // 移动的总距离
|
||||||
|
velocity: number; // 移动速度(像素/毫秒)
|
||||||
|
direction: SwipeDirection; // 移动方向: 'left' | 'right' | 'up' | 'down' | null
|
||||||
|
event: React.TouchEvent | React.MouseEvent; // 原始事件
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 返回值
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface UseTouchReturn {
|
||||||
|
isTouching: boolean; // 是否正在触摸
|
||||||
|
touchData: TouchEventData | null; // 当前触摸数据
|
||||||
|
handlers: {
|
||||||
|
onTouchStart: (e: React.TouchEvent) => void;
|
||||||
|
onTouchMove: (e: React.TouchEvent) => void;
|
||||||
|
onTouchEnd: (e: React.TouchEvent) => void;
|
||||||
|
onMouseDown: (e: React.MouseEvent) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **事件处理器自动包含鼠标和触摸事件**,无需分别绑定
|
||||||
|
2. **长按和滑动互斥**:如果移动距离超过阈值,会取消长按
|
||||||
|
3. **双击优先级高于单击**:在双击延迟内的第二次点击会触发双击而非单击
|
||||||
|
4. **性能优化**:使用 `useCallback` 避免不必要的重渲染
|
||||||
|
5. **跨平台支持**:同时支持移动端触摸和桌面端鼠标操作
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 避免与滚动冲突
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const { handlers } = useTouch({
|
||||||
|
onSwipeLeft: handleSwipe,
|
||||||
|
preventDefault: false, // 不阻止默认行为,允许页面滚动
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 性能优化
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 使用 useMemo 缓存配置
|
||||||
|
const touchOptions = useMemo(
|
||||||
|
() => ({
|
||||||
|
onSwipe: handleSwipe,
|
||||||
|
swipeThreshold: 50,
|
||||||
|
}),
|
||||||
|
[handleSwipe]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { handlers } = useTouch(touchOptions);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 条件禁用
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const { handlers } = useTouch({
|
||||||
|
enabled: !isLoading, // 加载时禁用触摸
|
||||||
|
onTap: handleTap,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 完整示例
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useTouch } from "@/hooks";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
function TouchDemo() {
|
||||||
|
const [log, setLog] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const addLog = (message: string) => {
|
||||||
|
setLog((prev) => [...prev, message].slice(-5));
|
||||||
|
};
|
||||||
|
|
||||||
|
const { handlers, isTouching, touchData } = useTouch({
|
||||||
|
onTap: () => addLog("点击"),
|
||||||
|
onDoubleTap: () => addLog("双击"),
|
||||||
|
onLongPress: () => addLog("长按"),
|
||||||
|
onSwipeLeft: () => addLog("向左滑动"),
|
||||||
|
onSwipeRight: () => addLog("向右滑动"),
|
||||||
|
onSwipeUp: () => addLog("向上滑动"),
|
||||||
|
onSwipeDown: () => addLog("向下滑动"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
{...handlers}
|
||||||
|
style={{
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
border: "2px solid #ccc",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
userSelect: "none",
|
||||||
|
background: isTouching ? "#f0f0f0" : "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
触摸测试区域
|
||||||
|
{touchData && (
|
||||||
|
<div style={{ fontSize: 12 }}>
|
||||||
|
Δx: {touchData.deltaX.toFixed(0)}
|
||||||
|
Δy: {touchData.deltaY.toFixed(0)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 20 }}>
|
||||||
|
<h3>操作日志:</h3>
|
||||||
|
{log.map((msg, i) => (
|
||||||
|
<div key={i}>{msg}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
`useTouch` Hook 提供了:
|
||||||
|
|
||||||
|
- ✅ 完整的手势识别(点击、双击、长按、滑动)
|
||||||
|
- ✅ 灵活的配置选项
|
||||||
|
- ✅ 详细的触摸数据
|
||||||
|
- ✅ 跨平台支持(触摸 + 鼠标)
|
||||||
|
- ✅ TypeScript 类型安全
|
||||||
|
- ✅ 性能优化
|
||||||
|
|
||||||
|
适用于各种触摸交互场景!
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export { useDragAndDrop } from "./useDragAndDrop";
|
||||||
|
export { usePageScale } from "./usePageScale";
|
||||||
|
export { useTouch } from "./useTouch";
|
||||||
|
export type {
|
||||||
|
UseTouchOptions,
|
||||||
|
UseTouchReturn,
|
||||||
|
TouchEventData,
|
||||||
|
SwipeDirection,
|
||||||
|
} from "./useTouch";
|
||||||
@@ -0,0 +1,485 @@
|
|||||||
|
import { useRef, useCallback, useState } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触摸点信息
|
||||||
|
*/
|
||||||
|
interface TouchPoint {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 滑动方向
|
||||||
|
*/
|
||||||
|
export type SwipeDirection = "left" | "right" | "up" | "down";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手势事件回调参数
|
||||||
|
*/
|
||||||
|
export interface TouchEventData {
|
||||||
|
/** 起始位置 */
|
||||||
|
startPoint: TouchPoint;
|
||||||
|
/** 当前位置 */
|
||||||
|
currentPoint: TouchPoint;
|
||||||
|
/** 移动距离 X */
|
||||||
|
deltaX: number;
|
||||||
|
/** 移动距离 Y */
|
||||||
|
deltaY: number;
|
||||||
|
/** 移动的总距离 */
|
||||||
|
distance: number;
|
||||||
|
/** 移动速度(像素/毫秒) */
|
||||||
|
velocity: number;
|
||||||
|
/** 移动方向 */
|
||||||
|
direction: SwipeDirection | null;
|
||||||
|
/** 原始事件 */
|
||||||
|
event: React.TouchEvent | React.MouseEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useTouch 配置选项
|
||||||
|
*/
|
||||||
|
export interface UseTouchOptions {
|
||||||
|
/** 是否启用,默认 true */
|
||||||
|
enabled?: boolean;
|
||||||
|
|
||||||
|
// ========== 滑动配置 ==========
|
||||||
|
/** 触发滑动的最小距离(像素),默认 10 */
|
||||||
|
swipeThreshold?: number;
|
||||||
|
/** 触发快速滑动的最小速度(像素/毫秒),默认 0.5 */
|
||||||
|
swipeVelocityThreshold?: number;
|
||||||
|
|
||||||
|
// ========== 长按配置 ==========
|
||||||
|
/** 触发长按的时间(毫秒),默认 500 */
|
||||||
|
longPressDelay?: number;
|
||||||
|
/** 长按时允许的最大移动距离(像素),默认 10 */
|
||||||
|
longPressMoveThreshold?: number;
|
||||||
|
|
||||||
|
// ========== 双击配置 ==========
|
||||||
|
/** 双击的最大时间间隔(毫秒),默认 300 */
|
||||||
|
doubleTapDelay?: number;
|
||||||
|
/** 双击的最大移动距离(像素),默认 10 */
|
||||||
|
doubleTapMoveThreshold?: number;
|
||||||
|
|
||||||
|
// ========== 事件回调 ==========
|
||||||
|
/** 触摸开始 */
|
||||||
|
onTouchStart?: (data: TouchEventData) => void;
|
||||||
|
/** 触摸移动 */
|
||||||
|
onTouchMove?: (data: TouchEventData) => void;
|
||||||
|
/** 触摸结束 */
|
||||||
|
onTouchEnd?: (data: TouchEventData) => void;
|
||||||
|
/** 点击 */
|
||||||
|
onTap?: (data: TouchEventData) => void;
|
||||||
|
/** 双击 */
|
||||||
|
onDoubleTap?: (data: TouchEventData) => void;
|
||||||
|
/** 长按 */
|
||||||
|
onLongPress?: (data: TouchEventData) => void;
|
||||||
|
/** 滑动 */
|
||||||
|
onSwipe?: (data: TouchEventData) => void;
|
||||||
|
/** 向左滑动 */
|
||||||
|
onSwipeLeft?: (data: TouchEventData) => void;
|
||||||
|
/** 向右滑动 */
|
||||||
|
onSwipeRight?: (data: TouchEventData) => void;
|
||||||
|
/** 向上滑动 */
|
||||||
|
onSwipeUp?: (data: TouchEventData) => void;
|
||||||
|
/** 向下滑动 */
|
||||||
|
onSwipeDown?: (data: TouchEventData) => void;
|
||||||
|
|
||||||
|
// ========== 其他配置 ==========
|
||||||
|
/** 是否阻止默认行为,默认 false */
|
||||||
|
preventDefault?: boolean;
|
||||||
|
/** 是否阻止事件冒泡,默认 false */
|
||||||
|
stopPropagation?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useTouch 返回值
|
||||||
|
*/
|
||||||
|
export interface UseTouchReturn {
|
||||||
|
/** 是否正在触摸 */
|
||||||
|
isTouching: boolean;
|
||||||
|
/** 触摸事件处理器 */
|
||||||
|
handlers: {
|
||||||
|
onTouchStart: (e: React.TouchEvent) => void;
|
||||||
|
onTouchMove: (e: React.TouchEvent) => void;
|
||||||
|
onTouchEnd: (e: React.TouchEvent) => void;
|
||||||
|
onMouseDown: (e: React.MouseEvent) => void;
|
||||||
|
};
|
||||||
|
/** 当前触摸数据 */
|
||||||
|
touchData: TouchEventData | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算两点之间的距离
|
||||||
|
*/
|
||||||
|
function getDistance(p1: TouchPoint, p2: TouchPoint): number {
|
||||||
|
const dx = p2.x - p1.x;
|
||||||
|
const dy = p2.y - p1.y;
|
||||||
|
return Math.sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算滑动方向
|
||||||
|
*/
|
||||||
|
function getDirection(deltaX: number, deltaY: number): SwipeDirection | null {
|
||||||
|
const absDeltaX = Math.abs(deltaX);
|
||||||
|
const absDeltaY = Math.abs(deltaY);
|
||||||
|
|
||||||
|
if (absDeltaX < 10 && absDeltaY < 10) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (absDeltaX > absDeltaY) {
|
||||||
|
return deltaX > 0 ? "right" : "left";
|
||||||
|
} else {
|
||||||
|
return deltaY > 0 ? "down" : "up";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 灵活的触摸手势处理 Hook
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* const { handlers, isTouching } = useTouch({
|
||||||
|
* onSwipeLeft: () => console.log('向左滑动'),
|
||||||
|
* onSwipeRight: () => console.log('向右滑动'),
|
||||||
|
* onTap: () => console.log('点击'),
|
||||||
|
* onDoubleTap: () => console.log('双击'),
|
||||||
|
* onLongPress: () => console.log('长按'),
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* return <div {...handlers}>可触摸区域</div>;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function useTouch(options: UseTouchOptions = {}): UseTouchReturn {
|
||||||
|
const {
|
||||||
|
enabled = true,
|
||||||
|
swipeThreshold = 10,
|
||||||
|
swipeVelocityThreshold = 0.5,
|
||||||
|
longPressDelay = 500,
|
||||||
|
longPressMoveThreshold = 10,
|
||||||
|
doubleTapDelay = 300,
|
||||||
|
doubleTapMoveThreshold = 10,
|
||||||
|
onTouchStart,
|
||||||
|
onTouchMove,
|
||||||
|
onTouchEnd,
|
||||||
|
onTap,
|
||||||
|
onDoubleTap,
|
||||||
|
onLongPress,
|
||||||
|
onSwipe,
|
||||||
|
onSwipeLeft,
|
||||||
|
onSwipeRight,
|
||||||
|
onSwipeUp,
|
||||||
|
onSwipeDown,
|
||||||
|
preventDefault = false,
|
||||||
|
stopPropagation = false,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
const [isTouching, setIsTouching] = useState(false);
|
||||||
|
const [touchData, setTouchData] = useState<TouchEventData | null>(null);
|
||||||
|
|
||||||
|
// 引用
|
||||||
|
const startPointRef = useRef<TouchPoint | null>(null);
|
||||||
|
const currentPointRef = useRef<TouchPoint | null>(null);
|
||||||
|
const lastTapTimeRef = useRef<number>(0);
|
||||||
|
const longPressTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const isLongPressTriggeredRef = useRef(false);
|
||||||
|
const isMouseDownRef = useRef(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建触摸点
|
||||||
|
*/
|
||||||
|
const createTouchPoint = useCallback(
|
||||||
|
(x: number, y: number): TouchPoint => ({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建触摸事件数据
|
||||||
|
*/
|
||||||
|
const createTouchEventData = useCallback(
|
||||||
|
(
|
||||||
|
startPoint: TouchPoint,
|
||||||
|
currentPoint: TouchPoint,
|
||||||
|
event: React.TouchEvent | React.MouseEvent
|
||||||
|
): TouchEventData => {
|
||||||
|
const deltaX = currentPoint.x - startPoint.x;
|
||||||
|
const deltaY = currentPoint.y - startPoint.y;
|
||||||
|
const distance = getDistance(startPoint, currentPoint);
|
||||||
|
const deltaTime = currentPoint.timestamp - startPoint.timestamp;
|
||||||
|
const velocity = deltaTime > 0 ? distance / deltaTime : 0;
|
||||||
|
const direction = getDirection(deltaX, deltaY);
|
||||||
|
|
||||||
|
return {
|
||||||
|
startPoint,
|
||||||
|
currentPoint,
|
||||||
|
deltaX,
|
||||||
|
deltaY,
|
||||||
|
distance,
|
||||||
|
velocity,
|
||||||
|
direction,
|
||||||
|
event,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除长按定时器
|
||||||
|
*/
|
||||||
|
const clearLongPressTimer = useCallback(() => {
|
||||||
|
if (longPressTimerRef.current) {
|
||||||
|
clearTimeout(longPressTimerRef.current);
|
||||||
|
longPressTimerRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理触摸开始
|
||||||
|
*/
|
||||||
|
const handleStart = useCallback(
|
||||||
|
(x: number, y: number, event: React.TouchEvent | React.MouseEvent) => {
|
||||||
|
if (!enabled) return;
|
||||||
|
|
||||||
|
if (preventDefault) event.preventDefault();
|
||||||
|
if (stopPropagation) event.stopPropagation();
|
||||||
|
|
||||||
|
const point = createTouchPoint(x, y);
|
||||||
|
startPointRef.current = point;
|
||||||
|
currentPointRef.current = point;
|
||||||
|
isLongPressTriggeredRef.current = false;
|
||||||
|
|
||||||
|
setIsTouching(true);
|
||||||
|
|
||||||
|
const data = createTouchEventData(point, point, event);
|
||||||
|
setTouchData(data);
|
||||||
|
onTouchStart?.(data);
|
||||||
|
|
||||||
|
// 设置长按定时器
|
||||||
|
if (onLongPress) {
|
||||||
|
clearLongPressTimer();
|
||||||
|
longPressTimerRef.current = setTimeout(() => {
|
||||||
|
if (startPointRef.current && currentPointRef.current) {
|
||||||
|
const distance = getDistance(
|
||||||
|
startPointRef.current,
|
||||||
|
currentPointRef.current
|
||||||
|
);
|
||||||
|
if (distance < longPressMoveThreshold) {
|
||||||
|
isLongPressTriggeredRef.current = true;
|
||||||
|
const longPressData = createTouchEventData(
|
||||||
|
startPointRef.current,
|
||||||
|
currentPointRef.current,
|
||||||
|
event
|
||||||
|
);
|
||||||
|
onLongPress(longPressData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, longPressDelay);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
enabled,
|
||||||
|
preventDefault,
|
||||||
|
stopPropagation,
|
||||||
|
createTouchPoint,
|
||||||
|
createTouchEventData,
|
||||||
|
onTouchStart,
|
||||||
|
onLongPress,
|
||||||
|
longPressDelay,
|
||||||
|
longPressMoveThreshold,
|
||||||
|
clearLongPressTimer,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理触摸移动
|
||||||
|
*/
|
||||||
|
const handleMove = useCallback(
|
||||||
|
(x: number, y: number, event: React.TouchEvent | React.MouseEvent) => {
|
||||||
|
if (!enabled || !startPointRef.current) return;
|
||||||
|
|
||||||
|
if (preventDefault) event.preventDefault();
|
||||||
|
if (stopPropagation) event.stopPropagation();
|
||||||
|
|
||||||
|
const point = createTouchPoint(x, y);
|
||||||
|
currentPointRef.current = point;
|
||||||
|
|
||||||
|
const data = createTouchEventData(startPointRef.current, point, event);
|
||||||
|
setTouchData(data);
|
||||||
|
onTouchMove?.(data);
|
||||||
|
|
||||||
|
// 如果移动距离超过阈值,取消长按
|
||||||
|
if (data.distance > longPressMoveThreshold) {
|
||||||
|
clearLongPressTimer();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
enabled,
|
||||||
|
preventDefault,
|
||||||
|
stopPropagation,
|
||||||
|
createTouchPoint,
|
||||||
|
createTouchEventData,
|
||||||
|
onTouchMove,
|
||||||
|
longPressMoveThreshold,
|
||||||
|
clearLongPressTimer,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理触摸结束
|
||||||
|
*/
|
||||||
|
const handleEnd = useCallback(
|
||||||
|
(event: React.TouchEvent | React.MouseEvent) => {
|
||||||
|
if (!enabled || !startPointRef.current || !currentPointRef.current)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (preventDefault) event.preventDefault();
|
||||||
|
if (stopPropagation) event.stopPropagation();
|
||||||
|
|
||||||
|
clearLongPressTimer();
|
||||||
|
|
||||||
|
const data = createTouchEventData(
|
||||||
|
startPointRef.current,
|
||||||
|
currentPointRef.current,
|
||||||
|
event
|
||||||
|
);
|
||||||
|
onTouchEnd?.(data);
|
||||||
|
|
||||||
|
// 判断手势类型
|
||||||
|
const { distance, velocity, direction } = data;
|
||||||
|
|
||||||
|
// 长按已触发,不处理其他手势
|
||||||
|
if (isLongPressTriggeredRef.current) {
|
||||||
|
setIsTouching(false);
|
||||||
|
setTouchData(null);
|
||||||
|
startPointRef.current = null;
|
||||||
|
currentPointRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滑动手势
|
||||||
|
if (distance > swipeThreshold || velocity > swipeVelocityThreshold) {
|
||||||
|
onSwipe?.(data);
|
||||||
|
if (direction === "left") onSwipeLeft?.(data);
|
||||||
|
if (direction === "right") onSwipeRight?.(data);
|
||||||
|
if (direction === "up") onSwipeUp?.(data);
|
||||||
|
if (direction === "down") onSwipeDown?.(data);
|
||||||
|
}
|
||||||
|
// 点击或双击
|
||||||
|
else if (distance < doubleTapMoveThreshold) {
|
||||||
|
const now = Date.now();
|
||||||
|
const timeSinceLastTap = now - lastTapTimeRef.current;
|
||||||
|
|
||||||
|
if (timeSinceLastTap < doubleTapDelay && onDoubleTap) {
|
||||||
|
// 双击
|
||||||
|
onDoubleTap(data);
|
||||||
|
lastTapTimeRef.current = 0; // 重置,避免三击触发两次双击
|
||||||
|
} else {
|
||||||
|
// 单击
|
||||||
|
onTap?.(data);
|
||||||
|
lastTapTimeRef.current = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsTouching(false);
|
||||||
|
setTouchData(null);
|
||||||
|
startPointRef.current = null;
|
||||||
|
currentPointRef.current = null;
|
||||||
|
},
|
||||||
|
[
|
||||||
|
enabled,
|
||||||
|
preventDefault,
|
||||||
|
stopPropagation,
|
||||||
|
createTouchEventData,
|
||||||
|
onTouchEnd,
|
||||||
|
onSwipe,
|
||||||
|
onSwipeLeft,
|
||||||
|
onSwipeRight,
|
||||||
|
onSwipeUp,
|
||||||
|
onSwipeDown,
|
||||||
|
onTap,
|
||||||
|
onDoubleTap,
|
||||||
|
swipeThreshold,
|
||||||
|
swipeVelocityThreshold,
|
||||||
|
doubleTapMoveThreshold,
|
||||||
|
doubleTapDelay,
|
||||||
|
clearLongPressTimer,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 触摸事件处理器
|
||||||
|
const handleTouchStart = useCallback(
|
||||||
|
(e: React.TouchEvent) => {
|
||||||
|
if (e.touches.length === 1) {
|
||||||
|
const touch = e.touches[0];
|
||||||
|
handleStart(touch?.clientX || 0, touch?.clientY || 0, e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleStart]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTouchMove = useCallback(
|
||||||
|
(e: React.TouchEvent) => {
|
||||||
|
if (e.touches.length === 1) {
|
||||||
|
const touch = e.touches[0];
|
||||||
|
handleMove(touch?.clientX || 0, touch?.clientY || 0, e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleMove]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTouchEnd = useCallback(
|
||||||
|
(e: React.TouchEvent) => {
|
||||||
|
handleEnd(e);
|
||||||
|
},
|
||||||
|
[handleEnd]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 鼠标事件处理器(桌面端支持)
|
||||||
|
const handleMouseDown = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
isMouseDownRef.current = true;
|
||||||
|
handleStart(e.clientX, e.clientY, e);
|
||||||
|
|
||||||
|
const handleMouseMove = (event: MouseEvent) => {
|
||||||
|
if (isMouseDownRef.current) {
|
||||||
|
handleMove(
|
||||||
|
event.clientX,
|
||||||
|
event.clientY,
|
||||||
|
e as unknown as React.MouseEvent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = (event: MouseEvent) => {
|
||||||
|
if (isMouseDownRef.current) {
|
||||||
|
isMouseDownRef.current = false;
|
||||||
|
handleEnd(e as unknown as React.MouseEvent);
|
||||||
|
window.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
window.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", handleMouseMove);
|
||||||
|
window.addEventListener("mouseup", handleMouseUp);
|
||||||
|
},
|
||||||
|
[handleStart, handleMove, handleEnd]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isTouching,
|
||||||
|
handlers: {
|
||||||
|
onTouchStart: handleTouchStart,
|
||||||
|
onTouchMove: handleTouchMove,
|
||||||
|
onTouchEnd: handleTouchEnd,
|
||||||
|
onMouseDown: handleMouseDown,
|
||||||
|
},
|
||||||
|
touchData,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -206,10 +206,10 @@ const TabContent = ({
|
|||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-gradient-to-r from-[#82B2D6] to-[#6a9bc4] text-white">
|
<tr className="bg-gradient-to-r from-[#82B2D6] to-[#6a9bc4] text-white">
|
||||||
<th className="px-6 py-4 text-left text-lg font-semibold w-[70%]">
|
<th className="px-6 py-4 text-left text-3xl font-semibold w-[70%]">
|
||||||
活动名称
|
活动名称
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-4 text-left text-lg font-semibold w-[30%]">
|
<th className="px-6 py-4 text-left text-3xl font-semibold w-[30%]">
|
||||||
时间
|
时间
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -220,7 +220,7 @@ const TabContent = ({
|
|||||||
key={activity.id}
|
key={activity.id}
|
||||||
onClick={() => handleActivityClick(activity)}
|
onClick={() => handleActivityClick(activity)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"border-b border-gray-200 cursor-pointer transition-all duration-200",
|
"border-b border-gray-200 cursor-pointer transition-all duration-200 text-3xl",
|
||||||
"hover:bg-[#82B2D6]/10 hover:shadow-md",
|
"hover:bg-[#82B2D6]/10 hover:shadow-md",
|
||||||
index % 2 === 0 ? "bg-white" : "bg-gray-50"
|
index % 2 === 0 ? "bg-white" : "bg-gray-50"
|
||||||
)}
|
)}
|
||||||
@@ -259,10 +259,10 @@ const TabContent = ({
|
|||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-gradient-to-r from-[#82B2D6] to-[#6a9bc4] text-white">
|
<tr className="bg-gradient-to-r from-[#82B2D6] to-[#6a9bc4] text-white">
|
||||||
<th className="px-6 py-4 text-left text-lg font-semibold w-[70%]">
|
<th className="px-6 py-4 text-left text-3xl font-semibold w-[70%]">
|
||||||
活动名称
|
活动名称
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-4 text-left text-lg font-semibold w-[30%]">
|
<th className="px-6 py-4 text-left text-3xl font-semibold w-[30%]">
|
||||||
时间
|
时间
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -276,7 +276,7 @@ const TabContent = ({
|
|||||||
handleActivitiesClick(activity)
|
handleActivitiesClick(activity)
|
||||||
}
|
}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"border-b border-gray-200 cursor-pointer transition-all duration-200",
|
"border-b border-gray-200 cursor-pointer transition-all duration-200 text-3xl",
|
||||||
"hover:bg-[#82B2D6]/10 hover:shadow-md",
|
"hover:bg-[#82B2D6]/10 hover:shadow-md",
|
||||||
index % 2 === 0 ? "bg-white" : "bg-gray-50"
|
index % 2 === 0 ? "bg-white" : "bg-gray-50"
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { BackgroundImage, Image } from "@/components";
|
||||||
|
import { getStaticImage, getPublicPath } from "@/utils";
|
||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
|
||||||
|
export default function Guide() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [showVideo, setShowVideo] = useState(true);
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
|
||||||
|
// 监听视频播放完成
|
||||||
|
const handleVideoEnd = () => {
|
||||||
|
setShowVideo(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 跳过引导,直接进入宣传页
|
||||||
|
const handleSkip = () => {
|
||||||
|
navigate("/publicity");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BackgroundImage
|
||||||
|
src={getStaticImage("guide/引导页背景.png")}
|
||||||
|
className="h-full w-full relative"
|
||||||
|
backgroundSize="cover"
|
||||||
|
backgroundPosition="center"
|
||||||
|
>
|
||||||
|
{/* 跳过按钮 */}
|
||||||
|
<Image
|
||||||
|
src={getStaticImage("components/按钮-跳过.png")}
|
||||||
|
alt="跳过"
|
||||||
|
button
|
||||||
|
className="absolute top-0 right-0"
|
||||||
|
onClick={handleSkip}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 标题 */}
|
||||||
|
<Image
|
||||||
|
src={getStaticImage("publicity/名师工作室-标题.png")}
|
||||||
|
alt="标题"
|
||||||
|
className="absolute left-0 top-30"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 引导视频 - 仅在首次显示 */}
|
||||||
|
{
|
||||||
|
<div className="absolute w-[500px] left-20 -bottom-20 z-50">
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
src={getPublicPath("video/5_alpha channel.webm")}
|
||||||
|
autoPlay
|
||||||
|
playsInline
|
||||||
|
onEnded={handleVideoEnd}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{/* 引导内容区域 */}
|
||||||
|
{
|
||||||
|
<div className="absolute right-10 bottom-0 z-10">
|
||||||
|
{/* 发言底板 */}
|
||||||
|
<BackgroundImage
|
||||||
|
className="p-20"
|
||||||
|
width={1220}
|
||||||
|
height={763}
|
||||||
|
src={getStaticImage("guide/框.png")}
|
||||||
|
>
|
||||||
|
<p className="text-white text-3xl tracking-wide leading-15 text-left">
|
||||||
|
各位朋友好!我是曹君,智慧商旅系主任、网络营销与直播电商专业带头人。我的名师工作室聚焦产教融合,紧扣新兴行业需求,对接虹口北外滩发展战略。
|
||||||
|
带教以实战为核心,依托全国行业资源,让学员深入企业、乡村参与直播全流程;同时兼顾实践与教学能力双提升,致力培养复合型数字技能人才,期待与大家共成长!
|
||||||
|
</p>
|
||||||
|
</BackgroundImage>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</BackgroundImage>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { BackgroundImage, Image, Alert } from "@/components";
|
import { BackgroundImage, Image, Alert } from "@/components";
|
||||||
import { getStaticImage } from "@/utils";
|
import { getStaticImage } from "@/utils";
|
||||||
import { useState } from "react";
|
import { useState, Fragment, useRef, useEffect } from "react";
|
||||||
import { IframeModal } from "@/components/Modal";
|
import { useTouch } from "@/hooks";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 卢航-未选中.png
|
* 卢航-未选中.png
|
||||||
@@ -16,8 +16,8 @@ const teachers = [
|
|||||||
avatar: getStaticImage("menu/卢航-未选中.png"),
|
avatar: getStaticImage("menu/卢航-未选中.png"),
|
||||||
selectedAvatar: getStaticImage("menu/卢航-选中.png"),
|
selectedAvatar: getStaticImage("menu/卢航-选中.png"),
|
||||||
position: {
|
position: {
|
||||||
top: "20%",
|
top: "20.5%",
|
||||||
left: "12%",
|
left: "15%",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -36,8 +36,8 @@ const teachers = [
|
|||||||
avatar: getStaticImage("menu/龚丹-未选中.png"),
|
avatar: getStaticImage("menu/龚丹-未选中.png"),
|
||||||
selectedAvatar: getStaticImage("menu/龚丹-选中.png"),
|
selectedAvatar: getStaticImage("menu/龚丹-选中.png"),
|
||||||
position: {
|
position: {
|
||||||
top: "20%",
|
top: "20.5%",
|
||||||
left: "65%",
|
left: "63%",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -45,9 +45,73 @@ const teachers = [
|
|||||||
export default function Home() {
|
export default function Home() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [selectedTeacherId, setSelectedTeacherId] = useState(0);
|
const [selectedTeacherId, setSelectedTeacherId] = useState(0);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
const [showTipModal, setShowTipModal] = useState(false);
|
const [showTipModal, setShowTipModal] = useState(false);
|
||||||
|
|
||||||
|
// 旋转相关状态
|
||||||
|
const [rotation, setRotation] = useState(0);
|
||||||
|
const [isAutoRotating, setIsAutoRotating] = useState(true);
|
||||||
|
const autoRotateTimerRef = useRef<number | null>(null);
|
||||||
|
const resumeTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const startRotationRef = useRef(0);
|
||||||
|
|
||||||
|
// 自动旋转效果
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAutoRotating) {
|
||||||
|
const animate = () => {
|
||||||
|
setRotation((prev) => prev + 0.2);
|
||||||
|
autoRotateTimerRef.current = requestAnimationFrame(animate);
|
||||||
|
};
|
||||||
|
autoRotateTimerRef.current = requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (autoRotateTimerRef.current !== null) {
|
||||||
|
cancelAnimationFrame(autoRotateTimerRef.current);
|
||||||
|
autoRotateTimerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isAutoRotating]);
|
||||||
|
|
||||||
|
// 组件卸载时清理定时器
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (autoRotateTimerRef.current !== null) {
|
||||||
|
cancelAnimationFrame(autoRotateTimerRef.current);
|
||||||
|
}
|
||||||
|
if (resumeTimerRef.current !== null) {
|
||||||
|
clearTimeout(resumeTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 使用 useTouch 处理触摸手势
|
||||||
|
const { handlers } = useTouch({
|
||||||
|
onTouchStart: () => {
|
||||||
|
// 停止自动旋转
|
||||||
|
setIsAutoRotating(false);
|
||||||
|
|
||||||
|
// 清除恢复定时器
|
||||||
|
if (resumeTimerRef.current) {
|
||||||
|
clearTimeout(resumeTimerRef.current);
|
||||||
|
resumeTimerRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录开始时的旋转角度
|
||||||
|
startRotationRef.current = rotation;
|
||||||
|
},
|
||||||
|
onTouchMove: (data) => {
|
||||||
|
// 根据水平移动距离计算旋转角度(灵敏度 0.5)
|
||||||
|
const deltaRotation = data.deltaX * 0.5;
|
||||||
|
setRotation(startRotationRef.current + deltaRotation);
|
||||||
|
},
|
||||||
|
onTouchEnd: () => {
|
||||||
|
// 1秒后恢复自动旋转
|
||||||
|
resumeTimerRef.current = setTimeout(() => {
|
||||||
|
setIsAutoRotating(true);
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleSelectTeacher = (teacherId: number) => {
|
const handleSelectTeacher = (teacherId: number) => {
|
||||||
if (teacherId !== 2) {
|
if (teacherId !== 2) {
|
||||||
// 点击卢航或龚丹时显示提示
|
// 点击卢航或龚丹时显示提示
|
||||||
@@ -55,20 +119,7 @@ export default function Home() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSelectedTeacherId(teacherId);
|
setSelectedTeacherId(teacherId);
|
||||||
navigate("/Publicity");
|
navigate("/guide");
|
||||||
};
|
|
||||||
|
|
||||||
const handleStartExperiment = () => {
|
|
||||||
console.log("点击开启实验");
|
|
||||||
// navigate("/experiment");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAvatarClick = () => {
|
|
||||||
setIsModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseModal = () => {
|
|
||||||
setIsModalOpen(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -92,36 +143,6 @@ export default function Home() {
|
|||||||
className="absolute bottom-0 left-1/2 transform -translate-x-1/2"
|
className="absolute bottom-0 left-1/2 transform -translate-x-1/2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 数字人头像 */}
|
|
||||||
{!isModalOpen && (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className="flex flex-col items-center absolute right-4 bottom-2 cursor-pointer hover:scale-105 transition-all duration-300 z-20"
|
|
||||||
onClick={handleAvatarClick}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={getStaticImage("publicity/数字人-头像框.png")}
|
|
||||||
alt="数字人头像"
|
|
||||||
/>
|
|
||||||
<span className="text-white text-2xl font-bold tracking-wide">
|
|
||||||
点击我随时为你解答
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 嵌入式客服对话框 */}
|
|
||||||
<IframeModal
|
|
||||||
isOpen={isModalOpen}
|
|
||||||
onClose={handleCloseModal}
|
|
||||||
url="https://www.edutech.host/digitalHuman/"
|
|
||||||
title="数字人助手"
|
|
||||||
mode="embedded"
|
|
||||||
position="bottom-right"
|
|
||||||
width="420px"
|
|
||||||
height="650px"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 提示弹窗 */}
|
{/* 提示弹窗 */}
|
||||||
<Alert
|
<Alert
|
||||||
isOpen={showTipModal}
|
isOpen={showTipModal}
|
||||||
@@ -132,21 +153,30 @@ export default function Home() {
|
|||||||
|
|
||||||
<BackgroundImage
|
<BackgroundImage
|
||||||
src={getStaticImage("menu/转盘合.png")}
|
src={getStaticImage("menu/转盘合.png")}
|
||||||
className="absolute w-full h-500 -bottom-1/5 left-0"
|
width={2058}
|
||||||
|
height={2046}
|
||||||
|
className="absolute -bottom-1/5 -left-14.5 cursor-grab active:cursor-grabbing select-none"
|
||||||
backgroundSize="cover"
|
backgroundSize="cover"
|
||||||
backgroundPosition="center"
|
backgroundPosition="center"
|
||||||
|
{...handlers}
|
||||||
|
style={{
|
||||||
|
transform: `rotate(${rotation}deg)`,
|
||||||
|
transition: "none",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{teachers.map((teacher) => (
|
{teachers.map((teacher, _index) => (
|
||||||
<>
|
<Fragment key={teacher.id}>
|
||||||
<Image
|
<Image
|
||||||
key={teacher.id}
|
|
||||||
src={
|
src={
|
||||||
selectedTeacherId === teacher.id
|
selectedTeacherId === teacher.id
|
||||||
? teacher.selectedAvatar
|
? teacher.selectedAvatar
|
||||||
: teacher.avatar
|
: teacher.avatar
|
||||||
}
|
}
|
||||||
alt={teacher.name}
|
alt={teacher.name}
|
||||||
onClick={() => handleSelectTeacher(teacher.id)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleSelectTeacher(teacher.id);
|
||||||
|
}}
|
||||||
className="absolute"
|
className="absolute"
|
||||||
style={{
|
style={{
|
||||||
top:
|
top:
|
||||||
@@ -157,7 +187,8 @@ export default function Home() {
|
|||||||
selectedTeacherId === teacher.id
|
selectedTeacherId === teacher.id
|
||||||
? "36%"
|
? "36%"
|
||||||
: teacher.position.left,
|
: teacher.position.left,
|
||||||
transition: "transform 0.2s ease, z-index 0.2s ease",
|
transform: `rotate(${-rotation}deg)`,
|
||||||
|
transition: "none",
|
||||||
}}
|
}}
|
||||||
button={selectedTeacherId !== teacher.id}
|
button={selectedTeacherId !== teacher.id}
|
||||||
/>
|
/>
|
||||||
@@ -168,7 +199,7 @@ export default function Home() {
|
|||||||
<div className="absolute top-[25%] left-[65%] text-gray-500 text-3xl font-bold cursor-not-allowed">
|
<div className="absolute top-[25%] left-[65%] text-gray-500 text-3xl font-bold cursor-not-allowed">
|
||||||
该名师工作室数字睿教正在筹划中
|
该名师工作室数字睿教正在筹划中
|
||||||
</div> */}
|
</div> */}
|
||||||
</>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</BackgroundImage>
|
</BackgroundImage>
|
||||||
</BackgroundImage>
|
</BackgroundImage>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { BackgroundImage, Image } from "@/components";
|
import { BackgroundImage, Image } from "@/components";
|
||||||
import { IframeModal } from "@/components/Modal";
|
import { IframeModal } from "@/components/Modal";
|
||||||
import { getStaticImage } from "@/utils";
|
import { getPublicPath, getStaticImage } from "@/utils";
|
||||||
import { useState } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useFirstVisitStore } from "@/store";
|
||||||
|
|
||||||
// 解答弹窗组件
|
// 解答弹窗组件
|
||||||
interface DialogBoxProps {
|
interface DialogBoxProps {
|
||||||
@@ -116,6 +117,33 @@ export default function PublicityPage() {
|
|||||||
const [selectedFunctionId, setSelectedFunctionId] = useState(0);
|
const [selectedFunctionId, setSelectedFunctionId] = useState(0);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
// 首次访问状态管理
|
||||||
|
const { hasSeenIntro, setHasSeenIntro, isExpired, reset } =
|
||||||
|
useFirstVisitStore();
|
||||||
|
const [showIntroVideo, setShowIntroVideo] = useState(false);
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
|
||||||
|
// 检查是否显示引导视频
|
||||||
|
useEffect(() => {
|
||||||
|
// 如果存储已过期,重置状态
|
||||||
|
if (isExpired()) {
|
||||||
|
reset();
|
||||||
|
setShowIntroVideo(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasSeenIntro) {
|
||||||
|
setShowIntroVideo(true);
|
||||||
|
}
|
||||||
|
}, [hasSeenIntro, isExpired, reset]);
|
||||||
|
|
||||||
|
// 监听视频播放完成
|
||||||
|
const handleVideoEnd = () => {
|
||||||
|
setShowIntroVideo(false);
|
||||||
|
setHasSeenIntro();
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const handleOpenDialog = () => {
|
const handleOpenDialog = () => {
|
||||||
setIsDialogOpen(true);
|
setIsDialogOpen(true);
|
||||||
};
|
};
|
||||||
@@ -170,7 +198,7 @@ export default function PublicityPage() {
|
|||||||
className="absolute left-0 top-30"
|
className="absolute left-0 top-30"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 数字人 */}
|
{/* 数字人 - 未播放引导视频时显示 */}
|
||||||
<Image
|
<Image
|
||||||
src={getStaticImage("publicity/曹君名师数字人分身.png")}
|
src={getStaticImage("publicity/曹君名师数字人分身.png")}
|
||||||
alt="数字人"
|
alt="数字人"
|
||||||
@@ -178,7 +206,7 @@ export default function PublicityPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 数字人头像 */}
|
{/* 数字人头像 */}
|
||||||
{!isModalOpen && (
|
{/* {!isModalOpen && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="flex flex-col items-center absolute right-4 bottom-2 cursor-pointer hover:scale-105 transition-all duration-300 z-10"
|
className="flex flex-col items-center absolute right-4 bottom-2 cursor-pointer hover:scale-105 transition-all duration-300 z-10"
|
||||||
@@ -193,7 +221,7 @@ export default function PublicityPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)} */}
|
||||||
{/* 提示 */}
|
{/* 提示 */}
|
||||||
{/* <Image
|
{/* <Image
|
||||||
src={getStaticImage("publicity/点击提示.png")}
|
src={getStaticImage("publicity/点击提示.png")}
|
||||||
@@ -204,10 +232,10 @@ export default function PublicityPage() {
|
|||||||
/> */}
|
/> */}
|
||||||
|
|
||||||
{/* 解答弹窗 */}
|
{/* 解答弹窗 */}
|
||||||
<DialogBox isOpen={isDialogOpen} onClose={handleCloseDialog} />
|
{/* <DialogBox isOpen={isDialogOpen} onClose={handleCloseDialog} /> */}
|
||||||
|
|
||||||
{/* 嵌入式客服对话框 */}
|
{/* 嵌入式客服对话框 */}
|
||||||
<IframeModal
|
{/* <IframeModal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={handleCloseModal}
|
onClose={handleCloseModal}
|
||||||
url="https://www.edutech.host/digitalHuman/"
|
url="https://www.edutech.host/digitalHuman/"
|
||||||
@@ -216,7 +244,7 @@ export default function PublicityPage() {
|
|||||||
position="bottom-right"
|
position="bottom-right"
|
||||||
width="420px"
|
width="420px"
|
||||||
height="650px"
|
height="650px"
|
||||||
/>
|
/> */}
|
||||||
</BackgroundImage>
|
</BackgroundImage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import Menu from "@/pages/Menu";
|
|||||||
import NotFound from "@/pages/NotFound";
|
import NotFound from "@/pages/NotFound";
|
||||||
import Publicity from "@/pages/Publicity";
|
import Publicity from "@/pages/Publicity";
|
||||||
import Content from "@/pages/Content";
|
import Content from "@/pages/Content";
|
||||||
|
import Guide from "@/pages/Guide";
|
||||||
/**
|
/**
|
||||||
* 应用路由配置
|
* 应用路由配置
|
||||||
* 使用 React Router v7 的 createHashRouter
|
* 使用 React Router v7 的 createHashRouter
|
||||||
@@ -22,6 +23,10 @@ const router = createHashRouter([
|
|||||||
path: "/menu",
|
path: "/menu",
|
||||||
element: <Menu />,
|
element: <Menu />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/guide",
|
||||||
|
element: <Guide />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/publicity",
|
path: "/publicity",
|
||||||
element: <Publicity />,
|
element: <Publicity />,
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { persist } from "zustand/middleware";
|
||||||
|
|
||||||
|
interface FirstVisitState {
|
||||||
|
hasSeenIntro: boolean;
|
||||||
|
timestamp: number | null; // 存储时间戳
|
||||||
|
setHasSeenIntro: () => void;
|
||||||
|
isExpired: () => boolean; // 检查是否过期
|
||||||
|
reset: () => void; // 重置状态
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储有效期(毫秒),默认5分钟
|
||||||
|
const STORAGE_EXPIRY_TIME = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
export const useFirstVisitStore = create<FirstVisitState>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
hasSeenIntro: false,
|
||||||
|
timestamp: null,
|
||||||
|
setHasSeenIntro: () => {
|
||||||
|
set({ hasSeenIntro: true, timestamp: Date.now() });
|
||||||
|
},
|
||||||
|
isExpired: () => {
|
||||||
|
const state = get();
|
||||||
|
if (!state.timestamp) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const now = Date.now();
|
||||||
|
return now - state.timestamp > STORAGE_EXPIRY_TIME;
|
||||||
|
},
|
||||||
|
reset: () => {
|
||||||
|
set({ hasSeenIntro: false, timestamp: null });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "first-visit-storage", // 存储到localStorage的key
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./firstVisitStore";
|
||||||