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.

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