| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378 |
- <template>
- <view class="page-rank">
- <scroll-view class="scroll-view" scroll-y>
- <view class="content-wrapper" :class="{ 'blur-content': !isLoggedIn }">
- <!-- 上证指数卡片 -->
- <view class="index-card">
- <view class="index-left">
- <view class="index-price-row">
- <text :class="['index-price', getIndexChangeClass(indexData.changePercent)]">
- {{ formatIndexPrice(indexData.currentPrice) }}
- </text>
- <text :class="['index-change', getIndexChangeClass(indexData.changePercent)]">
- {{ indexData.priceChange || '--' }}
- </text>
- </view>
- <view class="index-name-row">
- <text class="index-name">{{ indexData.stockName || '上证指数' }}</text>
- <text :class="['index-percent', getIndexChangeClass(indexData.changePercent)]">
- {{ indexData.changePercent || '--' }}
- </text>
- </view>
- </view>
- </view>
- <!-- 分段控制器 -->
- <view class="segment-control">
- <view class="segment-slider" :class="{ 'slider-right': viewMode === 'table' }"></view>
- <view
- class="segment-item"
- :class="{ 'segment-active': viewMode === 'list' }"
- @click="setViewMode('list')"
- >
- <text class="segment-text">热力图</text>
- </view>
- <view
- class="segment-item"
- :class="{ 'segment-active': viewMode === 'table' }"
- @click="setViewMode('table')"
- >
- <text class="segment-text">详情</text>
- </view>
- </view>
- <!-- 热力图视图 -->
- <view v-show="viewMode === 'list'" class="stock-list-card" :class="{ 'hidden-list': myStocks.length === 0 }">
- <view class="list-header">
- <view class="list-dot"></view>
- <text class="list-title">我的自选</text>
- <text class="list-count">{{ myStocks.length }}只</text>
- </view>
- <view class="stock-list">
- <view class="stock-item-wrapper" v-for="(stock, index) in myStocks" :key="stock.code">
- <movable-area class="movable-area">
- <movable-view
- class="movable-view"
- direction="horizontal"
- :x="slideStates[stock.code]?.x || 0"
- :damping="40"
- :friction="5"
- :out-of-bounds="false"
- @change="(e) => handleSlideChange(e, stock.code)"
- @touchend="() => handleSlideEnd(stock.code)"
- >
- <view class="stock-item">
- <view class="stock-main">
- <view class="stock-info">
- <text class="stock-name">{{ stock.name }}</text>
- <view class="stock-code-row">
- <text :class="['stock-tag', getMarketClass(stock.code)]">{{ getMarketTag(stock.code) }}</text>
- <text class="stock-code">{{ stock.code }}</text>
- </view>
- </view>
- <!-- 趋势图 - 使用 canvas 2d 模式解决滚动分离问题 -->
- <view class="stock-chart">
- <canvas
- type="2d"
- :id="'chart-' + stock.code"
- class="trend-canvas"
- ></canvas>
- </view>
- <view class="stock-quote">
- <text class="stock-price">{{ stock.currentPrice || '-' }}</text>
- <text :class="['stock-change', getProfitClass(stock.changePercent)]">{{ stock.changePercent || '-' }}</text>
- </view>
- </view>
- <!-- 滑动删除按钮 -->
- <view class="slide-delete-btn" @click.stop="removeStock(index)">
- <view class="delete-icon-circle">
- <text class="delete-icon-text">−</text>
- </view>
- </view>
- </view>
- </movable-view>
- </movable-area>
- </view>
- </view>
- </view>
- <!-- 详情表格视图 -->
- <view v-show="viewMode === 'table'" class="stock-table-card" :class="{ 'hidden-list': myStocks.length === 0 }">
- <view class="list-header">
- <view class="list-dot list-dot-blue"></view>
- <text class="list-title">详情数据</text>
- </view>
- <!-- 表头 -->
- <view class="table-header">
- <text class="th-name">股票</text>
- <text class="th-date">自选日</text>
- <text class="th-price">自选价</text>
- <text class="th-profit">收益</text>
- </view>
- <!-- 表格内容 -->
- <view class="table-list">
- <view
- v-for="(stock, index) in myStocks"
- :key="stock.code"
- class="table-row"
- >
- <view class="td-name">
- <text class="stock-name">{{ stock.name }}</text>
- <view class="stock-code-row">
- <text :class="['stock-tag', getMarketClass(stock.code)]">{{ getMarketTag(stock.code) }}</text>
- <text class="stock-code">{{ stock.code }}</text>
- </view>
- </view>
- <text class="td-date">{{ stock.addDate || '--' }}</text>
- <text class="td-price">{{ formatPrice(stock.addPrice) }}</text>
- <view class="td-profit-wrap">
- <text :class="['td-profit', getProfitClass(stock.profitPercent)]">
- {{ stock.profitPercent || '--' }}
- </text>
- </view>
- </view>
- </view>
- </view>
- <!-- 空状态 -->
- <view v-if="myStocks.length === 0" class="empty-content">
- <view class="empty-icon">📊</view>
- <text class="empty-text">暂无收藏股票</text>
- <text class="empty-desc">在强势池中点击"+"按钮添加股票</text>
- </view>
- <!-- 底部安全区域 -->
- <view class="bottom-safe-area"></view>
- </view>
- </scroll-view>
- <!-- 未登录遮罩层 -->
- <view v-if="!isLoggedIn" class="login-mask">
- <view class="login-prompt">
- <view class="lock-icon">🔒</view>
- <text class="prompt-title">登录后查看我的股票</text>
- <text class="prompt-desc">使用微信授权快速登录</text>
- <button class="login-button-native" @click="goToLogin">
- <text>登录</text>
- </button>
- </view>
- </view>
- </view>
- </template>
- <script setup>
- import { ref, nextTick, getCurrentInstance, reactive } from 'vue'
- import { onLoad, onShow, onHide, onUnload } from '@dcloudio/uni-app'
- import { isLoggedIn as checkLoginStatus } from '../../utils/auth.js'
- import { getStockQuotes, getIndexQuote, getUserStocks, deleteUserStock } from '../../utils/api.js'
- // 保存组件实例
- let componentInstance = null
- const isLoggedIn = ref(false)
- const myStocks = ref([])
- const viewMode = ref('list') // 'list' 或 'table'
- const isLoading = ref(false) // 加载状态
- const lastLoadTime = ref(0) // 上次加载时间
- const CACHE_DURATION = 5000 // 缓存有效期5秒
- const isPageVisible = ref(false) // 页面是否可见
- // 获取随机刷新间隔 (2000-3000ms)
- const getRandomInterval = () => 2000 + Math.random() * 1000
- // 滑动删除相关
- const SLIDE_WIDTH = 100 // 滑动距离(三分之一宽度约100rpx转换)
- const slideStates = reactive({}) // 每个股票的滑动状态
- let slideTimers = {} // 自动还原计时器
- const AUTO_RESET_DELAY = 2000 // 2秒后自动还原
- // 初始化滑动状态
- const initSlideState = (code) => {
- if (!slideStates[code]) {
- slideStates[code] = { x: 0, currentX: 0 }
- }
- }
- // 处理滑动变化
- const handleSlideChange = (e, code) => {
- initSlideState(code)
- slideStates[code].currentX = e.detail.x
- }
- // 处理滑动结束
- const handleSlideEnd = (code) => {
- initSlideState(code)
- const state = slideStates[code]
-
- // 滑动超过20px就显示删除按钮(吸附到三分之一处)
- if (state.currentX < -20) {
- state.x = -SLIDE_WIDTH
- // 启动自动还原计时器(2秒)
- startAutoResetTimer(code)
- } else {
- // 滑回初始位置
- resetSlide(code)
- }
- }
- // 重置滑动位置
- const resetSlide = (code) => {
- if (slideStates[code]) {
- slideStates[code].x = 0
- slideStates[code].currentX = 0
- }
- clearSlideTimer(code)
- }
- // 启动自动还原计时器
- const startAutoResetTimer = (code) => {
- clearSlideTimer(code)
- slideTimers[code] = setTimeout(() => {
- resetSlide(code)
- }, AUTO_RESET_DELAY) // 2秒后自动还原
- }
- // 清除计时器
- const clearSlideTimer = (code) => {
- if (slideTimers[code]) {
- clearTimeout(slideTimers[code])
- delete slideTimers[code]
- }
- }
- // 清除所有计时器
- const clearAllSlideTimers = () => {
- Object.keys(slideTimers).forEach(code => {
- clearSlideTimer(code)
- })
- }
- // 设置视图模式
- const setViewMode = (mode) => {
- viewMode.value = mode
-
- // 切换到热力图视图时,先清空再重绘趋势图
- if (mode === 'list' && myStocks.value.length > 0) {
- nextTick(() => {
- clearAllCanvases()
- drawAllTrendCharts()
- })
- }
- }
- const indexData = ref({
- stockCode: '000001',
- stockName: '上证指数',
- currentPrice: null,
- priceChange: null,
- changePercent: null
- })
- let priceRefreshTimer = null // 价格刷新定时器 (2-3秒)
- let trendRefreshTimer = null // 趋势图刷新定时器 (10秒)
- // 获取上证指数数据
- const fetchIndexData = async () => {
- try {
- const res = await getIndexQuote('000001')
- if (res.code === 200 && res.data) {
- indexData.value = { ...indexData.value, ...res.data }
- }
- } catch (e) {
- console.error('[上证指数] 获取失败:', e.message)
- }
- }
- // 格式化指数价格
- const formatIndexPrice = (price) => {
- if (!price) return '--'
- return parseFloat(price).toFixed(2)
- }
- // 格式化价格
- const formatPrice = (price) => {
- if (!price) return '--'
- return parseFloat(price).toFixed(2)
- }
- // 获取指数涨跌样式类
- const getIndexChangeClass = (changePercent) => {
- if (!changePercent) return ''
- const str = String(changePercent).replace('%', '').replace('+', '')
- const value = parseFloat(str)
- if (value > 0) return 'index-up'
- if (value < 0) return 'index-down'
- return ''
- }
- // 获取收益样式类
- const getProfitClass = (profitPercent) => {
- if (!profitPercent) return ''
- const str = String(profitPercent).replace('%', '').replace('+', '')
- const value = parseFloat(str)
- if (value > 0) return 'profit-up'
- if (value < 0) return 'profit-down'
- return ''
- }
- // 获取市场标签
- const getMarketTag = (code) => {
- if (code.startsWith('6')) return '沪'
- if (code.startsWith('0')) return '深'
- if (code.startsWith('3')) return '创'
- return '沪'
- }
- const getMarketClass = (code) => {
- if (code.startsWith('6')) return 'market-sh'
- if (code.startsWith('0')) return 'market-sz'
- if (code.startsWith('3')) return 'market-cy'
- return 'market-sh'
- }
- // 获取设备像素比(使用新API替代废弃的getSystemInfoSync)
- const getDevicePixelRatio = () => {
- try {
- const windowInfo = wx.getWindowInfo()
- return windowInfo.pixelRatio || 2
- } catch (e) {
- return 2 // 默认值
- }
- }
- // 清空单个 canvas
- const clearCanvas = (canvasId) => {
- const query = uni.createSelectorQuery()
- query.select('#' + canvasId)
- .fields({ node: true, size: true })
- .exec((res) => {
- if (!res || !res[0] || !res[0].node) return
-
- const canvas = res[0].node
- const ctx = canvas.getContext('2d')
- const dpr = getDevicePixelRatio()
- const width = res[0].width
- const height = res[0].height
-
- canvas.width = width * dpr
- canvas.height = height * dpr
- ctx.setTransform(1, 0, 0, 1, 0, 0)
- ctx.clearRect(0, 0, width * dpr, height * dpr)
- })
- }
- // 清空所有趋势图 canvas
- const clearAllCanvases = () => {
- myStocks.value.forEach(stock => {
- clearCanvas('chart-' + stock.code)
- })
- }
- // 绘制单个股票的趋势图 (使用 Canvas 2D API)
- const drawTrendChart = (stock) => {
- const canvasId = 'chart-' + stock.code
- let trendData = stock.trendData
-
- // 如果没有趋势数据,生成模拟数据
- if (!trendData || !Array.isArray(trendData) || trendData.length === 0) {
- trendData = generateMockTrendData(stock.changePercent)
- }
-
- // 使用 Canvas 2D 模式
- const query = uni.createSelectorQuery()
- query.select('#' + canvasId)
- .fields({ node: true, size: true })
- .exec((res) => {
- if (!res || !res[0] || !res[0].node) {
- console.warn('[趋势图] 获取canvas节点失败:', canvasId)
- return
- }
-
- const canvas = res[0].node
- const ctx = canvas.getContext('2d')
- const dpr = getDevicePixelRatio()
-
- // 设置 canvas 实际尺寸
- const width = res[0].width
- const height = res[0].height
-
- // 重置 canvas 尺寸(强制清除之前的绘制状态,解决页面切换后显示异常)
- canvas.width = width * dpr
- canvas.height = height * dpr
-
- // 重置变换矩阵后再设置缩放
- ctx.setTransform(1, 0, 0, 1, 0, 0)
- ctx.scale(dpr, dpr)
-
- const padding = 1
-
- const maxValue = Math.max(...trendData)
- const minValue = Math.min(...trendData)
- const dataRange = maxValue - minValue
- const avgValue = (maxValue + minValue) / 2
- const minRange = avgValue * 0.03 || 1
- const range = Math.max(dataRange, minRange)
-
- const baseValue = trendData[0]
- const baseY = height - padding - ((baseValue - minValue) / range) * (height - padding * 2)
-
- const changePercent = parseFloat(String(stock.changePercent || '0').replace('%', '').replace('+', ''))
- const isUp = changePercent >= 0
- const lineColor = isUp ? '#ef4444' : '#22c55e'
- const fillColor = isUp ? 'rgba(239, 68, 68, 0.15)' : 'rgba(34, 197, 94, 0.15)'
-
- // 清除画布
- ctx.clearRect(0, 0, width, height)
-
- // 绘制基准虚线
- ctx.beginPath()
- ctx.strokeStyle = '#e5e7eb'
- ctx.lineWidth = 0.5
- ctx.setLineDash([2, 2])
- ctx.moveTo(padding, baseY)
- ctx.lineTo(width - padding, baseY)
- ctx.stroke()
- ctx.setLineDash([])
-
- // 绘制填充区域
- ctx.beginPath()
- ctx.moveTo(padding, baseY)
-
- trendData.forEach((value, index) => {
- const x = padding + (index / (trendData.length - 1)) * (width - padding * 2)
- const y = height - padding - ((value - minValue) / range) * (height - padding * 2)
- ctx.lineTo(x, y)
- })
-
- ctx.lineTo(width - padding, baseY)
- ctx.closePath()
- ctx.fillStyle = fillColor
- ctx.fill()
-
- // 绘制折线
- ctx.beginPath()
-
- trendData.forEach((value, index) => {
- const x = padding + (index / (trendData.length - 1)) * (width - padding * 2)
- const y = height - padding - ((value - minValue) / range) * (height - padding * 2)
-
- if (index === 0) {
- ctx.moveTo(x, y)
- } else {
- ctx.lineTo(x, y)
- }
- })
-
- ctx.strokeStyle = lineColor
- ctx.lineWidth = 1.5
- ctx.stroke()
- })
- }
- // 生成模拟趋势数据
- const generateMockTrendData = (changePercent) => {
- const change = parseFloat(String(changePercent || '0').replace('%', '').replace('+', ''))
- const points = 15
- const data = []
-
- let baseValue = 100
- const trend = change / 100
-
- for (let i = 0; i < points; i++) {
- const randomChange = (Math.random() - 0.5) * 6
- const trendChange = (i / points) * trend * 100
- baseValue = baseValue + randomChange + trendChange / points
- data.push(baseValue)
- }
-
- return data
- }
- // 绘制所有股票的趋势图
- const drawAllTrendCharts = () => {
- if (myStocks.value.length === 0) return
-
- // 确保页面可见时才绘制
- if (!isPageVisible.value) return
-
- nextTick(() => {
- // 增加延迟确保 canvas 节点完全渲染(解决页面切换后 canvas 上下文丢失问题)
- setTimeout(() => {
- if (!isPageVisible.value) return
-
- myStocks.value.forEach((stock, index) => {
- setTimeout(() => {
- if (isPageVisible.value) {
- drawTrendChart(stock)
- }
- }, index * 30)
- })
- }, 350)
- })
- }
- // 加载我的股票列表(从服务器数据库查询)
- const loadMyStocks = async (forceRefresh = false) => {
- console.log('[我的股票] loadMyStocks 开始执行, isLoggedIn=', isLoggedIn.value, 'forceRefresh=', forceRefresh)
-
- if (!isLoggedIn.value) {
- myStocks.value = []
- stopAutoRefresh()
- return
- }
-
- // 如果正在加载中,跳过
- if (isLoading.value) {
- console.log('[我的股票] 正在加载中,跳过')
- return
- }
-
- // 如果有缓存数据且未过期,只刷新行情不重新加载列表
- const now = Date.now()
- if (!forceRefresh && myStocks.value.length > 0 && (now - lastLoadTime.value) < CACHE_DURATION) {
- console.log('[我的股票] 使用缓存数据,只刷新行情')
- // 只刷新行情数据
- await fetchIndexData()
- await refreshAllQuotes()
- // 重新绘制趋势图(解决页面切换回来后canvas显示异常的问题)
- drawAllTrendCharts()
- startAutoRefresh()
- return
- }
-
- try {
- isLoading.value = true
-
- // 只有首次加载或强制刷新时显示loading
- let showedLoading = false
- if (myStocks.value.length === 0 || forceRefresh) {
- uni.showLoading({ title: '加载中...' })
- showedLoading = true
- }
-
- // 从服务器获取用户自选股票
- console.log('[我的股票] 调用 getUserStocks 接口')
- const res = await getUserStocks()
- console.log('[我的股票] 服务器返回:', JSON.stringify(res))
-
- if (showedLoading) {
- uni.hideLoading()
- }
-
- if (res.code === 200 && res.data) {
- // 转换数据格式
- myStocks.value = res.data.map(item => ({
- code: item.stockCode,
- name: item.stockName,
- addPrice: item.addPrice,
- addDate: item.addDate,
- currentPrice: item.currentPrice,
- profitPercent: item.profitPercent,
- priceChange: item.priceChange,
- changePercent: item.changePercent,
- trendData: item.trendData
- }))
- lastLoadTime.value = Date.now()
- console.log('[我的股票] 加载完成, 股票数量:', myStocks.value.length)
- } else {
- myStocks.value = []
- console.log('[我的股票] 返回数据为空')
- }
-
- // 获取上证指数
- await fetchIndexData()
-
- // 如果有股票数据,刷新行情
- if (myStocks.value.length > 0) {
- await refreshAllQuotes()
- // 绘制趋势图
- drawAllTrendCharts()
- }
-
- // 登录后启动定时刷新
- startAutoRefresh()
- } catch (e) {
- // 安全地隐藏loading
- try { uni.hideLoading() } catch (err) {}
- console.error('[我的股票] 加载失败:', e)
- // 加载失败时不清空已有数据
- if (myStocks.value.length === 0) {
- myStocks.value = []
- }
- startAutoRefresh()
- } finally {
- isLoading.value = false
- }
- }
- // 批量刷新所有股票行情(只更新价格,不更新趋势图)
- const refreshPriceOnly = async () => {
- if (myStocks.value.length === 0) return
-
- try {
- const codes = myStocks.value.map(stock => stock.code).join(',')
- const quoteRes = await getStockQuotes(codes)
-
- if (quoteRes.code === 200 && quoteRes.data && quoteRes.data.length > 0) {
- quoteRes.data.forEach(quoteData => {
- const index = myStocks.value.findIndex(stock => stock.code === quoteData.stockCode)
- if (index !== -1) {
- const stock = myStocks.value[index]
- stock.priceChange = quoteData.priceChange
- stock.changePercent = quoteData.changePercent
- stock.currentPrice = quoteData.currentPrice
- stock.name = quoteData.stockName || stock.name
- // 不更新 trendData
-
- // 计算自选收益(当前价格相对于加入价格的涨跌幅)
- if (stock.addPrice && quoteData.currentPrice) {
- const addPrice = parseFloat(stock.addPrice)
- const currentPrice = parseFloat(quoteData.currentPrice)
- if (addPrice > 0) {
- const profit = ((currentPrice - addPrice) / addPrice * 100).toFixed(2)
- stock.profitPercent = profit >= 0 ? `+${profit}%` : `${profit}%`
- }
- }
- }
- })
- }
- } catch (e) {
- console.error('[我的股票] 价格刷新异常:', e.message)
- }
- }
- // 批量刷新所有股票行情(包含趋势图)
- const refreshAllQuotes = async () => {
- if (myStocks.value.length === 0) return
-
- try {
- const codes = myStocks.value.map(stock => stock.code).join(',')
- const quoteRes = await getStockQuotes(codes)
-
- if (quoteRes.code === 200 && quoteRes.data && quoteRes.data.length > 0) {
- quoteRes.data.forEach(quoteData => {
- const index = myStocks.value.findIndex(stock => stock.code === quoteData.stockCode)
- if (index !== -1) {
- const stock = myStocks.value[index]
- stock.priceChange = quoteData.priceChange
- stock.changePercent = quoteData.changePercent
- stock.currentPrice = quoteData.currentPrice
- stock.name = quoteData.stockName || stock.name
- stock.trendData = quoteData.trendData || null
-
- // 计算自选收益(当前价格相对于加入价格的涨跌幅)
- if (stock.addPrice && quoteData.currentPrice) {
- const addPrice = parseFloat(stock.addPrice)
- const currentPrice = parseFloat(quoteData.currentPrice)
- if (addPrice > 0) {
- const profit = ((currentPrice - addPrice) / addPrice * 100).toFixed(2)
- stock.profitPercent = profit >= 0 ? `+${profit}%` : `${profit}%`
- }
- }
- }
- })
- }
- } catch (e) {
- console.error('[我的股票] 刷新异常:', e.message)
- }
- }
- // 启动定时刷新(价格2-3秒,趋势图10秒)
- const startAutoRefresh = () => {
- if (!isLoggedIn.value || !isPageVisible.value) return
- stopAutoRefresh()
-
- // 价格刷新定时器 (2-3秒随机间隔)
- const schedulePriceRefresh = () => {
- if (!isPageVisible.value) {
- stopAutoRefresh()
- return
- }
-
- priceRefreshTimer = setTimeout(async () => {
- if (!isPageVisible.value) {
- stopAutoRefresh()
- return
- }
-
- await fetchIndexData()
- if (myStocks.value.length > 0) {
- await refreshPriceOnly()
- }
- schedulePriceRefresh()
- }, getRandomInterval())
- }
-
- // 趋势图刷新定时器 (10秒固定间隔)
- const scheduleTrendRefresh = () => {
- if (!isPageVisible.value) {
- stopAutoRefresh()
- return
- }
-
- trendRefreshTimer = setTimeout(async () => {
- if (!isPageVisible.value) {
- stopAutoRefresh()
- return
- }
-
- if (myStocks.value.length > 0) {
- await refreshAllQuotes()
- drawAllTrendCharts()
- }
- scheduleTrendRefresh()
- }, 10000)
- }
-
- schedulePriceRefresh()
- scheduleTrendRefresh()
- }
- // 停止定时刷新
- const stopAutoRefresh = () => {
- if (priceRefreshTimer) {
- clearTimeout(priceRefreshTimer)
- priceRefreshTimer = null
- }
- if (trendRefreshTimer) {
- clearTimeout(trendRefreshTimer)
- trendRefreshTimer = null
- }
- }
- // 跳转到登录页
- const goToLogin = () => {
- uni.navigateTo({ url: '/pages/login/login' })
- }
- // 删除股票
- const removeStock = async (idx) => {
- const stock = myStocks.value[idx]
- // 先重置滑动状态
- resetSlide(stock.code)
-
- uni.showModal({
- title: '确认删除',
- content: `确定要删除 ${stock.name} 吗?`,
- confirmText: '删除',
- cancelText: '取消',
- success: async (res) => {
- if (res.confirm) {
- try {
- // 调用服务器删除接口
- await deleteUserStock(stock.code)
- // 清理滑动状态
- delete slideStates[stock.code]
- clearSlideTimer(stock.code)
- // 从本地列表删除
- myStocks.value.splice(idx, 1)
- uni.showToast({ title: '删除成功', icon: 'success' })
-
- if (myStocks.value.length === 0) {
- stopAutoRefresh()
- }
- } catch (e) {
- console.error('删除失败:', e)
- uni.showToast({ title: '删除失败', icon: 'none' })
- }
- }
- }
- })
- }
- onLoad(() => {
- console.log('[我的股票] onLoad 触发')
- componentInstance = getCurrentInstance()
- isLoggedIn.value = checkLoginStatus()
- isPageVisible.value = true
- loadMyStocks(true) // 首次加载强制刷新
- })
- onShow(() => {
- console.log('[我的股票] onShow 触发')
- isPageVisible.value = true
-
- // 立即清空所有 canvas,避免显示异常的长条
- if (myStocks.value.length > 0) {
- clearAllCanvases()
- }
-
- const wasLoggedIn = isLoggedIn.value
- isLoggedIn.value = checkLoginStatus()
-
- // 如果登录状态变化了,强制刷新
- if (wasLoggedIn !== isLoggedIn.value) {
- loadMyStocks(true)
- } else {
- // 否则使用缓存策略
- loadMyStocks(false)
- }
-
- uni.setNavigationBarTitle({ title: '量化交易大师' })
- })
- onHide(() => {
- console.log('[我的股票] onHide 触发')
- isPageVisible.value = false
- stopAutoRefresh()
- })
- onUnload(() => {
- console.log('[我的股票] onUnload 触发')
- isPageVisible.value = false
- stopAutoRefresh()
- clearAllSlideTimers()
- })
- </script>
- <style>
- .page-rank {
- display: flex;
- flex-direction: column;
- background: #f5f6fb;
- height: 100vh;
- }
- .scroll-view {
- flex: 1;
- height: 0;
- }
- .content-wrapper {
- padding: 32rpx;
- min-height: 100%;
- }
- /* 上证指数卡片 */
- .index-card {
- display: flex;
- align-items: center;
- justify-content: space-between;
- background: #ffffff;
- border-radius: 24rpx;
- padding: 32rpx;
- margin-bottom: 24rpx;
- box-shadow: 0 8rpx 24rpx rgba(37, 52, 94, 0.08);
- }
- .index-left {
- display: flex;
- flex-direction: column;
- }
- .index-price-row {
- display: flex;
- align-items: baseline;
- margin-bottom: 8rpx;
- }
- .index-price {
- font-size: 48rpx;
- font-weight: 700;
- margin-right: 16rpx;
- }
- .index-change {
- font-size: 28rpx;
- font-weight: 600;
- }
- .index-name-row {
- display: flex;
- align-items: center;
- }
- .index-name {
- font-size: 26rpx;
- color: #666666;
- margin-right: 12rpx;
- }
- .index-percent {
- font-size: 26rpx;
- font-weight: 600;
- }
- .index-up {
- color: #FF3B30;
- }
- .index-down {
- color: #34C759;
- }
- /* 分段控制器 */
- .segment-control {
- display: flex;
- align-items: center;
- background: #F5F7FA;
- border-radius: 32rpx;
- padding: 6rpx;
- margin-bottom: 24rpx;
- position: relative;
- height: 64rpx;
- box-sizing: border-box;
- }
- .segment-slider {
- position: absolute;
- left: 6rpx;
- top: 6rpx;
- width: calc(50% - 6rpx);
- height: calc(100% - 12rpx);
- background: #ffffff;
- border-radius: 26rpx;
- box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.05);
- transition: transform 0.3s ease;
- z-index: 0;
- }
- .segment-slider.slider-right {
- transform: translateX(100%);
- }
- .segment-item {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- height: 100%;
- z-index: 1;
- transition: color 0.3s ease;
- }
- .segment-icon {
- font-size: 24rpx;
- margin-right: 8rpx;
- color: #999999;
- transition: color 0.3s ease;
- }
- .segment-text {
- font-size: 26rpx;
- color: #999999;
- font-weight: 500;
- transition: color 0.3s ease;
- }
- .segment-active .segment-icon,
- .segment-active .segment-text {
- color: #E53935;
- }
- /* 隐藏空列表 */
- .hidden-list {
- display: none !important;
- }
- /* 股票列表卡片 */
- .stock-list-card,
- .stock-table-card {
- background: #ffffff;
- border-radius: 32rpx;
- padding: 32rpx;
- box-shadow: 0 16rpx 48rpx rgba(37, 52, 94, 0.1);
- }
- .list-header {
- display: flex;
- align-items: center;
- margin-bottom: 28rpx;
- }
- .list-dot {
- width: 12rpx;
- height: 12rpx;
- border-radius: 50%;
- background: #3abf81;
- margin-right: 12rpx;
- }
- .list-dot-blue {
- background: #5B5AEA;
- }
- .list-title {
- font-size: 28rpx;
- font-weight: 600;
- color: #1a1a2e;
- letter-spacing: 1rpx;
- }
- .list-count {
- font-size: 24rpx;
- color: #9ca3af;
- margin-left: 12rpx;
- }
- /* 股票列表 */
- .stock-list {
- display: flex;
- flex-direction: column;
- gap: 16rpx;
- }
- /* 滑动删除容器 */
- .stock-item-wrapper {
- position: relative;
- height: 160rpx;
- overflow: hidden;
- border-radius: 20rpx;
- background: #fafbfc;
- }
- .movable-area {
- width: 100%;
- height: 100%;
- }
- .movable-view {
- width: calc(100% + 160rpx);
- height: 100%;
- }
- .stock-item {
- display: flex;
- align-items: center;
- padding: 20rpx 16rpx;
- background: #fafbfc;
- border-radius: 20rpx;
- height: 100%;
- box-sizing: border-box;
- }
- /* 滑动删除按钮 */
- .slide-delete-btn {
- flex-shrink: 0;
- width: 160rpx;
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #fafbfc;
- }
- .delete-icon-circle {
- width: 72rpx;
- height: 72rpx;
- background: #ef4444;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- box-shadow: 0 6rpx 16rpx rgba(239, 68, 68, 0.4);
- }
- .delete-icon-text {
- font-size: 40rpx;
- color: #ffffff;
- font-weight: bold;
- line-height: 1;
- }
- .stock-main {
- display: flex;
- align-items: center;
- flex: 1;
- width: 100%;
- }
- .stock-info {
- display: flex;
- flex-direction: column;
- min-width: 130rpx;
- flex-shrink: 0;
- }
- .stock-name {
- font-size: 30rpx;
- font-weight: 700;
- color: #1a1a2e;
- margin-bottom: 8rpx;
- letter-spacing: 0.5rpx;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- }
- .stock-code-row {
- display: flex;
- align-items: center;
- }
- .stock-tag {
- font-size: 18rpx;
- padding: 3rpx 8rpx;
- border-radius: 6rpx;
- color: #ffffff;
- font-weight: 600;
- margin-right: 6rpx;
- flex-shrink: 0;
- }
- .market-sh {
- background: #ef4444;
- }
- .market-sz {
- background: #22c55e;
- }
- .market-cy {
- background: #f59e0b;
- }
- .stock-code {
- font-size: 22rpx;
- color: #6b7280;
- font-weight: 500;
- text-rendering: optimizeLegibility;
- }
- .stock-quote {
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- flex-shrink: 0;
- min-width: 140rpx;
- }
- /* 趋势图 */
- .stock-chart {
- flex: 1;
- height: 56rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- margin: 0 16rpx;
- }
- .trend-canvas {
- width: 100%;
- height: 56rpx;
- }
- .stock-price {
- font-size: 32rpx;
- font-weight: 700;
- color: #1a1a2e;
- margin-bottom: 6rpx;
- font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', sans-serif;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- }
- .stock-change {
- font-size: 22rpx;
- font-weight: 600;
- padding: 4rpx 10rpx;
- border-radius: 8rpx;
- text-rendering: optimizeLegibility;
- }
- /* 表格视图 */
- .table-header {
- display: flex;
- align-items: center;
- padding: 20rpx 16rpx;
- background: #f8f9fc;
- border-radius: 12rpx;
- margin-bottom: 16rpx;
- }
- .table-header text {
- font-size: 24rpx;
- color: #9ca3af;
- font-weight: 600;
- }
- .th-name {
- flex: 1;
- }
- .th-date {
- width: 140rpx;
- text-align: center;
- }
- .th-price {
- width: 120rpx;
- text-align: center;
- }
- .th-profit {
- width: 120rpx;
- text-align: right;
- }
- .table-list {
- display: flex;
- flex-direction: column;
- gap: 12rpx;
- }
- .table-row {
- display: flex;
- align-items: center;
- padding: 24rpx 16rpx;
- background: #fafbfc;
- border-radius: 16rpx;
- }
- .td-name {
- flex: 1;
- display: flex;
- flex-direction: column;
- }
- .td-name .stock-name {
- font-size: 28rpx;
- font-weight: 700;
- color: #1a1a2e;
- margin-bottom: 4rpx;
- }
- .td-date {
- width: 140rpx;
- text-align: center;
- font-size: 24rpx;
- color: #6b7280;
- }
- .td-price {
- width: 120rpx;
- text-align: center;
- font-size: 26rpx;
- font-weight: 600;
- color: #1a1a2e;
- }
- .td-profit-wrap {
- width: 120rpx;
- display: flex;
- justify-content: flex-end;
- }
- .td-profit {
- font-size: 24rpx;
- font-weight: 600;
- padding: 6rpx 12rpx;
- border-radius: 8rpx;
- }
- .profit-up {
- color: #ef4444;
- background: rgba(239, 68, 68, 0.1);
- }
- .profit-down {
- color: #22c55e;
- background: rgba(34, 197, 94, 0.1);
- }
- /* 空状态 */
- .empty-content {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 200rpx 60rpx;
- }
- .empty-icon {
- font-size: 120rpx;
- margin-bottom: 40rpx;
- }
- .empty-text {
- font-size: 32rpx;
- font-weight: 600;
- color: #333333;
- margin-bottom: 16rpx;
- }
- .empty-desc {
- font-size: 26rpx;
- color: #999999;
- text-align: center;
- line-height: 1.6;
- }
- .bottom-safe-area {
- height: 80rpx;
- }
- /* 模糊效果 */
- .blur-content {
- filter: blur(8rpx);
- pointer-events: none;
- }
- /* 登录遮罩层 */
- .login-mask {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.4);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 999;
- }
- .login-prompt {
- width: 560rpx;
- max-width: 560rpx;
- background: #ffffff;
- border-radius: 24rpx;
- padding: 50rpx 40rpx;
- margin: 0 auto;
- text-align: center;
- box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3);
- box-sizing: border-box;
- }
- .lock-icon {
- font-size: 72rpx;
- margin-bottom: 20rpx;
- }
- .prompt-title {
- display: block;
- font-size: 32rpx;
- font-weight: 600;
- color: #222222;
- margin-bottom: 12rpx;
- }
- .prompt-desc {
- display: block;
- font-size: 24rpx;
- color: #999999;
- line-height: 1.5;
- margin-bottom: 32rpx;
- }
- .login-button-native {
- width: 100%;
- height: 80rpx;
- background: linear-gradient(135deg, #4CAF50, #66BB6A);
- color: #ffffff;
- border-radius: 40rpx;
- font-size: 30rpx;
- font-weight: 600;
- box-shadow: 0 8rpx 24rpx rgba(76, 175, 80, 0.4);
- display: flex;
- align-items: center;
- justify-content: center;
- border: none;
- padding: 0;
- line-height: 80rpx;
- }
- .login-button-native::after {
- border: none;
- }
- </style>
|