fix(servers): 搜索改为手动触发,进页默认全量列表

不再自动恢复 last 搜索;下拉选历史、回车或清空才请求;输入过程不触发过滤。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Nexus Agent
2026-06-11 17:54:46 +08:00
parent af912662de
commit d3c0e57a08
4 changed files with 113 additions and 22 deletions
@@ -0,0 +1,36 @@
# 审计 — Servers 搜索手动触发与 null 修复
## 范围(文件清单)
| 文件 | 变更 |
|------|------|
| `frontend/src/composables/useServerSearchHistory.ts` | 仅加载历史、不恢复 lastrecord null 安全 |
| `frontend/src/composables/useServerPagination.ts` | search null 安全 trim |
| `frontend/src/pages/ServersPage.vue` | searchDraft + 回车/下拉/清空才搜索 |
| `server/utils/validation_errors_zh.py` | 422 剥离不可序列化 ctx |
## Step 3 规则扫描
| H | 规则 | 结论 |
|---|------|------|
| H1 | 进页不自动过滤 | PASS — refresh 重置 search |
| H2 | 无静默吞错 | PASS — record/load 失败非阻塞 |
| H3 | 422 可 JSON 序列化 | PASS — ctx 不写入响应 |
## Closure
| H | 判定 | 依据 |
|---|------|------|
| H1 | PASS | loadHistoryOnly 不写 search |
| H2 | PASS | try/catch 保留 |
| H3 | PASS | translate_validation_errors |
## DoD
- [x] npm run type-check
- [x] changelog 2026-06-11-servers-search-manual-trigger.md
- [x] 进页全量列表;手动搜索才请求
## 验证
Servers 页进入 → 无搜索 chip;下拉选历史或回车 → 过滤;清空 → 全量。
@@ -0,0 +1,28 @@
# Changelog — Servers 搜索改为手动触发
**日期**2026-06-11
## 摘要
进入 Servers 页默认展示全量列表,不再自动恢复上次搜索;搜索框仅在下拉选历史、回车或清空时生效,输入过程中不触发列表请求。
## 动机
自动恢复 `last` 搜索导致进页即过滤;`v-combobox` 每次输入都 debounce 请求,交互混乱。
## 涉及文件
- `frontend/src/composables/useServerSearchHistory.ts`
- `frontend/src/pages/ServersPage.vue`
## 迁移 / 重启
- 仅前端构建部署
## 验证
```bash
cd frontend && npm run type-check
```
进 Servers → 全量列表;下拉选历史 / 回车 → 才搜索;清空 → 恢复全量。
@@ -1,4 +1,4 @@
import { ref, type Ref } from 'vue'
import { ref } from 'vue'
import { http } from '@/api'
const LEGACY_HISTORY = 'nexus_servers_search_history'
@@ -38,10 +38,9 @@ export function useServerSearchHistory() {
const items = ref<string[]>([])
const bootstrapped = ref(false)
async function load(): Promise<string> {
async function load(): Promise<void> {
const res = await http.get<ServerSearchHistoryResponse>('/servers/search-history')
items.value = res.history || []
return res.last || ''
}
async function migrateLegacyIfEmpty(): Promise<boolean> {
@@ -59,16 +58,16 @@ export function useServerSearchHistory() {
return true
}
async function bootstrap(searchRef: Ref<string>) {
/** Load dropdown history only — does not apply any search to the list. */
async function loadHistoryOnly(): Promise<void> {
if (bootstrapped.value) return
try {
let last = await load()
await load()
if (!items.value.length && (await migrateLegacyIfEmpty())) {
last = await load()
await load()
}
if (last) searchRef.value = last
} catch {
// offline / auth — keep empty search
// offline / auth — keep empty history
} finally {
bootstrapped.value = true
}
@@ -86,5 +85,5 @@ export function useServerSearchHistory() {
}
}
return { items, bootstrapped, load, bootstrap, record }
return { items, bootstrapped, load, loadHistoryOnly, record }
}
+41 -13
View File
@@ -39,7 +39,7 @@
服务器列表
<v-spacer />
<v-combobox
v-model="search"
v-model="searchDraft"
:items="serverSearchHistory"
prepend-inner-icon="mdi-magnify"
label="搜索名称/IP(含未设路径)"
@@ -50,8 +50,9 @@
:auto-select-first="false"
style="max-width: 240px"
variant="outlined"
placeholder="输入或选择历史搜索"
@update:model-value="onSearch"
placeholder="输入后回车,或从历史选择"
@update:model-value="onSearchComboboxUpdate"
@keydown.enter.prevent="commitSearch"
/>
<v-btn color="primary" variant="flat" class="ml-3" prepend-icon="mdi-plus" size="small" @click="openAddServer">
添加
@@ -726,7 +727,6 @@ onMounted(() => {
let offBatchComplete: (() => void) | null = null
onBeforeUnmount(() => {
offBatchComplete?.()
clearTimeout(searchDebounceTimer)
})
defineOptions({ name: 'ServersPage' })
@@ -757,20 +757,46 @@ const {
loadAllServers, listQueryParams,
} = useServerPagination({ targetPathUnset: false })
const { items: serverSearchHistory, record: recordServerSearch, bootstrap: bootstrapServerSearch } =
const { items: serverSearchHistory, record: recordServerSearch, loadHistoryOnly: loadServerSearchHistory } =
useServerSearchHistory()
let searchDebounceTimer: ReturnType<typeof setTimeout>
function onSearch() {
if (search.value == null) search.value = ''
clearTimeout(searchDebounceTimer)
searchDebounceTimer = setTimeout(() => {
void recordServerSearch(search.value)
/** Combobox draft; applied filter is `search` from useServerPagination. */
const searchDraft = ref('')
function commitSearch() {
const q = String(searchDraft.value ?? '').trim()
if (q === String(search.value ?? '').trim()) return
search.value = q
searchDraft.value = q
page.value = 1
unsetPathPage.value = 1
void recordServerSearch(q)
void loadServers()
void loadPendingServers(true)
}
function onSearchComboboxUpdate(val: string | null) {
searchDraft.value = val ?? ''
if (val == null || val === '') {
if (!String(search.value ?? '').trim()) return
search.value = ''
page.value = 1
unsetPathPage.value = 1
void recordServerSearch('')
void loadServers()
void loadPendingServers(true)
}, 300)
return
}
const picked = String(val).trim()
if (serverSearchHistory.value.includes(picked)) {
search.value = picked
searchDraft.value = picked
page.value = 1
unsetPathPage.value = 1
void recordServerSearch(picked)
void loadServers()
void loadPendingServers(true)
}
}
const unsetPathServers = ref<ServerApiItem[]>([])
@@ -1696,7 +1722,9 @@ async function doDelete() {
}
async function refreshServersPage(silent = false) {
await bootstrapServerSearch(search)
await loadServerSearchHistory()
search.value = ''
searchDraft.value = ''
await Promise.all([
loadStats(silent),
loadCategories(silent),