fix(servers): 搜索改为手动触发,进页默认全量列表
不再自动恢复 last 搜索;下拉选历史、回车或清空才请求;输入过程不触发过滤。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# 审计 — Servers 搜索手动触发与 null 修复
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `frontend/src/composables/useServerSearchHistory.ts` | 仅加载历史、不恢复 last;record 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 }
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user