feat: 涨停生态 vipdoc 路径可配置 — 应用级 KV 设置 + 页面配置行

- 新增 AppSettingsStore(~/.easy_tdx/settings.db,KV 持久化,重启不丢)
- GET/PUT /settings/vipdoc:读取/保存本地 vipdoc 目录(保存即校验目录
  存在,并清空涨停生态/历史缓存,下次扫描立用新路径;空串=恢复自动检测)
- vipdoc 解析优先级:显式参数 > 已存设置 > 自动检测(TDX_HOME/常见路径)
- /limitup-ecology 响应携带 vipdoc_path 供页面展示生效目录
- 涨停生态页头部新增路径配置行(输入框+保存+恢复自动检测),
  未检测到数据的空态文案引导填写
This commit is contained in:
Justin Gu
2026-09-05 11:24:48 +08:00
parent a36043f962
commit aead122478
6 changed files with 326 additions and 4 deletions
+18
View File
@@ -831,6 +831,24 @@ export async function clearLlmHistory(): Promise<number> {
return (await resp.json()).deleted as number
}
/** 读取本地 vipdoc 路径设置(stored=已保存;resolved=当前自动检测生效值)。 */
export async function fetchVipdocSetting(): Promise<{ stored: string | null; resolved: string | null }> {
const resp = await fetch(`${BASE}/settings/vipdoc`)
if (!resp.ok) await throwError(resp)
return (await resp.json()) as { stored: string | null; resolved: string | null }
}
/** 保存/清除本地 vipdoc 路径设置(空串 = 恢复自动检测;服务端随之清空涨停缓存)。 */
export async function saveVipdocSetting(path: string): Promise<{ stored: string; resolved: string | null }> {
const resp = await fetch(`${BASE}/settings/vipdoc`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path }),
})
if (!resp.ok) await throwError(resp)
return (await resp.json()) as { stored: string; resolved: string | null }
}
/** 涨停生态(连板天梯/炸板/跌停,本地 vipdoc 离线回算,服务端缓存 60s)。
* data_date 为 vipdoc 数据日期;name 字段需前端经 fetchSymbolName 补齐。 */
export async function fetchLimitUpEcology(): Promise<LimitUpEcologyResp> {
+2
View File
@@ -672,6 +672,8 @@ export interface LimitUpEcologyResp {
/** vipdoc 数据日期 YYYYMMDD —— 新鲜度取决于本机通达信客户端 */
data_date: number
total: number
/** 实际生效的 vipdoc 目录(自动检测或已存设置),未检测到为 null */
vipdoc_path?: string | null
summary: {
limit_up_count: number
limit_down_count: number
+67 -2
View File
@@ -4,7 +4,7 @@
// 单击任意股票打开 StockDialog。120s 轮询 + 手动刷新。
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { fetchLimitUpEcology, fetchSymbolName, formatError } from '../api'
import { fetchLimitUpEcology, fetchSymbolName, fetchVipdocSetting, formatError, saveVipdocSetting } from '../api'
import StockDialog from '../components/StockDialog.vue'
import type { LimitUpEntry } from '../types'
@@ -13,11 +13,46 @@ const loading = ref(false)
const error = ref('')
const lastRefresh = ref('')
const vipdocStored = ref<string | null>(null)
const vipdocResolved = ref<string | null>(null)
const vipdocInput = ref('')
const vipdocBusy = ref(false)
const vipdocMsg = ref('')
async function loadVipdocSetting() {
try {
const r = await fetchVipdocSetting()
vipdocStored.value = r.stored
vipdocResolved.value = r.resolved
vipdocInput.value = r.stored ?? ''
} catch {
// 设置读取失败不打扰主流程
}
}
async function saveVipdoc() {
vipdocBusy.value = true
vipdocMsg.value = ''
try {
const r = await saveVipdocSetting(vipdocInput.value.trim())
vipdocStored.value = r.stored || null
vipdocResolved.value = r.resolved
vipdocMsg.value = '已保存,正在按新路径重新扫描…'
await load()
vipdocMsg.value = '已保存'
} catch (e) {
vipdocMsg.value = formatError(e)
} finally {
vipdocBusy.value = false
}
}
async function load() {
loading.value = resp.value === null
error.value = ''
try {
resp.value = await fetchLimitUpEcology()
vipdocResolved.value = resp.value.vipdoc_path ?? vipdocResolved.value
lastRefresh.value = new Date().toLocaleTimeString('zh-CN', { hour12: false })
fillNames(allEntries.value)
} catch (e) {
@@ -110,6 +145,7 @@ function tick() {
}
onMounted(() => {
loadVipdocSetting()
load()
timer = window.setInterval(tick, 120_000)
})
@@ -137,6 +173,21 @@ function pctClass(pct: number): string {
<button class="manual-refresh" @click="load"> 刷新</button>
</div>
<div class="vipdoc-bar card">
<span class="dim">vipdoc 数据目录</span>
<input
v-model="vipdocInput"
class="vipdoc-input mono"
type="text"
:placeholder="vipdocResolved || '自动检测(TDX_HOME 或常见安装路径)'"
spellcheck="false"
/>
<button :disabled="vipdocBusy" @click="saveVipdoc">{{ vipdocBusy ? '保存中' : '保存' }}</button>
<button :disabled="vipdocBusy || !vipdocStored" title="清除已存路径,恢复自动检测" @click="vipdocInput = ''; saveVipdoc()">自动检测</button>
<span v-if="vipdocMsg" class="dim">{{ vipdocMsg }}</span>
<span v-else-if="vipdocResolved && !vipdocStored" class="dim">当前自动检测{{ vipdocResolved }}</span>
</div>
<div v-if="error" class="err card">
加载失败{{ error }}
<button @click="load">重试</button>
@@ -145,7 +196,7 @@ function pctClass(pct: number): string {
<template v-else-if="resp">
<div v-if="resp.total === 0" class="err card">
未检测到本地通达信 vipdoc 日线数据请确认本机已安装通达信
未检测到本地通达信 vipdoc 日线数据可在上方填写安装目录 D:\new_tdx\vipdoc后保存或确认通达信已安装
<code> vipdoc/{sh,sz}/lday/*.day </code> 存在自动检测失败时可在 CLI 侧指定路径
</div>
@@ -245,6 +296,20 @@ function pctClass(pct: number): string {
</template>
<style scoped>
.vipdoc-bar {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
font-size: 12px;
padding: 8px 12px;
}
.vipdoc-input {
flex: 1;
min-width: 260px;
padding: 4px 10px;
font-size: 12px;
}
.limitup-view {
height: 100%;
overflow-y: auto;