mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 21:34:21 +08:00
revert: 移除拼音声母搜索,回到 6 位代码输入
拼音搜索的底层依赖(首次需爬沪深 A 股 5000 条全名单,几十次 TDX 协议 往返,慢机器几十秒到超时)太重,反复优化(按需加载/遮罩/单飞/预热) 都无法兼顾'不阻塞核心行情请求'与'首次可用'。用户决定放弃此功能, 回到简单稳定的 6 位代码输入。 回退 e2bf29e..2523605 共 6 个 commit 的全部改动: - 删除 StockSearchInput / AppInitOverlay / useStockSearch - 移除 pypinyin 依赖、/security/search-index 端点、lifespan 预热 - SymbolPicker / StocksPicker 恢复为纯 6 位代码输入 - README / CHANGELOG 同步回退 代码状态等同 v1.18.1(一键寻优多进程并发)发布后的干净基线
This commit is contained in:
@@ -1,130 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
// 全局初始化遮罩:股票搜索索引加载期间盖住全站,给用户明确的初始化反馈。
|
||||
// App 根挂载时调 eagerLoad(),本组件监听 loading/ready/failed 三态:
|
||||
// - loading:全屏灰底 + spinner + "正在初始化股票列表…"
|
||||
// - ready / failed:fade-out 消失(失败也消失,避免卡死;搜索降级为只能输代码)
|
||||
//
|
||||
// 方案 A:无"跳过"按钮,强制等待(索引构建是一次性代价,之后整会话秒回)。
|
||||
// 唯一的"逃逸阀"是加载失败——失败时遮罩消失,搜索不可用但不阻塞其他功能。
|
||||
|
||||
import { useStockSearch } from '../composables/useStockSearch'
|
||||
|
||||
const { loading, ready, failed, loadError } = useStockSearch()
|
||||
|
||||
// 是否显示遮罩:加载中显示;就绪或失败后淡出
|
||||
// ready 已就绪过的会话(缓存命中)loading 不会变 true,遮罩根本不显示
|
||||
function shouldShow() {
|
||||
return loading.value && !ready.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="overlay-fade">
|
||||
<div v-if="shouldShow()" class="app-init-overlay">
|
||||
<div class="init-card">
|
||||
<div class="spinner" />
|
||||
<h2>正在初始化股票列表…</h2>
|
||||
<p class="sub">首次加载需从通达信服务器拉取沪深 A 股全名单(约 5000 只),</p>
|
||||
<p class="sub">预计 30-60 秒,请稍候。</p>
|
||||
<p class="hint">完成后即可使用拼音声母搜索(如 zjxc → 中际旭创)</p>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<!-- 失败提示条(非阻塞,顶部小横幅) -->
|
||||
<Transition name="banner-slide">
|
||||
<div v-if="failed" class="init-failed-banner">
|
||||
⚠ 股票搜索初始化失败:{{ loadError }}。拼音搜索不可用,但仍可手动输入 6 位代码。
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app-init-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(15, 17, 23, 0.88);
|
||||
backdrop-filter: blur(3px);
|
||||
-webkit-backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.init-card {
|
||||
text-align: center;
|
||||
padding: 40px 48px;
|
||||
max-width: 440px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto 24px;
|
||||
border: 3px solid var(--border);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.init-card h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.6;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* 淡出过渡 */
|
||||
.overlay-fade-leave-active {
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
.overlay-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 失败横幅 */
|
||||
.init-failed-banner {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9998;
|
||||
padding: 10px 20px;
|
||||
background: rgba(239, 65, 70, 0.12);
|
||||
border-bottom: 1px solid var(--up);
|
||||
color: var(--up);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.banner-slide-enter-active,
|
||||
.banner-slide-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.banner-slide-enter-from,
|
||||
.banner-slide-leave-to {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,242 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
// 股票搜索输入框:支持 6 位代码 / 中文名 / 拼音声母(如 zjxc→中际旭创)。
|
||||
// 下拉建议 + 键盘导航(↑↓/Enter/Esc)。v-model 绑定 6 位代码。
|
||||
// 数据源:useStockSearch composable(模块级缓存索引,整会话拉一次)。
|
||||
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import { useStockSearch } from '../composables/useStockSearch'
|
||||
import { detectMarket, marketLabel } from '../market'
|
||||
import type { StockSearchEntry } from '../types'
|
||||
|
||||
const code = defineModel<string>({ default: '' })
|
||||
|
||||
const props = withDefaults(defineProps<{ placeholder?: string }>(), {
|
||||
placeholder: '代码 / 拼音 / 名字',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
/** 选中某只股票(code + name)时触发,供父组件做额外处理(如回填名称) */
|
||||
select: [entry: StockSearchEntry]
|
||||
/** 无下拉时按 Enter 触发(输入满 6 位代码的"确认"场景,供组合页接"添加") */
|
||||
confirm: [code: string]
|
||||
}>()
|
||||
|
||||
const { ready, loadError, search } = useStockSearch()
|
||||
|
||||
// 输入框文本(可能是代码片段、拼音、中文)。与 code 解耦:
|
||||
// code 是最终选定的 6 位代码,inputText 是用户正在敲的内容
|
||||
const inputText = ref(code.value)
|
||||
const suggestions = ref<StockSearchEntry[]>([])
|
||||
const showDropdown = ref(false)
|
||||
const activeIndex = ref(-1) // 键盘高亮项,-1 表示不高亮
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
// 输入满 6 位纯数字 → 直接当成选定代码(保留"直接敲代码"的老习惯)
|
||||
const isFullCode = computed(() => /^\d{6}$/.test(inputText.value.trim()))
|
||||
|
||||
// 智能识别的市场(用于提示展示)
|
||||
const detectedMarket = computed(() =>
|
||||
code.value && /^\d{6}$/.test(code.value) ? marketLabel(detectMarket(code.value)) : '',
|
||||
)
|
||||
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
async function refreshSuggestions() {
|
||||
const q = inputText.value.trim().toLowerCase()
|
||||
// 满 6 位纯数字:清空下拉(已经是有效代码,无需搜索)
|
||||
if (/^\d{6}$/.test(q)) {
|
||||
suggestions.value = []
|
||||
showDropdown.value = false
|
||||
activeIndex.value = -1
|
||||
return
|
||||
}
|
||||
if (!q || q.length < 1) {
|
||||
suggestions.value = []
|
||||
showDropdown.value = false
|
||||
activeIndex.value = -1
|
||||
return
|
||||
}
|
||||
if (!ready.value) return // 索引未就绪,等加载完再过滤
|
||||
suggestions.value = await search(q, 30)
|
||||
showDropdown.value = suggestions.value.length > 0
|
||||
activeIndex.value = suggestions.value.length > 0 ? 0 : -1
|
||||
}
|
||||
|
||||
watch(inputText, () => {
|
||||
// 同步纯数字输入到 code(边敲代码边更新市场标签)
|
||||
if (/^\d{6}$/.test(inputText.value.trim())) {
|
||||
code.value = inputText.value.trim()
|
||||
}
|
||||
// 防抖 120ms
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(refreshSuggestions, 120)
|
||||
})
|
||||
|
||||
function selectEntry(entry: StockSearchEntry) {
|
||||
inputText.value = entry.code
|
||||
code.value = entry.code
|
||||
suggestions.value = []
|
||||
showDropdown.value = false
|
||||
activeIndex.value = -1
|
||||
emit('select', entry)
|
||||
inputRef.value?.focus()
|
||||
}
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (!showDropdown.value || suggestions.value.length === 0) {
|
||||
// 无下拉时,Enter 且输入是有效代码 → 通知父组件"确认"(如组合页添加标的)
|
||||
if (e.key === 'Enter' && isFullCode.value) {
|
||||
emit('confirm', code.value)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
activeIndex.value = (activeIndex.value + 1) % suggestions.value.length
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
activeIndex.value =
|
||||
(activeIndex.value - 1 + suggestions.value.length) % suggestions.value.length
|
||||
} else if (e.key === 'Enter') {
|
||||
if (activeIndex.value >= 0 && activeIndex.value < suggestions.value.length) {
|
||||
e.preventDefault()
|
||||
selectEntry(suggestions.value[activeIndex.value])
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
showDropdown.value = false
|
||||
activeIndex.value = -1
|
||||
}
|
||||
}
|
||||
|
||||
function onBlur() {
|
||||
// 延迟关闭,给 click 事件时间触发(mousedown 在 blur 前,但 click 在后)
|
||||
setTimeout(() => {
|
||||
showDropdown.value = false
|
||||
}, 150)
|
||||
}
|
||||
|
||||
function onFocus() {
|
||||
// 索引由 App 根挂载时 eagerLoad 触发(详见 App.vue),这里聚焦时无需再触发。
|
||||
// 聚焦时若已有输入且非完整代码,重新展示建议
|
||||
nextTick(() => {
|
||||
if (inputText.value.trim() && !isFullCode.value && suggestions.value.length > 0) {
|
||||
showDropdown.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 父组件外部更新 code 时(如 URL 回填),同步到输入框
|
||||
watch(code, (newCode) => {
|
||||
if (newCode !== inputText.value) {
|
||||
inputText.value = newCode
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="stock-search-input">
|
||||
<input
|
||||
ref="inputRef"
|
||||
v-model="inputText"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:placeholder="props.placeholder"
|
||||
@keydown="onKeydown"
|
||||
@blur="onBlur"
|
||||
@focus="onFocus"
|
||||
/>
|
||||
<span v-if="detectedMarket" class="market-tag">{{ detectedMarket }}</span>
|
||||
<span v-if="loadError" class="load-err" :title="loadError">⚠</span>
|
||||
|
||||
<ul v-if="showDropdown" class="suggestions">
|
||||
<li
|
||||
v-for="(s, i) in suggestions"
|
||||
:key="s.code"
|
||||
:class="{ active: i === activeIndex }"
|
||||
@mousedown.prevent="selectEntry(s)"
|
||||
@mouseenter="activeIndex = i"
|
||||
>
|
||||
<span class="code">{{ s.code }}</span>
|
||||
<span class="name">{{ s.name }}</span>
|
||||
<span v-if="s.initials" class="initials">{{ s.initials }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.stock-search-input {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.stock-search-input input {
|
||||
width: 100%;
|
||||
padding-right: 70px;
|
||||
}
|
||||
.market-tag {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.load-err {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
color: var(--up);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
top: calc(100% + 2px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.suggestions li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 7px 10px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
.suggestions li:hover,
|
||||
.suggestions li.active {
|
||||
background: var(--bg-elevated);
|
||||
}
|
||||
.suggestions .code {
|
||||
font-family: var(--font-mono);
|
||||
color: var(--text-dim);
|
||||
width: 64px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.suggestions .name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.suggestions .initials {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
opacity: 0.7;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
</style>
|
||||
@@ -2,11 +2,9 @@
|
||||
// 多标的输入(组合回测用)。逐个添加 6 位代码,市场自动识别。
|
||||
// 删除手动市场选择(沪市/深市/北交所),由 detectMarket 智能匹配。
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { detectMarket } from '../market'
|
||||
import StockSearchInput from './StockSearchInput.vue'
|
||||
import type { StockSearchEntry } from '../types'
|
||||
import { detectMarket, marketLabel } from '../market'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string[]
|
||||
@@ -14,6 +12,9 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string[]] }>()
|
||||
|
||||
const code = ref('')
|
||||
const detectedMarket = computed(() => (code.value && /^\d{6}$/.test(code.value)
|
||||
? marketLabel(detectMarket(code.value))
|
||||
: ''))
|
||||
|
||||
function add() {
|
||||
if (!/^\d{6}$/.test(code.value)) return
|
||||
@@ -24,15 +25,6 @@ function add() {
|
||||
code.value = ''
|
||||
}
|
||||
|
||||
/** 选中下拉建议时,直接添加并清空输入框(组合页"选中即添加"的快捷流) */
|
||||
function onSelectEntry(entry: StockSearchEntry) {
|
||||
const sym = `${detectMarket(entry.code)}:${entry.code}`
|
||||
if (!props.modelValue.includes(sym)) {
|
||||
emit('update:modelValue', [...props.modelValue, sym])
|
||||
}
|
||||
code.value = ''
|
||||
}
|
||||
|
||||
function remove(sym: string) {
|
||||
emit('update:modelValue', props.modelValue.filter((s) => s !== sym))
|
||||
}
|
||||
@@ -41,14 +33,15 @@ function remove(sym: string) {
|
||||
<template>
|
||||
<div class="stocks-picker">
|
||||
<div class="row add-row">
|
||||
<StockSearchInput
|
||||
<input
|
||||
v-model="code"
|
||||
placeholder="6位代码 / 拼音 / 名字"
|
||||
@select="onSelectEntry"
|
||||
@confirm="add"
|
||||
maxlength="6"
|
||||
placeholder="6位代码(市场自动识别)"
|
||||
@keyup.enter="add"
|
||||
/>
|
||||
<button @click="add">添加</button>
|
||||
</div>
|
||||
<p v-if="detectedMarket" class="market-hint">将识别为:{{ detectedMarket }}</p>
|
||||
|
||||
<div v-if="modelValue.length" class="stock-list">
|
||||
<span v-for="s in modelValue" :key="s" class="stock-tag">
|
||||
@@ -64,12 +57,15 @@ function remove(sym: string) {
|
||||
.add-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
/* StockSearchInput 根元素填满剩余宽度 */
|
||||
.add-row :deep(.stock-search-input) {
|
||||
.add-row input {
|
||||
flex: 1;
|
||||
}
|
||||
.market-hint {
|
||||
color: var(--text-dim);
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.stock-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
// 后端 /bars 仅支持 count(上限 800,约 3.2 年),固定拉满后前端按日期过滤。
|
||||
// 默认:结束日=今天(最近交易日),开始日=2020-01-06。
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { fetchBars, formatError } from '../api'
|
||||
import { detectMarket } from '../market'
|
||||
import StockSearchInput from './StockSearchInput.vue'
|
||||
import { detectMarket, marketLabel } from '../market'
|
||||
import { useBacktestStore } from '../stores/backtest'
|
||||
import type { Category } from '../types'
|
||||
|
||||
@@ -37,6 +36,11 @@ const loading = ref(false)
|
||||
|
||||
const CATEGORIES: Category[] = ['DAY', 'WEEK', 'MONTH', 'MIN_5', 'MIN_15', 'MIN_30', 'MIN_60']
|
||||
|
||||
// 智能识别的市场(用于提示展示)
|
||||
const detectedMarket = computed(() => (code.value && /^\d{6}$/.test(code.value)
|
||||
? marketLabel(detectMarket(code.value))
|
||||
: ''))
|
||||
|
||||
/** 取行情(由父组件在点击「开始回测/开始寻优」时调用)。
|
||||
* 成功返回 true,失败返回 false(并把错误写入 store.error 供父组件感知)。 */
|
||||
async function loadBars(): Promise<boolean> {
|
||||
@@ -89,7 +93,12 @@ defineExpose({ loadBars, loading })
|
||||
<div class="symbol-picker">
|
||||
<div class="field code-field">
|
||||
<label>代码</label>
|
||||
<StockSearchInput v-model="code" placeholder="6位代码 / 拼音 / 名字" />
|
||||
<input
|
||||
v-model="code"
|
||||
maxlength="6"
|
||||
placeholder="6位代码(市场自动识别)"
|
||||
/>
|
||||
<span v-if="detectedMarket" class="market-tag">{{ detectedMarket }}</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
@@ -121,6 +130,20 @@ defineExpose({ loadBars, loading })
|
||||
.code-field {
|
||||
position: relative;
|
||||
}
|
||||
.code-field input {
|
||||
padding-right: 70px;
|
||||
}
|
||||
.market-tag {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.err {
|
||||
color: var(--up);
|
||||
font-size: 12px;
|
||||
|
||||
Reference in New Issue
Block a user