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.

393 lines
12 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. <template>
  2. <view class="main">
  3. <view class="table">
  4. <view class="tableHeader">
  5. <scroll-view class="tabs" scroll-x="true">
  6. <view v-for="(item,index) in tabsData" :key="index" :class="['tabItem', { 'tabItem-active': item === activeTab }]" @click="handleTab(item)">
  7. {{item}}
  8. </view>
  9. </scroll-view>
  10. </view>
  11. <view class="tableContent">
  12. <image class="showAll" src="/static/deepExploration-images/showAll.png" mode="aspectFill"></image>
  13. <scroll-view scroll-x="true" show-scrollbar="false">
  14. <view class="tableBox">
  15. <view class="box_header">
  16. <view class="name">名称</view>
  17. <view class="other" v-for="(item,index) in tableContentHeaderData" :key="index">
  18. <text>{{item}}</text>
  19. <image v-show="ifASC" src="/static/deepExploration-images/ASC.png" mode="aspectFill">
  20. </image>
  21. <image v-show="!ifASC" src="/static/deepExploration-images/DESC.png" mode="aspectFill">
  22. </image>
  23. </view>
  24. </view>
  25. <view class="box_content">
  26. <view class="row" v-for="(item,index) in strategyData" :key="index" :class="{ 'increase-positive': item.increase.startsWith('+'),
  27. 'increase-negative': item.increase.startsWith('-')}">
  28. <view class="name_colum">
  29. <text class="stockName">{{item.name}}</text>
  30. <text class="stockCode">{{item.stockCode}}</text>
  31. </view>
  32. <view class="other_colum">{{item.latest}}</view>
  33. <view class="other_colum">{{item.increase}}</view>
  34. <view class="other_colum">{{item.decrease}}</view>
  35. <view class="other_colum">{{item.previousClose}}</view>
  36. <view class="other_colum">{{item.volume}}</view>
  37. <view class="other_colum">{{item.turnover}}</view>
  38. <view class="other_colum">{{item.openingPrice}}</view>
  39. <view class="other_colum">{{item.highestPrice}}</view>
  40. <view class="other_colum">{{item.lowestPrice}}</view>
  41. </view>
  42. </view>
  43. </view>
  44. </scroll-view>
  45. </view>
  46. </view>
  47. </view>
  48. </template>
  49. <script setup>
  50. import { ref, onMounted } from 'vue'
  51. import { stocSelectApi, stocSelectByNameApi } from '@/api/deepExploration/deepExploration.js'
  52. import { useUserStore } from '@/stores/modules/userInfo.js'
  53. import { useDeviceStore } from '@/stores/modules/deviceInfo.js'
  54. import { LoginApi } from '@/api/start/login.js'
  55. const tabsData = ref(['全部', '抄底卖顶', '波段行情', '价值投资', '资金及仓位管理', ])
  56. const activeTab = ref('全部')
  57. // 点击标签:激活并按需触发名称查询
  58. const handleTab = async (item) => {
  59. activeTab.value = item
  60. const nameMap = {
  61. '抄底卖顶': '北京',
  62. '波段行情': '安徽',
  63. '价值投资': '重庆',
  64. '资金及仓位管理': '黑龙江'
  65. }
  66. if (item === '全部') {
  67. await loadStrategy()
  68. uni.showToast({ title: `查看 ${item} 详情`, icon: 'none', duration: 1500 })
  69. return
  70. }
  71. const apiName = nameMap[item]
  72. if (apiName) {
  73. await loadByName(apiName)
  74. uni.showToast({ title: `${item}数据已更新`, icon: 'none', duration: 1500 })
  75. } else {
  76. uni.showToast({ title: `暂不支持:${item}`, icon: 'none' })
  77. }
  78. }
  79. //表头是否升序
  80. const ifASC = ref(true)
  81. //表头数据
  82. const tableContentHeaderData = ref(['最新', '涨幅', '涨跌', '昨收', '成交量', '成交额', '开盘价', '最高价', '最低价'])
  83. // 渲染用数据
  84. const strategyData = ref([])
  85. // 游客登录兜底,确保移动端有token
  86. const ensureAuth = async () => {
  87. const userStore = useUserStore()
  88. if (userStore.userInfo?.token) return
  89. try {
  90. const deviceStore = useDeviceStore()
  91. let deviceId = deviceStore.deviceInfo?.deviceId
  92. if (!deviceId) {
  93. const cached = uni.getStorageSync('deviceId')
  94. deviceId = cached || `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
  95. uni.setStorageSync('deviceId', deviceId)
  96. deviceStore.setDeviceInfo({ ...(deviceStore.deviceInfo || {}), deviceId })
  97. }
  98. const res = await LoginApi({
  99. loginType: 'VISITOR',
  100. account: deviceId,
  101. useCode: false
  102. })
  103. if (res?.code === 200 && res?.data?.token) {
  104. userStore.setUserInfo(res.data)
  105. console.log('游客登录成功,token=', res.data.token)
  106. } else {
  107. console.warn('游客登录失败', res)
  108. }
  109. } catch (err) {
  110. console.warn('游客登录异常', err)
  111. }
  112. }
  113. const formatPctChg = (val) => {
  114. if (val === null || val === undefined || val === '') return ''
  115. const num = Number(val)
  116. if (!isFinite(num)) return String(val)
  117. const sign = num > 0 ? '+' : ''
  118. return `${sign}${num.toFixed(2)}%`
  119. }
  120. // 默认根据涨幅( pctChg )由大到小排序
  121. const sortByPctDesc = (arr) => arr.sort((a, b) => (Number(b.pctChg) || -Infinity) - (Number(a.pctChg) || -Infinity))
  122. // 按名称加载(抄底卖顶/波段行情/价值投资/资金及仓位管理)
  123. const loadByName = async (apiName) => {
  124. try {
  125. const userStore = useUserStore()
  126. if (!userStore.userInfo?.token) {
  127. await ensureAuth()
  128. }
  129. const token = useUserStore().userInfo?.token
  130. const res = await stocSelectByNameApi({ name: apiName, token })
  131. const raw = res?.data
  132. const dataObj = raw?.data || raw
  133. let list = []
  134. if (dataObj && typeof dataObj === 'object' && !Array.isArray(dataObj)) {
  135. const target = dataObj[apiName]
  136. if (Array.isArray(target)) list = target
  137. else {
  138. const firstArr = Object.values(dataObj).find(v => Array.isArray(v))
  139. if (Array.isArray(firstArr)) list = firstArr
  140. }
  141. }
  142. if ((!Array.isArray(list) || !list.length) && Array.isArray(raw)) {
  143. list = raw
  144. }
  145. // 排序:涨幅由大到小
  146. if (Array.isArray(list)) list = sortByPctDesc(list)
  147. if (Array.isArray(list) && list.length) {
  148. strategyData.value = list.map(item => ({
  149. name: item.stockName ?? item.name ?? item.tsName ?? item.tsname ?? item.secName ?? '',
  150. stockCode: item.tsCode ?? item.tscode ?? item.code ?? item.symbol ?? '',
  151. latest: item.close ?? item.lastClose ?? '',
  152. increase: formatPctChg(item.pctChg),
  153. decrease: item.change ?? item.chg ?? '',
  154. previousClose: item.preClose ?? item.preclose ?? item.prevClose ?? '',
  155. volume: item.vol ?? item.volume ?? '',
  156. turnover: item.amount ?? item.turnover ?? '',
  157. openingPrice: item.open ?? item.openPrice ?? '',
  158. highestPrice: item.high ?? item.highPrice ?? '',
  159. lowestPrice: item.low ?? item.lowPrice ?? ''
  160. }))
  161. console.log(`按名称(${apiName})加载成功,条数:`, strategyData.value.length, '首项:', strategyData.value[0])
  162. if (!strategyData.value[0]?.name) {
  163. console.warn('名称字段未命中,原始keys示例:', Object.keys(list[0] || {}))
  164. }
  165. } else {
  166. console.warn('getStrategyByName 返回空列表或结构不匹配', raw)
  167. }
  168. } catch (e) {
  169. console.error('getStrategyByName 接口调用失败', e)
  170. uni.showToast({ title: '按名称加载失败', icon: 'none' })
  171. }
  172. }
  173. const loadStrategy = async () => {
  174. try {
  175. const userStore = useUserStore()
  176. if (!userStore.userInfo?.token) {
  177. await ensureAuth()
  178. }
  179. const token = useUserStore().userInfo?.token
  180. const res = await stocSelectApi({ language: 'cn', token })
  181. const raw = res?.data
  182. const listCandidates = [
  183. raw?.list,
  184. raw?.data?.list,
  185. raw?.data?.rows,
  186. raw?.rows,
  187. Array.isArray(raw) ? raw : null
  188. ].filter(Array.isArray)
  189. let list = listCandidates.length ? listCandidates[0] : []
  190. if ((!Array.isArray(list) || !list.length) && raw && typeof raw === 'object' && !Array.isArray(raw)) {
  191. const arrays = Object.values(raw).filter(Array.isArray)
  192. if (arrays.length) list = arrays.flat()
  193. }
  194. // 排序:涨幅由大到小
  195. if (Array.isArray(list)) list = sortByPctDesc(list)
  196. if (Array.isArray(list) && list.length) {
  197. strategyData.value = list.map(item => ({
  198. name: item.stockName ?? item.name ?? item.tsName ?? item.tsname ?? item.secName ?? '',
  199. stockCode: item.tsCode ?? item.tscode ?? item.code ?? item.symbol ?? '',
  200. latest: item.close ?? item.lastClose ?? '',
  201. increase: formatPctChg(item.pctChg),
  202. decrease: item.change ?? item.chg ?? '',
  203. previousClose: item.preClose ?? item.preclose ?? item.prevClose ?? '',
  204. volume: item.vol ?? item.volume ?? '',
  205. turnover: item.amount ?? item.turnover ?? '',
  206. openingPrice: item.open ?? item.openPrice ?? '',
  207. highestPrice: item.high ?? item.highPrice ?? '',
  208. lowestPrice: item.low ?? item.lowPrice ?? ''
  209. }))
  210. console.log('stockSelectDetail 加载成功(已按涨幅降序),条数:', strategyData.value.length, '首项:', strategyData.value[0])
  211. if (!strategyData.value[0]?.name) {
  212. console.warn('名称字段未命中,原始keys示例:', Object.keys(list[0] || {}))
  213. }
  214. } else {
  215. console.warn('stockSelectDetail 接口返回空列表或结构不匹配', raw)
  216. }
  217. } catch (e) {
  218. console.error('stockSelectDetail 接口调用失败', e)
  219. uni.showToast({ title: '选股策略详情加载失败', icon: 'none' })
  220. }
  221. }
  222. onMounted(() => {
  223. loadStrategy()
  224. })
  225. </script>
  226. <style scoped lang="scss">
  227. .main {
  228. width: 100%;
  229. height: 100vh;
  230. background-color: #fff;
  231. .table {
  232. margin-top: 10rpx;
  233. box-shadow: 0 -2rpx 3rpx -1rpx rgba(0, 0, 0, 0.5);
  234. .tableHeader {
  235. .tabs {
  236. white-space: nowrap;
  237. padding-top: 20rpx;
  238. padding-left: 40rpx;
  239. ::-webkit-scrollbar {
  240. //隐藏 滚动条
  241. display: none;
  242. }
  243. .tabItem {
  244. display: inline-block;
  245. color: rgb(175, 175, 175);
  246. border-radius: 10rpx;
  247. padding: 5rpx 30rpx;
  248. margin-right: 20rpx;
  249. font-size: 28rpx;
  250. background-color: rgb(243, 243, 243);
  251. }
  252. .tabItem-active {
  253. background-color: #DB1F1D; // 红色
  254. color: #fff;
  255. }
  256. }
  257. }
  258. .tableContent {
  259. width: 100%;
  260. background-color: #fff;
  261. position: relative;
  262. .showAll {
  263. position: absolute;
  264. top: 35rpx;
  265. right: 20rpx;
  266. width: 40rpx;
  267. height: 40rpx;
  268. z-index: 100;
  269. }
  270. scroll-view {
  271. width: 100%;
  272. white-space: nowrap;
  273. ::-webkit-scrollbar {
  274. //隐藏 滚动条
  275. display: none;
  276. }
  277. }
  278. .tableBox {
  279. padding-left: 40rpx;
  280. .box_header {
  281. margin-bottom: 19rpx;
  282. display: flex;
  283. width: max-content;
  284. margin-top: 40rpx;
  285. color: rgb(109, 109, 109);
  286. border-radius: 10rpx;
  287. margin-right: 20rpx;
  288. font-size: 23rpx;
  289. .name {
  290. flex: 0 0 375rpx;
  291. }
  292. .other {
  293. flex: 0 0 195rpx;
  294. text {
  295. margin-right: 5rpx;
  296. }
  297. image {
  298. width: 20rpx;
  299. height: 20rpx;
  300. }
  301. }
  302. }
  303. .box_content {
  304. width: max-content;
  305. .row {
  306. padding: 5rpx;
  307. display: flex;
  308. border-top: 1rpx dashed #eee;
  309. width: 210%;
  310. &.increase-positive {
  311. .other_colum {
  312. color: #2DD357;
  313. font-weight: 200;
  314. }
  315. }
  316. &.increase-negative {
  317. .other_colum {
  318. color: #FF4150;
  319. font-weight: 200;
  320. }
  321. }
  322. .name_colum {
  323. flex: 0 0 375rpx;
  324. display: flex;
  325. flex-direction: column;
  326. gap: 4rpx;
  327. .stockName {
  328. color: #333333;
  329. width: 100%;
  330. max-width: 305rpx;
  331. font-size: 28rpx;
  332. font-weight: 400;
  333. line-height: 36rpx;
  334. white-space: nowrap;
  335. overflow: hidden;
  336. text-overflow: ellipsis;
  337. }
  338. .stockCode {
  339. color: #c5c5c5;
  340. ;
  341. font-size: 24rpx;
  342. font-weight: 400;
  343. line-height: 30rpx;
  344. }
  345. }
  346. .other_colum {
  347. flex: 0 0 195rpx;
  348. display: flex;
  349. align-items: center;
  350. }
  351. }
  352. }
  353. }
  354. }
  355. }
  356. }
  357. </style>