You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

779 lines
16 KiB

  1. <template>
  2. <view class="main">
  3. <!-- 顶部状态栏占位 -->
  4. <view class="top" :style="{ height: iSMT + 'px' }"></view>
  5. <!-- 固定头部 -->
  6. <view class="header_fixed" :style="{ top: iSMT + 'px' }">
  7. <view class="header_content">
  8. <view class="header_input_wrapper">
  9. <image class="search_icon" src="/static/marketSituation-image/search.png" mode="" @click="onSearchClick"></image>
  10. <input class="header_input" type="text" placeholder="搜索"
  11. placeholder-style="color: #A6A6A6; font-size: 22rpx;" v-model="searchValue"
  12. @input="onSearchInput" @confirm="onSearchConfirm" />
  13. </view>
  14. <view class="header_icons">
  15. <view class="header_icon" @click="selected">
  16. <image src="/static/marketSituation-image/mySeclected.png" mode=""></image>
  17. </view>
  18. <view class="header_icon" @click="history">
  19. <image src="/static/marketSituation-image/history.png" mode=""></image>
  20. </view>
  21. </view>
  22. </view>
  23. <view class="channel_li" v-if="channelData.length > 0">
  24. <scroll-view class="channel_wrap" scroll-x="true" :scroll-into-view="scrollToView"
  25. :scroll-with-animation="true" show-scrollbar="false">
  26. <view class="channel_innerWrap">
  27. <view v-for="(item, index) in channelData" :key="item.id" :id="'nav' + item.id"
  28. :class="['channel_item', index === pageIndex ? 'active' : '']" @click="navClick(index)">
  29. <text class="channel_text">{{ item.title }}</text>
  30. <view v-if="index === pageIndex" class="active_indicator"></view>
  31. </view>
  32. </view>
  33. </scroll-view>
  34. <view class="scroll_indicator" @click="channel_more">
  35. <image src="/static/marketSituation-image/menu.png" mode="aspectFit"></image>
  36. </view>
  37. </view>
  38. </view>
  39. <!-- 可滚动内容区域 -->
  40. <scroll-view class="content_scroll" scroll-y="true" :style="{ top: contentTopPosition + 'px' }">
  41. <view class="content">
  42. <view class="map">
  43. <image src="/static/marketSituation-image/map.png" mode="widthFix"></image>
  44. </view>
  45. <view class="global_index">
  46. <view class="global_index_title">
  47. {{ $t('marketSituation.globalIndex') }}
  48. </view>
  49. <view class="global_index_more">
  50. <text>{{ $t('marketSituation.globalIndexMore') }}</text>
  51. <image src="/static/marketSituation-image/more.png" mode="aspectFit"></image>
  52. </view>
  53. </view>
  54. <!-- 卡片网格 -->
  55. <view class="cards_grid">
  56. <view v-for="(card, index) in cardData" :key="index" class="card_item">
  57. <IndexCard
  58. :flagIcon="card.flagIcon"
  59. :indexName="card.indexName"
  60. :currentPrice="card.currentPrice"
  61. :changeAmount="card.changeAmount"
  62. :changePercent="card.changePercent"
  63. :isRising="card.isRising"
  64. />
  65. </view>
  66. </view>
  67. <!-- 底部安全区域防止被导航栏遮挡 -->
  68. <view class="bottom_safe_area"></view>
  69. </view>
  70. </scroll-view>
  71. </view>
  72. <footerBar class="static-footer" :type="type"></footerBar>
  73. <!-- 更多tab弹窗 -->
  74. <view v-if="showCountryModal" class="modal_overlay" @click="closeModal">
  75. <view class="modal_content" @click.stop>
  76. <view class="modal_header">
  77. <text class="modal_title">全部栏目</text>
  78. <view class="modal_close" @click="closeModal">
  79. <text>×</text>
  80. </view>
  81. </view>
  82. <view class="modal_body">
  83. <view class="country_grid">
  84. <view v-for="(country, index) in countryList" :key="index"
  85. :class="['country_item', selectedCountry === country ? 'selected' : '']"
  86. @click="selectCountry(country)">
  87. <text class="country_text">{{ country }}</text>
  88. </view>
  89. </view>
  90. </view>
  91. </view>
  92. </view>
  93. </template>
  94. <script setup>
  95. import { ref, onMounted, watch, nextTick, computed } from 'vue'
  96. import footerBar from '../../components/footerBar.vue'
  97. import IndexCard from '../../components/IndexCard.vue'
  98. const type = ref('marketSituation')
  99. const iSMT = ref(0)
  100. const searchValue = ref('')
  101. const contentHeight = ref(0)
  102. const headerHeight = ref(0) // 动态计算的header高度
  103. // Tab 栏相关数据
  104. const channelData = ref([
  105. { id: 1, title: '概况' },
  106. { id: 2, title: '新加坡' },
  107. { id: 3, title: '马来西亚' },
  108. { id: 4, title: '印度尼西亚' },
  109. { id: 5, title: '美国' },
  110. { id: 6, title: '中国香港' },
  111. { id: 7, title: '泰国' },
  112. { id: 8, title: '中国' },
  113. { id: 9, title: '加拿大' },
  114. { id: 10, title: '越南' },
  115. { id: 11, title: '外汇' },
  116. { id: 12, title: '贵金属' },
  117. ])
  118. const pageIndex = ref(0)
  119. const scrollToView = ref('')
  120. // 计算属性:精准计算content区域的top值
  121. const contentTopPosition = computed(() => {
  122. const statusBarHeight = iSMT.value || 0
  123. const currentHeaderHeight = headerHeight.value > 0 ? headerHeight.value : 140
  124. return statusBarHeight + currentHeaderHeight
  125. })
  126. // 弹窗相关数据
  127. const showCountryModal = ref(false)
  128. const selectedCountry = ref('概况')
  129. const countryList = ref([
  130. '概况', '新加坡', '马来西亚', '印度尼西亚', '美国', '中国香港',
  131. '泰国', '中国', '加拿大', '越南', '外汇', '贵金属'
  132. ])
  133. // 卡片数据
  134. const cardData = ref([
  135. {
  136. flagIcon: '🇺🇸',
  137. indexName: '道琼斯',
  138. currentPrice: '45757.90',
  139. changeAmount: '-125.22',
  140. changePercent: '-0.27%',
  141. isRising: false
  142. },
  143. {
  144. flagIcon: '🇺🇸',
  145. indexName: '纳斯达克',
  146. currentPrice: '22333.96',
  147. changeAmount: '+125.22',
  148. changePercent: '+0.47%',
  149. isRising: true
  150. },
  151. {
  152. flagIcon: '🇺🇸',
  153. indexName: '标普500',
  154. currentPrice: '6606.08',
  155. changeAmount: '+125.22',
  156. changePercent: '+0.27%',
  157. isRising: true
  158. },
  159. {
  160. flagIcon: '🇨🇳',
  161. indexName: '上证指数',
  162. currentPrice: '3333.96',
  163. changeAmount: '+125.22',
  164. changePercent: '+0.27%',
  165. isRising: true
  166. },
  167. {
  168. flagIcon: '🇨🇳',
  169. indexName: '科创50',
  170. currentPrice: '757.90',
  171. changeAmount: '-25.22',
  172. changePercent: '-0.27%',
  173. isRising: false
  174. },
  175. {
  176. flagIcon: '🇭🇰',
  177. indexName: '恒生指数',
  178. currentPrice: '19757.90',
  179. changeAmount: '-125.22',
  180. changePercent: '-0.63%',
  181. isRising: false
  182. },
  183. {
  184. flagIcon: '🇸🇬',
  185. indexName: '道琼斯',
  186. currentPrice: '3757.90',
  187. changeAmount: '+85.22',
  188. changePercent: '+2.31%',
  189. isRising: true
  190. },
  191. {
  192. flagIcon: '🇲🇾',
  193. indexName: '纳斯达克',
  194. currentPrice: '1657.90',
  195. changeAmount: '-15.22',
  196. changePercent: '-0.91%',
  197. isRising: false
  198. },
  199. {
  200. flagIcon: '🇹🇭',
  201. indexName: '标普500',
  202. currentPrice: '1457.90',
  203. changeAmount: '+35.22',
  204. changePercent: '+2.48%',
  205. isRising: true
  206. }
  207. ])
  208. // 搜索输入事件
  209. const onSearchInput = (e) => {
  210. searchValue.value = e.detail.value
  211. }
  212. // 搜索确认事件
  213. const onSearchConfirm = (e) => {
  214. console.log('搜索内容:', e.detail.value)
  215. // 这里可以添加搜索逻辑
  216. performSearch(e.detail.value)
  217. }
  218. // 搜索图标点击事件
  219. const onSearchClick = () => {
  220. if (searchValue.value.trim()) {
  221. performSearch(searchValue.value)
  222. }
  223. }
  224. // 执行搜索
  225. const performSearch = (keyword) => {
  226. if (!keyword.trim()) {
  227. uni.showToast({
  228. title: '请输入搜索内容',
  229. icon: 'none'
  230. })
  231. return
  232. }
  233. uni.showToast({
  234. title: `搜索: ${keyword}`,
  235. icon: 'none'
  236. })
  237. // 这里添加实际的搜索逻辑
  238. }
  239. // 我的收藏点击事件
  240. const selected = () => {
  241. uni.showToast({
  242. title: '我的收藏',
  243. icon: 'none'
  244. })
  245. // 这里可以跳转到收藏页面
  246. }
  247. // 历史记录点击事件
  248. const history = () => {
  249. uni.showToast({
  250. title: '历史记录',
  251. icon: 'none'
  252. })
  253. // 这里可以跳转到历史页面
  254. }
  255. // Tab 栏点击事件
  256. const navClick = (index) => {
  257. pageIndex.value = index
  258. const currentItem = channelData.value[index]
  259. scrollToView.value = 'nav' + currentItem.id
  260. // 同步更新弹窗中的选中状态
  261. selectedCountry.value = currentItem.title
  262. uni.showToast({
  263. title: `切换到: ${currentItem.title}`,
  264. icon: 'none'
  265. })
  266. // 这里可以添加切换 tab 后的数据加载逻辑
  267. console.log('当前选中的 tab:', currentItem)
  268. }
  269. // 更多选项点击事件
  270. const channel_more = () => {
  271. showCountryModal.value = true
  272. }
  273. // 选择国家
  274. const selectCountry = (country) => {
  275. selectedCountry.value = country
  276. // 查找对应的tab索引
  277. const targetIndex = channelData.value.findIndex(item => item.title === country)
  278. if (targetIndex !== -1) {
  279. // 同步更新页面tab
  280. pageIndex.value = targetIndex
  281. const currentItem = channelData.value[targetIndex]
  282. scrollToView.value = 'nav' + currentItem.id
  283. console.log('选中了:' + country + ',同步到tab索引:' + targetIndex)
  284. uni.showToast({
  285. title: '已切换到:' + country,
  286. icon: 'none',
  287. duration: 2000
  288. })
  289. } else {
  290. // 如果是"概况"或其他特殊选项,默认切换到第一个tab
  291. if (country === '概况' || country === '全部') {
  292. pageIndex.value = 0
  293. scrollToView.value = 'nav' + channelData.value[0].id
  294. }
  295. console.log('选中了:' + country)
  296. uni.showToast({
  297. title: '已选择:' + country,
  298. icon: 'none',
  299. duration: 2000
  300. })
  301. }
  302. // 这里可以添加切换到对应国家/地区数据的逻辑
  303. // 例如:loadMarketData(country)
  304. closeModal()
  305. }
  306. // 关闭弹窗
  307. const closeModal = () => {
  308. showCountryModal.value = false
  309. }
  310. onMounted(() => {
  311. // 状态栏高度
  312. iSMT.value = uni.getSystemInfoSync().statusBarHeight;
  313. // 初始化 tab 栏
  314. if (channelData.value.length > 0) {
  315. pageIndex.value = 0
  316. scrollToView.value = 'nav' + channelData.value[0].id
  317. }
  318. // 确保DOM渲染完成后再查询高度
  319. nextTick(() => {
  320. // 动态计算header实际高度
  321. uni.createSelectorQuery().select('.header_fixed').boundingClientRect((rect) => {
  322. if (rect) {
  323. headerHeight.value = rect.height
  324. console.log('Header实际高度:', headerHeight.value, 'px')
  325. }
  326. }).exec()
  327. })
  328. })
  329. // 监听headerHeight变化,重新计算contentHeight
  330. watch(headerHeight, (newHeight) => {
  331. if (newHeight > 0) {
  332. const systemInfo = uni.getSystemInfoSync()
  333. const windowHeight = systemInfo.windowHeight
  334. const statusBarHeight = systemInfo.statusBarHeight || 0
  335. const footerHeight = 100
  336. contentHeight.value = windowHeight - statusBarHeight - newHeight - footerHeight
  337. console.log('重新计算contentHeight:', contentHeight.value)
  338. }
  339. })
  340. </script>
  341. <style scoped>
  342. /* 状态栏占位 */
  343. .top {
  344. position: fixed;
  345. top: 0;
  346. left: 0;
  347. right: 0;
  348. z-index: 1001;
  349. background-color: #ffffff;
  350. }
  351. /* 固定头部样式 */
  352. .header_fixed {
  353. position: fixed;
  354. left: 0;
  355. right: 0;
  356. z-index: 1000;
  357. background-color: #ffffff;
  358. padding: 20rpx 0 0 0;
  359. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
  360. }
  361. /* 可滚动内容区域 */
  362. .content_scroll {
  363. position: fixed;
  364. left: 0;
  365. right: 0;
  366. bottom: 100rpx; /* 底部导航栏高度 */
  367. overflow-y: auto;
  368. }
  369. .header_content {
  370. display: flex;
  371. align-items: center;
  372. justify-content: space-between;
  373. height: 80rpx;
  374. padding: 0 20rpx;
  375. margin-bottom: 10rpx;
  376. }
  377. .header_input_wrapper {
  378. display: flex;
  379. align-items: center;
  380. width: 100%;
  381. margin: 0 20rpx 0 0;
  382. height: 70rpx;
  383. border-radius: 35rpx;
  384. background-color: #ffffff;
  385. border: 1rpx solid #e9ecef;
  386. padding: 0 80rpx 0 30rpx;
  387. font-size: 28rpx;
  388. color: #5c5c5c;
  389. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
  390. }
  391. .search_icon {
  392. width: 40rpx;
  393. height: 40rpx;
  394. opacity: 0.6;
  395. }
  396. .header_input {
  397. margin-left: 10rpx;
  398. }
  399. .header_icons {
  400. display: flex;
  401. align-items: center;
  402. gap: 15rpx;
  403. }
  404. .header_icon {
  405. width: 40rpx;
  406. height: 40rpx;
  407. display: flex;
  408. align-items: center;
  409. justify-content: center;
  410. }
  411. .header_icon image {
  412. width: 40rpx;
  413. height: 40rpx;
  414. }
  415. /* Tab 栏样式 */
  416. .channel_li {
  417. display: flex;
  418. align-items: center;
  419. height: 80rpx;
  420. background-color: #ffffff;
  421. border-bottom: 1rpx solid #f0f0f0;
  422. }
  423. .channel_wrap {
  424. width: calc(100% - 60rpx);
  425. height: 100%;
  426. overflow: hidden;
  427. flex-shrink: 0;
  428. }
  429. .channel_innerWrap {
  430. display: flex;
  431. align-items: center;
  432. height: 100%;
  433. padding: 0 20rpx;
  434. white-space: nowrap;
  435. }
  436. .channel_item {
  437. position: relative;
  438. display: flex;
  439. flex-direction: column;
  440. align-items: center;
  441. justify-content: center;
  442. height: 60rpx;
  443. padding: 0 20rpx;
  444. border-radius: 30rpx;
  445. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  446. cursor: pointer;
  447. white-space: nowrap;
  448. flex-shrink: 0;
  449. }
  450. .channel_item:active {
  451. transform: scale(0.98);
  452. }
  453. .channel_item.active {
  454. color: #333;
  455. font-weight: bold;
  456. }
  457. .channel_text {
  458. font-size: 28rpx;
  459. font-weight: 500;
  460. color: #666666;
  461. transition: color 0.3s ease;
  462. white-space: nowrap;
  463. }
  464. .channel_item.active .channel_text {
  465. color: #333333;
  466. font-weight: 400;
  467. z-index: 2;
  468. }
  469. .active_indicator {
  470. position: absolute;
  471. left: 50%;
  472. top: 60%;
  473. transform: translateX(-45%);
  474. width: calc(100% - 20rpx);
  475. min-width: 40rpx;
  476. max-width: 120rpx;
  477. height: 8rpx;
  478. background-image: url('/static/marketSituation-image/bg.png');
  479. background-size: cover;
  480. background-position: center;
  481. background-repeat: no-repeat;
  482. animation: slideIn 0.1s ease;
  483. border-radius: 8rpx;
  484. z-index: 1;
  485. }
  486. @keyframes slideIn {
  487. from {
  488. width: 0;
  489. opacity: 0;
  490. }
  491. to {
  492. width: 40rpx;
  493. opacity: 1;
  494. }
  495. }
  496. .scroll_indicator {
  497. border-left: 1rpx solid #b6b6b6;
  498. display: flex;
  499. align-items: center;
  500. justify-content: center;
  501. width: 60rpx;
  502. height: 30rpx;
  503. background-color: #ffffff;
  504. flex-shrink: 0;
  505. }
  506. .scroll_indicator image {
  507. width: 20rpx;
  508. height: 20rpx;
  509. opacity: 0.5;
  510. }
  511. .content {
  512. margin-top: 20rpx;
  513. background-color: white;
  514. }
  515. .map {
  516. width: calc(100% - 60rpx);
  517. margin: 0 30rpx;
  518. display: flex;
  519. align-items: center;
  520. justify-content: center;
  521. background-color: #F3F3F3;
  522. border-radius: 30rpx;
  523. border: 1rpx solid #E0E0E0;
  524. padding: 30rpx 20rpx;
  525. box-sizing: border-box;
  526. /* 设置最小高度保护,但允许内容撑开 */
  527. min-height: 200rpx;
  528. }
  529. .map image {
  530. width: 100%;
  531. height: auto;
  532. max-width: 100%;
  533. display: block;
  534. /* widthFix模式下,高度会自动按比例调整 */
  535. /* 设置最大高度避免图片过大 */
  536. max-height: 60vh;
  537. /* 添加平滑过渡效果 */
  538. transition: all 0.3s ease;
  539. max-height: 60vh;
  540. }
  541. /* 响应式优化 */
  542. @media screen and (max-width: 750rpx) {
  543. .map {
  544. margin: 0 20rpx;
  545. width: calc(100% - 40rpx);
  546. padding: 20rpx 15rpx;
  547. }
  548. }
  549. @media screen and (max-width: 480rpx) {
  550. .map {
  551. margin: 0 15rpx;
  552. width: calc(100% - 30rpx);
  553. padding: 15rpx 10rpx;
  554. }
  555. }
  556. .static-footer {
  557. position: fixed;
  558. bottom: 0;
  559. }
  560. /* 弹窗样式 */
  561. .modal_overlay {
  562. position: fixed;
  563. top: 0;
  564. left: 0;
  565. right: 0;
  566. bottom: 0;
  567. background-color: rgba(0, 0, 0, 0.5);
  568. display: flex;
  569. align-items: flex-end;
  570. z-index: 1000;
  571. }
  572. .modal_content {
  573. width: 100%;
  574. background-color: #fff;
  575. border-radius: 20rpx 20rpx 0 0;
  576. max-height: 80vh;
  577. overflow: hidden;
  578. }
  579. .modal_header {
  580. position: relative;
  581. display: flex;
  582. justify-content: center;
  583. align-items: center;
  584. padding: 30rpx 40rpx;
  585. border-bottom: 1rpx solid #f0f0f0;
  586. }
  587. .modal_title {
  588. font-size: 32rpx;
  589. font-weight: bold;
  590. color: #333333;
  591. text-align: center;
  592. }
  593. .modal_close {
  594. position: absolute;
  595. right: 40rpx;
  596. top: 50%;
  597. transform: translateY(-50%);
  598. width: 60rpx;
  599. height: 60rpx;
  600. display: flex;
  601. align-items: center;
  602. justify-content: center;
  603. font-size: 40rpx;
  604. color: #999;
  605. }
  606. .modal_body {
  607. padding: 40rpx;
  608. }
  609. .country_grid {
  610. display: grid;
  611. grid-template-columns: 1fr 1fr;
  612. gap: 20rpx;
  613. }
  614. .country_item {
  615. padding: 24rpx 30rpx;
  616. border-radius: 12rpx;
  617. background-color: #f8f8f8;
  618. display: flex;
  619. align-items: center;
  620. justify-content: center;
  621. transition: all 0.3s ease;
  622. }
  623. .country_item.selected {
  624. background-color: #ff4444;
  625. color: #fff;
  626. }
  627. .country_text {
  628. font-size: 28rpx;
  629. color: #333;
  630. }
  631. .country_item.selected .country_text {
  632. color: #fff;
  633. }
  634. .global_index{
  635. margin: 30rpx 20rpx 0 20rpx;
  636. display: flex;
  637. justify-content: space-between;
  638. }
  639. .global_index_title{
  640. margin-left: 20rpx;
  641. font-size: 40rpx;
  642. font-weight: 100;
  643. color: #333333;
  644. align-items: center;
  645. }
  646. .global_index_more{
  647. display: flex;
  648. gap: 10rpx;
  649. font-size: 28rpx;
  650. color: #333333;
  651. align-items: center;
  652. }
  653. .global_index_more image{
  654. width: 40rpx;
  655. height: 40rpx;
  656. align-items: center;
  657. }
  658. /* 卡片网格样式 */
  659. .cards_grid {
  660. display: grid;
  661. grid-template-columns: repeat(3, 1fr);
  662. margin: 0;
  663. box-sizing: border-box;
  664. width: 100%;
  665. }
  666. .card_item {
  667. width: 100%;
  668. box-sizing: border-box;
  669. min-width: 0; /* 防止内容溢出 */
  670. }
  671. /* 响应式布局 - 小屏幕时改为两列 */
  672. @media (max-width: 600rpx) {
  673. .cards_grid {
  674. grid-template-columns: repeat(2, 1fr);
  675. padding: 30rpx 20rpx;
  676. }
  677. }
  678. /* 超小屏幕时改为单列 */
  679. @media (max-width: 400rpx) {
  680. .cards_grid {
  681. grid-template-columns: 1fr;
  682. padding: 30rpx 20rpx;
  683. }
  684. }
  685. /* 底部安全区域 */
  686. .bottom_safe_area {
  687. height: 40rpx;
  688. background-color: transparent;
  689. }
  690. /* 主容器样式调整 */
  691. .main {
  692. position: relative;
  693. height: 100vh;
  694. overflow: hidden;
  695. background-color: white;
  696. }
  697. </style>