index.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. import React, { useCallback, useEffect, useState, useRef } from 'react'
  2. import styles from './index.module.scss'
  3. import A21vrOff from './components/A21vrOff'
  4. import A22hotListWrap from './components/A22hotListWrap'
  5. import A23pinTop from './components/A23pinTop'
  6. import A24drawerList from './components/A24drawerList'
  7. import { useSelector } from 'react-redux'
  8. import { RootState } from '@/store'
  9. import classNames from 'classnames'
  10. import A3hotList from '../A3hotList'
  11. import { message } from 'antd'
  12. // import isMobile from '@/utils/isMobile'
  13. const imgArrTemp = ['like.png', 'likeAc.png']
  14. const imgArr = imgArrTemp.map(item => require(`@/assets/img/${item}`))
  15. const SCENE_CODE = 'SG-WVqbTpbkApi_96'
  16. const LIKE_STORAGE_KEY = `count_like_${SCENE_CODE}`
  17. const VIEW_STORAGE_KEY = `count_view_${SCENE_CODE}`
  18. const DEFAULT_LIKE_NUM = 100
  19. const DEFAULT_VIEW_NUM = 300
  20. const getLocalCount = (key: string, fallback: number) => {
  21. try {
  22. const raw = window.localStorage.getItem(key)
  23. if (raw == null) return fallback
  24. const n = Number(raw)
  25. return Number.isFinite(n) ? n : fallback
  26. } catch {
  27. return fallback
  28. }
  29. }
  30. const setLocalCount = (key: string, value: number) => {
  31. try {
  32. window.localStorage.setItem(key, String(value))
  33. } catch {
  34. // 隐私模式等无法写入时忽略
  35. }
  36. }
  37. const pickCount = (value: unknown) => (typeof value === 'number' && Number.isFinite(value) ? value : null)
  38. const fetchCountApi = async (url: string) => {
  39. const response = await fetch(url)
  40. if (!response.ok) throw new Error('count api failed')
  41. return response.json()
  42. }
  43. const bumpLocalView = () => {
  44. try {
  45. const stored = window.localStorage.getItem(VIEW_STORAGE_KEY)
  46. const next = stored == null ? DEFAULT_VIEW_NUM : getLocalCount(VIEW_STORAGE_KEY, DEFAULT_VIEW_NUM) + 1
  47. setLocalCount(VIEW_STORAGE_KEY, next)
  48. return next
  49. } catch {
  50. return DEFAULT_VIEW_NUM
  51. }
  52. }
  53. function A2main() {
  54. // 漫游的状态
  55. const { state3d } = useSelector((state: RootState) => state.three)
  56. // 点赞
  57. const [like, setLike] = useState(false)
  58. const [likeNum, setLikeNum] = useState(DEFAULT_LIKE_NUM)
  59. const [viewNum, setViewNum] = useState(DEFAULT_VIEW_NUM)
  60. const [isShowSection, setIsShowSection] = useState(false)
  61. useEffect(() => {
  62. fetchCountApi(`https://count.4dage.com/api/count/detail/${SCENE_CODE}`)
  63. .then(res => {
  64. const num = pickCount(res.data?.starSum)
  65. if (num != null) {
  66. setLikeNum(num)
  67. setLocalCount(LIKE_STORAGE_KEY, num)
  68. return
  69. }
  70. setLikeNum(getLocalCount(LIKE_STORAGE_KEY, DEFAULT_LIKE_NUM))
  71. })
  72. .catch(() => {
  73. setLikeNum(getLocalCount(LIKE_STORAGE_KEY, DEFAULT_LIKE_NUM))
  74. })
  75. fetchCountApi(`https://count.4dage.com/api/count/saveVisit/${SCENE_CODE}`)
  76. .then(res => {
  77. const num = pickCount(res.data?.visitSum)
  78. if (num != null) {
  79. setViewNum(num)
  80. setLocalCount(VIEW_STORAGE_KEY, num)
  81. return
  82. }
  83. setViewNum(bumpLocalView())
  84. })
  85. .catch(() => {
  86. setViewNum(bumpLocalView())
  87. })
  88. }, [])
  89. // 点击点赞
  90. const likeFu = useCallback(() => {
  91. if (like) return
  92. setLike(true)
  93. setTimeout(() => {
  94. const applyLocalLike = () => {
  95. setLikeNum(n => {
  96. const next = n + 1
  97. setLocalCount(LIKE_STORAGE_KEY, next)
  98. return next
  99. })
  100. setLike(false)
  101. }
  102. fetchCountApi(`https://count.4dage.com/api/count/saveStar/${SCENE_CODE}`)
  103. .then(res => {
  104. if (pickCount(res.data?.starSum) != null) {
  105. const num = pickCount(res.data.starSum)!
  106. setLikeNum(num)
  107. setLocalCount(LIKE_STORAGE_KEY, num)
  108. setLike(false)
  109. return
  110. }
  111. applyLocalLike()
  112. })
  113. .catch(applyLocalLike)
  114. }, 2000)
  115. }, [like])
  116. // 点击热点列表
  117. const [hotListShow, setHotListShow] = useState(false)
  118. // 点击分享
  119. const copyWithTextarea = (text: string) => {
  120. const textArea = document.createElement('textarea')
  121. textArea.value = text
  122. textArea.style.position = 'fixed'
  123. textArea.style.left = '-9999px'
  124. textArea.style.top = '-9999px'
  125. document.body.appendChild(textArea)
  126. textArea.select()
  127. const copyTXT = document.execCommand('copy')
  128. document.body.removeChild(textArea)
  129. return copyTXT
  130. }
  131. const handleShareClick = () => {
  132. if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
  133. navigator.clipboard.writeText(window.location.href)
  134. message.success('链接已复制到剪贴板')
  135. } else {
  136. copyWithTextarea(window.location.href)
  137. message.success('链接已复制到剪贴板')
  138. }
  139. }
  140. const sectionBoxRef = useRef<HTMLDivElement>(null);
  141. const [isActive, setIsActive] = useState(true)
  142. // 滚动到最左
  143. const scrollToLeft = () => {
  144. if (sectionBoxRef.current) {
  145. sectionBoxRef.current.scrollTo({
  146. left: 0,
  147. behavior: 'smooth'
  148. });
  149. setIsActive(true)
  150. }
  151. };
  152. // 滚动到最右
  153. const scrollToRight = () => {
  154. if (sectionBoxRef.current) {
  155. sectionBoxRef.current.scrollTo({
  156. left: sectionBoxRef.current.scrollWidth,
  157. behavior: 'smooth'
  158. });
  159. setIsActive(false)
  160. }
  161. };
  162. useEffect(() => {
  163. const sections = document.querySelectorAll('.section1, .section2, .section3, .section4, .section5')
  164. sections.forEach((section, index) => {
  165. section.setAttribute('data-index', index.toString());
  166. console.log(section)
  167. })
  168. }, [])
  169. return (
  170. <div className={styles.A2main} id='gui' style={{ display: 'none' }}>
  171. <A21vrOff />
  172. <A22hotListWrap />
  173. {/* 顶部介绍 */}
  174. <A23pinTop />
  175. {/* 主要修改的图标部分 */}
  176. <div className={classNames('pinBottom-container', state3d === 'panorama' ? '' : 'pinBottom-containerNo')}>
  177. <div className='pinBottom center'>
  178. <div id='view-controllers'></div>
  179. </div>
  180. <div className='pinBottom left'>
  181. <div className="logo"><img src={require('@/assets/img/logo.png')} alt="" /></div>
  182. <div id='gui-modes-map' className='ui-icon double active'>
  183. <div data-original-title='全景漫游' id='gui-modes-inside' rel='tooltip'>
  184. <img className='icon icon-inside' src={`images/inside${state3d === 'panorama' ? '_active' : ''}.png`} alt='' />
  185. {/* 鼠标移入 */}
  186. <div className='hoveImg'>全景漫游</div>
  187. </div>
  188. <div id='play' className='ui-icon' rel='tooltip' data-original-title='播放'>
  189. {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
  190. <a style={{ width: '100%', height: '100%' }}>
  191. <img src='images/play.png' alt='' />
  192. </a>
  193. {/* 鼠标移入 */}
  194. <div className='hoveImg'>自动漫游</div>
  195. </div>
  196. <div id='pause' className='ui-icon' rel='tooltip' style={{ display: 'none' }}>
  197. {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
  198. <a style={{ width: '100%', height: '100%' }}>
  199. <img rel='tooltip' src='images/pause.png' alt='' />
  200. </a>
  201. {/* 鼠标移入 */}
  202. <div className='hoveImg'>暂停漫游</div>
  203. </div>
  204. <div id='next' className='next desktop-only ui-icon wide' style={{ display: 'none' }}>
  205. {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
  206. <a>
  207. <i rel='tooltip' title='' className='icon icon-dpad-right' data-original-title='下一个'></i>
  208. </a>
  209. </div>
  210. <div data-original-title='导览' id='pullTab' rel='tooltip' title=''>
  211. <img className='icon icon-inside' src='images/auto.png' alt='' />
  212. {/* 鼠标移入 */}
  213. <div className='hoveImg'>导览列表</div>
  214. </div>
  215. {/* 原始热点列表---隐藏 */}
  216. <div data-original-title='热点列表' id='hotList' rel='tooltip' style={{ display: 'none' }}>
  217. <img className='icon icon-inside' src='images/hotlist.png' title='热点列表' alt='' />
  218. </div>
  219. {/* 新的热点列表 */}
  220. <div
  221. className='A2hotIcon'
  222. rel='tooltip'
  223. onClick={() => {
  224. window.player.director.stopTour()
  225. setHotListShow(true)
  226. // hiddenMap(true)
  227. }}
  228. >
  229. <img src='images/hotlist.png' alt='' />
  230. <div className='hoveImg'>热点列表</div>
  231. </div>
  232. <div data-original-title='迷你模型' id='gui-modes-dollhouse' rel='tooltip'>
  233. <img className='icon icon-inside' src={`images/dollhouse${state3d === 'dollhouse' ? '_active' : ''}.png`} alt='' />
  234. {/* 鼠标移入 */}
  235. <div className='hoveImg'>迷你模型</div>
  236. </div>
  237. <div data-original-title='俯视图' id='gui-modes-floorplan' rel='tooltip'>
  238. <img className='icon icon-inside' src={`images/floor${state3d === 'floorplan' ? '_active' : ''}.png`} alt='' />
  239. {/* 鼠标移入 */}
  240. <div className='hoveImg'>顶部视图</div>
  241. </div>
  242. {/* <div
  243. data-original-title='VR'
  244. id='vr'
  245. rel='tooltip'
  246. title=''
  247. style={{ display: 'none' }}
  248. >
  249. <img className='icon icon-inside' src='images/VR.png' title='VR' alt='' />
  250. </div> */}
  251. {/* <div
  252. data-original-title='消除外壳'
  253. id='gui-remove-face'
  254. rel='tooltip'
  255. title=''
  256. style={{ display: 'none', float: 'left' }}
  257. >
  258. <img className='icon icon-inside' src='images/face.jpg' alt='' />
  259. </div> */}
  260. </div>
  261. <img className='splitImg' src={require('../../assets/img/splitImg.png')} alt='' draggable='false' />
  262. <div className='rightViewContainer clearfix'>
  263. <div className='gui-floor'>
  264. <div className='gui-floor-title'></div>
  265. <div className='gui-floor-icon'>
  266. <span className='gui-floor-number'></span>
  267. </div>
  268. <div className='container'></div>
  269. </div>
  270. <div id='volume' className='ui-icon wide'>
  271. {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
  272. <a>
  273. <img src='images/Volume btn_on.png' alt='' />
  274. </a>
  275. <div className='hoveImg'>音乐开关</div>
  276. </div>
  277. {/* 地图 */}
  278. {/* <div
  279. className='A2_map_box'
  280. style={!isMobile() ? { display: 'none' } : {}}
  281. onClick={() => {
  282. if (!cad?.style.visibility) {
  283. hiddenMap(true)
  284. }
  285. const isShow = cad?.style.visibility === 'hidden'
  286. hiddenMap(!isShow)
  287. }}
  288. >
  289. <img src={require('@/assets/img/map.png')} alt='' />
  290. <div className='hoveImg'>地图</div>
  291. </div> */}
  292. {/* 点赞 */}
  293. <div className='likeBox' onClick={likeFu}>
  294. <img src={like ? imgArr[1] : imgArr[0]} alt='' />
  295. <div className='hoveImg'>点赞{likeNum}</div>
  296. <div className={classNames('likeMove', like ? 'likeMoveAc' : '')}>+1</div>
  297. </div>
  298. <div className='A2_share_box' onClick={handleShareClick}>
  299. <img src={require('@/assets/img/share.png')} alt='' />
  300. <div className='hoveImg'>分享</div>
  301. </div>
  302. <div className='A2_view_box'>
  303. <img src={require('@/assets/img/view.png')} alt='' />
  304. <div className='hoveImg'>浏览量{viewNum}</div>
  305. </div>
  306. <div id='vr' className='ui-icon wide hidden' style={{ display: 'none' }}>
  307. {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
  308. <a>
  309. <i rel='tooltip' title='{[{ VIEW_IN_VR }]}' className='icon icon-webvr'></i>
  310. </a>
  311. </div>
  312. <div id='sharing' className='ui-icon wide hidden' style={{ display: 'none' }}>
  313. {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
  314. <a>
  315. <i rel='tooltip' title='{[{ SOCIAL_SHARING }]}' className='icon icon-share'></i>
  316. </a>
  317. </div>
  318. <div id='gui-fullscreen' className='ui-icon wide' data-placement='top' rel='tooltip'>
  319. {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
  320. <a>
  321. <img src='images/full.png' alt='' />
  322. </a>
  323. {/* 鼠标移入 */}
  324. <div className='hoveImg'>全屏</div>
  325. </div>
  326. <div id='gui-fullscreen-exit' className='ui-icon wide' data-placement='top' rel='tooltip' style={{ display: 'none' }}>
  327. {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
  328. <a>
  329. <img src='images/fullx.png' alt='' />
  330. </a>
  331. {/* 鼠标移入 */}
  332. <div className='hoveImg'>关闭全屏</div>
  333. </div>
  334. <div className='pull-right terms terms2'>
  335. {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
  336. <a>{'{[{ TERMS }]}'}</a>
  337. </div>
  338. </div>
  339. </div>
  340. </div>
  341. <div
  342. className='sectionIndex'
  343. style={{display:'none'}}
  344. onClick={() => {
  345. setIsShowSection(true)
  346. // hiddenMap(true)
  347. }}
  348. >
  349. <img className='sectionIndexImg' src={require('../../assets/img/section.png')} alt='' draggable='false' />
  350. </div>
  351. <div className='section' style={{
  352. pointerEvents: isShowSection ? 'auto' : 'none',
  353. opacity: isShowSection ? '1' : '0'
  354. }}>
  355. {/* <div className="arrL" style={{ opacity: isActive ? '0.1' : '1', cursor: isActive ? 'not-allowed' : 'pointer' }} onClick={scrollToLeft}><img src={require('../../assets/img/preP.png')} alt="" /></div> */}
  356. {/* <div className="arrR" style={{ opacity: isActive ? '1' : '0.1', cursor: isActive ? 'pointer' : 'not-allowed' }} onClick={scrollToRight}><img src={require('../../assets/img/nextP.png')} alt="" /></div> */}
  357. <div className='sectionBox' ref={sectionBoxRef} onClick={() => setIsShowSection(false)}>
  358. <div className='section1 sectionCont' >
  359. </div>
  360. <div className='section2 sectionCont'>
  361. </div>
  362. <div className='section3 sectionCont'>
  363. </div>
  364. <div className='section4 sectionCont'>
  365. </div>
  366. <div className='section5 sectionCont'>
  367. </div>
  368. </div>
  369. <div
  370. className='sectionClose'
  371. onClick={() => {
  372. setIsShowSection(false)
  373. setIsActive(true)
  374. // hiddenMap(false)
  375. }}
  376. >
  377. <img src={require('../../assets/img/close.png')} alt='' />
  378. </div>
  379. </div>
  380. {/* 底部导览列表 */}
  381. <A24drawerList />
  382. {/* 热点列表 */}
  383. <A3hotList
  384. show={hotListShow}
  385. setShow={setHotListShow}
  386. closeFu={() => {
  387. setHotListShow(false)
  388. // hiddenMap(false)
  389. }}
  390. />
  391. </div >
  392. )
  393. }
  394. const MemoA2main = React.memo(A2main)
  395. export default MemoA2main