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.

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