From 8d1162a2afbd700a3f253c310c174347bec6a2df Mon Sep 17 00:00:00 2001
From: dongzp <975303544@qq.com>
Date: Wed, 22 Jul 2026 11:19:10 +0800
Subject: [PATCH] =?UTF-8?q?AI=E5=88=86=E6=9E=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/api/baidu/mapQuote.ts | 21 ++++++++++++++++
src/api/baidu/mapWsQuote.ts | 18 ++++++++++++++
src/components/ai/AiAdviceDrawer.vue | 5 +++-
src/components/quote/QuoteStats.vue | 8 ++++++
src/composables/useAiAdvice.ts | 37 ++++++++++++++++++++++------
src/stores/quota.ts | 1 +
src/types/index.ts | 2 ++
7 files changed, 83 insertions(+), 9 deletions(-)
diff --git a/src/api/baidu/mapQuote.ts b/src/api/baidu/mapQuote.ts
index ad3ee1b..3a22e0b 100644
--- a/src/api/baidu/mapQuote.ts
+++ b/src/api/baidu/mapQuote.ts
@@ -22,6 +22,22 @@ function parseAmountYi(value: string | undefined, rawAmount?: string): number {
return toNum(value)
}
+/** 将「-4.20万」「+8.29万手」或原始手数转为手数 */
+function parseDeltaHands(text: string): number {
+ const cleaned = text.replace(/手/g, '')
+ if (cleaned.includes('万')) return Math.round(toNum(cleaned.replace('万', '')) * 10000)
+ return toNum(cleaned)
+}
+
+function parseAmountDelta(value: string | undefined, raw?: string): number {
+ if (raw != null && raw !== '' && raw !== '--' && !String(raw).includes('万')) {
+ return toNum(raw)
+ }
+ if (value && value !== '--') return parseDeltaHands(value)
+ if (raw && String(raw).includes('万')) return parseDeltaHands(String(raw))
+ return toNum(raw)
+}
+
function parseIntraday(result: BaiduQuotationResult): IntradayPoint[] {
const days = result.newMarketData?.marketData ?? []
const points: IntradayPoint[] = []
@@ -99,6 +115,10 @@ export function mapBaiduQuotationToQuote(result: BaiduQuotationResult): QuoteDat
result.pankouinfos?.list?.find((i) => i.ename === 'amount')?.value,
op?.amount,
)
+ const amountDelta = parseAmountDelta(
+ result.pankouinfos?.list?.find((i) => i.ename === 'amountDelta')?.value,
+ op?.amountDelta,
+ )
return {
name: basic?.name || contractConfig.name,
@@ -118,6 +138,7 @@ export function mapBaiduQuotationToQuote(result: BaiduQuotationResult): QuoteDat
volume: toNum(op?.volume),
amount: amountYi,
openInterest: toNum(op?.holdingAmount),
+ amountDelta,
amplitude: toNum(op?.amplitudeRatio),
outerVol: outside,
innerVol: inside,
diff --git a/src/api/baidu/mapWsQuote.ts b/src/api/baidu/mapWsQuote.ts
index e64fea5..9e8ba46 100644
--- a/src/api/baidu/mapWsQuote.ts
+++ b/src/api/baidu/mapWsQuote.ts
@@ -28,6 +28,15 @@ function parseAmountYi(value: string | undefined, originValue?: number): number
return toNum(value)
}
+/** Parse 日增「+8.29万手」; prefer originValue (hands). */
+function parseAmountDelta(value: string | undefined, originValue?: number): number {
+ if (originValue != null && Number.isFinite(originValue)) return originValue
+ if (!value || value === '--') return 0
+ const cleaned = value.replace(/手/g, '')
+ if (cleaned.includes('万')) return Math.round(toNum(cleaned.replace('万', '')) * 10000)
+ return toNum(cleaned)
+}
+
/** Extract HH:mm from 「07-15 10:59」 or 「10:59」. */
function hhmmFromPointTime(time: string): string {
if (time.includes(' ')) return time.split(' ')[1]!.slice(0, 5)
@@ -126,6 +135,15 @@ export function applyWsSnapshot(quote: QuoteData, data: BaiduWsSnapshotData): Qu
if (by.amount) {
next.amount = parseAmountYi(by.amount.value, by.amount.originValue)
}
+ if (by.amountDelta) {
+ const raw = by.amountDelta.originValue
+ const display = by.amountDelta.value
+ const unavailable =
+ (raw == null || !Number.isFinite(raw)) && (!display || display === '--')
+ if (!unavailable) {
+ next.amountDelta = parseAmountDelta(display, raw)
+ }
+ }
}
if (data.askinfos || data.buyinfos) {
diff --git a/src/components/ai/AiAdviceDrawer.vue b/src/components/ai/AiAdviceDrawer.vue
index e70500f..47f9742 100644
--- a/src/components/ai/AiAdviceDrawer.vue
+++ b/src/components/ai/AiAdviceDrawer.vue
@@ -5,7 +5,10 @@
AI
{{ data.action }}
{{ data.summary }}
- 置信度 {{ data.confidence }}% · {{ data.updatedAt }}
+
+ 置信度 {{ data.confidence }}%
+ · 执行 {{ data.updatedAt }}
+
一键获取建议
diff --git a/src/components/quote/QuoteStats.vue b/src/components/quote/QuoteStats.vue
index 3a4687e..9f2718f 100644
--- a/src/components/quote/QuoteStats.vue
+++ b/src/components/quote/QuoteStats.vue
@@ -23,9 +23,16 @@ const props = defineProps<{
quote: QuoteData
}>()
+function formatWanSigned(hands: number): string {
+ const wan = hands / 10000
+ const sign = wan > 0 ? '+' : ''
+ return `${sign}${wan.toFixed(2)}万手`
+}
+
const columns = computed((): StatCell[][] => {
const q = props.quote
const chgClass = q.change >= 0 ? 'price-up' : 'price-down'
+ const deltaClass = q.amountDelta >= 0 ? 'price-up' : 'price-down'
return [
[
{ label: '开盘', value: q.open.toFixed(2) },
@@ -50,6 +57,7 @@ const columns = computed((): StatCell[][] => {
[
{ label: '最低', value: q.low.toFixed(2), className: 'price-down' },
{ label: '昨结', value: q.prevSettlement.toFixed(2) },
+ { label: '日增', value: formatWanSigned(q.amountDelta), className: deltaClass },
],
]
})
diff --git a/src/composables/useAiAdvice.ts b/src/composables/useAiAdvice.ts
index aea60dc..c67bf63 100644
--- a/src/composables/useAiAdvice.ts
+++ b/src/composables/useAiAdvice.ts
@@ -10,6 +10,9 @@ import { useTitleBlink } from './useTitleBlink'
const KLINE_PERIODS: KlinePeriod[] = ['day', 'week', 'month']
+/** 调度轮询间隔:检查距上次执行是否已超过设定分钟 */
+const SCHEDULE_TICK_MS = 15_000
+
function emptyAdvice(): AiAdvice {
return {
action: '—',
@@ -25,6 +28,8 @@ export function useAiAdvice() {
const data = ref
(emptyAdvice())
const loading = ref(false)
const error = ref(null)
+ /** 上次成功执行分析的时间戳(ms) */
+ const lastExecutedAt = ref(null)
const quota = useQuotaStore()
const settings = useSettingsStore()
@@ -76,6 +81,7 @@ export function useAiAdvice() {
console.log('[AI] 提交给 DeepSeek 的数据快照', payload.snapshot)
data.value = await fetchDeepSeekAdvice(apiKey, payload)
+ lastExecutedAt.value = Date.now()
if (!silent) ElMessage.success('AI 建议已更新')
} catch (e) {
error.value = e
@@ -87,6 +93,25 @@ export function useAiAdvice() {
}
}
+ /** 距上次成功执行是否已满设定间隔(从未执行则视为应执行) */
+ function shouldRunScheduled(): boolean {
+ if (!settings.scheduledAnalysisEnabled) return false
+ if (!isWsTradingSession()) return false
+ if (loading.value) return false
+ if (!settings.apiKey.trim()) return false
+
+ const minutes = Math.max(1, settings.scheduledAnalysisMinutes || 5)
+ const last = lastExecutedAt.value
+ if (last == null) return true
+ return Date.now() - last >= minutes * 60_000
+ }
+
+ function tickSchedule() {
+ if (shouldRunScheduled()) {
+ void refresh({ silent: true })
+ }
+ }
+
function clearSchedule() {
if (scheduleTimer) {
clearInterval(scheduleTimer)
@@ -97,13 +122,9 @@ export function useAiAdvice() {
function setupSchedule() {
clearSchedule()
if (!settings.scheduledAnalysisEnabled) return
- const minutes = Math.max(1, settings.scheduledAnalysisMinutes || 5)
- scheduleTimer = setInterval(
- () => {
- void refresh({ silent: true })
- },
- minutes * 60_000,
- )
+ // 开启后立即检查一次,之后按短周期轮询「是否已超过间隔」
+ tickSchedule()
+ scheduleTimer = setInterval(tickSchedule, SCHEDULE_TICK_MS)
}
watch(
@@ -115,7 +136,7 @@ export function useAiAdvice() {
onUnmounted(clearSchedule)
- return { data, loading, error, refresh }
+ return { data, loading, error, lastExecutedAt, refresh }
}
function formatError(e: unknown): string {
diff --git a/src/stores/quota.ts b/src/stores/quota.ts
index 9939147..ab02c68 100644
--- a/src/stores/quota.ts
+++ b/src/stores/quota.ts
@@ -70,6 +70,7 @@ function emptyQuote(): QuoteData {
volume: 0,
amount: 0,
openInterest: 0,
+ amountDelta: 0,
amplitude: 0,
outerVol: 0,
innerVol: 0,
diff --git a/src/types/index.ts b/src/types/index.ts
index 506bf6b..5fa2d74 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -45,6 +45,8 @@ export interface QuoteData {
volume: number
amount: number
openInterest: number
+ /** 日增仓(手),对应盘口 amountDelta / 日增 */
+ amountDelta: number
amplitude: number
outerVol: number
innerVol: number