行情数据做状态管理
This commit is contained in:
parent
671a6355da
commit
6a81305bad
@ -24,7 +24,7 @@ import {
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import VChart from 'vue-echarts'
|
||||
import type { Candle, QuoteData } from '../../types'
|
||||
import type { KlinePeriod } from '../../composables/useQuote'
|
||||
import type { KlinePeriod } from '../../stores/quota'
|
||||
|
||||
use([
|
||||
CanvasRenderer,
|
||||
|
||||
@ -1,32 +1,22 @@
|
||||
import { onMounted, ref } from 'vue'
|
||||
import type { NewsItem } from '../types'
|
||||
import { newsMock } from '../mocks/news'
|
||||
import { contractConfig } from '../config/contract'
|
||||
import { getFuturesNews } from '../api/baidu/news'
|
||||
import { mapBaiduNewsToItems } from '../api/baidu/mapNews'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted } from 'vue'
|
||||
import { useQuotaStore } from '../stores/quota'
|
||||
|
||||
/**
|
||||
* 新闻 composable:状态统一落在 quota store。
|
||||
*/
|
||||
export function useNews() {
|
||||
const data = ref<NewsItem[]>(structuredClone(newsMock))
|
||||
const loading = ref(false)
|
||||
const error = ref<unknown>(null)
|
||||
|
||||
async function refresh() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const list = await getFuturesNews({ code: contractConfig.code })
|
||||
data.value = mapBaiduNewsToItems(list)
|
||||
} catch (e) {
|
||||
error.value = e
|
||||
console.error('[useNews] 拉取新闻失败', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
const store = useQuotaStore()
|
||||
const { news: data, newsLoading: loading, newsError: error } = storeToRefs(store)
|
||||
|
||||
onMounted(() => {
|
||||
void refresh()
|
||||
void store.fetchNews()
|
||||
})
|
||||
|
||||
return { data, loading, error, refresh }
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
refresh: () => store.fetchNews(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,28 +1,21 @@
|
||||
import { ref } from 'vue'
|
||||
import type { PositionsData } from '../types'
|
||||
import { positionsMock } from '../mocks/positions'
|
||||
|
||||
function delay(ms = 300): Promise<void> {
|
||||
return new Promise((r) => setTimeout(r, ms))
|
||||
}
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useQuotaStore } from '../stores/quota'
|
||||
|
||||
/**
|
||||
* 机构持仓 composable:状态统一落在 quota store(暂 mock)。
|
||||
*/
|
||||
export function usePositions() {
|
||||
const data = ref<PositionsData>(structuredClone(positionsMock))
|
||||
const loading = ref(false)
|
||||
const error = ref<unknown>(null)
|
||||
const store = useQuotaStore()
|
||||
const {
|
||||
positions: data,
|
||||
positionsLoading: loading,
|
||||
positionsError: error,
|
||||
} = storeToRefs(store)
|
||||
|
||||
async function refresh() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
await delay()
|
||||
data.value = structuredClone(positionsMock)
|
||||
} catch (e) {
|
||||
error.value = e
|
||||
} finally {
|
||||
loading.value = false
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
refresh: () => store.fetchPositions(),
|
||||
}
|
||||
}
|
||||
|
||||
return { data, loading, error, refresh }
|
||||
}
|
||||
|
||||
@ -1,81 +1,28 @@
|
||||
import { onMounted, ref } from 'vue'
|
||||
import type { Candle, QuoteData } from '../types'
|
||||
import { quoteMock } from '../mocks/quote'
|
||||
import { contractConfig } from '../config/contract'
|
||||
import { getStockKline, getStockQuotation } from '../api/baidu/quotation'
|
||||
import { mapBaiduQuotationToQuote } from '../api/baidu/mapQuote'
|
||||
import { mapBaiduKlineToCandles } from '../api/baidu/mapKline'
|
||||
import type { BaiduKlineType } from '../api/baidu/types'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted } from 'vue'
|
||||
import { useQuotaStore, type KlinePeriod } from '../stores/quota'
|
||||
|
||||
export type KlinePeriod = 'day' | 'week' | 'month'
|
||||
|
||||
const KTYPE_MAP: Record<KlinePeriod, BaiduKlineType> = {
|
||||
day: 1,
|
||||
week: 2,
|
||||
month: 3,
|
||||
}
|
||||
export type { KlinePeriod }
|
||||
|
||||
/**
|
||||
* 行情 composable:状态统一落在 quota store,便于 AI 分析读取。
|
||||
* 页面打开时只拉分时/盘口;K 线点击 Tab 再拉。
|
||||
*/
|
||||
export function useQuote() {
|
||||
const data = ref<QuoteData>(structuredClone(quoteMock))
|
||||
const loading = ref(false)
|
||||
const klineLoading = ref(false)
|
||||
const error = ref<unknown>(null)
|
||||
const store = useQuotaStore()
|
||||
const { quote: data, quoteLoading: loading, klineLoading, quoteError: error } =
|
||||
storeToRefs(store)
|
||||
|
||||
/** 已加载过的周期,避免重复请求 */
|
||||
const loadedKlines = ref<Record<KlinePeriod, boolean>>({
|
||||
day: false,
|
||||
week: false,
|
||||
month: false,
|
||||
})
|
||||
|
||||
async function refresh() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const result = await getStockQuotation({ code: contractConfig.code })
|
||||
const quote = mapBaiduQuotationToQuote(result)
|
||||
// 分时刷新时清空 K 线缓存,点击 Tab 再拉
|
||||
quote.candles = { day: [], week: [], month: [] }
|
||||
loadedKlines.value = { day: false, week: false, month: false }
|
||||
data.value = quote
|
||||
} catch (e) {
|
||||
error.value = e
|
||||
console.error('[useQuote] 拉取行情失败', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 按需加载日/周/月 K 线;已加载则跳过 */
|
||||
async function loadKline(period: KlinePeriod): Promise<Candle[]> {
|
||||
if (loadedKlines.value[period]) {
|
||||
return data.value.candles[period]
|
||||
}
|
||||
|
||||
klineLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const result = await getStockKline({
|
||||
code: contractConfig.code,
|
||||
ktype: KTYPE_MAP[period],
|
||||
})
|
||||
const candles = mapBaiduKlineToCandles(result)
|
||||
data.value.candles[period] = candles
|
||||
loadedKlines.value[period] = true
|
||||
return candles
|
||||
} catch (e) {
|
||||
error.value = e
|
||||
console.error(`[useQuote] 拉取${period}K线失败`, e)
|
||||
throw e
|
||||
} finally {
|
||||
klineLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 页面打开时只请求分时/盘口;K 线点击 Tab 再拉
|
||||
onMounted(() => {
|
||||
void refresh()
|
||||
void store.fetchQuote()
|
||||
})
|
||||
|
||||
return { data, loading, klineLoading, error, refresh, loadKline }
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
klineLoading,
|
||||
error,
|
||||
refresh: () => store.fetchQuote(),
|
||||
loadKline: (period: KlinePeriod) => store.loadKline(period),
|
||||
}
|
||||
}
|
||||
|
||||
186
src/stores/quota.ts
Normal file
186
src/stores/quota.ts
Normal file
@ -0,0 +1,186 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import type { Candle, NewsItem, PositionsData, QuoteData } from '../types'
|
||||
import { quoteMock } from '../mocks/quote'
|
||||
import { newsMock } from '../mocks/news'
|
||||
import { positionsMock } from '../mocks/positions'
|
||||
import { contractConfig } from '../config/contract'
|
||||
import { getStockKline, getStockQuotation } from '../api/baidu/quotation'
|
||||
import { getFuturesNews } from '../api/baidu/news'
|
||||
import { mapBaiduQuotationToQuote } from '../api/baidu/mapQuote'
|
||||
import { mapBaiduKlineToCandles } from '../api/baidu/mapKline'
|
||||
import { mapBaiduNewsToItems } from '../api/baidu/mapNews'
|
||||
import type { BaiduKlineType } from '../api/baidu/types'
|
||||
|
||||
/** K 线周期(日/周/月) */
|
||||
export type KlinePeriod = 'day' | 'week' | 'month'
|
||||
|
||||
const KTYPE_MAP: Record<KlinePeriod, BaiduKlineType> = {
|
||||
day: 1,
|
||||
week: 2,
|
||||
month: 3,
|
||||
}
|
||||
|
||||
/** 供 AI 分析消费的数据快照(可序列化) */
|
||||
export interface AnalysisSnapshot {
|
||||
contract: {
|
||||
code: string
|
||||
name: string
|
||||
exchange: string
|
||||
}
|
||||
quote: QuoteData | null
|
||||
news: NewsItem[]
|
||||
positions: PositionsData | null
|
||||
fetchedAt: string
|
||||
}
|
||||
|
||||
function emptyCandles(): QuoteData['candles'] {
|
||||
return { day: [], week: [], month: [] }
|
||||
}
|
||||
|
||||
export const useQuotaStore = defineStore('quota', () => {
|
||||
// ─── 行情 ───────────────────────────────────────────────
|
||||
const quote = ref<QuoteData>(structuredClone(quoteMock))
|
||||
const quoteLoading = ref(false)
|
||||
const klineLoading = ref(false)
|
||||
const quoteError = ref<unknown>(null)
|
||||
const loadedKlines = ref<Record<KlinePeriod, boolean>>({
|
||||
day: false,
|
||||
week: false,
|
||||
month: false,
|
||||
})
|
||||
|
||||
// ─── 新闻 ───────────────────────────────────────────────
|
||||
const news = ref<NewsItem[]>(structuredClone(newsMock))
|
||||
const newsLoading = ref(false)
|
||||
const newsError = ref<unknown>(null)
|
||||
|
||||
// ─── 机构持仓(暂 mock,后续接 API)────────────────────
|
||||
const positions = ref<PositionsData>(structuredClone(positionsMock))
|
||||
const positionsLoading = ref(false)
|
||||
const positionsError = ref<unknown>(null)
|
||||
|
||||
/** 是否已有至少一次成功的行情拉取(可用于 AI 门禁) */
|
||||
const hasQuote = computed(() => quote.value.last > 0 && quote.value.intraday.length > 0)
|
||||
const hasNews = computed(() => news.value.length > 0)
|
||||
|
||||
async function fetchQuote() {
|
||||
quoteLoading.value = true
|
||||
quoteError.value = null
|
||||
try {
|
||||
const result = await getStockQuotation({ code: contractConfig.code })
|
||||
const mapped = mapBaiduQuotationToQuote(result)
|
||||
mapped.candles = emptyCandles()
|
||||
loadedKlines.value = { day: false, week: false, month: false }
|
||||
quote.value = mapped
|
||||
} catch (e) {
|
||||
quoteError.value = e
|
||||
console.error('[quota] 拉取行情失败', e)
|
||||
} finally {
|
||||
quoteLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 按需加载日/周/月 K 线;已加载则直接返回缓存 */
|
||||
async function loadKline(period: KlinePeriod): Promise<Candle[]> {
|
||||
if (loadedKlines.value[period]) {
|
||||
return quote.value.candles[period]
|
||||
}
|
||||
|
||||
klineLoading.value = true
|
||||
quoteError.value = null
|
||||
try {
|
||||
const result = await getStockKline({
|
||||
code: contractConfig.code,
|
||||
ktype: KTYPE_MAP[period],
|
||||
})
|
||||
const candles = mapBaiduKlineToCandles(result)
|
||||
quote.value.candles[period] = candles
|
||||
loadedKlines.value[period] = true
|
||||
return candles
|
||||
} catch (e) {
|
||||
quoteError.value = e
|
||||
console.error(`[quota] 拉取${period}K线失败`, e)
|
||||
throw e
|
||||
} finally {
|
||||
klineLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchNews() {
|
||||
newsLoading.value = true
|
||||
newsError.value = null
|
||||
try {
|
||||
const list = await getFuturesNews({ code: contractConfig.code })
|
||||
news.value = mapBaiduNewsToItems(list)
|
||||
} catch (e) {
|
||||
newsError.value = e
|
||||
console.error('[quota] 拉取新闻失败', e)
|
||||
} finally {
|
||||
newsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPositions() {
|
||||
positionsLoading.value = true
|
||||
positionsError.value = null
|
||||
try {
|
||||
// TODO: 接入真实持仓 API 后替换
|
||||
await new Promise((r) => setTimeout(r, 300))
|
||||
positions.value = structuredClone(positionsMock)
|
||||
} catch (e) {
|
||||
positionsError.value = e
|
||||
console.error('[quota] 拉取持仓失败', e)
|
||||
} finally {
|
||||
positionsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 并行刷新行情 + 新闻 + 持仓(K 线仍按需) */
|
||||
async function fetchAll() {
|
||||
await Promise.all([fetchQuote(), fetchNews(), fetchPositions()])
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出当前全部接口数据快照,供下一步 AI 分析拼接 Prompt。
|
||||
* 返回深拷贝,避免分析过程中被行情刷新污染。
|
||||
*/
|
||||
function getAnalysisSnapshot(): AnalysisSnapshot {
|
||||
return {
|
||||
contract: {
|
||||
code: quote.value.code || contractConfig.code,
|
||||
name: quote.value.name || contractConfig.name,
|
||||
exchange: quote.value.exchange || contractConfig.exchange,
|
||||
},
|
||||
quote: quote.value ? structuredClone(quote.value) : null,
|
||||
news: structuredClone(news.value),
|
||||
positions: positions.value ? structuredClone(positions.value) : null,
|
||||
fetchedAt: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// state
|
||||
quote,
|
||||
quoteLoading,
|
||||
klineLoading,
|
||||
quoteError,
|
||||
loadedKlines,
|
||||
news,
|
||||
newsLoading,
|
||||
newsError,
|
||||
positions,
|
||||
positionsLoading,
|
||||
positionsError,
|
||||
// computed
|
||||
hasQuote,
|
||||
hasNews,
|
||||
// actions
|
||||
fetchQuote,
|
||||
loadKline,
|
||||
fetchNews,
|
||||
fetchPositions,
|
||||
fetchAll,
|
||||
getAnalysisSnapshot,
|
||||
}
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user