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.

589 lines
13 KiB

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