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.

596 lines
14 KiB

4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
3 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. // 跳转到自定义股票列表页面
  161. uni.navigateTo({
  162. url: '/pages/customStockList/customStockList'
  163. })
  164. };
  165. // Tab 栏点击事件
  166. const navClick = (index) => {
  167. pageIndex.value = index;
  168. const currentItem = channelData.value[index];
  169. scrollToView.value = "nav" + currentItem.id;
  170. // 同步更新弹窗中的选中状态
  171. selectedCountry.value = currentItem.title;
  172. // 切换tab后,查询当前国家的股票数据
  173. queryStockDataAPI({
  174. parentId: currentItem.id,
  175. tradeId: 1,
  176. }).then((res) => {
  177. if (res.code == 200) {
  178. currentTabData.value = {
  179. type: 'country',
  180. data: res.data
  181. };
  182. }
  183. });
  184. // 这里可以添加切换 tab 后的数据加载逻辑
  185. console.log("当前选中的 tab:", currentItem);
  186. };
  187. // 更多选项点击事件
  188. const channel_more = () => {
  189. showCountryModal.value = true;
  190. };
  191. // 选择国家
  192. const selectCountry = (country) => {
  193. selectedCountry.value = country;
  194. // 查找对应的tab索引
  195. const targetIndex = channelData.value.findIndex((item) => item.title === country);
  196. if (targetIndex !== -1) {
  197. // 同步更新页面tab
  198. pageIndex.value = targetIndex;
  199. const currentItem = channelData.value[targetIndex];
  200. scrollToView.value = "nav" + currentItem.id;
  201. console.log("选中了:" + country + ",同步到tab索引:" + targetIndex);
  202. uni.showToast({
  203. title: "已切换到:" + country,
  204. icon: "none",
  205. duration: 2000,
  206. });
  207. } else {
  208. // 如果是"概况"或其他特殊选项,默认切换到第一个tab
  209. if (country === "概况" || country === "全部") {
  210. pageIndex.value = 0;
  211. scrollToView.value = "nav" + channelData.value[0].id;
  212. }
  213. console.log("选中了:" + country);
  214. uni.showToast({
  215. title: "已选择:" + country,
  216. icon: "none",
  217. duration: 2000,
  218. });
  219. }
  220. // 这里可以添加切换到对应国家/地区数据的逻辑
  221. // 例如:loadMarketData(country)
  222. closeModal();
  223. };
  224. // 关闭弹窗
  225. const closeModal = () => {
  226. showCountryModal.value = false;
  227. };
  228. onMounted(async () => {
  229. await getAllTabs();
  230. // 状态栏高度
  231. iSMT.value = uni.getSystemInfoSync().statusBarHeight;
  232. // 初始化 tab 栏
  233. if (channelData.value.length > 0) {
  234. pageIndex.value = 0;
  235. scrollToView.value = "nav" + channelData.value[0].id;
  236. }
  237. // 确保DOM渲染完成后再查询高度
  238. nextTick(() => {
  239. // 动态计算header实际高度
  240. uni
  241. .createSelectorQuery()
  242. .select(".header_fixed")
  243. .boundingClientRect((rect) => {
  244. if (rect) {
  245. headerHeight.value = rect.height;
  246. console.log("Header实际高度:", headerHeight.value, "px");
  247. }
  248. })
  249. .exec();
  250. });
  251. });
  252. // 监听headerHeight变化,重新计算contentHeight
  253. watch(headerHeight, (newHeight) => {
  254. if (newHeight > 0) {
  255. const systemInfo = uni.getSystemInfoSync();
  256. const windowHeight = systemInfo.windowHeight;
  257. const statusBarHeight = systemInfo.statusBarHeight || 0;
  258. const footerHeight = 100;
  259. contentHeight.value = windowHeight - statusBarHeight - newHeight - footerHeight;
  260. console.log("重新计算contentHeight:", contentHeight.value);
  261. }
  262. });
  263. </script>
  264. <style scoped>
  265. /* 主容器样式调整 */
  266. .main {
  267. position: relative;
  268. height: 100vh;
  269. overflow: hidden;
  270. background-color: white;
  271. }
  272. /* 状态栏占位 */
  273. .top {
  274. position: fixed;
  275. top: 0;
  276. left: 0;
  277. right: 0;
  278. z-index: 1001;
  279. background-color: #ffffff;
  280. }
  281. /* 固定头部样式 */
  282. .header_fixed {
  283. position: fixed;
  284. left: 0;
  285. right: 0;
  286. z-index: 1000;
  287. background-color: #ffffff;
  288. padding: 20rpx 0 0 0;
  289. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
  290. }
  291. /* 可滚动内容区域 */
  292. .content_scroll {
  293. position: fixed;
  294. left: 0;
  295. right: 0;
  296. bottom: 100rpx;
  297. /* 底部导航栏高度 */
  298. overflow-y: auto;
  299. }
  300. .header_content {
  301. display: flex;
  302. align-items: center;
  303. justify-content: space-between;
  304. height: 80rpx;
  305. padding: 0 20rpx;
  306. margin-bottom: 10rpx;
  307. }
  308. .header_input_wrapper {
  309. display: flex;
  310. align-items: center;
  311. width: 100%;
  312. margin: 0 20rpx 0 0;
  313. height: 70rpx;
  314. border-radius: 35rpx;
  315. background-color: #ffffff;
  316. border: 1rpx solid #e9ecef;
  317. padding: 0 80rpx 0 30rpx;
  318. font-size: 28rpx;
  319. color: #5c5c5c;
  320. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
  321. }
  322. .search_icon {
  323. width: 40rpx;
  324. height: 40rpx;
  325. opacity: 0.6;
  326. }
  327. .header_input {
  328. margin-left: 10rpx;
  329. }
  330. .header_icons {
  331. display: flex;
  332. align-items: center;
  333. gap: 15rpx;
  334. }
  335. .header_icon {
  336. width: 40rpx;
  337. height: 40rpx;
  338. display: flex;
  339. align-items: center;
  340. justify-content: center;
  341. }
  342. .header_icon image {
  343. width: 40rpx;
  344. height: 40rpx;
  345. }
  346. /* Tab 栏样式 */
  347. .channel_li {
  348. display: flex;
  349. align-items: center;
  350. height: 80rpx;
  351. background-color: #ffffff;
  352. border-bottom: 1rpx solid #f0f0f0;
  353. }
  354. .channel_wrap {
  355. width: calc(100% - 60rpx);
  356. height: 100%;
  357. overflow: hidden;
  358. flex-shrink: 0;
  359. }
  360. .channel_innerWrap {
  361. display: flex;
  362. align-items: center;
  363. height: 100%;
  364. padding: 0 20rpx;
  365. white-space: nowrap;
  366. }
  367. .channel_item {
  368. position: relative;
  369. display: flex;
  370. flex-direction: column;
  371. align-items: center;
  372. justify-content: center;
  373. height: 60rpx;
  374. padding: 0 20rpx;
  375. border-radius: 30rpx;
  376. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  377. cursor: pointer;
  378. white-space: nowrap;
  379. flex-shrink: 0;
  380. }
  381. .channel_item:active {
  382. transform: scale(0.98);
  383. }
  384. .channel_item.active {
  385. color: #333;
  386. font-weight: bold;
  387. }
  388. .channel_text {
  389. font-size: 28rpx;
  390. font-weight: 500;
  391. color: #666666;
  392. transition: color 0.3s ease;
  393. white-space: nowrap;
  394. }
  395. .channel_item.active .channel_text {
  396. color: #333333;
  397. font-weight: 400;
  398. z-index: 2;
  399. }
  400. .active_indicator {
  401. position: absolute;
  402. left: 50%;
  403. top: 60%;
  404. transform: translateX(-45%);
  405. width: calc(100% - 20rpx);
  406. min-width: 40rpx;
  407. max-width: 120rpx;
  408. height: 8rpx;
  409. background-image: url("/static/marketSituation-image/bg.png");
  410. background-size: cover;
  411. background-position: center;
  412. background-repeat: no-repeat;
  413. animation: slideIn 0.1s ease;
  414. border-radius: 8rpx;
  415. z-index: 1;
  416. }
  417. @keyframes slideIn {
  418. from {
  419. width: 0;
  420. opacity: 0;
  421. }
  422. to {
  423. width: 40rpx;
  424. opacity: 1;
  425. }
  426. }
  427. .scroll_indicator {
  428. border-left: 1rpx solid #b6b6b6;
  429. display: flex;
  430. align-items: center;
  431. justify-content: center;
  432. width: 60rpx;
  433. height: 30rpx;
  434. background-color: #ffffff;
  435. flex-shrink: 0;
  436. }
  437. .scroll_indicator image {
  438. width: 30rpx;
  439. height: 30rpx;
  440. opacity: 0.5;
  441. }
  442. .content {
  443. margin-top: 20rpx;
  444. background-color: white;
  445. }
  446. .static-footer {
  447. position: fixed;
  448. bottom: 0;
  449. }
  450. /* 弹窗样式 */
  451. .modal_overlay {
  452. position: fixed;
  453. top: 0;
  454. left: 0;
  455. right: 0;
  456. bottom: 0;
  457. background-color: rgba(0, 0, 0, 0.5);
  458. display: flex;
  459. align-items: flex-end;
  460. z-index: 1000;
  461. }
  462. .modal_content {
  463. width: 100%;
  464. background-color: #fff;
  465. border-radius: 20rpx 20rpx 0 0;
  466. max-height: 80vh;
  467. overflow: hidden;
  468. }
  469. .modal_header {
  470. position: relative;
  471. display: flex;
  472. justify-content: center;
  473. align-items: center;
  474. padding: 30rpx 40rpx;
  475. border-bottom: 1rpx solid #f0f0f0;
  476. }
  477. .modal_title {
  478. font-size: 32rpx;
  479. font-weight: bold;
  480. color: #333333;
  481. text-align: center;
  482. }
  483. .modal_close {
  484. position: absolute;
  485. right: 40rpx;
  486. top: 50%;
  487. transform: translateY(-50%);
  488. width: 60rpx;
  489. height: 60rpx;
  490. display: flex;
  491. align-items: center;
  492. justify-content: center;
  493. font-size: 40rpx;
  494. color: #999;
  495. }
  496. .modal_body {
  497. padding: 40rpx;
  498. }
  499. .country_grid {
  500. display: grid;
  501. grid-template-columns: 1fr 1fr;
  502. gap: 20rpx;
  503. }
  504. .country_item {
  505. padding: 24rpx 30rpx;
  506. border-radius: 12rpx;
  507. background-color: #f8f8f8;
  508. display: flex;
  509. align-items: center;
  510. justify-content: center;
  511. transition: all 0.3s ease;
  512. }
  513. .country_item.selected {
  514. background-color: #ff4444;
  515. color: #fff;
  516. }
  517. .country_text {
  518. font-size: 28rpx;
  519. color: #333;
  520. }
  521. .country_item.selected .country_text {
  522. color: #fff;
  523. }
  524. </style>