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.

592 lines
13 KiB

4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
  1. <template>
  2. <view>
  3. <view class="main">
  4. <!-- 固定头部 -->
  5. <view class="header_fixed" :style="{ top: iSMT + 'px' }">
  6. <view class="header_content">
  7. <view class="header_input_wrapper">
  8. <image class="search_icon" src="/static/marketSituation-image/search.png" mode=""
  9. @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. <!-- 动态组件切换 -->
  42. <component :is="currentComponent" :countryId="currentChannelId" />
  43. </scroll-view>
  44. </view>
  45. <footerBar class="static-footer" :type="type"></footerBar>
  46. <!-- 更多tab弹窗 -->
  47. <view v-if="showCountryModal" class="modal_overlay" @click="closeModal">
  48. <view class="modal_content" @click.stop>
  49. <view class="modal_header">
  50. <text class="modal_title">全部栏目</text>
  51. <view class="modal_close" @click="closeModal">
  52. <text>×</text>
  53. </view>
  54. </view>
  55. <view class="modal_body">
  56. <view class="country_grid">
  57. <view v-for="(country, index) in countryList" :key="index"
  58. :class="['country_item', selectedCountry === country ? 'selected' : '']"
  59. @click="selectCountry(country)">
  60. <text class="country_text">{{ country }}</text>
  61. </view>
  62. </view>
  63. </view>
  64. </view>
  65. </view>
  66. </view>
  67. </template>
  68. <script setup>
  69. import { ref, onMounted, watch, nextTick, computed } from 'vue'
  70. import footerBar from './footerBar.vue'
  71. import forexMetals from './forexMetals.vue'
  72. import marketOverview from './marketOverview.vue'
  73. import countryMarket from './countryMarket.vue'
  74. const type = ref('marketSituation')
  75. const iSMT = ref(0)
  76. const searchValue = ref('')
  77. const contentHeight = ref(0)
  78. const headerHeight = ref(0) // 动态计算的header高度
  79. // Tab 栏相关数据
  80. const channelData = ref([
  81. { id: 1, title: '概况' },
  82. { id: 2, title: '新加坡' },
  83. { id: 3, title: '马来西亚' },
  84. { id: 4, title: '印度尼西亚' },
  85. { id: 5, title: '美国' },
  86. { id: 6, title: '中国香港' },
  87. { id: 7, title: '泰国' },
  88. { id: 8, title: '中国' },
  89. { id: 9, title: '加拿大' },
  90. { id: 10, title: '越南' },
  91. { id: 11, title: '外汇' },
  92. { id: 12, title: '贵金属' },
  93. ])
  94. const pageIndex = ref(0)
  95. const scrollToView = ref('')
  96. // 动态组件相关
  97. const currentChannelId = computed(() => {
  98. return channelData.value[pageIndex.value]?.id || 1
  99. })
  100. const currentComponent = computed(() => {
  101. const channelId = currentChannelId.value
  102. // 概况页面使用 MarketOverview 组件
  103. if (pageIndex.value === 0) {
  104. return marketOverview
  105. }
  106. // 外汇(id=11)和贵金属(id=12)使用 ForexMetals 组件
  107. else if (channelId === 11 || channelId === 12) {
  108. return forexMetals
  109. }
  110. // 其他国家/地区页面使用 CountryMarket 组件
  111. else {
  112. return countryMarket
  113. }
  114. })
  115. // 计算属性:精准计算content区域的top值
  116. const contentTopPosition = computed(() => {
  117. const statusBarHeight = iSMT.value || 0
  118. const currentHeaderHeight = headerHeight.value > 0 ? headerHeight.value : 140
  119. return statusBarHeight + currentHeaderHeight
  120. })
  121. // 弹窗相关数据
  122. const showCountryModal = ref(false)
  123. const selectedCountry = ref('概况')
  124. const countryList = ref([
  125. '概况', '新加坡', '马来西亚', '印度尼西亚', '美国', '中国香港',
  126. '泰国', '中国', '加拿大', '越南', '外汇', '贵金属'
  127. ])
  128. // 搜索输入事件
  129. const onSearchInput = (e) => {
  130. searchValue.value = e.detail.value
  131. }
  132. // 搜索确认事件
  133. const onSearchConfirm = (e) => {
  134. console.log('搜索内容:', e.detail.value)
  135. // 这里可以添加搜索逻辑
  136. performSearch(e.detail.value)
  137. }
  138. // 搜索图标点击事件
  139. const onSearchClick = () => {
  140. if (searchValue.value.trim()) {
  141. performSearch(searchValue.value)
  142. }
  143. }
  144. // 执行搜索
  145. const performSearch = (keyword) => {
  146. if (!keyword.trim()) {
  147. uni.showToast({
  148. title: '请输入搜索内容',
  149. icon: 'none'
  150. })
  151. return
  152. }
  153. uni.showToast({
  154. title: `搜索: ${keyword}`,
  155. icon: 'none'
  156. })
  157. // 这里添加实际的搜索逻辑
  158. }
  159. // 我的收藏点击事件
  160. const selected = () => {
  161. uni.showToast({
  162. title: '我的收藏',
  163. icon: 'none'
  164. })
  165. // 这里可以跳转到收藏页面
  166. }
  167. // 历史记录点击事件
  168. const history = () => {
  169. uni.showToast({
  170. title: '历史记录',
  171. icon: 'none'
  172. })
  173. // 这里可以跳转到历史页面
  174. }
  175. // Tab 栏点击事件
  176. const navClick = (index) => {
  177. pageIndex.value = index
  178. const currentItem = channelData.value[index]
  179. scrollToView.value = 'nav' + currentItem.id
  180. // 同步更新弹窗中的选中状态
  181. selectedCountry.value = currentItem.title
  182. uni.showToast({
  183. title: `切换到: ${currentItem.title}`,
  184. icon: 'none'
  185. })
  186. // 这里可以添加切换 tab 后的数据加载逻辑
  187. console.log('当前选中的 tab:', currentItem)
  188. }
  189. // 更多选项点击事件
  190. const channel_more = () => {
  191. showCountryModal.value = true
  192. }
  193. // 选择国家
  194. const selectCountry = (country) => {
  195. selectedCountry.value = country
  196. // 查找对应的tab索引
  197. const targetIndex = channelData.value.findIndex(item => item.title === country)
  198. if (targetIndex !== -1) {
  199. // 同步更新页面tab
  200. pageIndex.value = targetIndex
  201. const currentItem = channelData.value[targetIndex]
  202. scrollToView.value = 'nav' + currentItem.id
  203. console.log('选中了:' + country + ',同步到tab索引:' + targetIndex)
  204. uni.showToast({
  205. title: '已切换到:' + country,
  206. icon: 'none',
  207. duration: 2000
  208. })
  209. } else {
  210. // 如果是"概况"或其他特殊选项,默认切换到第一个tab
  211. if (country === '概况' || country === '全部') {
  212. pageIndex.value = 0
  213. scrollToView.value = 'nav' + channelData.value[0].id
  214. }
  215. console.log('选中了:' + country)
  216. uni.showToast({
  217. title: '已选择:' + country,
  218. icon: 'none',
  219. duration: 2000
  220. })
  221. }
  222. // 这里可以添加切换到对应国家/地区数据的逻辑
  223. // 例如:loadMarketData(country)
  224. closeModal()
  225. }
  226. // 关闭弹窗
  227. const closeModal = () => {
  228. showCountryModal.value = false
  229. }
  230. onMounted(() => {
  231. // 状态栏高度
  232. iSMT.value = uni.getSystemInfoSync().statusBarHeight;
  233. // 初始化 tab 栏
  234. if (channelData.value.length > 0) {
  235. pageIndex.value = 0
  236. scrollToView.value = 'nav' + channelData.value[0].id
  237. }
  238. // 确保DOM渲染完成后再查询高度
  239. nextTick(() => {
  240. // 动态计算header实际高度
  241. uni.createSelectorQuery().select('.header_fixed').boundingClientRect((rect) => {
  242. if (rect) {
  243. headerHeight.value = rect.height
  244. console.log('Header实际高度:', headerHeight.value, 'px')
  245. }
  246. }).exec()
  247. })
  248. })
  249. // 监听headerHeight变化,重新计算contentHeight
  250. watch(headerHeight, (newHeight) => {
  251. if (newHeight > 0) {
  252. const systemInfo = uni.getSystemInfoSync()
  253. const windowHeight = systemInfo.windowHeight
  254. const statusBarHeight = systemInfo.statusBarHeight || 0
  255. const footerHeight = 100
  256. contentHeight.value = windowHeight - statusBarHeight - newHeight - footerHeight
  257. console.log('重新计算contentHeight:', contentHeight.value)
  258. }
  259. })
  260. </script>
  261. <style scoped>
  262. /* 主容器样式调整 */
  263. .main {
  264. position: relative;
  265. height: 100vh;
  266. overflow: hidden;
  267. background-color: white;
  268. }
  269. /* 状态栏占位 */
  270. .top {
  271. position: fixed;
  272. top: 0;
  273. left: 0;
  274. right: 0;
  275. z-index: 1001;
  276. background-color: #ffffff;
  277. }
  278. /* 固定头部样式 */
  279. .header_fixed {
  280. position: fixed;
  281. left: 0;
  282. right: 0;
  283. z-index: 1000;
  284. background-color: #ffffff;
  285. padding: 20rpx 0 0 0;
  286. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
  287. }
  288. /* 可滚动内容区域 */
  289. .content_scroll {
  290. position: fixed;
  291. left: 0;
  292. right: 0;
  293. bottom: 100rpx;
  294. /* 底部导航栏高度 */
  295. overflow-y: auto;
  296. }
  297. .header_content {
  298. display: flex;
  299. align-items: center;
  300. justify-content: space-between;
  301. height: 80rpx;
  302. padding: 0 20rpx;
  303. margin-bottom: 10rpx;
  304. }
  305. .header_input_wrapper {
  306. display: flex;
  307. align-items: center;
  308. width: 100%;
  309. margin: 0 20rpx 0 0;
  310. height: 70rpx;
  311. border-radius: 35rpx;
  312. background-color: #ffffff;
  313. border: 1rpx solid #e9ecef;
  314. padding: 0 80rpx 0 30rpx;
  315. font-size: 28rpx;
  316. color: #5c5c5c;
  317. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
  318. }
  319. .search_icon {
  320. width: 40rpx;
  321. height: 40rpx;
  322. opacity: 0.6;
  323. }
  324. .header_input {
  325. margin-left: 10rpx;
  326. }
  327. .header_icons {
  328. display: flex;
  329. align-items: center;
  330. gap: 15rpx;
  331. }
  332. .header_icon {
  333. width: 40rpx;
  334. height: 40rpx;
  335. display: flex;
  336. align-items: center;
  337. justify-content: center;
  338. }
  339. .header_icon image {
  340. width: 40rpx;
  341. height: 40rpx;
  342. }
  343. /* Tab 栏样式 */
  344. .channel_li {
  345. display: flex;
  346. align-items: center;
  347. height: 80rpx;
  348. background-color: #ffffff;
  349. border-bottom: 1rpx solid #f0f0f0;
  350. }
  351. .channel_wrap {
  352. width: calc(100% - 60rpx);
  353. height: 100%;
  354. overflow: hidden;
  355. flex-shrink: 0;
  356. }
  357. .channel_innerWrap {
  358. display: flex;
  359. align-items: center;
  360. height: 100%;
  361. padding: 0 20rpx;
  362. white-space: nowrap;
  363. }
  364. .channel_item {
  365. position: relative;
  366. display: flex;
  367. flex-direction: column;
  368. align-items: center;
  369. justify-content: center;
  370. height: 60rpx;
  371. padding: 0 20rpx;
  372. border-radius: 30rpx;
  373. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  374. cursor: pointer;
  375. white-space: nowrap;
  376. flex-shrink: 0;
  377. }
  378. .channel_item:active {
  379. transform: scale(0.98);
  380. }
  381. .channel_item.active {
  382. color: #333;
  383. font-weight: bold;
  384. }
  385. .channel_text {
  386. font-size: 28rpx;
  387. font-weight: 500;
  388. color: #666666;
  389. transition: color 0.3s ease;
  390. white-space: nowrap;
  391. }
  392. .channel_item.active .channel_text {
  393. color: #333333;
  394. font-weight: 400;
  395. z-index: 2;
  396. }
  397. .active_indicator {
  398. position: absolute;
  399. left: 50%;
  400. top: 60%;
  401. transform: translateX(-45%);
  402. width: calc(100% - 20rpx);
  403. min-width: 40rpx;
  404. max-width: 120rpx;
  405. height: 8rpx;
  406. background-image: url('/static/marketSituation-image/bg.png');
  407. background-size: cover;
  408. background-position: center;
  409. background-repeat: no-repeat;
  410. animation: slideIn 0.1s ease;
  411. border-radius: 8rpx;
  412. z-index: 1;
  413. }
  414. @keyframes slideIn {
  415. from {
  416. width: 0;
  417. opacity: 0;
  418. }
  419. to {
  420. width: 40rpx;
  421. opacity: 1;
  422. }
  423. }
  424. .scroll_indicator {
  425. border-left: 1rpx solid #b6b6b6;
  426. display: flex;
  427. align-items: center;
  428. justify-content: center;
  429. width: 60rpx;
  430. height: 30rpx;
  431. background-color: #ffffff;
  432. flex-shrink: 0;
  433. }
  434. .scroll_indicator image {
  435. width: 20rpx;
  436. height: 20rpx;
  437. opacity: 0.5;
  438. }
  439. .content {
  440. margin-top: 20rpx;
  441. background-color: white;
  442. }
  443. .static-footer {
  444. position: fixed;
  445. bottom: 0;
  446. }
  447. /* 弹窗样式 */
  448. .modal_overlay {
  449. position: fixed;
  450. top: 0;
  451. left: 0;
  452. right: 0;
  453. bottom: 0;
  454. background-color: rgba(0, 0, 0, 0.5);
  455. display: flex;
  456. align-items: flex-end;
  457. z-index: 1000;
  458. }
  459. .modal_content {
  460. width: 100%;
  461. background-color: #fff;
  462. border-radius: 20rpx 20rpx 0 0;
  463. max-height: 80vh;
  464. overflow: hidden;
  465. }
  466. .modal_header {
  467. position: relative;
  468. display: flex;
  469. justify-content: center;
  470. align-items: center;
  471. padding: 30rpx 40rpx;
  472. border-bottom: 1rpx solid #f0f0f0;
  473. }
  474. .modal_title {
  475. font-size: 32rpx;
  476. font-weight: bold;
  477. color: #333333;
  478. text-align: center;
  479. }
  480. .modal_close {
  481. position: absolute;
  482. right: 40rpx;
  483. top: 50%;
  484. transform: translateY(-50%);
  485. width: 60rpx;
  486. height: 60rpx;
  487. display: flex;
  488. align-items: center;
  489. justify-content: center;
  490. font-size: 40rpx;
  491. color: #999;
  492. }
  493. .modal_body {
  494. padding: 40rpx;
  495. }
  496. .country_grid {
  497. display: grid;
  498. grid-template-columns: 1fr 1fr;
  499. gap: 20rpx;
  500. }
  501. .country_item {
  502. padding: 24rpx 30rpx;
  503. border-radius: 12rpx;
  504. background-color: #f8f8f8;
  505. display: flex;
  506. align-items: center;
  507. justify-content: center;
  508. transition: all 0.3s ease;
  509. }
  510. .country_item.selected {
  511. background-color: #ff4444;
  512. color: #fff;
  513. }
  514. .country_text {
  515. font-size: 28rpx;
  516. color: #333;
  517. }
  518. .country_item.selected .country_text {
  519. color: #fff;
  520. }
  521. </style>