修复导览与讲解位置预览逻辑
This commit is contained in:
@@ -18,17 +18,18 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dcloudio/uni-app": "3.0.0-alpha-5010120260519001",
|
"@dcloudio/uni-app": "3.0.0-alpha-5010120260519001",
|
||||||
"@dcloudio/uni-app-plus": "3.0.0-alpha-5010120260519001",
|
"@dcloudio/uni-app-plus": "3.0.0-alpha-5010120260519001",
|
||||||
|
"@dcloudio/uni-components": "3.0.0-alpha-5010120260519001",
|
||||||
"@dcloudio/uni-h5": "3.0.0-alpha-5010120260519001",
|
"@dcloudio/uni-h5": "3.0.0-alpha-5010120260519001",
|
||||||
"@dcloudio/uni-mp-weixin": "3.0.0-alpha-5010120260519001",
|
"@dcloudio/uni-mp-weixin": "3.0.0-alpha-5010120260519001",
|
||||||
"three": "0.163.0",
|
"three": "0.163.0",
|
||||||
"vue": "3.4.21"
|
"vue": "3.4.21"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/three": "0.163.0",
|
|
||||||
"@dcloudio/types": "3.4.31",
|
"@dcloudio/types": "3.4.31",
|
||||||
"@dcloudio/uni-automator": "3.0.0-alpha-5010120260519001",
|
"@dcloudio/uni-automator": "3.0.0-alpha-5010120260519001",
|
||||||
"@dcloudio/uni-cli-shared": "3.0.0-alpha-5010120260519001",
|
"@dcloudio/uni-cli-shared": "3.0.0-alpha-5010120260519001",
|
||||||
"@dcloudio/vite-plugin-uni": "3.0.0-alpha-5010120260519001",
|
"@dcloudio/vite-plugin-uni": "3.0.0-alpha-5010120260519001",
|
||||||
|
"@types/three": "0.163.0",
|
||||||
"@typescript-eslint/eslint-plugin": "7.18.0",
|
"@typescript-eslint/eslint-plugin": "7.18.0",
|
||||||
"@typescript-eslint/parser": "7.18.0",
|
"@typescript-eslint/parser": "7.18.0",
|
||||||
"@vue/runtime-core": "3.4.21",
|
"@vue/runtime-core": "3.4.21",
|
||||||
|
|||||||
109
pnpm-lock.yaml
generated
109
pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
|||||||
'@dcloudio/uni-app-plus':
|
'@dcloudio/uni-app-plus':
|
||||||
specifier: 3.0.0-alpha-5010120260519001
|
specifier: 3.0.0-alpha-5010120260519001
|
||||||
version: 3.0.0-alpha-5010120260519001(postcss@8.5.15)(rollup@4.60.4)(vite@5.2.8(@types/node@25.9.1)(sass@1.99.0)(terser@5.47.1))(vue@3.4.21(typescript@5.3.3))
|
version: 3.0.0-alpha-5010120260519001(postcss@8.5.15)(rollup@4.60.4)(vite@5.2.8(@types/node@25.9.1)(sass@1.99.0)(terser@5.47.1))(vue@3.4.21(typescript@5.3.3))
|
||||||
|
'@dcloudio/uni-components':
|
||||||
|
specifier: 3.0.0-alpha-5010120260519001
|
||||||
|
version: 3.0.0-alpha-5010120260519001(postcss@8.5.15)(rollup@4.60.4)(vite@5.2.8(@types/node@25.9.1)(sass@1.99.0)(terser@5.47.1))(vue@3.4.21(typescript@5.3.3))
|
||||||
'@dcloudio/uni-h5':
|
'@dcloudio/uni-h5':
|
||||||
specifier: 3.0.0-alpha-5010120260519001
|
specifier: 3.0.0-alpha-5010120260519001
|
||||||
version: 3.0.0-alpha-5010120260519001(postcss@8.5.15)(rollup@4.60.4)(vite@5.2.8(@types/node@25.9.1)(sass@1.99.0)(terser@5.47.1))(vue@3.4.21(typescript@5.3.3))
|
version: 3.0.0-alpha-5010120260519001(postcss@8.5.15)(rollup@4.60.4)(vite@5.2.8(@types/node@25.9.1)(sass@1.99.0)(terser@5.47.1))(vue@3.4.21(typescript@5.3.3))
|
||||||
@@ -136,6 +139,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
|
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/helper-globals@7.29.7':
|
||||||
|
resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/helper-member-expression-to-functions@7.28.5':
|
'@babel/helper-member-expression-to-functions@7.28.5':
|
||||||
resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==}
|
resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -178,14 +185,26 @@ packages:
|
|||||||
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
|
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/helper-string-parser@7.29.7':
|
||||||
|
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@7.28.5':
|
'@babel/helper-validator-identifier@7.28.5':
|
||||||
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/helper-validator-identifier@7.29.7':
|
||||||
|
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/helper-validator-option@7.27.1':
|
'@babel/helper-validator-option@7.27.1':
|
||||||
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
|
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/helper-validator-option@7.29.7':
|
||||||
|
resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/helper-wrap-function@7.28.6':
|
'@babel/helper-wrap-function@7.28.6':
|
||||||
resolution: {integrity: sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==}
|
resolution: {integrity: sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -2420,8 +2439,8 @@ packages:
|
|||||||
debug:
|
debug:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
form-data@3.0.4:
|
form-data@3.0.5:
|
||||||
resolution: {integrity: sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==}
|
resolution: {integrity: sha512-j23EibVLnp4zNXGW7LjryXYa2X6U/M96yoOX+ybZxwkYajdxRNEqYY3zhh7y0i6kfISKS2jr+EJq1YTUDEv5+w==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
forwarded@0.2.0:
|
forwarded@0.2.0:
|
||||||
@@ -2485,9 +2504,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
glob@7.2.3:
|
glob@7.2.0:
|
||||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==}
|
||||||
deprecated: Glob versions prior to v9 are no longer supported
|
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
|
|
||||||
global@4.4.0:
|
global@4.4.0:
|
||||||
resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==}
|
resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==}
|
||||||
@@ -2533,6 +2552,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
|
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
hasown@2.0.4:
|
||||||
|
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
he@1.2.0:
|
he@1.2.0:
|
||||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -2836,8 +2859,8 @@ packages:
|
|||||||
js-tokens@9.0.1:
|
js-tokens@9.0.1:
|
||||||
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
|
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
|
||||||
|
|
||||||
js-yaml@3.14.2:
|
js-yaml@3.15.0:
|
||||||
resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
|
resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
js-yaml@4.2.0:
|
js-yaml@4.2.0:
|
||||||
@@ -3097,8 +3120,8 @@ packages:
|
|||||||
nth-check@2.1.1:
|
nth-check@2.1.1:
|
||||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||||
|
|
||||||
nwsapi@2.2.23:
|
nwsapi@2.2.24:
|
||||||
resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==}
|
resolution: {integrity: sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==}
|
||||||
|
|
||||||
object-inspect@1.13.4:
|
object-inspect@1.13.4:
|
||||||
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
||||||
@@ -4054,8 +4077,8 @@ packages:
|
|||||||
write-file-atomic@3.0.3:
|
write-file-atomic@3.0.3:
|
||||||
resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==}
|
resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==}
|
||||||
|
|
||||||
ws@7.5.10:
|
ws@7.5.11:
|
||||||
resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==}
|
resolution: {integrity: sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==}
|
||||||
engines: {node: '>=8.3.0'}
|
engines: {node: '>=8.3.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
bufferutil: ^4.0.1
|
bufferutil: ^4.0.1
|
||||||
@@ -4128,8 +4151,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
|
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
yargs@16.2.0:
|
yargs@16.2.2:
|
||||||
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
|
resolution: {integrity: sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
yocto-queue@0.1.0:
|
yocto-queue@0.1.0:
|
||||||
@@ -4150,7 +4173,7 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/code-frame@7.29.0':
|
'@babel/code-frame@7.29.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
|
|
||||||
@@ -4218,7 +4241,7 @@ snapshots:
|
|||||||
'@babel/helper-compilation-targets@7.28.6':
|
'@babel/helper-compilation-targets@7.28.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/compat-data': 7.29.3
|
'@babel/compat-data': 7.29.3
|
||||||
'@babel/helper-validator-option': 7.27.1
|
'@babel/helper-validator-option': 7.29.7
|
||||||
browserslist: 4.28.2
|
browserslist: 4.28.2
|
||||||
lru-cache: 5.1.1
|
lru-cache: 5.1.1
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
@@ -4256,6 +4279,8 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/helper-globals@7.28.0': {}
|
'@babel/helper-globals@7.28.0': {}
|
||||||
|
|
||||||
|
'@babel/helper-globals@7.29.7': {}
|
||||||
|
|
||||||
'@babel/helper-member-expression-to-functions@7.28.5':
|
'@babel/helper-member-expression-to-functions@7.28.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/traverse': 7.29.0
|
'@babel/traverse': 7.29.0
|
||||||
@@ -4274,7 +4299,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.25.2
|
'@babel/core': 7.25.2
|
||||||
'@babel/helper-module-imports': 7.28.6
|
'@babel/helper-module-imports': 7.28.6
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
'@babel/traverse': 7.29.0
|
'@babel/traverse': 7.29.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -4283,7 +4308,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
'@babel/helper-module-imports': 7.28.6
|
'@babel/helper-module-imports': 7.28.6
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
'@babel/traverse': 7.29.0
|
'@babel/traverse': 7.29.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -4321,10 +4346,16 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/helper-string-parser@7.27.1': {}
|
'@babel/helper-string-parser@7.27.1': {}
|
||||||
|
|
||||||
|
'@babel/helper-string-parser@7.29.7': {}
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@7.28.5': {}
|
'@babel/helper-validator-identifier@7.28.5': {}
|
||||||
|
|
||||||
|
'@babel/helper-validator-identifier@7.29.7': {}
|
||||||
|
|
||||||
'@babel/helper-validator-option@7.27.1': {}
|
'@babel/helper-validator-option@7.27.1': {}
|
||||||
|
|
||||||
|
'@babel/helper-validator-option@7.29.7': {}
|
||||||
|
|
||||||
'@babel/helper-wrap-function@7.28.6':
|
'@babel/helper-wrap-function@7.28.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/template': 7.28.6
|
'@babel/template': 7.28.6
|
||||||
@@ -4955,7 +4986,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.0
|
'@babel/code-frame': 7.29.0
|
||||||
'@babel/generator': 7.29.1
|
'@babel/generator': 7.29.1
|
||||||
'@babel/helper-globals': 7.28.0
|
'@babel/helper-globals': 7.29.7
|
||||||
'@babel/parser': 7.29.3
|
'@babel/parser': 7.29.3
|
||||||
'@babel/template': 7.28.6
|
'@babel/template': 7.28.6
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.0
|
||||||
@@ -4971,8 +5002,8 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/types@7.29.0':
|
'@babel/types@7.29.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-string-parser': 7.27.1
|
'@babel/helper-string-parser': 7.29.7
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
|
|
||||||
'@bcoe/v8-coverage@0.2.3': {}
|
'@bcoe/v8-coverage@0.2.3': {}
|
||||||
|
|
||||||
@@ -5559,7 +5590,7 @@ snapshots:
|
|||||||
camelcase: 5.3.1
|
camelcase: 5.3.1
|
||||||
find-up: 4.1.0
|
find-up: 4.1.0
|
||||||
get-package-type: 0.1.0
|
get-package-type: 0.1.0
|
||||||
js-yaml: 3.14.2
|
js-yaml: 3.15.0
|
||||||
resolve-from: 5.0.0
|
resolve-from: 5.0.0
|
||||||
|
|
||||||
'@istanbuljs/schema@0.1.6': {}
|
'@istanbuljs/schema@0.1.6': {}
|
||||||
@@ -5643,7 +5674,7 @@ snapshots:
|
|||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
collect-v8-coverage: 1.0.3
|
collect-v8-coverage: 1.0.3
|
||||||
exit: 0.1.2
|
exit: 0.1.2
|
||||||
glob: 7.2.3
|
glob: 7.2.0
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
istanbul-lib-coverage: 3.2.2
|
istanbul-lib-coverage: 3.2.2
|
||||||
istanbul-lib-instrument: 5.2.1
|
istanbul-lib-instrument: 5.2.1
|
||||||
@@ -7271,12 +7302,12 @@ snapshots:
|
|||||||
|
|
||||||
follow-redirects@1.16.0: {}
|
follow-redirects@1.16.0: {}
|
||||||
|
|
||||||
form-data@3.0.4:
|
form-data@3.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
asynckit: 0.4.0
|
asynckit: 0.4.0
|
||||||
combined-stream: 1.0.8
|
combined-stream: 1.0.8
|
||||||
es-set-tostringtag: 2.1.0
|
es-set-tostringtag: 2.1.0
|
||||||
hasown: 2.0.3
|
hasown: 2.0.4
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
|
|
||||||
forwarded@0.2.0: {}
|
forwarded@0.2.0: {}
|
||||||
@@ -7336,7 +7367,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
|
|
||||||
glob@7.2.3:
|
glob@7.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
fs.realpath: 1.0.0
|
fs.realpath: 1.0.0
|
||||||
inflight: 1.0.6
|
inflight: 1.0.6
|
||||||
@@ -7385,6 +7416,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
hasown@2.0.4:
|
||||||
|
dependencies:
|
||||||
|
function-bind: 1.1.2
|
||||||
|
|
||||||
he@1.2.0: {}
|
he@1.2.0: {}
|
||||||
|
|
||||||
html-encoding-sniffer@2.0.1:
|
html-encoding-sniffer@2.0.1:
|
||||||
@@ -7567,7 +7602,7 @@ snapshots:
|
|||||||
jest-util: 27.5.1
|
jest-util: 27.5.1
|
||||||
jest-validate: 27.5.1
|
jest-validate: 27.5.1
|
||||||
prompts: 2.4.2
|
prompts: 2.4.2
|
||||||
yargs: 16.2.0
|
yargs: 16.2.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- canvas
|
- canvas
|
||||||
@@ -7584,7 +7619,7 @@ snapshots:
|
|||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
ci-info: 3.9.0
|
ci-info: 3.9.0
|
||||||
deepmerge: 4.3.1
|
deepmerge: 4.3.1
|
||||||
glob: 7.2.3
|
glob: 7.2.0
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
jest-circus: 27.5.1
|
jest-circus: 27.5.1
|
||||||
jest-environment-jsdom: 27.5.1
|
jest-environment-jsdom: 27.5.1
|
||||||
@@ -7789,7 +7824,7 @@ snapshots:
|
|||||||
cjs-module-lexer: 1.4.3
|
cjs-module-lexer: 1.4.3
|
||||||
collect-v8-coverage: 1.0.3
|
collect-v8-coverage: 1.0.3
|
||||||
execa: 5.1.1
|
execa: 5.1.1
|
||||||
glob: 7.2.3
|
glob: 7.2.0
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
jest-haste-map: 27.5.1
|
jest-haste-map: 27.5.1
|
||||||
jest-message-util: 27.5.1
|
jest-message-util: 27.5.1
|
||||||
@@ -7898,7 +7933,7 @@ snapshots:
|
|||||||
|
|
||||||
js-tokens@9.0.1: {}
|
js-tokens@9.0.1: {}
|
||||||
|
|
||||||
js-yaml@3.14.2:
|
js-yaml@3.15.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
argparse: 1.0.10
|
argparse: 1.0.10
|
||||||
esprima: 4.0.1
|
esprima: 4.0.1
|
||||||
@@ -7918,12 +7953,12 @@ snapshots:
|
|||||||
decimal.js: 10.6.0
|
decimal.js: 10.6.0
|
||||||
domexception: 2.0.1
|
domexception: 2.0.1
|
||||||
escodegen: 2.1.0
|
escodegen: 2.1.0
|
||||||
form-data: 3.0.4
|
form-data: 3.0.5
|
||||||
html-encoding-sniffer: 2.0.1
|
html-encoding-sniffer: 2.0.1
|
||||||
http-proxy-agent: 4.0.1
|
http-proxy-agent: 4.0.1
|
||||||
https-proxy-agent: 5.0.1
|
https-proxy-agent: 5.0.1
|
||||||
is-potential-custom-element-name: 1.0.1
|
is-potential-custom-element-name: 1.0.1
|
||||||
nwsapi: 2.2.23
|
nwsapi: 2.2.24
|
||||||
parse5: 6.0.1
|
parse5: 6.0.1
|
||||||
saxes: 5.0.1
|
saxes: 5.0.1
|
||||||
symbol-tree: 3.2.4
|
symbol-tree: 3.2.4
|
||||||
@@ -7934,7 +7969,7 @@ snapshots:
|
|||||||
whatwg-encoding: 1.0.5
|
whatwg-encoding: 1.0.5
|
||||||
whatwg-mimetype: 2.3.0
|
whatwg-mimetype: 2.3.0
|
||||||
whatwg-url: 8.7.0
|
whatwg-url: 8.7.0
|
||||||
ws: 7.5.10
|
ws: 7.5.11
|
||||||
xml-name-validator: 3.0.0
|
xml-name-validator: 3.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
@@ -8140,7 +8175,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
boolbase: 1.0.0
|
boolbase: 1.0.0
|
||||||
|
|
||||||
nwsapi@2.2.23: {}
|
nwsapi@2.2.24: {}
|
||||||
|
|
||||||
object-inspect@1.13.4: {}
|
object-inspect@1.13.4: {}
|
||||||
|
|
||||||
@@ -8478,7 +8513,7 @@ snapshots:
|
|||||||
|
|
||||||
rimraf@3.0.2:
|
rimraf@3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
glob: 7.2.3
|
glob: 7.2.0
|
||||||
|
|
||||||
rollup@4.60.4:
|
rollup@4.60.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8812,7 +8847,7 @@ snapshots:
|
|||||||
test-exclude@6.0.0:
|
test-exclude@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@istanbuljs/schema': 0.1.6
|
'@istanbuljs/schema': 0.1.6
|
||||||
glob: 7.2.3
|
glob: 7.2.0
|
||||||
minimatch: 3.1.5
|
minimatch: 3.1.5
|
||||||
|
|
||||||
text-table@0.2.0: {}
|
text-table@0.2.0: {}
|
||||||
@@ -9069,7 +9104,7 @@ snapshots:
|
|||||||
signal-exit: 3.0.7
|
signal-exit: 3.0.7
|
||||||
typedarray-to-buffer: 3.1.5
|
typedarray-to-buffer: 3.1.5
|
||||||
|
|
||||||
ws@7.5.10: {}
|
ws@7.5.11: {}
|
||||||
|
|
||||||
ws@8.18.0: {}
|
ws@8.18.0: {}
|
||||||
|
|
||||||
@@ -9111,7 +9146,7 @@ snapshots:
|
|||||||
|
|
||||||
yargs-parser@20.2.9: {}
|
yargs-parser@20.2.9: {}
|
||||||
|
|
||||||
yargs@16.2.0:
|
yargs@16.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
cliui: 7.0.4
|
cliui: 7.0.4
|
||||||
escalade: 3.2.0
|
escalade: 3.2.0
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
这些 JSON 是早期演示数据,当前导览模块不再以这里作为数据源。
|
这些 JSON 是早期演示数据,当前导览模块不再以这里作为数据源。
|
||||||
|
|
||||||
当前室内导览、设施搜索、位置预览统一通过 Provider / Adapter / Repository / UseCase 数据边界读取 `static/nav-assets/codex_nav_20260621_175310_museum_default_centerline_finalxy`,页面和展示组件不要直接依赖静态包 JSON 文件。
|
当前馆内导览、设施搜索、位置预览统一通过 Provider / Adapter / Repository / UseCase 数据边界读取 `static/nav-assets/codex_nav_20260621_175310_museum_default_centerline_finalxy`,页面和展示组件不要直接依赖静态包 JSON 文件。
|
||||||
|
|
||||||
当前展厅、单元、讲解点、展品、讲解词和 POI 业务内容统一通过 `static/guide-data` 静态数据包读取,默认数据源模式为 `VITE_GUIDE_CONTENT_SOURCE_MODE=static`。
|
当前展厅、单元、讲解点、展品、讲解词和 POI 业务内容统一通过 `static/guide-data` 静态数据包读取,默认数据源模式为 `VITE_GUIDE_CONTENT_SOURCE_MODE=static`。
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,6 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<view class="section-heading">
|
|
||||||
<text class="section-title">请选择展厅</text>
|
|
||||||
<text class="section-count">{{ filteredHalls.length }} 个展厅</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view v-if="filteredHalls.length" class="hall-list">
|
<view v-if="filteredHalls.length" class="hall-list">
|
||||||
<view
|
<view
|
||||||
v-for="hall in filteredHalls"
|
v-for="hall in filteredHalls"
|
||||||
@@ -37,10 +32,10 @@
|
|||||||
@tap="handleHallClick(hall.id)"
|
@tap="handleHallClick(hall.id)"
|
||||||
>
|
>
|
||||||
<image
|
<image
|
||||||
v-if="hall.image"
|
v-if="hallIconUrl(hall)"
|
||||||
class="hall-thumb"
|
class="hall-thumb"
|
||||||
:src="hall.image"
|
:src="hallIconUrl(hall)"
|
||||||
mode="aspectFill"
|
mode="aspectFit"
|
||||||
/>
|
/>
|
||||||
<view v-else class="hall-thumb placeholder">
|
<view v-else class="hall-thumb placeholder">
|
||||||
<text class="hall-thumb-text">{{ hallIconText(hall.name) }}</text>
|
<text class="hall-thumb-text">{{ hallIconText(hall.name) }}</text>
|
||||||
@@ -96,8 +91,26 @@ const emit = defineEmits<{
|
|||||||
back: []
|
back: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const HALL_ICON_BASE = '/static/icons/halls'
|
||||||
|
|
||||||
|
const hallIconMap: Record<string, string> = {
|
||||||
|
宇宙厅: `${HALL_ICON_BASE}/universe.jpg`,
|
||||||
|
地球厅: `${HALL_ICON_BASE}/earth.jpg`,
|
||||||
|
演化厅: `${HALL_ICON_BASE}/evolution.jpg`,
|
||||||
|
恐龙厅: `${HALL_ICON_BASE}/dinosaur.jpg`,
|
||||||
|
人类厅: `${HALL_ICON_BASE}/human.jpg`,
|
||||||
|
动物厅: `${HALL_ICON_BASE}/animal.jpg`,
|
||||||
|
生物厅: `${HALL_ICON_BASE}/biology.jpg`,
|
||||||
|
生态厅: `${HALL_ICON_BASE}/ecology.jpg`,
|
||||||
|
家园厅: `${HALL_ICON_BASE}/homeland.jpg`
|
||||||
|
}
|
||||||
|
|
||||||
const filteredHalls = computed(() => props.halls)
|
const filteredHalls = computed(() => props.halls)
|
||||||
|
|
||||||
|
const hallIconUrl = (hall: ExplainHallSelectItem) => {
|
||||||
|
return hallIconMap[hall.name.trim()] || hall.image || ''
|
||||||
|
}
|
||||||
|
|
||||||
const hallIconText = (name: string) => name.trim().slice(0, 1) || '讲'
|
const hallIconText = (name: string) => name.trim().slice(0, 1) || '讲'
|
||||||
|
|
||||||
const handleHallClick = (hallId: string) => {
|
const handleHallClick = (hallId: string) => {
|
||||||
@@ -124,8 +137,6 @@ const handleBack = () => {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title,
|
|
||||||
.section-count,
|
|
||||||
.state-title,
|
.state-title,
|
||||||
.state-desc,
|
.state-desc,
|
||||||
.hall-name,
|
.hall-name,
|
||||||
@@ -180,29 +191,6 @@ const handleBack = () => {
|
|||||||
color: #151713;
|
color: #151713;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-heading {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 19px;
|
|
||||||
line-height: 27px;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #151713;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-count {
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 18px;
|
|
||||||
color: #7b8275;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hall-list {
|
.hall-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -231,7 +219,10 @@ const handleBack = () => {
|
|||||||
width: 68px;
|
width: 68px;
|
||||||
height: 68px;
|
height: 68px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #f1f2ee;
|
background: #ffffff;
|
||||||
|
border: 1px solid rgba(36, 49, 42, 0.06);
|
||||||
|
box-sizing: border-box;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hall-thumb.placeholder {
|
.hall-thumb.placeholder {
|
||||||
|
|||||||
@@ -38,10 +38,10 @@
|
|||||||
|
|
||||||
<!-- 功能按钮 -->
|
<!-- 功能按钮 -->
|
||||||
<cover-view class="action-buttons">
|
<cover-view class="action-buttons">
|
||||||
<!-- 进入 3D 室内模式按钮 -->
|
<!-- 进入 3D 馆内模式按钮 -->
|
||||||
<cover-view class="action-btn indoor-mode-btn" @tap="handleEnter3D">
|
<cover-view class="action-btn indoor-mode-btn" @tap="handleEnter3D">
|
||||||
<cover-view class="btn-text">🏛</cover-view>
|
<cover-view class="btn-text">🏛</cover-view>
|
||||||
<cover-view class="btn-sub-text">室内</cover-view>
|
<cover-view class="btn-sub-text">馆内</cover-view>
|
||||||
</cover-view>
|
</cover-view>
|
||||||
<cover-view class="action-btn" @tap="handleLocation">
|
<cover-view class="action-btn" @tap="handleLocation">
|
||||||
<cover-view class="btn-text">📍</cover-view>
|
<cover-view class="btn-text">📍</cover-view>
|
||||||
@@ -129,7 +129,7 @@ const emit = defineEmits<{
|
|||||||
// POI ID: 7043042949572197975
|
// POI ID: 7043042949572197975
|
||||||
const mapCenter = ref<MapCenter>({
|
const mapCenter = ref<MapCenter>({
|
||||||
latitude: 22.692763, // 深圳自然博物馆纬度
|
latitude: 22.692763, // 深圳自然博物馆纬度
|
||||||
longitude: 114.363572 // 深圳自然博物馆经度(调整到室外 2D 视觉中心)
|
longitude: 114.363572 // 深圳自然博物馆经度(调整到馆外 2D 视觉中心)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 地图缩放级别(调整为适中比例,既能看到建筑全貌又能看清细节)
|
// 地图缩放级别(调整为适中比例,既能看到建筑全貌又能看清细节)
|
||||||
@@ -378,9 +378,9 @@ const handleZoomOut = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 进入 3D 室内模式
|
// 进入 3D 馆内模式
|
||||||
const handleEnter3D = () => {
|
const handleEnter3D = () => {
|
||||||
console.log('进入 3D 室内模式')
|
console.log('进入 3D 馆内模式')
|
||||||
emit('enter3DMode')
|
emit('enter3DMode')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,7 +490,7 @@ onMounted(async () => {
|
|||||||
color: #333333;
|
color: #333333;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 室内模式按钮
|
// 馆内模式按钮
|
||||||
.indoor-mode-btn {
|
.indoor-mode-btn {
|
||||||
width: 46px;
|
width: 46px;
|
||||||
height: 64px !important;
|
height: 64px !important;
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ type TouchGestureMode = 'orbit' | 'pan'
|
|||||||
type ZoomCameraSource = 'button' | 'gesture'
|
type ZoomCameraSource = 'button' | 'gesture'
|
||||||
|
|
||||||
const CANVAS_FONT_FAMILY = '"HarmonyOS Sans SC", "HarmonyOS Sans", "鸿蒙黑体", "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", Arial, sans-serif'
|
const CANVAS_FONT_FAMILY = '"HarmonyOS Sans SC", "HarmonyOS Sans", "鸿蒙黑体", "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", Arial, sans-serif'
|
||||||
|
const FLOOR_CAMERA_DISTANCE_FACTOR = 0.78
|
||||||
|
|
||||||
interface CameraFitOptions {
|
interface CameraFitOptions {
|
||||||
distanceFactor?: number
|
distanceFactor?: number
|
||||||
@@ -203,7 +204,7 @@ const props = withDefaults(defineProps<{
|
|||||||
showPoi: false,
|
showPoi: false,
|
||||||
targetFocus: null,
|
targetFocus: null,
|
||||||
touchGestureMode: 'orbit',
|
touchGestureMode: 'orbit',
|
||||||
targetFocusDistanceFactor: 0.22,
|
targetFocusDistanceFactor: 0.36,
|
||||||
routePreview: null,
|
routePreview: null,
|
||||||
showRoute: false,
|
showRoute: false,
|
||||||
autoSwitch: true,
|
autoSwitch: true,
|
||||||
@@ -327,6 +328,9 @@ const syncControlInteractionOptions = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const corePoiCategories = new Set([
|
const corePoiCategories = new Set([
|
||||||
|
'touring_poi',
|
||||||
|
'exhibition_hall',
|
||||||
|
'exhibition_hall_entrance',
|
||||||
'basic_service_facility',
|
'basic_service_facility',
|
||||||
'transport_circulation',
|
'transport_circulation',
|
||||||
'accessibility_special_service'
|
'accessibility_special_service'
|
||||||
@@ -334,10 +338,12 @@ const corePoiCategories = new Set([
|
|||||||
|
|
||||||
const poiCategoryPriority: Record<string, number> = {
|
const poiCategoryPriority: Record<string, number> = {
|
||||||
target_preview: 100,
|
target_preview: 100,
|
||||||
|
exhibition_hall: 96,
|
||||||
|
exhibition_hall_entrance: 92,
|
||||||
|
touring_poi: 90,
|
||||||
accessibility_special_service: 88,
|
accessibility_special_service: 88,
|
||||||
basic_service_facility: 82,
|
basic_service_facility: 82,
|
||||||
transport_circulation: 76,
|
transport_circulation: 76,
|
||||||
touring_poi: 58,
|
|
||||||
operation_experience: 46
|
operation_experience: 46
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,6 +357,9 @@ const poiGlyphMap: Record<string, string> = {
|
|||||||
stair: '梯',
|
stair: '梯',
|
||||||
experience: '展',
|
experience: '展',
|
||||||
theater: '演',
|
theater: '演',
|
||||||
|
exhibition_hall: '展',
|
||||||
|
hall_entrance: '门',
|
||||||
|
entrance_exit: '门',
|
||||||
target_preview: '位'
|
target_preview: '位'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2217,6 +2226,11 @@ const fitCameraToObject = (object: THREE.Object3D, preset: CameraPreset = 'obliq
|
|||||||
screenOffsetRatio: new THREE.Vector2(-0.045, 0.02),
|
screenOffsetRatio: new THREE.Vector2(-0.045, 0.02),
|
||||||
up: new THREE.Vector3(0, 1, 0)
|
up: new THREE.Vector3(0, 1, 0)
|
||||||
}
|
}
|
||||||
|
: activeView.value === 'floor'
|
||||||
|
? {
|
||||||
|
distanceFactor: FLOOR_CAMERA_DISTANCE_FACTOR,
|
||||||
|
up: new THREE.Vector3(0, 1, 0)
|
||||||
|
}
|
||||||
: {}
|
: {}
|
||||||
|
|
||||||
setCameraView(center, maxDim, direction, overviewFitOptions)
|
setCameraView(center, maxDim, direction, overviewFitOptions)
|
||||||
@@ -2918,6 +2932,8 @@ const showFocusPoiAffordances = (poi: RenderPoi) => {
|
|||||||
const getPoiColor = (category: string) => {
|
const getPoiColor = (category: string) => {
|
||||||
const colorMap: Record<string, string> = {
|
const colorMap: Record<string, string> = {
|
||||||
touring_poi: '#2f6fed',
|
touring_poi: '#2f6fed',
|
||||||
|
exhibition_hall: '#2f6fed',
|
||||||
|
exhibition_hall_entrance: '#4659d8',
|
||||||
basic_service_facility: '#1f8f5f',
|
basic_service_facility: '#1f8f5f',
|
||||||
transport_circulation: '#d77b20',
|
transport_circulation: '#d77b20',
|
||||||
accessibility_special_service: '#8a5cf6',
|
accessibility_special_service: '#8a5cf6',
|
||||||
@@ -3166,7 +3182,7 @@ const triggerPoiTapFeedback = (sprite: THREE.Sprite) => {
|
|||||||
getPoiSpriteUserData(sprite).feedbackUntil = performance.now() + poiTapFeedbackDurationMs
|
getPoiSpriteUserData(sprite).feedbackUntil = performance.now() + poiTapFeedbackDurationMs
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSceneTap = (event: PointerEvent) => {
|
const handleSceneTap = async (event: PointerEvent) => {
|
||||||
if (!camera || !renderer || !getContainerElement()) return
|
if (!camera || !renderer || !getContainerElement()) return
|
||||||
|
|
||||||
const rect = renderer.domElement.getBoundingClientRect()
|
const rect = renderer.domElement.getBoundingClientRect()
|
||||||
@@ -3177,7 +3193,7 @@ const handleSceneTap = (event: PointerEvent) => {
|
|||||||
const raycaster = new THREE.Raycaster()
|
const raycaster = new THREE.Raycaster()
|
||||||
raycaster.setFromCamera(pointer, camera)
|
raycaster.setFromCamera(pointer, camera)
|
||||||
|
|
||||||
if (activeView.value === 'floor' && poiGroup) {
|
if ((activeView.value === 'floor' || activeView.value === 'overview') && poiGroup) {
|
||||||
const poiHits = raycaster.intersectObjects([...getPoiHitTargets(), ...getPoiSprites()], false)
|
const poiHits = raycaster.intersectObjects([...getPoiHitTargets(), ...getPoiSprites()], false)
|
||||||
const hit = poiHits.find((item) => (
|
const hit = poiHits.find((item) => (
|
||||||
item.object.visible
|
item.object.visible
|
||||||
@@ -3187,10 +3203,27 @@ const handleSceneTap = (event: PointerEvent) => {
|
|||||||
const hitMarker = hit ? getPoiMarkerFromHit(hit.object) : findNearestPoiMarkerByScreenPoint(event, rect)
|
const hitMarker = hit ? getPoiMarkerFromHit(hit.object) : findNearestPoiMarkerByScreenPoint(event, rect)
|
||||||
|
|
||||||
if (hitMarker?.userData.poi) {
|
if (hitMarker?.userData.poi) {
|
||||||
const poi = hitMarker.userData.poi as RenderPoi
|
const selectedPoi = hitMarker.userData.poi as RenderPoi
|
||||||
|
if (activeView.value === 'overview') {
|
||||||
|
disableAutoSwitchTemporarily(10000)
|
||||||
|
try {
|
||||||
|
await loadFloor(selectedPoi.floorId, {
|
||||||
|
preserveCurrentSceneUntilReady: hasRenderableSceneForFloorTransition(),
|
||||||
|
suppressProgress: true,
|
||||||
|
allowOverviewFallback: true
|
||||||
|
})
|
||||||
|
emit('floorChange', selectedPoi.floorId)
|
||||||
|
} catch (error) {
|
||||||
|
if (isStaleModelLoadError(error)) return
|
||||||
|
console.error('展厅点位楼层切换失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const poi = findLoadedPoi(selectedPoi.id) || selectedPoi
|
||||||
|
const focusMarker = findPoiSprite(poi.id) || hitMarker
|
||||||
selectedPOI.value = poi
|
selectedPOI.value = poi
|
||||||
activeFocusPoiId.value = poi.id
|
activeFocusPoiId.value = poi.id
|
||||||
triggerPoiTapFeedback(hitMarker)
|
triggerPoiTapFeedback(focusMarker)
|
||||||
updatePoiMarkerFocus()
|
updatePoiMarkerFocus()
|
||||||
showFocusPoiAffordances(poi)
|
showFocusPoiAffordances(poi)
|
||||||
focusCameraOnPoi(poi)
|
focusCameraOnPoi(poi)
|
||||||
@@ -3231,7 +3264,7 @@ const handlePointerUp = (event: PointerEvent) => {
|
|||||||
|
|
||||||
if (moveDistance > 8) return
|
if (moveDistance > 8) return
|
||||||
|
|
||||||
handleSceneTap(event)
|
void handleSceneTap(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
const emitTargetFocus = (
|
const emitTargetFocus = (
|
||||||
@@ -3302,9 +3335,12 @@ const focusTargetPoi = async (request: TargetPoiFocusRequest) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (activeView.value !== 'floor' || currentFloor.value !== request.floorId) {
|
if (activeView.value !== 'floor' || currentFloor.value !== request.floorId) {
|
||||||
isLoading.value = true
|
|
||||||
loadError.value = false
|
loadError.value = false
|
||||||
await loadFloor(request.floorId)
|
await loadFloor(request.floorId, {
|
||||||
|
preserveCurrentSceneUntilReady: hasRenderableSceneForFloorTransition(),
|
||||||
|
suppressProgress: true,
|
||||||
|
allowOverviewFallback: true
|
||||||
|
})
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
emit('floorChange', request.floorId)
|
emit('floorChange', request.floorId)
|
||||||
} else if (shouldRenderPoiMarkers.value && !getPoiSprites().length) {
|
} else if (shouldRenderPoiMarkers.value && !getPoiSprites().length) {
|
||||||
@@ -3419,31 +3455,19 @@ const init3DScene = async () => {
|
|||||||
|
|
||||||
const handleFloorChange = async (floorId: string) => {
|
const handleFloorChange = async (floorId: string) => {
|
||||||
const hasSceneToKeep = hasRenderableSceneForFloorTransition()
|
const hasSceneToKeep = hasRenderableSceneForFloorTransition()
|
||||||
const showLoading = !hasSceneToKeep && !canLoadFloorSilently(floorId)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (showLoading) {
|
|
||||||
isLoading.value = true
|
|
||||||
}
|
|
||||||
loadError.value = false
|
loadError.value = false
|
||||||
await loadFloor(floorId, {
|
await loadFloor(floorId, {
|
||||||
preserveCurrentSceneUntilReady: hasSceneToKeep,
|
preserveCurrentSceneUntilReady: hasSceneToKeep,
|
||||||
suppressProgress: !showLoading
|
suppressProgress: true,
|
||||||
|
allowOverviewFallback: true
|
||||||
})
|
})
|
||||||
updatePoiVisibilityByDistance()
|
updatePoiVisibilityByDistance()
|
||||||
if (showLoading) {
|
isLoading.value = false
|
||||||
isLoading.value = false
|
|
||||||
} else {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
emit('floorChange', floorId)
|
emit('floorChange', floorId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isStaleModelLoadError(error)) {
|
if (isStaleModelLoadError(error)) return
|
||||||
if (showLoading) {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('楼层模型加载失败:', error)
|
console.error('楼层模型加载失败:', error)
|
||||||
loadError.value = true
|
loadError.value = true
|
||||||
|
|||||||
@@ -126,11 +126,15 @@
|
|||||||
<text class="layer-mode-label">{{ layerModeActionLabel }}</text>
|
<text class="layer-mode-label">{{ layerModeActionLabel }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view
|
<scroll-view
|
||||||
v-if="showIndoorRightControls && showFloor"
|
v-if="showIndoorRightControls && showFloor"
|
||||||
class="floor-switcher"
|
class="floor-switcher"
|
||||||
:class="`side-${floorSide}`"
|
:class="`side-${floorSide}`"
|
||||||
:style="floorSwitcherStyle"
|
:style="floorSwitcherStyle"
|
||||||
|
scroll-y
|
||||||
|
:show-scrollbar="false"
|
||||||
|
:scroll-into-view="activeFloorScrollIntoView"
|
||||||
|
:scroll-with-animation="true"
|
||||||
>
|
>
|
||||||
<view
|
<view
|
||||||
v-if="showFloorHeader"
|
v-if="showFloorHeader"
|
||||||
@@ -142,15 +146,16 @@
|
|||||||
<text class="floor-header-label">楼层</text>
|
<text class="floor-header-label">楼层</text>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
v-for="floor in floorLabels"
|
v-for="floor in floorItems"
|
||||||
:key="floor"
|
:id="floor.scrollId"
|
||||||
|
:key="floor.id"
|
||||||
class="floor-item"
|
class="floor-item"
|
||||||
:class="{ active: activeFloor === floor && layerMode !== 'multi' }"
|
:class="{ active: activeFloorId === floor.id && layerMode !== 'multi' }"
|
||||||
@tap="handleFloorChange(floor)"
|
@tap="handleFloorChange(floor.label)"
|
||||||
>
|
>
|
||||||
<text class="floor-label">{{ floor }}</text>
|
<text class="floor-label">{{ floor.label }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</scroll-view>
|
||||||
|
|
||||||
<view v-if="showZoomControls" class="zoom-controls" :style="zoomControlsStyle">
|
<view v-if="showZoomControls" class="zoom-controls" :style="zoomControlsStyle">
|
||||||
<view class="zoom-btn" @tap="handleZoomClick('in')">
|
<view class="zoom-btn" @tap="handleZoomClick('in')">
|
||||||
@@ -364,7 +369,7 @@ const props = withDefaults(defineProps<{
|
|||||||
dimmed: false,
|
dimmed: false,
|
||||||
targetFocusRequest: null,
|
targetFocusRequest: null,
|
||||||
touchGestureMode: 'orbit',
|
touchGestureMode: 'orbit',
|
||||||
targetFocusDistanceFactor: 0.22,
|
targetFocusDistanceFactor: 0.36,
|
||||||
routePreview: null,
|
routePreview: null,
|
||||||
showRoute: false,
|
showRoute: false,
|
||||||
disableAutoExit: false,
|
disableAutoExit: false,
|
||||||
@@ -415,7 +420,11 @@ const indoorRendererRef = ref<{
|
|||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
const indoorFloors = computed(() => props.floors.filter((floor) => isIndoorNavigableFloor(floor)))
|
const indoorFloors = computed(() => props.floors.filter((floor) => isIndoorNavigableFloor(floor)))
|
||||||
const floorLabels = computed(() => indoorFloors.value.map((floor) => floor.label))
|
const createFloorScrollId = (floorId: string) => `guide-floor-${floorId.replace(/[^a-zA-Z0-9_-]/g, '-')}`
|
||||||
|
const floorItems = computed(() => indoorFloors.value.map((floor) => ({
|
||||||
|
...floor,
|
||||||
|
scrollId: createFloorScrollId(floor.id)
|
||||||
|
})))
|
||||||
// #ifdef H5
|
// #ifdef H5
|
||||||
const effectiveIndoorModelSource = computed(() => props.indoorModelSource)
|
const effectiveIndoorModelSource = computed(() => props.indoorModelSource)
|
||||||
// #endif
|
// #endif
|
||||||
@@ -435,6 +444,29 @@ const activeFloorId = computed(() => {
|
|||||||
))
|
))
|
||||||
return normalizedFloor?.id || normalizedFloorId
|
return normalizedFloor?.id || normalizedFloorId
|
||||||
})
|
})
|
||||||
|
const activeFloorScrollIntoView = ref('')
|
||||||
|
|
||||||
|
const syncActiveFloorScroll = () => {
|
||||||
|
if (props.mapType !== 'indoor' || props.indoorView === 'overview' || props.layerMode === 'multi') {
|
||||||
|
activeFloorScrollIntoView.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchedFloor = floorItems.value.find((floor) => floor.id === activeFloorId.value)
|
||||||
|
activeFloorScrollIntoView.value = matchedFloor?.scrollId || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [
|
||||||
|
activeFloorId.value,
|
||||||
|
props.indoorView,
|
||||||
|
props.layerMode,
|
||||||
|
props.mapType,
|
||||||
|
floorItems.value.map((floor) => floor.id).join('|')
|
||||||
|
],
|
||||||
|
syncActiveFloorScroll,
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
const searchFieldStyle = computed(() => ({
|
const searchFieldStyle = computed(() => ({
|
||||||
top: props.searchTop
|
top: props.searchTop
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
<view class="action-row">
|
<view class="action-row">
|
||||||
<view class="action-btn secondary" @tap="emit('viewOutdoorMap')">
|
<view class="action-btn secondary" @tap="emit('viewOutdoorMap')">
|
||||||
<text class="action-text">室外入口参考</text>
|
<text class="action-text">馆外入口参考</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="action-btn primary" @tap="handleShowTargetLocation">
|
<view class="action-btn primary" @tap="handleShowTargetLocation">
|
||||||
<text class="action-text">查看三维位置</text>
|
<text class="action-text">查看三维位置</text>
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ const canNavigate = computed(() => {
|
|||||||
|
|
||||||
const navigateButtonText = computed(() => {
|
const navigateButtonText = computed(() => {
|
||||||
if (props.loading) return '规划中...'
|
if (props.loading) return '规划中...'
|
||||||
if (hasRoute.value) return '开始导航'
|
if (hasRoute.value) return '打开地图'
|
||||||
return '规划路线'
|
return '规划路线'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -195,17 +195,11 @@ const canViewRoute = computed(() => Boolean(
|
|||||||
|
|
||||||
const canPrimaryAction = computed(() => canViewRoute.value && !props.error)
|
const canPrimaryAction = computed(() => canViewRoute.value && !props.error)
|
||||||
|
|
||||||
const primaryActionText = computed(() => (
|
const primaryActionText = computed(() => '查看位置关系')
|
||||||
props.hasRoutePreview
|
|
||||||
? '开始导览'
|
|
||||||
: (props.routeReady ? '查看路线' : '查看位置关系')
|
|
||||||
))
|
|
||||||
|
|
||||||
const routeOptionsTitle = computed(() => '馆内导览')
|
const routeOptionsTitle = computed(() => '馆内导览')
|
||||||
|
|
||||||
const loadingText = computed(() => (
|
const loadingText = computed(() => '正在生成馆内位置关系')
|
||||||
props.routeReady ? '正在规划馆内导览路线' : '正在生成馆内位置关系'
|
|
||||||
))
|
|
||||||
|
|
||||||
const collapsedSummary = computed(() => {
|
const collapsedSummary = computed(() => {
|
||||||
if (props.startPoint && props.endPoint) {
|
if (props.startPoint && props.endPoint) {
|
||||||
@@ -217,7 +211,7 @@ const collapsedSummary = computed(() => {
|
|||||||
|
|
||||||
const routeOptions = computed(() => {
|
const routeOptions = computed(() => {
|
||||||
const unavailable = Boolean(props.error)
|
const unavailable = Boolean(props.error)
|
||||||
const summaryText = props.summary || (props.routeReady ? '选择后规划真实路线' : '选择后查看位置关系')
|
const summaryText = props.summary || '选择后查看位置关系'
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'recommended',
|
id: 'recommended',
|
||||||
@@ -226,23 +220,21 @@ const routeOptions = computed(() => {
|
|||||||
? '当前暂不可用'
|
? '当前暂不可用'
|
||||||
: props.hasRoutePreview
|
: props.hasRoutePreview
|
||||||
? summaryText
|
? summaryText
|
||||||
: props.routeReady
|
: '查看起点终点位置关系',
|
||||||
? '调用 SGS SDK 路线规划'
|
|
||||||
: '查看起点终点位置关系',
|
|
||||||
active: !unavailable,
|
active: !unavailable,
|
||||||
disabled: unavailable
|
disabled: unavailable
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'stairs',
|
id: 'stairs',
|
||||||
title: '少走楼梯',
|
title: '少走楼梯',
|
||||||
meta: props.routeReady ? '偏好能力待接入' : '偏好预览,待路线数据验证',
|
meta: '偏好预览,待路线数据验证',
|
||||||
active: false,
|
active: false,
|
||||||
disabled: true
|
disabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'elevator',
|
id: 'elevator',
|
||||||
title: '电梯优先',
|
title: '电梯优先',
|
||||||
meta: props.routeReady ? '偏好能力待接入' : '偏好预览,待路线数据验证',
|
meta: '偏好预览,待路线数据验证',
|
||||||
active: false,
|
active: false,
|
||||||
disabled: true
|
disabled: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,5 +67,7 @@ export const toMuseumPoi = (poi: StaticNavPoiPayload): MuseumPoi => ({
|
|||||||
sourceObjectName: poi.sourceObjectName,
|
sourceObjectName: poi.sourceObjectName,
|
||||||
sourceConfidence: poi.sourceConfidence,
|
sourceConfidence: poi.sourceConfidence,
|
||||||
navigationReadiness: poi.navigationReadiness,
|
navigationReadiness: poi.navigationReadiness,
|
||||||
accessible: isPoiAccessible(poi)
|
accessible: isPoiAccessible(poi),
|
||||||
|
kind: poi.primaryCategory === 'touring_poi' ? 'hall' : 'facility',
|
||||||
|
hallName: poi.primaryCategory === 'touring_poi' ? poi.name : undefined
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
GuideRouteTarget
|
GuideRouteTarget
|
||||||
} from '@/domain/museum'
|
} from '@/domain/museum'
|
||||||
import type {
|
import type {
|
||||||
|
SgsRoutePathPointPayload,
|
||||||
SgsRoutePlanResponsePayload
|
SgsRoutePlanResponsePayload
|
||||||
} from '@/data/providers/sgsSdkApiProvider'
|
} from '@/data/providers/sgsSdkApiProvider'
|
||||||
import type {
|
import type {
|
||||||
@@ -31,10 +32,25 @@ type GeoJsonLineString = {
|
|||||||
coordinates?: Array<[number, number] | [number, number, number]>
|
coordinates?: Array<[number, number] | [number, number, number]>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const finiteNumber = (value: unknown) => {
|
||||||
|
const numericValue = Number(value)
|
||||||
|
return Number.isFinite(numericValue) ? numericValue : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const stringifyRouteId = (value: unknown, fallback = '') => {
|
||||||
|
if (value === null || typeof value === 'undefined' || value === '') return fallback
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const samePosition = (
|
||||||
|
a: [number, number, number],
|
||||||
|
b: [number, number, number]
|
||||||
|
) => a.every((value, index) => Math.abs(value - b[index]) < 0.001)
|
||||||
|
|
||||||
const pointPosition = (point: SgsRoutePathNode): [number, number, number] => [
|
const pointPosition = (point: SgsRoutePathNode): [number, number, number] => [
|
||||||
Number(point.x || 0),
|
finiteNumber(point.x) ?? 0,
|
||||||
Number(point.y || 0),
|
finiteNumber(point.y) ?? 0,
|
||||||
Number(point.z || 0)
|
finiteNumber(point.z) ?? 0
|
||||||
]
|
]
|
||||||
|
|
||||||
const targetEndpoint = (target: GuideRouteTarget): GuideRouteEndpoint => ({
|
const targetEndpoint = (target: GuideRouteTarget): GuideRouteEndpoint => ({
|
||||||
@@ -165,8 +181,10 @@ export const toGuideRouteResultFromSgs = (
|
|||||||
const floorLabelFor = (
|
const floorLabelFor = (
|
||||||
floorId: string,
|
floorId: string,
|
||||||
startTarget: GuideRouteTarget,
|
startTarget: GuideRouteTarget,
|
||||||
endTarget: GuideRouteTarget
|
endTarget: GuideRouteTarget,
|
||||||
|
fallbackLabel?: string | null
|
||||||
) => {
|
) => {
|
||||||
|
if (fallbackLabel) return fallbackLabel
|
||||||
if (floorId === startTarget.floorId) return startTarget.floorLabel
|
if (floorId === startTarget.floorId) return startTarget.floorLabel
|
||||||
if (floorId === endTarget.floorId) return endTarget.floorLabel
|
if (floorId === endTarget.floorId) return endTarget.floorLabel
|
||||||
return floorId
|
return floorId
|
||||||
@@ -176,19 +194,54 @@ const pointFromBackendNode = (
|
|||||||
node: NonNullable<SgsRoutePlanResponsePayload['nodePaths']>[number],
|
node: NonNullable<SgsRoutePlanResponsePayload['nodePaths']>[number],
|
||||||
index: number,
|
index: number,
|
||||||
fallbackFloorId: string
|
fallbackFloorId: string
|
||||||
): GuideRoutePoint => ({
|
): GuideRoutePoint | null => {
|
||||||
nodeId: String(node.id || `sgs-route-node-${index}`),
|
const x = finiteNumber(node.x)
|
||||||
floorId: toAppFloorId(node.floorId ?? fallbackFloorId),
|
const z = finiteNumber(node.y)
|
||||||
position: [
|
if (typeof x !== 'number' || typeof z !== 'number') return null
|
||||||
Number(node.x || 0),
|
|
||||||
0,
|
return {
|
||||||
Number(node.y || 0)
|
nodeId: stringifyRouteId(node.id, `sgs-route-node-${index}`),
|
||||||
]
|
floorId: stringifyRouteId(node.floorId, fallbackFloorId),
|
||||||
})
|
position: [
|
||||||
|
x,
|
||||||
|
finiteNumber(node.z) ?? 0,
|
||||||
|
z
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pointFromPathPoint = (
|
||||||
|
point: SgsRoutePathPointPayload,
|
||||||
|
floorId: string,
|
||||||
|
nodeId: string
|
||||||
|
): GuideRoutePoint | null => {
|
||||||
|
const x = finiteNumber(point.x)
|
||||||
|
const z = finiteNumber(point.z)
|
||||||
|
if (typeof x !== 'number' || typeof z !== 'number') return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodeId,
|
||||||
|
floorId,
|
||||||
|
position: [
|
||||||
|
x,
|
||||||
|
finiteNumber(point.y) ?? 0,
|
||||||
|
z
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pointsFromPathPoints = (
|
||||||
|
pathPoints: SgsRoutePathPointPayload[] | null | undefined,
|
||||||
|
floorId: string,
|
||||||
|
nodeIdPrefix: string
|
||||||
|
) => (pathPoints || [])
|
||||||
|
.map((point, index) => pointFromPathPoint(point, floorId, `${nodeIdPrefix}-pp-${index}`))
|
||||||
|
.filter((point): point is GuideRoutePoint => Boolean(point))
|
||||||
|
|
||||||
const pointsFromGeoJson = (
|
const pointsFromGeoJson = (
|
||||||
geoJson: string | null | undefined,
|
geoJson: string | null | undefined,
|
||||||
floorId: string
|
floorId: string,
|
||||||
|
nodeIdPrefix = floorId
|
||||||
): GuideRoutePoint[] => {
|
): GuideRoutePoint[] => {
|
||||||
if (!geoJson) return []
|
if (!geoJson) return []
|
||||||
|
|
||||||
@@ -198,20 +251,147 @@ const pointsFromGeoJson = (
|
|||||||
? parsed.coordinates
|
? parsed.coordinates
|
||||||
: []
|
: []
|
||||||
|
|
||||||
return coordinates.map((coordinate, index) => ({
|
return coordinates
|
||||||
nodeId: `${floorId}-geo-${index}`,
|
.map((coordinate, index) => {
|
||||||
floorId,
|
const x = finiteNumber(coordinate[0])
|
||||||
position: [
|
const z = finiteNumber(coordinate[1])
|
||||||
Number(coordinate[0] || 0),
|
if (typeof x !== 'number' || typeof z !== 'number') return null
|
||||||
0,
|
|
||||||
Number(coordinate[1] || 0)
|
return {
|
||||||
]
|
nodeId: `${nodeIdPrefix}-geo-${index}`,
|
||||||
}))
|
floorId,
|
||||||
|
position: [
|
||||||
|
x,
|
||||||
|
finiteNumber(coordinate[2]) ?? 0,
|
||||||
|
z
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((point): point is GuideRoutePoint => Boolean(point))
|
||||||
} catch {
|
} catch {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const routePointFromEndpoint = (
|
||||||
|
endpoint: GuideRouteEndpoint,
|
||||||
|
suffix: string
|
||||||
|
): GuideRoutePoint => ({
|
||||||
|
nodeId: endpoint.routeNodeId || `${endpoint.poiId}-${suffix}`,
|
||||||
|
floorId: endpoint.floorId,
|
||||||
|
position: endpoint.position
|
||||||
|
})
|
||||||
|
|
||||||
|
const prependIfDifferent = (
|
||||||
|
points: GuideRoutePoint[],
|
||||||
|
point: GuideRoutePoint
|
||||||
|
) => {
|
||||||
|
const firstPoint = points[0]
|
||||||
|
return firstPoint && samePosition(firstPoint.position, point.position)
|
||||||
|
? points
|
||||||
|
: [point, ...points]
|
||||||
|
}
|
||||||
|
|
||||||
|
const appendIfDifferent = (
|
||||||
|
points: GuideRoutePoint[],
|
||||||
|
point: GuideRoutePoint
|
||||||
|
) => {
|
||||||
|
const lastPoint = points[points.length - 1]
|
||||||
|
return lastPoint && samePosition(lastPoint.position, point.position)
|
||||||
|
? points
|
||||||
|
: [...points, point]
|
||||||
|
}
|
||||||
|
|
||||||
|
const createPlanFloorSegments = (
|
||||||
|
points: GuideRoutePoint[],
|
||||||
|
startTarget: GuideRouteTarget,
|
||||||
|
endTarget: GuideRouteTarget
|
||||||
|
): GuideRouteFloorSegment[] => points.reduce<GuideRouteFloorSegment[]>((segments, point) => {
|
||||||
|
const current = segments[segments.length - 1]
|
||||||
|
if (current && current.floorId === point.floorId) {
|
||||||
|
current.points.push(point)
|
||||||
|
return segments
|
||||||
|
}
|
||||||
|
|
||||||
|
segments.push({
|
||||||
|
floorId: point.floorId,
|
||||||
|
floorLabel: floorLabelFor(point.floorId, startTarget, endTarget),
|
||||||
|
points: [point]
|
||||||
|
})
|
||||||
|
return segments
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const isWalkSegment = (
|
||||||
|
segment: NonNullable<SgsRoutePlanResponsePayload['segments']>[number]
|
||||||
|
) => {
|
||||||
|
const type = stringifyRouteId(segment.transferType || segment.segmentType || segment.type).toUpperCase()
|
||||||
|
return !type || type === 'WALK'
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSegmentFloorSegments = (
|
||||||
|
route: SgsRoutePlanResponsePayload,
|
||||||
|
start: GuideRouteEndpoint,
|
||||||
|
end: GuideRouteEndpoint,
|
||||||
|
startTarget: GuideRouteTarget,
|
||||||
|
endTarget: GuideRouteTarget,
|
||||||
|
nodePoints: GuideRoutePoint[]
|
||||||
|
): GuideRouteFloorSegment[] => {
|
||||||
|
const segments = (route.segments || [])
|
||||||
|
.filter(isWalkSegment)
|
||||||
|
.map<GuideRouteFloorSegment | null>((segment, segmentIndex) => {
|
||||||
|
const floorId = stringifyRouteId(segment.floorId, start.floorId)
|
||||||
|
const nodeIdPrefix = `${floorId}-segment-${segmentIndex}`
|
||||||
|
const nodePathIds = new Set(
|
||||||
|
(segment.nodePathIds || [])
|
||||||
|
.map((nodeId) => stringifyRouteId(nodeId))
|
||||||
|
.filter(Boolean)
|
||||||
|
)
|
||||||
|
const points = pointsFromPathPoints(segment.pathPoints, floorId, nodeIdPrefix)
|
||||||
|
const geoPoints = points.length
|
||||||
|
? points
|
||||||
|
: pointsFromGeoJson(segment.pathGeoJson, floorId, nodeIdPrefix)
|
||||||
|
const fallbackPoints = geoPoints.length
|
||||||
|
? geoPoints
|
||||||
|
: nodePoints.filter((point) => (
|
||||||
|
point.floorId === floorId
|
||||||
|
&& (!nodePathIds.size || nodePathIds.has(point.nodeId))
|
||||||
|
))
|
||||||
|
|
||||||
|
if (!fallbackPoints.length) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
floorId,
|
||||||
|
floorLabel: floorLabelFor(floorId, startTarget, endTarget, segment.floorName),
|
||||||
|
points: fallbackPoints.map((point, index) => ({
|
||||||
|
...point,
|
||||||
|
nodeId: point.nodeId || `${nodeIdPrefix}-${index}`
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((segment): segment is GuideRouteFloorSegment => Boolean(segment))
|
||||||
|
|
||||||
|
if (!segments.length) return []
|
||||||
|
|
||||||
|
const startPoint = routePointFromEndpoint(start, 'start')
|
||||||
|
const endPoint = routePointFromEndpoint(end, 'end')
|
||||||
|
const anchoredSegments = segments.map((segment) => ({
|
||||||
|
...segment,
|
||||||
|
points: [...segment.points]
|
||||||
|
}))
|
||||||
|
const firstSegment = anchoredSegments[0]
|
||||||
|
const lastSegment = anchoredSegments[anchoredSegments.length - 1]
|
||||||
|
|
||||||
|
if (firstSegment?.floorId === start.floorId) {
|
||||||
|
firstSegment.points = prependIfDifferent(firstSegment.points, startPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastSegment?.floorId === end.floorId) {
|
||||||
|
lastSegment.points = appendIfDifferent(lastSegment.points, endPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
return anchoredSegments
|
||||||
|
}
|
||||||
|
|
||||||
export const toGuideRouteResultFromSgsPlan = (
|
export const toGuideRouteResultFromSgsPlan = (
|
||||||
route: SgsRoutePlanResponsePayload,
|
route: SgsRoutePlanResponsePayload,
|
||||||
startTarget: GuideRouteTarget,
|
startTarget: GuideRouteTarget,
|
||||||
@@ -219,67 +399,39 @@ export const toGuideRouteResultFromSgsPlan = (
|
|||||||
): GuideRouteResult => {
|
): GuideRouteResult => {
|
||||||
const start = targetEndpoint(startTarget)
|
const start = targetEndpoint(startTarget)
|
||||||
const end = targetEndpoint(endTarget)
|
const end = targetEndpoint(endTarget)
|
||||||
const nodePoints = (route.nodePaths || []).map((node, index) => (
|
const startPoint = routePointFromEndpoint(start, 'start')
|
||||||
pointFromBackendNode(node, index, start.floorId)
|
const endPoint = routePointFromEndpoint(end, 'end')
|
||||||
))
|
const nodePoints = (route.nodePaths || [])
|
||||||
const routePoints = nodePoints.length
|
.map((node, index) => pointFromBackendNode(node, index, start.floorId))
|
||||||
? [
|
.filter((point): point is GuideRoutePoint => Boolean(point))
|
||||||
{
|
const segmentFloorSegments = createSegmentFloorSegments(
|
||||||
nodeId: start.routeNodeId,
|
route,
|
||||||
floorId: start.floorId,
|
start,
|
||||||
position: start.position
|
end,
|
||||||
},
|
startTarget,
|
||||||
...nodePoints,
|
endTarget,
|
||||||
{
|
nodePoints
|
||||||
nodeId: end.routeNodeId,
|
)
|
||||||
floorId: end.floorId,
|
const routeGeoPoints = pointsFromGeoJson(route.pathGeoJson, start.floorId, 'sgs-route')
|
||||||
position: end.position
|
const fallbackPoints = nodePoints.length
|
||||||
}
|
? [startPoint, ...nodePoints, endPoint]
|
||||||
]
|
: routeGeoPoints.length
|
||||||
: pointsFromGeoJson(route.pathGeoJson, start.floorId)
|
? [startPoint, ...routeGeoPoints, endPoint]
|
||||||
|
: [startPoint, endPoint]
|
||||||
const fallbackPoints = routePoints.length
|
const floorSegments = segmentFloorSegments.length
|
||||||
? routePoints
|
? segmentFloorSegments
|
||||||
: [
|
: createPlanFloorSegments(fallbackPoints, startTarget, endTarget)
|
||||||
{
|
const routePoints = floorSegments.flatMap((segment) => segment.points)
|
||||||
nodeId: start.routeNodeId,
|
|
||||||
floorId: start.floorId,
|
|
||||||
position: start.position
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodeId: end.routeNodeId,
|
|
||||||
floorId: end.floorId,
|
|
||||||
position: end.position
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const floorSegments = route.segments?.length
|
|
||||||
? route.segments.map((segment, segmentIndex) => {
|
|
||||||
const floorId = String(segment.floorId || start.floorId)
|
|
||||||
const points = pointsFromGeoJson(segment.pathGeoJson, floorId)
|
|
||||||
|
|
||||||
return {
|
|
||||||
floorId,
|
|
||||||
floorLabel: segment.floorName || floorLabelFor(floorId, startTarget, endTarget),
|
|
||||||
points: points.length
|
|
||||||
? points
|
|
||||||
: fallbackPoints.filter((point) => point.floorId === floorId).map((point, index) => ({
|
|
||||||
...point,
|
|
||||||
nodeId: point.nodeId || `${floorId}-segment-${segmentIndex}-${index}`
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}).filter((segment) => segment.points.length)
|
|
||||||
: createFloorSegments(fallbackPoints, startTarget, endTarget)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `sgs-api-route-${start.poiId}-${end.poiId}`,
|
id: `sgs-api-route-${start.poiId}-${end.poiId}`,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
distanceMeters: Number(route.distance || 0),
|
distanceMeters: Number(route.distance || 0),
|
||||||
nodeIds: fallbackPoints.map((point) => point.nodeId),
|
nodeIds: routePoints.map((point) => point.nodeId),
|
||||||
points: fallbackPoints,
|
points: routePoints,
|
||||||
floorSegments,
|
floorSegments,
|
||||||
connectorPoints: extractFloorConnectorPoints(fallbackPoints, startTarget, endTarget)
|
connectorPoints: extractFloorConnectorPoints(routePoints, startTarget, endTarget)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,8 +449,8 @@ const extractFloorConnectorPoints = (
|
|||||||
])
|
])
|
||||||
|
|
||||||
for (let i = 0; i < points.length - 1; i++) {
|
for (let i = 0; i < points.length - 1; i++) {
|
||||||
const currentFloorId = toAppFloorId(points[i].floorId)
|
const currentFloorId = points[i].floorId
|
||||||
const nextFloorId = toAppFloorId(points[i + 1].floorId)
|
const nextFloorId = points[i + 1].floorId
|
||||||
|
|
||||||
if (currentFloorId !== nextFloorId) {
|
if (currentFloorId !== nextFloorId) {
|
||||||
const fromLabel = labels.get(currentFloorId) || currentFloorId
|
const fromLabel = labels.get(currentFloorId) || currentFloorId
|
||||||
|
|||||||
@@ -161,19 +161,30 @@ export interface SgsRouteStepNodePayload {
|
|||||||
nodeType?: string | null
|
nodeType?: string | null
|
||||||
x?: number | null
|
x?: number | null
|
||||||
y?: number | null
|
y?: number | null
|
||||||
|
z?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SgsRoutePathPointPayload {
|
||||||
|
x?: number | null
|
||||||
|
y?: number | null
|
||||||
|
z?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SgsRouteSegmentPayload {
|
export interface SgsRouteSegmentPayload {
|
||||||
floorId?: string | number | null
|
floorId?: string | number | null
|
||||||
|
floorCode?: string | null
|
||||||
floorName?: string | null
|
floorName?: string | null
|
||||||
startNodeId?: string | number | null
|
startNodeId?: string | number | null
|
||||||
endNodeId?: string | number | null
|
endNodeId?: string | number | null
|
||||||
startNodeName?: string | null
|
startNodeName?: string | null
|
||||||
endNodeName?: string | null
|
endNodeName?: string | null
|
||||||
|
segmentType?: string | null
|
||||||
|
type?: string | null
|
||||||
transferType?: string | null
|
transferType?: string | null
|
||||||
distance?: number | null
|
distance?: number | null
|
||||||
duration?: number | null
|
duration?: number | null
|
||||||
pathGeoJson?: string | null
|
pathGeoJson?: string | null
|
||||||
|
pathPoints?: SgsRoutePathPointPayload[]
|
||||||
nodePathIds?: Array<string | number | null>
|
nodePathIds?: Array<string | number | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ export const createStaticNavAssetsProvider = (
|
|||||||
manifestRequest = requestJson<StaticNavManifestPayload>(provider.assetUrl('app_nav_manifest.json'))
|
manifestRequest = requestJson<StaticNavManifestPayload>(provider.assetUrl('app_nav_manifest.json'))
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.status !== 'pass') {
|
if (data.status !== 'pass') {
|
||||||
throw new Error('室内导览资源包状态不是 pass')
|
throw new Error('馆内导览资源包状态不是 pass')
|
||||||
}
|
}
|
||||||
|
|
||||||
manifestCache = data
|
manifestCache = data
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ interface FloorLike {
|
|||||||
const indoorFloorCodePattern = /^L-?\d+(?:\.\d+)?$/i
|
const indoorFloorCodePattern = /^L-?\d+(?:\.\d+)?$/i
|
||||||
const indoorFloorLabelPattern = /^(?:B\d+(?:\.\d+)?|\d+(?:\.\d+)?F)$/i
|
const indoorFloorLabelPattern = /^(?:B\d+(?:\.\d+)?|\d+(?:\.\d+)?F)$/i
|
||||||
const rawNumericIdPattern = /^\d{6,}$/
|
const rawNumericIdPattern = /^\d{6,}$/
|
||||||
const nonNavigableFloorPattern = /(EXTERIOR|OUTDOOR|BUILDING|FACADE|EXTERNAL|外立面|建筑外观|建筑外立面|室外模型|室外场景|室外底图|室外建筑|室外外观|室外)$/i
|
const nonNavigableFloorPattern = /(EXTERIOR|OUTDOOR|BUILDING|FACADE|EXTERNAL|外立面|建筑外观|建筑外立面|馆外模型|馆外场景|馆外底图|馆外建筑|馆外外观|馆外)$/i
|
||||||
|
|
||||||
const normalizeValue = (value: unknown) => (
|
const normalizeValue = (value: unknown) => (
|
||||||
value === null || typeof value === 'undefined' ? '' : String(value).trim()
|
value === null || typeof value === 'undefined' ? '' : String(value).trim()
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import type {
|
||||||
|
MuseumPoiEntrance,
|
||||||
|
MuseumPoiKind
|
||||||
|
} from '@/domain/museum'
|
||||||
|
|
||||||
export interface GuideRenderPoi {
|
export interface GuideRenderPoi {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -7,6 +12,10 @@ export interface GuideRenderPoi {
|
|||||||
iconType: string
|
iconType: string
|
||||||
positionGltf?: [number, number, number]
|
positionGltf?: [number, number, number]
|
||||||
sourceObjectName?: string
|
sourceObjectName?: string
|
||||||
|
kind?: MuseumPoiKind
|
||||||
|
hallId?: string
|
||||||
|
hallName?: string
|
||||||
|
entrances?: MuseumPoiEntrance[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GuideModelFloorAsset {
|
export interface GuideModelFloorAsset {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type {
|
|||||||
|
|
||||||
export const NAV_ROUTE_GRAPH_READY = false
|
export const NAV_ROUTE_GRAPH_READY = false
|
||||||
|
|
||||||
export const NAV_ROUTE_UNAVAILABLE_MESSAGE = '当前暂不支持正式导航,可查看位置预览、路线示意和室外参考'
|
export const NAV_ROUTE_UNAVAILABLE_MESSAGE = '当前可查看位置预览、位置关系和馆外参考,暂不提供正式室内导航'
|
||||||
|
|
||||||
export const NAV_ROUTE_READINESS: GuideRouteReadiness = {
|
export const NAV_ROUTE_READINESS: GuideRouteReadiness = {
|
||||||
ready: NAV_ROUTE_GRAPH_READY,
|
ready: NAV_ROUTE_GRAPH_READY,
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
<view class="action-bar">
|
<view class="action-bar">
|
||||||
<view
|
<view
|
||||||
class="location-btn"
|
class="location-btn"
|
||||||
:class="{ disabled: !exhibit.location }"
|
:class="{ disabled: !hallLocationId }"
|
||||||
@tap="handleNavigate"
|
@tap="handleNavigate"
|
||||||
>
|
>
|
||||||
<svg class="location-icon" width="19" height="19" viewBox="0 0 24 24" fill="none">
|
<svg class="location-icon" width="19" height="19" viewBox="0 0 24 24" fill="none">
|
||||||
@@ -134,12 +134,45 @@ const showAudioPlayer = ref(false)
|
|||||||
const currentAudio = ref<AudioItem | null>(null)
|
const currentAudio = ref<AudioItem | null>(null)
|
||||||
const isPlaying = ref(false)
|
const isPlaying = ref(false)
|
||||||
const activeTopTab = ref<GuideTopTab>('explain')
|
const activeTopTab = ref<GuideTopTab>('explain')
|
||||||
|
const resolvedHallId = ref('')
|
||||||
|
|
||||||
const heroImage = computed(() => exhibit.value.coverImages[0])
|
const heroImage = computed(() => exhibit.value.coverImages[0])
|
||||||
const detailMeta = computed(() => [
|
const detailMeta = computed(() => [
|
||||||
exhibit.value.hallName,
|
exhibit.value.hallName,
|
||||||
exhibit.value.floorLabel
|
exhibit.value.floorLabel
|
||||||
].filter(Boolean).join(' · '))
|
].filter(Boolean).join(' · '))
|
||||||
|
const hallLocationId = computed(() => exhibit.value.hallId || resolvedHallId.value)
|
||||||
|
|
||||||
|
const normalizePoiName = (value: string) => value
|
||||||
|
.trim()
|
||||||
|
.replace(/[\s()()【】[\]_-]/g, '')
|
||||||
|
.toLowerCase()
|
||||||
|
|
||||||
|
// 讲解详情页只定位到所属展厅,不再追踪具体展品点位。
|
||||||
|
const resolveHallGuidePoi = async () => {
|
||||||
|
const hallId = exhibit.value.hallId
|
||||||
|
if (hallId) {
|
||||||
|
const hallPoi = await guideUseCase.getPoiById(hallId)
|
||||||
|
if (hallPoi?.kind === 'hall' || hallPoi?.primaryCategory.id === 'exhibition_hall') {
|
||||||
|
return hallPoi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedTarget = normalizePoiName(exhibit.value.hallName || '')
|
||||||
|
if (!normalizedTarget) return null
|
||||||
|
|
||||||
|
const candidates = await guideUseCase.searchPois(exhibit.value.hallName || exhibit.value.title || '')
|
||||||
|
return candidates.find((candidate) => {
|
||||||
|
const candidateName = normalizePoiName(candidate.name)
|
||||||
|
const candidateHallName = normalizePoiName(candidate.hallName || '')
|
||||||
|
return candidate.kind === 'hall'
|
||||||
|
|| candidate.primaryCategory.id === 'exhibition_hall'
|
||||||
|
|| candidateName === normalizedTarget
|
||||||
|
|| candidateHallName === normalizedTarget
|
||||||
|
|| candidateName.includes(normalizedTarget)
|
||||||
|
|| normalizedTarget.includes(candidateName)
|
||||||
|
}) || candidates[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
onLoad(async (options: any = {}) => {
|
onLoad(async (options: any = {}) => {
|
||||||
const tab = Array.isArray(options.tab) ? options.tab[0] : options.tab
|
const tab = Array.isArray(options.tab) ? options.tab[0] : options.tab
|
||||||
@@ -153,6 +186,9 @@ onLoad(async (options: any = {}) => {
|
|||||||
const exhibitData = await explainUseCase.getExhibitById(exhibitId)
|
const exhibitData = await explainUseCase.getExhibitById(exhibitId)
|
||||||
if (exhibitData) {
|
if (exhibitData) {
|
||||||
exhibit.value = toExplainDetailPageViewModel(exhibitData)
|
exhibit.value = toExplainDetailPageViewModel(exhibitData)
|
||||||
|
if (exhibitData.hallId) {
|
||||||
|
resolvedHallId.value = exhibitData.hallId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -247,25 +283,34 @@ const handleAudioError = (_audio: AudioItem | null, message: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleNavigate = async () => {
|
const handleNavigate = async () => {
|
||||||
if (!exhibit.value.location?.poiId) {
|
const matchedHallPoi = await resolveHallGuidePoi()
|
||||||
|
if (!matchedHallPoi) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '该讲解暂无三维位置数据',
|
title: '该讲解暂无所属展厅位置数据',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const matchedPoi = await guideUseCase.getPoiById(exhibit.value.location.poiId)
|
resolvedHallId.value = matchedHallPoi.id
|
||||||
if (!matchedPoi) {
|
exhibit.value = {
|
||||||
uni.showToast({
|
...exhibit.value,
|
||||||
title: '该讲解位置尚未映射到当前三维导览资源',
|
hallId: matchedHallPoi.id,
|
||||||
icon: 'none'
|
hallName: matchedHallPoi.hallName || matchedHallPoi.name,
|
||||||
})
|
location: exhibit.value.location || {
|
||||||
return
|
status: 'hallFallback',
|
||||||
|
poiId: matchedHallPoi.id,
|
||||||
|
sourcePoiId: matchedHallPoi.id,
|
||||||
|
actionText: '查看所属展厅',
|
||||||
|
previewOnly: true,
|
||||||
|
floorId: matchedHallPoi.floorId,
|
||||||
|
floorLabel: matchedHallPoi.floorLabel,
|
||||||
|
note: '已定位到该讲解所属展厅。'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/route/detail?facilityId=${encodeURIComponent(exhibit.value.location.poiId)}&target=${encodeURIComponent(exhibit.value.title)}&state=preview`
|
url: `/pages/route/detail?facilityId=${encodeURIComponent(matchedHallPoi.id)}&target=${encodeURIComponent(exhibit.value.hallName || exhibit.value.title)}&state=preview`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,39 +5,41 @@
|
|||||||
:show-top-tabs="false"
|
:show-top-tabs="false"
|
||||||
show-bottom-nav
|
show-bottom-nav
|
||||||
show-back
|
show-back
|
||||||
:show-cancel="isStartPanelOpen"
|
|
||||||
@tab-change="handleTopTabChange"
|
@tab-change="handleTopTabChange"
|
||||||
@back="handlePageBack"
|
@back="handlePageBack"
|
||||||
@cancel="handleCancelStartSelection"
|
|
||||||
>
|
>
|
||||||
<GuideMapShell
|
<GuideMapShell
|
||||||
:search-text="facility.name"
|
:search-text="facility.name"
|
||||||
search-top="60px"
|
search-top="20px"
|
||||||
mode-top="104px"
|
floor-bottom="153px"
|
||||||
floor-top="208px"
|
floor-max-height="206px"
|
||||||
tools-top="176px"
|
floor-side="left"
|
||||||
:active-mode="activeMode"
|
:active-mode="activeMode"
|
||||||
:active-floor="activeFloor"
|
:active-floor="activeFloor"
|
||||||
:map-type="activeMode === '2d' ? 'outdoor' : 'indoor'"
|
map-type="indoor"
|
||||||
:tools="mapTools"
|
:tools="[]"
|
||||||
:indoor-model-source="indoorModelSource"
|
:indoor-model-source="indoorModelSource"
|
||||||
:floors="guideFloors"
|
:floors="guideFloors"
|
||||||
:normalize-floor-id="normalizeGuideFloorId"
|
:normalize-floor-id="normalizeGuideFloorId"
|
||||||
@search-tap="handleSearchTap"
|
:target-focus-request="targetFocusRequest"
|
||||||
@mode-change="handleModeChange"
|
:target-focus-distance-factor="0.86"
|
||||||
|
:show-search="false"
|
||||||
|
:show-mode-row="false"
|
||||||
|
show-floor-header
|
||||||
|
show-zoom-controls
|
||||||
|
zoom-controls-top="calc(100vh - 252px)"
|
||||||
@floor-change="handleFloorChange"
|
@floor-change="handleFloorChange"
|
||||||
@tool-click="handleToolClick"
|
|
||||||
>
|
>
|
||||||
<view class="detail-sheet">
|
<view v-if="isDetailPanelVisible" class="detail-sheet">
|
||||||
<view class="detail-title-row">
|
<view class="detail-title-row">
|
||||||
<text class="detail-title">{{ facility.name }}</text>
|
<view class="target-dot"></view>
|
||||||
<text class="open-status">{{ facility.status }}</text>
|
<view class="detail-title-copy">
|
||||||
</view>
|
<text class="detail-title">{{ facility.name }}</text>
|
||||||
|
<text class="detail-subtitle">{{ facility.location }}</text>
|
||||||
<view class="detail-lines">
|
</view>
|
||||||
<text class="detail-line">所在区域:{{ facility.location }}</text>
|
<view class="detail-close" @tap.stop="handleCloseDetailPanel">
|
||||||
<text class="detail-line">能力说明:{{ facility.traffic }}</text>
|
<text class="detail-close-text">×</text>
|
||||||
<text v-if="confirmedStart" class="detail-line">预览起点:{{ confirmedStart.label }}</text>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="tag-row">
|
<view class="tag-row">
|
||||||
@@ -51,82 +53,35 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="action-row">
|
<text class="detail-hint">已在对应楼层模型中标出点位</text>
|
||||||
<view class="action-btn secondary" @tap="handleChooseStart">
|
|
||||||
<text class="action-text">{{ startButtonLabel }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="action-btn primary" @tap="handleStartNavigation">
|
|
||||||
<text class="action-text">查看位置</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view
|
<view
|
||||||
v-if="isStartPanelOpen"
|
v-else
|
||||||
class="start-panel-mask"
|
class="detail-restore"
|
||||||
@tap="handleCancelStartSelection"
|
@tap.stop="handleOpenDetailPanel"
|
||||||
></view>
|
|
||||||
<view
|
|
||||||
v-if="isStartPanelOpen"
|
|
||||||
class="start-select-sheet"
|
|
||||||
@tap.stop=""
|
|
||||||
>
|
>
|
||||||
<text class="start-select-title">选择预览起点</text>
|
<view class="target-dot small"></view>
|
||||||
<text class="start-select-desc">当前支持选择预览起点,用于三维位置预览。</text>
|
<text class="detail-restore-text">点位</text>
|
||||||
|
|
||||||
<view class="start-chip-section">
|
|
||||||
<text class="start-chip-label">楼层</text>
|
|
||||||
<view class="start-chip-row">
|
|
||||||
<view
|
|
||||||
v-for="floor in startFloors"
|
|
||||||
:key="floor"
|
|
||||||
class="start-chip"
|
|
||||||
:class="{ active: floor === selectedStartFloor }"
|
|
||||||
@tap="handleStartFloorChange(floor)"
|
|
||||||
>
|
|
||||||
<text class="start-chip-text">{{ floor }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="start-chip-section area">
|
|
||||||
<text class="start-chip-label">区域</text>
|
|
||||||
<view class="start-chip-row">
|
|
||||||
<view
|
|
||||||
v-for="area in startAreas"
|
|
||||||
:key="area"
|
|
||||||
class="start-chip area"
|
|
||||||
:class="{ active: area === selectedStartArea }"
|
|
||||||
@tap="handleStartAreaChange(area)"
|
|
||||||
>
|
|
||||||
<text class="start-chip-text">{{ area }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="start-panel-actions">
|
|
||||||
<view class="start-panel-btn secondary" @tap="handleCancelStartSelection">
|
|
||||||
<text class="start-panel-btn-text">取消</text>
|
|
||||||
</view>
|
|
||||||
<view class="start-panel-btn primary" @tap="handleConfirmStartSelection">
|
|
||||||
<text class="start-panel-btn-text">确认起点</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</GuideMapShell>
|
</GuideMapShell>
|
||||||
</GuidePageFrame>
|
</GuidePageFrame>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
||||||
import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
|
import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
|
||||||
import {
|
import {
|
||||||
guideUseCase
|
guideUseCase
|
||||||
} from '@/usecases/guideUseCase'
|
} from '@/usecases/guideUseCase'
|
||||||
|
import {
|
||||||
|
explainUseCase
|
||||||
|
} from '@/usecases/explainUseCase'
|
||||||
import {
|
import {
|
||||||
toFacilityDetailViewModel,
|
toFacilityDetailViewModel,
|
||||||
|
toTargetFocusRequestViewModel,
|
||||||
|
type TargetPoiFocusRequestViewModel,
|
||||||
type FacilityDetailViewModel
|
type FacilityDetailViewModel
|
||||||
} from '@/view-models/guideViewModels'
|
} from '@/view-models/guideViewModels'
|
||||||
import {
|
import {
|
||||||
@@ -134,64 +89,51 @@ import {
|
|||||||
type GuideTopTab
|
type GuideTopTab
|
||||||
} from '@/utils/guideTopTabs'
|
} from '@/utils/guideTopTabs'
|
||||||
import type {
|
import type {
|
||||||
MuseumFloor
|
MuseumFloor,
|
||||||
|
MuseumPoi
|
||||||
} from '@/domain/museum'
|
} from '@/domain/museum'
|
||||||
|
import type {
|
||||||
|
GuideModelSource,
|
||||||
|
GuideRenderPoi
|
||||||
|
} from '@/domain/guideModel'
|
||||||
|
import {
|
||||||
|
StaticGuideModelRepository
|
||||||
|
} from '@/repositories/GuideModelRepository'
|
||||||
|
|
||||||
interface ConfirmedStart {
|
const fallbackIndoorModelSource = new StaticGuideModelRepository()
|
||||||
label: string
|
const indoorModelSource = ref<GuideModelSource>(fallbackIndoorModelSource)
|
||||||
floor: string
|
|
||||||
source: 'facility-detail'
|
|
||||||
}
|
|
||||||
|
|
||||||
const indoorModelSource = guideUseCase.getModelSource()
|
|
||||||
const guideFloors = ref<MuseumFloor[]>([])
|
const guideFloors = ref<MuseumFloor[]>([])
|
||||||
const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloorId(labelOrId)
|
const normalizeGuideFloorId = (labelOrId: string) =>
|
||||||
|
guideFloors.value.find((floor) => floor.id === labelOrId || floor.label === labelOrId)?.id
|
||||||
|
|| guideUseCase.normalizeFloorId(labelOrId)
|
||||||
const activeMode = ref<'2d' | '3d'>('3d')
|
const activeMode = ref<'2d' | '3d'>('3d')
|
||||||
const activeFloor = ref('1F')
|
const activeFloor = ref('1F')
|
||||||
|
const targetFocusRequest = ref<TargetPoiFocusRequestViewModel | null>(null)
|
||||||
|
const targetFocusRequestId = ref(0)
|
||||||
|
const isDetailPanelVisible = ref(true)
|
||||||
|
|
||||||
const facility = ref<FacilityDetailViewModel>({
|
const facility = ref<FacilityDetailViewModel>({
|
||||||
id: '',
|
id: '',
|
||||||
name: '目标位置',
|
name: '目标位置',
|
||||||
status: '可预览',
|
status: '可预览',
|
||||||
location: '中心线路网导览点位',
|
location: '正在定位三维点位',
|
||||||
traffic: guideUseCase.getRouteReadinessSnapshot().message,
|
traffic: guideUseCase.getRouteReadinessSnapshot().message,
|
||||||
tags: ['三维位置', '中心线路网']
|
tags: ['三维位置', '位置预览']
|
||||||
})
|
})
|
||||||
const isStartPanelOpen = ref(false)
|
|
||||||
const selectedStartFloor = ref('1F')
|
|
||||||
const selectedStartArea = ref('服务台附近')
|
|
||||||
const confirmedStart = ref<ConfirmedStart | null>(null)
|
|
||||||
const startFloors = computed(() => guideFloors.value.map((floor) => floor.label))
|
|
||||||
const mapTools = computed(() => ['重置', activeMode.value === '2d' ? '3D' : '2D'])
|
|
||||||
const startAreas = ['主入口', '服务台附近', '停车场入口']
|
|
||||||
|
|
||||||
const pendingStartLabel = computed(() => `${selectedStartFloor.value} ${selectedStartArea.value}`)
|
|
||||||
const startButtonLabel = computed(() => confirmedStart.value ? '更换起点' : '选择起点')
|
|
||||||
|
|
||||||
const resolveConfirmedStartArea = () => {
|
|
||||||
if (!confirmedStart.value) return '服务台附近'
|
|
||||||
|
|
||||||
const area = confirmedStart.value.floor
|
|
||||||
? confirmedStart.value.label.replace(`${confirmedStart.value.floor} `, '').trim()
|
|
||||||
: confirmedStart.value.label.trim()
|
|
||||||
|
|
||||||
return startAreas.includes(area) ? area : '服务台附近'
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncDraftStartSelection = () => {
|
|
||||||
selectedStartFloor.value = confirmedStart.value?.floor || '1F'
|
|
||||||
selectedStartArea.value = resolveConfirmedStartArea()
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadGuideFloors = async () => {
|
const loadGuideFloors = async () => {
|
||||||
try {
|
try {
|
||||||
const floors = await guideUseCase.getFloors()
|
const modelPackage = await fallbackIndoorModelSource.loadPackage()
|
||||||
guideFloors.value = floors
|
guideFloors.value = modelPackage.floors.map((floor) => ({
|
||||||
const defaultFloor = floors.find((floor) => floor.label === selectedStartFloor.value)?.label
|
id: floor.floorId,
|
||||||
|
label: floor.label,
|
||||||
|
order: floor.order
|
||||||
|
}))
|
||||||
|
const floors = guideFloors.value
|
||||||
|
const defaultFloor = floors.find((floor) => floor.label === activeFloor.value)?.label
|
||||||
|| floors.find((floor) => floor.id === 'L1')?.label
|
|| floors.find((floor) => floor.id === 'L1')?.label
|
||||||
|| floors[0]?.label
|
|| floors[0]?.label
|
||||||
|| selectedStartFloor.value
|
|| activeFloor.value
|
||||||
selectedStartFloor.value = defaultFloor
|
|
||||||
activeFloor.value = floors.find((floor) => floor.label === activeFloor.value)?.label
|
activeFloor.value = floors.find((floor) => floor.label === activeFloor.value)?.label
|
||||||
|| defaultFloor
|
|| defaultFloor
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -200,8 +142,148 @@ const loadGuideFloors = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const focusFacilityPoi = (poi: MuseumPoi) => {
|
||||||
|
if (!poi.floorId) return
|
||||||
|
|
||||||
|
activeMode.value = '3d'
|
||||||
|
activeFloor.value = poi.floorLabel
|
||||||
|
targetFocusRequestId.value += 1
|
||||||
|
targetFocusRequest.value = toTargetFocusRequestViewModel({
|
||||||
|
poiId: poi.id,
|
||||||
|
name: poi.name,
|
||||||
|
floorId: poi.floorId,
|
||||||
|
floorLabel: poi.floorLabel,
|
||||||
|
primaryCategoryZh: poi.primaryCategory.label,
|
||||||
|
positionGltf: poi.positionGltf,
|
||||||
|
sourceObjectName: poi.sourceObjectName
|
||||||
|
}, targetFocusRequestId.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusRenderPoi = (poi: GuideRenderPoi) => {
|
||||||
|
if (!poi.floorId) return
|
||||||
|
|
||||||
|
const floorLabel = guideFloors.value.find((floor) => floor.id === poi.floorId)?.label || poi.floorId
|
||||||
|
activeMode.value = '3d'
|
||||||
|
activeFloor.value = floorLabel
|
||||||
|
targetFocusRequestId.value += 1
|
||||||
|
targetFocusRequest.value = toTargetFocusRequestViewModel({
|
||||||
|
poiId: poi.id,
|
||||||
|
name: poi.name,
|
||||||
|
floorId: poi.floorId,
|
||||||
|
floorLabel,
|
||||||
|
primaryCategoryZh: poi.primaryCategoryZh,
|
||||||
|
positionGltf: poi.positionGltf,
|
||||||
|
sourceObjectName: poi.sourceObjectName,
|
||||||
|
kind: poi.kind,
|
||||||
|
hallId: poi.hallId,
|
||||||
|
hallName: poi.hallName,
|
||||||
|
entrances: poi.entrances
|
||||||
|
}, targetFocusRequestId.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRenderPoiFloorLabel = (poi: GuideRenderPoi) =>
|
||||||
|
guideFloors.value.find((floor) => floor.id === poi.floorId)?.label || poi.floorId
|
||||||
|
|
||||||
|
const applyRenderPoiToFacility = (poi: GuideRenderPoi, routeMessage: string) => {
|
||||||
|
const floorLabel = getRenderPoiFloorLabel(poi)
|
||||||
|
const categoryLabel = poi.primaryCategoryZh || '三维位置'
|
||||||
|
facility.value = {
|
||||||
|
id: poi.id,
|
||||||
|
name: poi.name,
|
||||||
|
status: '可预览',
|
||||||
|
location: `${floorLabel} · ${categoryLabel}`,
|
||||||
|
traffic: routeMessage,
|
||||||
|
tags: [
|
||||||
|
floorLabel,
|
||||||
|
categoryLabel,
|
||||||
|
'展示点位'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
focusRenderPoi(poi)
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizePoiName = (value: string) => value
|
||||||
|
.trim()
|
||||||
|
.replace(/[\s()()【】[\]_-]/g, '')
|
||||||
|
.toLowerCase()
|
||||||
|
|
||||||
|
const resolvePoiFromHall = async () => {
|
||||||
|
if (!facility.value.id) return null
|
||||||
|
|
||||||
|
const hall = await explainUseCase.getHallById(facility.value.id).catch(() => null)
|
||||||
|
const poiId = hall?.location?.poiId || hall?.poiId
|
||||||
|
return poiId ? guideUseCase.getPoiById(poiId) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveFacilityPoi = async () => {
|
||||||
|
const poiById = facility.value.id
|
||||||
|
? await guideUseCase.getPoiById(facility.value.id)
|
||||||
|
: null
|
||||||
|
if (poiById) return poiById
|
||||||
|
|
||||||
|
const poiByHall = await resolvePoiFromHall()
|
||||||
|
if (poiByHall) return poiByHall
|
||||||
|
|
||||||
|
const normalizedTarget = normalizePoiName(facility.value.name)
|
||||||
|
if (!normalizedTarget) return null
|
||||||
|
|
||||||
|
const candidates = await guideUseCase.searchPois(facility.value.name)
|
||||||
|
return candidates.find((candidate) => {
|
||||||
|
const candidateName = normalizePoiName(candidate.name)
|
||||||
|
const candidateHallName = normalizePoiName(candidate.hallName || '')
|
||||||
|
return candidateName === normalizedTarget
|
||||||
|
|| candidateHallName === normalizedTarget
|
||||||
|
|| candidateName.includes(normalizedTarget)
|
||||||
|
|| normalizedTarget.includes(candidateName)
|
||||||
|
}) || candidates[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveRenderPoiByName = async () => {
|
||||||
|
const normalizedTarget = normalizePoiName(facility.value.name)
|
||||||
|
if (!normalizedTarget) return null
|
||||||
|
|
||||||
|
const modelPackage = await indoorModelSource.value.loadPackage()
|
||||||
|
for (const floor of modelPackage.floors) {
|
||||||
|
const floorId = floor.floorId
|
||||||
|
const pois = await indoorModelSource.value.loadFloorPois(floorId).catch(() => [])
|
||||||
|
const matchedPoi = pois.find((poi) => {
|
||||||
|
const poiName = normalizePoiName(poi.name)
|
||||||
|
const hallName = normalizePoiName(poi.hallName || '')
|
||||||
|
const sourceObjectName = normalizePoiName(poi.sourceObjectName || '')
|
||||||
|
return poiName === normalizedTarget
|
||||||
|
|| hallName === normalizedTarget
|
||||||
|
|| sourceObjectName === normalizedTarget
|
||||||
|
|| poiName.includes(normalizedTarget)
|
||||||
|
|| normalizedTarget.includes(poiName)
|
||||||
|
|| sourceObjectName.includes(normalizedTarget)
|
||||||
|
|| normalizedTarget.includes(sourceObjectName)
|
||||||
|
})
|
||||||
|
if (matchedPoi) return matchedPoi
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const tryResolveRenderPoiByName = async () => {
|
||||||
|
try {
|
||||||
|
return await resolveRenderPoiByName()
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('当前模型源点位解析失败,尝试静态资源兜底:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tryResolveFacilityPoi = async () => {
|
||||||
|
try {
|
||||||
|
return await resolveFacilityPoi()
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('内容点位解析失败,继续使用模型点位预览:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onLoad(async (options: any) => {
|
onLoad(async (options: any) => {
|
||||||
void loadGuideFloors()
|
await loadGuideFloors()
|
||||||
|
|
||||||
if (options.id) {
|
if (options.id) {
|
||||||
facility.value.id = options.id
|
facility.value.id = options.id
|
||||||
@@ -211,108 +293,41 @@ onLoad(async (options: any) => {
|
|||||||
facility.value.name = decodeURIComponent(options.target)
|
facility.value.name = decodeURIComponent(options.target)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!facility.value.id) return
|
if (!facility.value.id && !normalizePoiName(facility.value.name)) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const routeReadiness = await guideUseCase.getRouteReadiness()
|
const routeReadinessMessage = guideUseCase.getRouteReadinessSnapshot().message
|
||||||
const poi = await guideUseCase.getPoiById(facility.value.id)
|
|
||||||
if (!poi) return
|
|
||||||
|
|
||||||
facility.value = toFacilityDetailViewModel(
|
// 详情页以位置预览为主,优先使用模型点位,避免旧内容兜底覆盖真实三维点位。
|
||||||
poi,
|
const renderPoi = await tryResolveRenderPoiByName()
|
||||||
routeReadiness.message
|
if (renderPoi) {
|
||||||
)
|
applyRenderPoiToFacility(renderPoi, routeReadinessMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const poi = await tryResolveFacilityPoi()
|
||||||
|
if (poi) {
|
||||||
|
facility.value = toFacilityDetailViewModel(
|
||||||
|
poi,
|
||||||
|
routeReadinessMessage
|
||||||
|
)
|
||||||
|
focusFacilityPoi(poi)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载中心线路网设施详情失败:', error)
|
console.error('加载设施位置详情失败:', error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSearchTap = () => {
|
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/search/index?keyword=${encodeURIComponent(facility.value.name)}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChooseStart = () => {
|
|
||||||
syncDraftStartSelection()
|
|
||||||
isStartPanelOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleStartFloorChange = (floor: string) => {
|
|
||||||
selectedStartFloor.value = floor
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleStartAreaChange = (area: string) => {
|
|
||||||
selectedStartArea.value = area
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCancelStartSelection = () => {
|
|
||||||
syncDraftStartSelection()
|
|
||||||
isStartPanelOpen.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleConfirmStartSelection = () => {
|
|
||||||
confirmedStart.value = {
|
|
||||||
label: pendingStartLabel.value,
|
|
||||||
floor: selectedStartFloor.value,
|
|
||||||
source: 'facility-detail'
|
|
||||||
}
|
|
||||||
isStartPanelOpen.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleStartNavigation = () => {
|
|
||||||
const params: Record<string, string> = {
|
|
||||||
facilityId: facility.value.id,
|
|
||||||
target: facility.value.name,
|
|
||||||
state: 'preview'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (confirmedStart.value) {
|
|
||||||
params.startLabel = confirmedStart.value.label
|
|
||||||
params.startFloor = confirmedStart.value.floor
|
|
||||||
params.startSource = confirmedStart.value.source
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = Object.entries(params)
|
|
||||||
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
|
||||||
.join('&')
|
|
||||||
|
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/route/detail?${query}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleModeChange = (mode: '2d' | '3d') => {
|
|
||||||
activeMode.value = mode
|
|
||||||
uni.showToast({
|
|
||||||
title: mode === '2d' ? '已切换馆外入口参考' : '已切换馆内三维位置',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFloorChange = (floor: string) => {
|
const handleFloorChange = (floor: string) => {
|
||||||
activeFloor.value = floor
|
activeFloor.value = floor
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleToolClick = (tool: string) => {
|
const handleCloseDetailPanel = () => {
|
||||||
if (tool === '2D') {
|
isDetailPanelVisible.value = false
|
||||||
handleModeChange('2d')
|
}
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tool === '3D') {
|
const handleOpenDetailPanel = () => {
|
||||||
handleModeChange('3d')
|
isDetailPanelVisible.value = true
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tool === '重置') {
|
|
||||||
activeMode.value = '3d'
|
|
||||||
}
|
|
||||||
|
|
||||||
uni.showToast({
|
|
||||||
title: tool === '重置' ? '已回到三维位置预览' : '该工具暂未开放',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTopTabChange = (tab: GuideTopTab) => {
|
const handleTopTabChange = (tab: GuideTopTab) => {
|
||||||
@@ -335,86 +350,106 @@ const handlePageBack = () => {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.detail-sheet {
|
.detail-sheet {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 84px;
|
||||||
right: 0;
|
right: 16px;
|
||||||
bottom: calc(env(safe-area-inset-bottom) + 116px);
|
bottom: calc(env(safe-area-inset-bottom) + 116px);
|
||||||
height: 252px;
|
min-height: 110px;
|
||||||
padding: 22px 20px 20px;
|
padding: 14px 14px 12px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: #ffffff;
|
background: rgba(255, 255, 255, 0.96);
|
||||||
border: 0;
|
border: 1px solid #e5e6de;
|
||||||
border-radius: 20px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.14);
|
box-shadow: 0 10px 24px rgba(36, 49, 42, 0.12);
|
||||||
z-index: 45;
|
z-index: 45;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-title-row {
|
.detail-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-dot {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
margin-top: 5px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: var(--museum-accent);
|
||||||
|
border: 3px solid #151713;
|
||||||
|
border-radius: 999px;
|
||||||
|
box-shadow: 0 0 0 4px rgba(224, 225, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-title-copy {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-close {
|
||||||
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #f4f5ef;
|
||||||
|
border: 1px solid #dde5df;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-close-text {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #30352f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-title {
|
.detail-title {
|
||||||
flex: 1;
|
font-size: 17px;
|
||||||
min-width: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #000000;
|
color: #151713;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.open-status {
|
.detail-subtitle {
|
||||||
width: 60px;
|
font-size: 12px;
|
||||||
flex-shrink: 0;
|
line-height: 17px;
|
||||||
text-align: right;
|
color: #696962;
|
||||||
font-size: 13px;
|
|
||||||
line-height: 18px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #1a7f37;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-lines {
|
|
||||||
margin-top: 17px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-line {
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 18px;
|
|
||||||
color: #6b7178;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-row {
|
.tag-row {
|
||||||
margin-top: 21px;
|
margin-top: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-tag {
|
.detail-tag {
|
||||||
height: 28px;
|
height: 26px;
|
||||||
min-width: 88px;
|
min-width: 68px;
|
||||||
padding: 0 15px;
|
padding: 0 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid #d9d9d9;
|
border: 1px solid #e5e6de;
|
||||||
border-radius: 14px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-tag.active {
|
.detail-tag.active {
|
||||||
background: #000000;
|
background: #151713;
|
||||||
border-color: #000000;
|
border-color: #151713;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-tag-text {
|
.detail-tag-text {
|
||||||
@@ -428,191 +463,58 @@ const handlePageBack = () => {
|
|||||||
color: var(--museum-accent);
|
color: var(--museum-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-row {
|
.detail-hint {
|
||||||
position: absolute;
|
|
||||||
left: 20px;
|
|
||||||
right: 20px;
|
|
||||||
bottom: 20px;
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
height: 44px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn.secondary {
|
|
||||||
width: 151px;
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn.primary {
|
|
||||||
width: 168px;
|
|
||||||
background: #000000;
|
|
||||||
border: 1px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-text {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 19px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn.primary .action-text {
|
|
||||||
color: var(--museum-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-panel-mask {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.2);
|
|
||||||
z-index: 52;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-select-sheet {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: calc(env(safe-area-inset-bottom) + 116px);
|
|
||||||
height: 276px;
|
|
||||||
padding: 22px 20px 16px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 20px;
|
|
||||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.16);
|
|
||||||
z-index: 55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-select-title {
|
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 18px;
|
margin-top: 10px;
|
||||||
line-height: 24px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-select-desc {
|
|
||||||
display: block;
|
|
||||||
margin-top: 6px;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 17px;
|
line-height: 17px;
|
||||||
color: #626970;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-chip-section {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-chip-section.area {
|
|
||||||
margin-top: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-chip-label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 16px;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #60666d;
|
color: #1a7f37;
|
||||||
}
|
}
|
||||||
|
|
||||||
.start-chip-row {
|
.detail-restore {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-chip {
|
|
||||||
height: 28px;
|
|
||||||
min-width: 48px;
|
|
||||||
padding: 0 13px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: #f4f4ef;
|
|
||||||
border: 1px solid #e7e8e2;
|
|
||||||
border-radius: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-chip.area {
|
|
||||||
min-width: 72px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-chip.active {
|
|
||||||
background: #000000;
|
|
||||||
border-color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-chip-text {
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #60666d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-chip.active .start-chip-text {
|
|
||||||
color: var(--museum-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-panel-actions {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 20px;
|
right: 18px;
|
||||||
right: 20px;
|
bottom: calc(env(safe-area-inset-bottom) + 266px);
|
||||||
bottom: 16px;
|
height: 40px;
|
||||||
display: flex;
|
min-width: 74px;
|
||||||
gap: 16px;
|
padding: 0 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.start-panel-btn {
|
|
||||||
height: 42px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: 7px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
border: 1px solid #dde5df;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 18px rgba(110, 127, 115, 0.12);
|
||||||
|
z-index: 45;
|
||||||
}
|
}
|
||||||
|
|
||||||
.start-panel-btn.secondary {
|
.target-dot.small {
|
||||||
width: 151px;
|
width: 10px;
|
||||||
background: #ffffff;
|
height: 10px;
|
||||||
border: 1px solid #000000;
|
margin-top: 0;
|
||||||
|
border-width: 2px;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.start-panel-btn.primary {
|
.detail-restore-text {
|
||||||
width: 168px;
|
font-size: 13px;
|
||||||
background: #000000;
|
line-height: 18px;
|
||||||
border: 1px solid #000000;
|
font-weight: 700;
|
||||||
}
|
color: #151713;
|
||||||
|
|
||||||
.start-panel-btn-text {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 19px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-panel-btn.primary .start-panel-btn-text {
|
|
||||||
color: var(--museum-accent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 360px) {
|
@media (max-width: 360px) {
|
||||||
.start-panel-actions {
|
.detail-sheet {
|
||||||
gap: 10px;
|
left: 78px;
|
||||||
|
right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.start-panel-btn.secondary,
|
.detail-tag {
|
||||||
.start-panel-btn.primary {
|
min-width: 0;
|
||||||
width: auto;
|
padding: 0 9px;
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<text class="hall-meta">{{ hall.floorLabel }} · {{ explainCountText }}</text>
|
<text class="hall-meta">{{ hall.floorLabel }} · {{ explainCountText }}</text>
|
||||||
<view
|
<view
|
||||||
class="location-action"
|
class="location-action"
|
||||||
:class="{ disabled: !hall.poiId }"
|
:class="{ disabled: !hallLocationPoiId }"
|
||||||
@tap="handleNavigate"
|
@tap="handleNavigate"
|
||||||
>
|
>
|
||||||
<svg class="location-icon" width="16" height="16" viewBox="0 0 24 24" fill="none">
|
<svg class="location-icon" width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||||
@@ -99,6 +99,7 @@ import {
|
|||||||
const activeTopTab = ref<GuideTopTab>('explain')
|
const activeTopTab = ref<GuideTopTab>('explain')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
const resolvedHallPoiId = ref('')
|
||||||
|
|
||||||
const hall = ref<HallDetailViewModel>({
|
const hall = ref<HallDetailViewModel>({
|
||||||
id: '',
|
id: '',
|
||||||
@@ -116,6 +117,37 @@ const explainCountText = computed(() => (
|
|||||||
exhibits.value.length > 0 ? `${exhibits.value.length} 条讲解` : '暂无讲解'
|
exhibits.value.length > 0 ? `${exhibits.value.length} 条讲解` : '暂无讲解'
|
||||||
))
|
))
|
||||||
|
|
||||||
|
const hallLocationPoiId = computed(() => hall.value.poiId || resolvedHallPoiId.value)
|
||||||
|
|
||||||
|
const normalizePoiName = (value: string) => value
|
||||||
|
.trim()
|
||||||
|
.replace(/[\s()()【】[\]_-]/g, '')
|
||||||
|
.toLowerCase()
|
||||||
|
|
||||||
|
// 展厅数据里可能没有直接写入 poiId,优先走内容层映射,再按展厅名称兜底匹配导览点位。
|
||||||
|
const resolveHallGuidePoi = async () => {
|
||||||
|
const directPoiId = hall.value.location?.poiId || hall.value.poiId
|
||||||
|
if (directPoiId) {
|
||||||
|
const directPoi = await guideUseCase.getPoiById(directPoiId)
|
||||||
|
if (directPoi) return directPoi
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedTarget = normalizePoiName(hall.value.name)
|
||||||
|
if (!normalizedTarget) return null
|
||||||
|
|
||||||
|
const candidates = await guideUseCase.searchPois(hall.value.name)
|
||||||
|
return candidates.find((candidate) => {
|
||||||
|
const candidateName = normalizePoiName(candidate.name)
|
||||||
|
const candidateHallName = normalizePoiName(candidate.hallName || '')
|
||||||
|
return candidate.kind === 'hall'
|
||||||
|
|| candidate.primaryCategory.id === 'exhibition_hall'
|
||||||
|
|| candidateName === normalizedTarget
|
||||||
|
|| candidateHallName === normalizedTarget
|
||||||
|
|| candidateName.includes(normalizedTarget)
|
||||||
|
|| normalizedTarget.includes(candidateName)
|
||||||
|
}) || candidates[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
onLoad(async (options: any = {}) => {
|
onLoad(async (options: any = {}) => {
|
||||||
const tab = Array.isArray(options.tab) ? options.tab[0] : options.tab
|
const tab = Array.isArray(options.tab) ? options.tab[0] : options.tab
|
||||||
if (isGuideTopTab(tab)) {
|
if (isGuideTopTab(tab)) {
|
||||||
@@ -141,6 +173,25 @@ onLoad(async (options: any = {}) => {
|
|||||||
hall.value = toHallDetailViewModel(hallData)
|
hall.value = toHallDetailViewModel(hallData)
|
||||||
const hallExhibits = await explainUseCase.listExhibitsByHallId(hallData.id)
|
const hallExhibits = await explainUseCase.listExhibitsByHallId(hallData.id)
|
||||||
exhibits.value = hallExhibits.map(toExplainExhibitViewModel)
|
exhibits.value = hallExhibits.map(toExplainExhibitViewModel)
|
||||||
|
|
||||||
|
const matchedPoi = await resolveHallGuidePoi()
|
||||||
|
if (matchedPoi) {
|
||||||
|
resolvedHallPoiId.value = matchedPoi.id
|
||||||
|
hall.value = {
|
||||||
|
...hall.value,
|
||||||
|
poiId: matchedPoi.id,
|
||||||
|
location: hall.value.location || {
|
||||||
|
status: 'exact',
|
||||||
|
poiId: matchedPoi.id,
|
||||||
|
sourcePoiId: matchedPoi.id,
|
||||||
|
actionText: '查看三维位置',
|
||||||
|
previewOnly: true,
|
||||||
|
floorId: matchedPoi.floorId,
|
||||||
|
floorLabel: matchedPoi.floorLabel,
|
||||||
|
note: '已定位到该展厅的三维点位。'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('加载展厅讲解失败:', err)
|
console.error('加载展厅讲解失败:', err)
|
||||||
error.value = '展厅讲解加载失败,请稍后重试'
|
error.value = '展厅讲解加载失败,请稍后重试'
|
||||||
@@ -163,7 +214,8 @@ const handleExhibitClick = (exhibit: ExplainExhibitViewModel) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleNavigate = async () => {
|
const handleNavigate = async () => {
|
||||||
if (!hall.value.poiId) {
|
const matchedPoi = await resolveHallGuidePoi()
|
||||||
|
if (!matchedPoi) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '该展厅暂无三维位置数据',
|
title: '该展厅暂无三维位置数据',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
@@ -171,17 +223,24 @@ const handleNavigate = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const matchedPoi = await guideUseCase.getPoiById(hall.value.poiId)
|
resolvedHallPoiId.value = matchedPoi.id
|
||||||
if (!matchedPoi) {
|
hall.value = {
|
||||||
uni.showToast({
|
...hall.value,
|
||||||
title: '该展厅位置尚未映射到当前三维导览资源',
|
poiId: matchedPoi.id,
|
||||||
icon: 'none'
|
location: hall.value.location || {
|
||||||
})
|
status: 'exact',
|
||||||
return
|
poiId: matchedPoi.id,
|
||||||
|
sourcePoiId: matchedPoi.id,
|
||||||
|
actionText: '查看三维位置',
|
||||||
|
previewOnly: true,
|
||||||
|
floorId: matchedPoi.floorId,
|
||||||
|
floorLabel: matchedPoi.floorLabel,
|
||||||
|
note: '已定位到该展厅的三维点位。'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/route/detail?facilityId=${encodeURIComponent(hall.value.poiId)}&target=${encodeURIComponent(hall.value.name)}&state=preview`
|
url: `/pages/route/detail?facilityId=${encodeURIComponent(matchedPoi.id)}&target=${encodeURIComponent(hall.value.name)}&state=preview`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,14 +29,14 @@
|
|||||||
:layer-mode="indoorLayerMode"
|
:layer-mode="indoorLayerMode"
|
||||||
:active-floor="activeGuideFloor"
|
:active-floor="activeGuideFloor"
|
||||||
layer-mode-toggle-top="154px"
|
layer-mode-toggle-top="154px"
|
||||||
floor-bottom="185px"
|
floor-bottom="153px"
|
||||||
floor-max-height="206px"
|
floor-max-height="206px"
|
||||||
floor-side="left"
|
floor-side="left"
|
||||||
show-floor-header
|
show-floor-header
|
||||||
:show-layer-mode-toggle="false"
|
:show-layer-mode-toggle="false"
|
||||||
:show-floor="is3DMode"
|
:show-floor="is3DMode"
|
||||||
show-zoom-controls
|
show-zoom-controls
|
||||||
zoom-controls-top="calc(100vh - 284px)"
|
zoom-controls-top="calc(100vh - 252px)"
|
||||||
:route-preview="activeRoutePreview"
|
:route-preview="activeRoutePreview"
|
||||||
:show-route="Boolean(activeRoutePreview)"
|
:show-route="Boolean(activeRoutePreview)"
|
||||||
:disable-auto-exit="disableIndoorAutoExit"
|
:disable-auto-exit="disableIndoorAutoExit"
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
<view v-if="isPoiCardCollapsed" class="poi-card-collapsed" @tap="expandPoiCard">
|
<view v-if="isPoiCardCollapsed" class="poi-card-collapsed" @tap="expandPoiCard">
|
||||||
<view class="poi-card-copy">
|
<view class="poi-card-copy">
|
||||||
<text class="poi-card-title">{{ selectedGuidePoi.name }}</text>
|
<text class="poi-card-title">{{ selectedGuidePoi.name }}</text>
|
||||||
<text class="poi-card-subtitle">位置预览 · 点按展开操作</text>
|
<text class="poi-card-subtitle">{{ selectedGuidePoiCollapsedSubtitle }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="poi-card-mini-action">
|
<view class="poi-card-mini-action">
|
||||||
<text class="poi-card-mini-text">展开</text>
|
<text class="poi-card-mini-text">展开</text>
|
||||||
@@ -92,15 +92,12 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="poi-card-actions">
|
<view
|
||||||
<view class="poi-action primary" @tap="handlePreviewSelectedPoi">
|
v-if="selectedGuidePoiIsHall"
|
||||||
<text class="poi-action-text">查看位置</text>
|
class="poi-card-actions"
|
||||||
</view>
|
>
|
||||||
<view class="poi-action" @tap="handleSetSelectedPoiAsStart">
|
<view class="poi-action primary" @tap="handleViewSelectedHall">
|
||||||
<text class="poi-action-text">从这里出发</text>
|
<text class="poi-action-text">查看展厅</text>
|
||||||
</view>
|
|
||||||
<view class="poi-action" @tap="handleSetSelectedPoiAsEnd">
|
|
||||||
<text class="poi-action-text">去这里</text>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="poi-action" @tap="handleSelectedPoiRelated">
|
<view class="poi-action" @tap="handleSelectedPoiRelated">
|
||||||
<text class="poi-action-text">相关讲解</text>
|
<text class="poi-action-text">相关讲解</text>
|
||||||
@@ -248,7 +245,8 @@ import type {
|
|||||||
GuideRouteResult,
|
GuideRouteResult,
|
||||||
GuideRouteTarget,
|
GuideRouteTarget,
|
||||||
MuseumExhibit,
|
MuseumExhibit,
|
||||||
MuseumFloor
|
MuseumFloor,
|
||||||
|
MuseumHall
|
||||||
} from '@/domain/museum'
|
} from '@/domain/museum'
|
||||||
import { MUSEUM_LOCATION, type TravelMode } from '@/config/museum'
|
import { MUSEUM_LOCATION, type TravelMode } from '@/config/museum'
|
||||||
import {
|
import {
|
||||||
@@ -400,13 +398,27 @@ const selectedGuidePoiSubtitle = computed(() => {
|
|||||||
return `${floorLabel} · ${poi.primaryCategoryZh || '位置预览'}`
|
return `${floorLabel} · ${poi.primaryCategoryZh || '位置预览'}`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectedGuidePoiIsHall = computed(() => {
|
||||||
|
const poi = selectedGuidePoi.value
|
||||||
|
return Boolean(
|
||||||
|
poi
|
||||||
|
&& (
|
||||||
|
poi.kind === 'hall'
|
||||||
|
|| poi.primaryCategory === 'touring_poi'
|
||||||
|
|| poi.primaryCategory === 'exhibition_hall'
|
||||||
|
|| poi.primaryCategory === 'exhibition_hall_entrance'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedGuidePoiCollapsedSubtitle = computed(() => (
|
||||||
|
selectedGuidePoiIsHall.value ? '已定位展厅 · 点按展开操作' : '已定位点位'
|
||||||
|
))
|
||||||
|
|
||||||
const routeSimulationStepText = computed(() => {
|
const routeSimulationStepText = computed(() => {
|
||||||
if (!activeRoutePreview.value) return routeReady.value ? '馆内导览 · 查看路线' : '馆内导览 · 查看位置'
|
if (!activeRoutePreview.value) return '位置预览 · 查看位置关系'
|
||||||
const hasConnector = activeRoutePreview.value.connectorPoints.length > 0
|
const hasConnector = activeRoutePreview.value.connectorPoints.length > 0
|
||||||
if (routeReady.value) {
|
return hasConnector ? '位置预览 · 跨楼层位置关系' : '位置预览 · 同层位置关系'
|
||||||
return hasConnector ? '馆内导览 · 跨楼层路线' : '馆内导览 · 同层路线'
|
|
||||||
}
|
|
||||||
return hasConnector ? '馆内导览 · 跨楼层位置关系' : '馆内导览 · 同层位置关系'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const explainHallItems = ref<ExplainHallSelectItem[]>([])
|
const explainHallItems = ref<ExplainHallSelectItem[]>([])
|
||||||
@@ -667,12 +679,12 @@ const handleIndoorToolClick = (tool: string) => {
|
|||||||
|
|
||||||
const handleGuidePoiClick = (poi: GuideRenderPoi) => {
|
const handleGuidePoiClick = (poi: GuideRenderPoi) => {
|
||||||
selectedGuidePoi.value = poi
|
selectedGuidePoi.value = poi
|
||||||
isPoiCardCollapsed.value = false
|
isPoiCardCollapsed.value = !selectedGuidePoiIsHall.value
|
||||||
indoorView.value = 'floor'
|
indoorView.value = 'floor'
|
||||||
isSimulatingRoute.value = false
|
isSimulatingRoute.value = false
|
||||||
const matchedFloor = guideFloors.value.find((floor) => floor.id === poi.floorId)
|
const matchedFloor = guideFloors.value.find((floor) => floor.id === poi.floorId)
|
||||||
activeGuideFloor.value = matchedFloor?.label || poi.floorId
|
activeGuideFloor.value = matchedFloor?.label || poi.floorId
|
||||||
showIndoorHint('已选中标记,可查看位置预览', 3200)
|
showIndoorHint('已在当前楼层标出点位', 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleGuideSelectionClear = () => {
|
const handleGuideSelectionClear = () => {
|
||||||
@@ -692,55 +704,84 @@ const togglePoiCardCollapsed = () => {
|
|||||||
isPoiCardCollapsed.value = !isPoiCardCollapsed.value
|
isPoiCardCollapsed.value = !isPoiCardCollapsed.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const findRouteOptionForPoi = async (poi: GuideRenderPoi) => {
|
const normalizeHallMatchText = (value?: string) => (value || '')
|
||||||
if (!routePointOptions.value.length) {
|
.trim()
|
||||||
await loadRouteTargets()
|
.replace(/[\s()()【】[\]_-]/g, '')
|
||||||
|
.toLowerCase()
|
||||||
|
|
||||||
|
const resolveSelectedPoiHall = async (): Promise<MuseumHall | null> => {
|
||||||
|
const poi = selectedGuidePoi.value
|
||||||
|
if (!poi) return null
|
||||||
|
|
||||||
|
const halls = await explainUseCase.listHalls()
|
||||||
|
const hallId = poi.hallId
|
||||||
|
if (hallId) {
|
||||||
|
const matchedById = halls.find((hall) => hall.id === hallId || hall.poiId === hallId)
|
||||||
|
if (matchedById) return matchedById
|
||||||
}
|
}
|
||||||
|
|
||||||
return routePointOptions.value.find((option) => option.poiId === poi.id) || null
|
const matchedByPoiId = halls.find((hall) => (
|
||||||
|
hall.poiId === poi.id
|
||||||
|
|| hall.location?.poiId === poi.id
|
||||||
|
|| (poi.entrances || []).some((entrance) => hall.poiId === entrance.sourcePlaceId)
|
||||||
|
))
|
||||||
|
if (matchedByPoiId) return matchedByPoiId
|
||||||
|
|
||||||
|
const poiHallName = normalizeHallMatchText(poi.hallName || poi.name)
|
||||||
|
return halls.find((hall) => {
|
||||||
|
const hallName = normalizeHallMatchText(hall.name)
|
||||||
|
return hallName && poiHallName && (hallName.includes(poiHallName) || poiHallName.includes(hallName))
|
||||||
|
}) || null
|
||||||
}
|
}
|
||||||
|
|
||||||
const setSelectedPoiAsRoutePoint = async (mode: 'start' | 'end') => {
|
const navigateToHallDetail = (hallId: string) => {
|
||||||
if (!selectedGuidePoi.value) return
|
uni.navigateTo({
|
||||||
|
url: `/pages/hall/detail?id=${encodeURIComponent(hallId)}&tab=explain`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const option = await findRouteOptionForPoi(selectedGuidePoi.value)
|
const handleViewSelectedHall = async () => {
|
||||||
if (!option) {
|
const hall = await resolveSelectedPoiHall()
|
||||||
|
if (!hall) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '该点位暂无线路数据,可先查看三维位置',
|
title: '该展厅详情暂未接入',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === 'start') {
|
navigateToHallDetail(hall.id)
|
||||||
routeStartPoint.value = option
|
}
|
||||||
} else {
|
|
||||||
routeEndPoint.value = option
|
const handleSelectedPoiRelated = async () => {
|
||||||
|
if (!selectedGuidePoi.value) return
|
||||||
|
|
||||||
|
const hall = await resolveSelectedPoiHall()
|
||||||
|
if (hall) {
|
||||||
|
navigateToHallDetail(hall.id)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
clearRoutePreviewState()
|
const keyword = selectedGuidePoi.value.hallName || selectedGuidePoi.value.name
|
||||||
openRoutePlanner()
|
const results = await explainUseCase.searchExplain(keyword)
|
||||||
}
|
const firstHall = results.find((item) => item.type === 'hall' && item.hallId)
|
||||||
|
const firstExhibit = results.find((item) => item.type === 'exhibit' && item.exhibitId)
|
||||||
|
|
||||||
const handlePreviewSelectedPoi = () => {
|
if (firstHall?.hallId) {
|
||||||
if (!selectedGuidePoi.value) return
|
navigateToHallDetail(firstHall.hallId)
|
||||||
indoorView.value = 'floor'
|
return
|
||||||
showRoutePlanner.value = false
|
}
|
||||||
isSimulatingRoute.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSetSelectedPoiAsStart = () => {
|
if (firstExhibit?.exhibitId) {
|
||||||
void setSelectedPoiAsRoutePoint('start')
|
uni.navigateTo({
|
||||||
}
|
url: `/pages/exhibit/detail?id=${encodeURIComponent(firstExhibit.exhibitId)}&tab=explain`
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const handleSetSelectedPoiAsEnd = () => {
|
uni.showToast({
|
||||||
void setSelectedPoiAsRoutePoint('end')
|
title: '该展厅暂无对应讲解',
|
||||||
}
|
icon: 'none'
|
||||||
|
|
||||||
const handleSelectedPoiRelated = () => {
|
|
||||||
if (!selectedGuidePoi.value) return
|
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/search/index?keyword=${encodeURIComponent(selectedGuidePoi.value.name)}`
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -769,15 +810,16 @@ const refreshRouteReadinessState = async () => {
|
|||||||
routeReady.value = readiness.ready
|
routeReady.value = readiness.ready
|
||||||
routePreviewUnavailableMessage.value = readiness.ready
|
routePreviewUnavailableMessage.value = readiness.ready
|
||||||
? ''
|
? ''
|
||||||
: readiness.message || '当前暂不支持正式导航,可查看位置预览和路线示意'
|
: readiness.message || '当前可查看位置预览和位置关系,暂不提供正式室内导航'
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('读取路线能力状态失败:', error)
|
console.warn('读取路线能力状态失败:', error)
|
||||||
routeReady.value = false
|
routeReady.value = false
|
||||||
routePreviewUnavailableMessage.value = '当前暂不支持正式导航,可查看位置预览和路线示意'
|
routePreviewUnavailableMessage.value = '当前可查看位置预览和位置关系,暂不提供正式室内导航'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openRoutePlanner = () => {
|
const openRoutePlanner = () => {
|
||||||
|
closeOutdoorNavPanel()
|
||||||
showRoutePlanner.value = true
|
showRoutePlanner.value = true
|
||||||
is3DMode.value = true
|
is3DMode.value = true
|
||||||
indoorView.value = indoorView.value === 'overview' ? 'floor' : indoorView.value
|
indoorView.value = indoorView.value === 'overview' ? 'floor' : indoorView.value
|
||||||
@@ -898,6 +940,7 @@ const handleRouteSimulate = () => {
|
|||||||
// 进入 3D 馆内模式
|
// 进入 3D 馆内模式
|
||||||
const handleEnter3DMode = () => {
|
const handleEnter3DMode = () => {
|
||||||
console.log('进入 3D 馆内模式')
|
console.log('进入 3D 馆内模式')
|
||||||
|
closeOutdoorNavPanel()
|
||||||
indoorView.value = 'overview'
|
indoorView.value = 'overview'
|
||||||
is3DMode.value = true
|
is3DMode.value = true
|
||||||
guideOutdoorState.value = 'home'
|
guideOutdoorState.value = 'home'
|
||||||
@@ -919,6 +962,7 @@ const handleModeChange = (mode: '2d' | '3d') => {
|
|||||||
isSimulatingRoute.value = false
|
isSimulatingRoute.value = false
|
||||||
clearRoutePreviewState()
|
clearRoutePreviewState()
|
||||||
} else {
|
} else {
|
||||||
|
closeOutdoorNavPanel()
|
||||||
indoorView.value = 'overview'
|
indoorView.value = 'overview'
|
||||||
showIndoorHint()
|
showIndoorHint()
|
||||||
if (!routePointOptions.value.length) {
|
if (!routePointOptions.value.length) {
|
||||||
@@ -970,18 +1014,21 @@ const clearOutdoorNavState = () => {
|
|||||||
outdoorNavManualMode.value = false
|
outdoorNavManualMode.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const closeOutdoorNavPanel = () => {
|
||||||
|
showOutdoorNavPanel.value = false
|
||||||
|
clearOutdoorNavState()
|
||||||
|
}
|
||||||
|
|
||||||
const handleMoreOutdoorNav = () => {
|
const handleMoreOutdoorNav = () => {
|
||||||
openOutdoorNavPanel()
|
openOutdoorNavPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOutdoorNavClose = () => {
|
const handleOutdoorNavClose = () => {
|
||||||
showOutdoorNavPanel.value = false
|
closeOutdoorNavPanel()
|
||||||
clearOutdoorNavState()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOutdoorNavBack = () => {
|
const handleOutdoorNavBack = () => {
|
||||||
showOutdoorNavPanel.value = false
|
closeOutdoorNavPanel()
|
||||||
clearOutdoorNavState()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOutdoorNavGpsStart = () => {
|
const handleOutdoorNavGpsStart = () => {
|
||||||
@@ -1291,7 +1338,7 @@ const handleExplainBack = () => {
|
|||||||
.poi-card-actions {
|
.poi-card-actions {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,7 @@
|
|||||||
@tap="handleStartIndoorGuide"
|
@tap="handleStartIndoorGuide"
|
||||||
>
|
>
|
||||||
<text class="indoor-guide-btn-text">
|
<text class="indoor-guide-btn-text">
|
||||||
{{ routePlanning ? '规划中' : routeReady ? '开始导览' : '导航不可用' }}
|
{{ routePlanning ? '生成中' : routeReady ? '查看位置关系' : '位置关系不可用' }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -304,7 +304,7 @@ const shellConfig = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
searchText: routeReady.value ? `馆内导览:${route.value.target}` : `位置预览:${route.value.target}`,
|
searchText: `位置预览:${route.value.target}`,
|
||||||
searchTop: '60px',
|
searchTop: '60px',
|
||||||
activeMode: '3d' as GuideMode,
|
activeMode: '3d' as GuideMode,
|
||||||
activeFloor: activeFloor.value,
|
activeFloor: activeFloor.value,
|
||||||
@@ -323,17 +323,17 @@ const shellConfig = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const indoorGuideTitle = computed(() => (
|
const indoorGuideTitle = computed(() => (
|
||||||
routeReady.value ? `馆内导览:${route.value.target}` : `位置预览:${route.value.target}`
|
`位置预览:${route.value.target}`
|
||||||
))
|
))
|
||||||
|
|
||||||
const indoorGuideDesc = computed(() => {
|
const indoorGuideDesc = computed(() => {
|
||||||
if (activeRoutePreview.value) {
|
if (activeRoutePreview.value) {
|
||||||
return `已规划 ${activeRoutePreview.value.start.name} 至 ${activeRoutePreview.value.end.name},约 ${Math.round(activeRoutePreview.value.distanceMeters)} 米。`
|
return `已生成 ${activeRoutePreview.value.start.name} 至 ${activeRoutePreview.value.end.name} 的位置关系,约 ${Math.round(activeRoutePreview.value.distanceMeters)} 米。`
|
||||||
}
|
}
|
||||||
|
|
||||||
return routeReady.value
|
return routeReady.value
|
||||||
? 'SGS 路线数据已就绪,可从已选起点开始规划馆内路线。'
|
? '可选择起点查看馆内位置关系,当前不作为正式室内导航。'
|
||||||
: routeReadinessMessage.value || '当前暂不支持正式导航,可查看位置预览。'
|
: routeReadinessMessage.value || '当前可查看位置预览,暂不提供正式室内导航。'
|
||||||
})
|
})
|
||||||
|
|
||||||
const safeDecode = (value: string) => {
|
const safeDecode = (value: string) => {
|
||||||
@@ -635,7 +635,7 @@ const handleStartIndoorGuide = async () => {
|
|||||||
: null)
|
: null)
|
||||||
|
|
||||||
if (!fallbackStart || !endTarget) {
|
if (!fallbackStart || !endTarget) {
|
||||||
routePlanError.value = '未找到可用于路线规划的起点或终点'
|
routePlanError.value = '未找到可用于位置关系预览的起点或终点'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,7 +645,7 @@ const handleStartIndoorGuide = async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!result.route) {
|
if (!result.route) {
|
||||||
routePlanError.value = result.error || '馆内导览路线规划失败'
|
routePlanError.value = result.error || '馆内位置关系生成失败'
|
||||||
activeRoutePreview.value = null
|
activeRoutePreview.value = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -653,7 +653,7 @@ const handleStartIndoorGuide = async () => {
|
|||||||
activeRoutePreview.value = result.route
|
activeRoutePreview.value = result.route
|
||||||
activeFloor.value = result.route.start.floorLabel
|
activeFloor.value = result.route.start.floorLabel
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
routePlanError.value = error instanceof Error ? error.message : '馆内导览路线规划失败'
|
routePlanError.value = error instanceof Error ? error.message : '馆内位置关系生成失败'
|
||||||
activeRoutePreview.value = null
|
activeRoutePreview.value = null
|
||||||
} finally {
|
} finally {
|
||||||
routePlanning.value = false
|
routePlanning.value = false
|
||||||
|
|||||||
@@ -36,12 +36,15 @@ import {
|
|||||||
toGuideMapDiagnostics,
|
toGuideMapDiagnostics,
|
||||||
toLocationPreviewFromPoi,
|
toLocationPreviewFromPoi,
|
||||||
toMuseumFloorFromSgs,
|
toMuseumFloorFromSgs,
|
||||||
|
toMuseumHallPoisFromSgs,
|
||||||
toMuseumPoiFromSgs,
|
toMuseumPoiFromSgs,
|
||||||
toRouteReadinessFromSgsDiagnostics
|
toRouteReadinessFromSgsDiagnostics
|
||||||
} from '@/data/adapters/sgsSdkGuideAdapter'
|
} from '@/data/adapters/sgsSdkGuideAdapter'
|
||||||
|
|
||||||
const searchableCategories = new Set([
|
const searchableCategories = new Set([
|
||||||
'touring_poi',
|
'touring_poi',
|
||||||
|
'exhibition_hall',
|
||||||
|
'exhibition_hall_entrance',
|
||||||
'basic_service_facility',
|
'basic_service_facility',
|
||||||
'transport_circulation',
|
'transport_circulation',
|
||||||
'accessibility_special_service',
|
'accessibility_special_service',
|
||||||
@@ -50,6 +53,7 @@ const searchableCategories = new Set([
|
|||||||
|
|
||||||
const toSearchText = (poi: MuseumPoi) => [
|
const toSearchText = (poi: MuseumPoi) => [
|
||||||
poi.name,
|
poi.name,
|
||||||
|
poi.hallName,
|
||||||
poi.floorId,
|
poi.floorId,
|
||||||
poi.floorLabel,
|
poi.floorLabel,
|
||||||
poi.primaryCategory.label,
|
poi.primaryCategory.label,
|
||||||
@@ -71,7 +75,11 @@ const toLocationPreview = (poi: MuseumPoi): GuideLocationPreview => ({
|
|||||||
floorLabel: poi.floorLabel,
|
floorLabel: poi.floorLabel,
|
||||||
primaryCategoryZh: poi.primaryCategory.label,
|
primaryCategoryZh: poi.primaryCategory.label,
|
||||||
positionGltf: poi.positionGltf,
|
positionGltf: poi.positionGltf,
|
||||||
sourceObjectName: poi.sourceObjectName
|
sourceObjectName: poi.sourceObjectName,
|
||||||
|
kind: poi.kind,
|
||||||
|
hallId: poi.hallId,
|
||||||
|
hallName: poi.hallName,
|
||||||
|
entrances: poi.entrances
|
||||||
})
|
})
|
||||||
|
|
||||||
const isSearchableIndoorPoi = (poi: MuseumPoi) => (
|
const isSearchableIndoorPoi = (poi: MuseumPoi) => (
|
||||||
@@ -79,6 +87,15 @@ const isSearchableIndoorPoi = (poi: MuseumPoi) => (
|
|||||||
&& isPoiOnIndoorNavigableFloor(poi)
|
&& isPoiOnIndoorNavigableFloor(poi)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const dedupePoisById = (pois: MuseumPoi[]) => {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
return pois.filter((poi) => {
|
||||||
|
if (!poi.id || seen.has(poi.id)) return false
|
||||||
|
seen.add(poi.id)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export interface GuideRepository {
|
export interface GuideRepository {
|
||||||
getAssetBaseUrl(): string
|
getAssetBaseUrl(): string
|
||||||
getFloors(): Promise<MuseumFloor[]>
|
getFloors(): Promise<MuseumFloor[]>
|
||||||
@@ -245,16 +262,38 @@ export class SgsSdkGuideRepository implements GuideRepository {
|
|||||||
const indoorFloors = manifest.floors.filter(isIndoorNavigableFloor)
|
const indoorFloors = manifest.floors.filter(isIndoorNavigableFloor)
|
||||||
const indoorFloorIds = new Set(indoorFloors.map((floor) => String(floor.floorId)))
|
const indoorFloorIds = new Set(indoorFloors.map((floor) => String(floor.floorId)))
|
||||||
this.floorAliases = buildSgsFloorAliases(indoorFloors)
|
this.floorAliases = buildSgsFloorAliases(indoorFloors)
|
||||||
const poiGroups = await Promise.all(
|
const floorDataGroups = await Promise.all(
|
||||||
indoorFloors.map((floor) => this.provider.getFloorPois(String(floor.floorId)))
|
indoorFloors.map(async (floor) => {
|
||||||
|
const floorId = String(floor.floorId)
|
||||||
|
const [pois, spaces, navigablePlaces] = await Promise.all([
|
||||||
|
this.provider.getFloorPois(floorId).catch(() => []),
|
||||||
|
this.provider.getFloorSpaces(floorId).catch(() => []),
|
||||||
|
this.provider.getNavigablePlaces(floorId).catch(() => [])
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
floorId,
|
||||||
|
pois,
|
||||||
|
hallPois: toMuseumHallPoisFromSgs(spaces, navigablePlaces, manifest.floors, floorId)
|
||||||
|
}
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
this.poiCache = poiGroups
|
const ordinaryPois = floorDataGroups
|
||||||
.flat()
|
.flatMap((group) => group.pois)
|
||||||
.map((poi) => toMuseumPoiFromSgs(poi, manifest.floors))
|
.map((poi) => toMuseumPoiFromSgs(poi, manifest.floors))
|
||||||
.filter((poi) => poi.id && poi.name)
|
.filter((poi) => poi.id && poi.name)
|
||||||
.filter((poi) => indoorFloorIds.has(String(poi.floorId)) || isPoiOnIndoorNavigableFloor(poi))
|
.filter((poi) => indoorFloorIds.has(String(poi.floorId)) || isPoiOnIndoorNavigableFloor(poi))
|
||||||
|
|
||||||
|
const hallPois = floorDataGroups
|
||||||
|
.flatMap((group) => group.hallPois)
|
||||||
|
.filter((poi) => indoorFloorIds.has(String(poi.floorId)) || isPoiOnIndoorNavigableFloor(poi))
|
||||||
|
|
||||||
|
this.poiCache = dedupePoisById([
|
||||||
|
...hallPois,
|
||||||
|
...ordinaryPois
|
||||||
|
])
|
||||||
|
|
||||||
return this.poiCache
|
return this.poiCache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -136,6 +136,10 @@ const createUnavailableReadiness = (message: string): GuideRouteReadiness => ({
|
|||||||
requiredData: ['route_graph', 'nav_data']
|
requiredData: ['route_graph', 'nav_data']
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const logRouteReadinessIssue = (message: string, payload: Record<string, unknown>) => {
|
||||||
|
console.error(`[RouteReadiness] ${message}`, payload)
|
||||||
|
}
|
||||||
|
|
||||||
export class StaticGuideRouteRepository implements GuideRouteRepository {
|
export class StaticGuideRouteRepository implements GuideRouteRepository {
|
||||||
private datasetCache: NavRouteDataset | null = null
|
private datasetCache: NavRouteDataset | null = null
|
||||||
private datasetRequest: Promise<NavRouteDataset> | null = null
|
private datasetRequest: Promise<NavRouteDataset> | null = null
|
||||||
@@ -156,11 +160,17 @@ export class StaticGuideRouteRepository implements GuideRouteRepository {
|
|||||||
try {
|
try {
|
||||||
const dataset = await this.loadDataset()
|
const dataset = await this.loadDataset()
|
||||||
if (!dataset.nodes.size || !dataset.edges.length || !dataset.anchorsByPoiId.size) {
|
if (!dataset.nodes.size || !dataset.edges.length || !dataset.anchorsByPoiId.size) {
|
||||||
return createUnavailableReadiness('路线示意数据不完整,当前暂不支持正式导航')
|
logRouteReadinessIssue('路线数据不完整', {
|
||||||
|
nodes: dataset.nodes.size,
|
||||||
|
edges: dataset.edges.length,
|
||||||
|
anchorsByPoiId: dataset.anchorsByPoiId.size
|
||||||
|
})
|
||||||
|
return createUnavailableReadiness('无法定位,当前仅支持点位位置预览')
|
||||||
}
|
}
|
||||||
|
|
||||||
return applyRouteReadinessGate(manifestReadiness)
|
return applyRouteReadinessGate(manifestReadiness)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('[RouteReadiness] 静态路线数据加载失败', error)
|
||||||
return createUnavailableReadiness(error instanceof Error ? error.message : '路线示意数据加载失败')
|
return createUnavailableReadiness(error instanceof Error ? error.message : '路线示意数据加载失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
toGuideRouteResultFromSgsPlan
|
toGuideRouteResultFromSgsPlan
|
||||||
} from '@/data/adapters/sgsSdkRouteAdapter'
|
} from '@/data/adapters/sgsSdkRouteAdapter'
|
||||||
import { stringifyId, formatSgsFloorLabel } from '@/data/adapters/sgsSdkGuideAdapter'
|
import { stringifyId, formatSgsFloorLabel } from '@/data/adapters/sgsSdkGuideAdapter'
|
||||||
import { toAppFloorId } from '@/services/sgs/SgsMapEventAdapter'
|
|
||||||
|
|
||||||
const createUnavailableReadiness = (message: string): GuideRouteReadiness => ({
|
const createUnavailableReadiness = (message: string): GuideRouteReadiness => ({
|
||||||
ready: false,
|
ready: false,
|
||||||
@@ -29,6 +28,10 @@ const createUnavailableReadiness = (message: string): GuideRouteReadiness => ({
|
|||||||
requiredData: ['route_graph', 'nav_data']
|
requiredData: ['route_graph', 'nav_data']
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const logSdkRouteReadinessIssue = (message: string, payload: Record<string, unknown>) => {
|
||||||
|
console.error(`[SDKRouteReadiness] ${message}`, payload)
|
||||||
|
}
|
||||||
|
|
||||||
const toNumber = (value: unknown) => (
|
const toNumber = (value: unknown) => (
|
||||||
Number.isFinite(Number(value)) ? Number(value) : undefined
|
Number.isFinite(Number(value)) ? Number(value) : undefined
|
||||||
)
|
)
|
||||||
@@ -51,11 +54,6 @@ const positionFromPlace = (place: SgsNavigablePlacePayload): [number, number, nu
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const numericId = (value: unknown) => {
|
|
||||||
const numberValue = Number(value)
|
|
||||||
return Number.isFinite(numberValue) ? numberValue : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SdkGuideRouteRepository {
|
export class SdkGuideRouteRepository {
|
||||||
private targetsCache: GuideRouteTarget[] | null = null
|
private targetsCache: GuideRouteTarget[] | null = null
|
||||||
|
|
||||||
@@ -84,7 +82,19 @@ export class SdkGuideRouteRepository {
|
|||||||
|
|
||||||
// 没有楼层有路线数据时不可用
|
// 没有楼层有路线数据时不可用
|
||||||
if (readyFloors.length === 0) {
|
if (readyFloors.length === 0) {
|
||||||
return createUnavailableReadiness('SDK 路线规划数据不完整,当前暂不支持正式导航')
|
logSdkRouteReadinessIssue('SDK 路线数据不完整', {
|
||||||
|
floorCount: floors.length,
|
||||||
|
readyFloorCount: readyFloors.length,
|
||||||
|
floors: floors.map((floor) => ({
|
||||||
|
floorId: floor.floorId,
|
||||||
|
floorName: floor.floorName,
|
||||||
|
floorCode: floor.floorCode,
|
||||||
|
routePlanningReady: floor.routePlanningReady,
|
||||||
|
routeNodeCount: floor.routeNodeCount,
|
||||||
|
routeEdgeCount: floor.routeEdgeCount
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
return createUnavailableReadiness('无法定位,当前仅支持点位位置预览')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 收集未就绪的楼层
|
// 收集未就绪的楼层
|
||||||
@@ -97,19 +107,20 @@ export class SdkGuideRouteRepository {
|
|||||||
.join('、')
|
.join('、')
|
||||||
return {
|
return {
|
||||||
ready: true,
|
ready: true,
|
||||||
message: `部分楼层路线数据未就绪:${floorLabels},已就绪楼层可正常使用导览功能`,
|
message: `部分楼层位置关系数据未就绪:${floorLabels},可查看已就绪楼层的位置关系`,
|
||||||
requiredData: []
|
requiredData: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ready: true,
|
ready: true,
|
||||||
message: 'SDK 路线数据已就绪',
|
message: 'SDK 位置关系数据已接入,可进行位置预览',
|
||||||
requiredData: []
|
requiredData: []
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('[SDKRouteReadiness] 读取 SDK 路线状态失败', error)
|
||||||
return createUnavailableReadiness(
|
return createUnavailableReadiness(
|
||||||
error instanceof Error ? error.message : 'SDK 路线数据加载失败'
|
error instanceof Error ? error.message : 'SDK 位置关系数据加载失败'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,8 +159,8 @@ export class SdkGuideRouteRepository {
|
|||||||
targets.push({
|
targets.push({
|
||||||
poiId: placeId,
|
poiId: placeId,
|
||||||
name: placeName,
|
name: placeName,
|
||||||
// 展示用标准化的 floorId(如 L1)
|
// 保留后端原始 floorId,确保与 SDK 模型楼层和 ThreeMap 当前楼层一致。
|
||||||
floorId: toAppFloorId(place.floorId ?? rawFloorId),
|
floorId: stringifyId(place.floorId ?? rawFloorId),
|
||||||
floorLabel,
|
floorLabel,
|
||||||
categoryLabel,
|
categoryLabel,
|
||||||
positionGltf,
|
positionGltf,
|
||||||
@@ -188,7 +199,7 @@ export class SdkGuideRouteRepository {
|
|||||||
const endTarget = targets.find((target) => target.poiId === endPoiId)
|
const endTarget = targets.find((target) => target.poiId === endPoiId)
|
||||||
|
|
||||||
if (!startTarget || !endTarget) {
|
if (!startTarget || !endTarget) {
|
||||||
throw new GuideRouteError('ROUTE_TARGET_MISSING', '起点或终点不在 SDK 可导航目的地列表中')
|
throw new GuideRouteError('ROUTE_TARGET_MISSING', '起点或终点不在 SDK 可预览目的地列表中')
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -196,24 +207,15 @@ export class SdkGuideRouteRepository {
|
|||||||
throw new GuideRouteError('ROUTE_TARGET_UNANCHORED', '起点或终点缺少可规划坐标')
|
throw new GuideRouteError('ROUTE_TARGET_UNANCHORED', '起点或终点缺少可规划坐标')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取原始 floorId 用于 API 调用
|
|
||||||
const diagnostics = await this.sdkApiProvider.getMapDiagnostics()
|
|
||||||
const floorIdMap = new Map<string, string>()
|
|
||||||
for (const floor of (diagnostics.floors || []).filter(isIndoorNavigableFloor)) {
|
|
||||||
floorIdMap.set(toAppFloorId(floor.floorId), stringifyId(floor.floorId))
|
|
||||||
}
|
|
||||||
const rawStartFloorId = floorIdMap.get(startTarget.floorId) || startTarget.floorId
|
|
||||||
const rawEndFloorId = floorIdMap.get(endTarget.floorId) || endTarget.floorId
|
|
||||||
|
|
||||||
const route = await this.sdkApiProvider.planRoute({
|
const route = await this.sdkApiProvider.planRoute({
|
||||||
startFloorId: rawStartFloorId,
|
startFloorId: startTarget.floorId,
|
||||||
startX: startTarget.positionGltf[0],
|
startX: startTarget.positionGltf[0],
|
||||||
startY: startTarget.positionGltf[2],
|
startY: startTarget.positionGltf[2],
|
||||||
startNodeId: numericId(startTarget.routeNodeId) || null,
|
startNodeId: startTarget.routeNodeId || null,
|
||||||
endFloorId: rawEndFloorId,
|
endFloorId: endTarget.floorId,
|
||||||
endX: endTarget.positionGltf[0],
|
endX: endTarget.positionGltf[0],
|
||||||
endY: endTarget.positionGltf[2],
|
endY: endTarget.positionGltf[2],
|
||||||
endNodeId: numericId(endTarget.routeNodeId) || null,
|
endNodeId: endTarget.routeNodeId || null,
|
||||||
wheelchair: 0
|
wheelchair: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -222,7 +224,7 @@ export class SdkGuideRouteRepository {
|
|||||||
if (error instanceof GuideRouteError) throw error
|
if (error instanceof GuideRouteError) throw error
|
||||||
throw new GuideRouteError(
|
throw new GuideRouteError(
|
||||||
'ROUTE_NOT_FOUND',
|
'ROUTE_NOT_FOUND',
|
||||||
error instanceof Error ? error.message : 'SGS 路线接口规划失败'
|
error instanceof Error ? error.message : 'SGS 位置关系接口生成失败'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* CSS 变量定义 */
|
/* CSS 变量定义 */
|
||||||
:root {
|
:root {
|
||||||
/* 字体 */
|
/* 字体 */
|
||||||
--museum-font-family: 'HarmonyOS Sans SC', 'HarmonyOS Sans', '鸿蒙黑体', 'Microsoft YaHei', 'PingFang SC', 'Noto Sans CJK SC', Arial, sans-serif;
|
--museum-font-family: '鸿蒙黑体', 'HarmonyOS Sans SC', 'HarmonyOS Sans', 'Microsoft YaHei', 'PingFang SC', 'Noto Sans CJK SC', Arial, sans-serif;
|
||||||
|
|
||||||
/* 主色调 */
|
/* 主色调 */
|
||||||
--museum-accent: #E0DF00;
|
--museum-accent: #E0DF00;
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export class GuideRouteUseCase {
|
|||||||
if (!readiness.ready) {
|
if (!readiness.ready) {
|
||||||
return {
|
return {
|
||||||
route: null,
|
route: null,
|
||||||
error: readiness.message || '当前暂不支持正式导航,可查看位置预览和路线示意'
|
error: readiness.message || '当前可查看位置预览和位置关系,暂不提供正式室内导航'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ export class GuideRouteUseCase {
|
|||||||
return error.message
|
return error.message
|
||||||
}
|
}
|
||||||
|
|
||||||
return error instanceof Error ? error.message : '室内导览位置关系生成失败'
|
return error instanceof Error ? error.message : '馆内位置关系生成失败'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export type GuideTopTab = 'guide' | 'explain'
|
|||||||
export const GUIDE_LOCATION_PREVIEW_STORAGE_KEY = 'museum-guide:last-location-preview-url'
|
export const GUIDE_LOCATION_PREVIEW_STORAGE_KEY = 'museum-guide:last-location-preview-url'
|
||||||
|
|
||||||
export const GUIDE_TOP_TABS: Array<{ id: GuideTopTab; label: string }> = [
|
export const GUIDE_TOP_TABS: Array<{ id: GuideTopTab; label: string }> = [
|
||||||
{ id: 'guide', label: '导览' },
|
{ id: 'guide', label: '馆内' },
|
||||||
{ id: 'explain', label: '讲解' }
|
{ id: 'explain', label: '讲解' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export interface ExplainDetailPageViewModel {
|
|||||||
subtitle?: string
|
subtitle?: string
|
||||||
contentType: ExplainContentType
|
contentType: ExplainContentType
|
||||||
coverImages: string[]
|
coverImages: string[]
|
||||||
|
hallId?: string
|
||||||
hallName?: string
|
hallName?: string
|
||||||
floorLabel?: string
|
floorLabel?: string
|
||||||
summary: string
|
summary: string
|
||||||
@@ -144,6 +145,7 @@ export const toExplainDetailPageViewModel = (exhibit: MuseumExhibit): ExplainDet
|
|||||||
subtitle: buildSubtitle(exhibit),
|
subtitle: buildSubtitle(exhibit),
|
||||||
contentType: contentTypeFor(exhibit),
|
contentType: contentTypeFor(exhibit),
|
||||||
coverImages: [exhibit.image || '/static/exhibit-placeholder.jpg'].filter(Boolean),
|
coverImages: [exhibit.image || '/static/exhibit-placeholder.jpg'].filter(Boolean),
|
||||||
|
hallId: exhibit.hallId,
|
||||||
hallName: exhibit.hallName,
|
hallName: exhibit.hallName,
|
||||||
floorLabel: exhibit.floorLabel,
|
floorLabel: exhibit.floorLabel,
|
||||||
summary: exhibit.guideText || exhibit.description || '该展项暂无讲解文稿。',
|
summary: exhibit.guideText || exhibit.description || '该展项暂无讲解文稿。',
|
||||||
|
|||||||
Reference in New Issue
Block a user