diff --git a/src/components/quote/ChartPanel.vue b/src/components/quote/ChartPanel.vue index 3d7a7b9..439ecbc 100644 --- a/src/components/quote/ChartPanel.vue +++ b/src/components/quote/ChartPanel.vue @@ -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, diff --git a/src/composables/useNews.ts b/src/composables/useNews.ts index 603a3b7..cefca27 100644 --- a/src/composables/useNews.ts +++ b/src/composables/useNews.ts @@ -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(structuredClone(newsMock)) - const loading = ref(false) - const error = ref(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(), + } } diff --git a/src/composables/usePositions.ts b/src/composables/usePositions.ts index ffd3510..ab6f84a 100644 --- a/src/composables/usePositions.ts +++ b/src/composables/usePositions.ts @@ -1,28 +1,21 @@ -import { ref } from 'vue' -import type { PositionsData } from '../types' -import { positionsMock } from '../mocks/positions' - -function delay(ms = 300): Promise { - 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(structuredClone(positionsMock)) - const loading = ref(false) - const error = ref(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 } } diff --git a/src/composables/useQuote.ts b/src/composables/useQuote.ts index a2b5e83..936f941 100644 --- a/src/composables/useQuote.ts +++ b/src/composables/useQuote.ts @@ -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 = { - day: 1, - week: 2, - month: 3, -} +export type { KlinePeriod } +/** + * 行情 composable:状态统一落在 quota store,便于 AI 分析读取。 + * 页面打开时只拉分时/盘口;K 线点击 Tab 再拉。 + */ export function useQuote() { - const data = ref(structuredClone(quoteMock)) - const loading = ref(false) - const klineLoading = ref(false) - const error = ref(null) + const store = useQuotaStore() + const { quote: data, quoteLoading: loading, klineLoading, quoteError: error } = + storeToRefs(store) - /** 已加载过的周期,避免重复请求 */ - const loadedKlines = ref>({ - 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 { - 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), + } } diff --git a/src/stores/quota.ts b/src/stores/quota.ts new file mode 100644 index 0000000..5eab710 --- /dev/null +++ b/src/stores/quota.ts @@ -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 = { + 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(structuredClone(quoteMock)) + const quoteLoading = ref(false) + const klineLoading = ref(false) + const quoteError = ref(null) + const loadedKlines = ref>({ + day: false, + week: false, + month: false, + }) + + // ─── 新闻 ─────────────────────────────────────────────── + const news = ref(structuredClone(newsMock)) + const newsLoading = ref(false) + const newsError = ref(null) + + // ─── 机构持仓(暂 mock,后续接 API)──────────────────── + const positions = ref(structuredClone(positionsMock)) + const positionsLoading = ref(false) + const positionsError = ref(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 { + 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, + } +})