内容: 1. Vue3_Pagination_Implementation_Guide.md (718 行) - Vue 3 + TypeScript + Tauri 2 完整实施指南 - 完整的代码示例 - 分页和 Session 管理 - 临时方案(兼容现有 API) 2. Quick_Implementation_Checklist.md (331 行) - 5 步快速实施(1-2 小时) - 只需修改 1 个文件(SearchView.vue) - 复制即用的代码片段 - 验证步骤 实施方案: - 添加状态(sessionId, hasMore, nextOffset 等) - 实现 loadMore() 函数 - 修改 executeSearch() 支持分页 - 添加 More 按钮 UI - 添加样式 预期效果: - 初始加载:~2s → ~0.5s(4x 更快) - 内存占用:渐进式加载 - 用户体验:立即反馈 技术栈适配: - useState → ref / reactive - useEffect → onMounted / watch - lucide-react → 内联 SVG
332 lines
7.3 KiB
Markdown
332 lines
7.3 KiB
Markdown
# Studio 分页功能快速实施清单
|
||
|
||
**日期**: 2026-10-11
|
||
**目标**: 在 SearchView 中添加 More 按钮和分页加载
|
||
**预计时间**: 1-2 小时
|
||
|
||
---
|
||
|
||
## 📝 需要修改的文件
|
||
|
||
**只需修改 1 个文件**: `src/views/SearchView.vue`
|
||
|
||
---
|
||
|
||
## 🚀 5 步快速实施
|
||
|
||
### 步骤 1: 添加状态(1 分钟)
|
||
|
||
在 `<script setup>` 中添加:
|
||
|
||
```typescript
|
||
// === 在现有状态后添加 ===
|
||
const sessionId = ref('')
|
||
const nextOffset = ref(0)
|
||
const hasMore = ref(false)
|
||
const totalResults = ref(0)
|
||
const isLoadingMore = ref(false)
|
||
|
||
// 修改 pageSize
|
||
const pageSize = 5 // 从 20 改为 5
|
||
```
|
||
|
||
---
|
||
|
||
### 步骤 2: 添加 loadMore 函数(5 分钟)
|
||
|
||
在 `executeSearch` 函数后添加:
|
||
|
||
```typescript
|
||
/**
|
||
* 加载更多结果
|
||
*/
|
||
async function loadMore() {
|
||
if (isLoadingMore.value || !hasMore.value) return
|
||
|
||
isLoadingMore.value = true
|
||
|
||
try {
|
||
const query = searchQuery.value.trim()
|
||
const mode = searchMode.value
|
||
|
||
// 使用 offset 参数(当前 API 支持)
|
||
let res: any
|
||
switch (mode) {
|
||
case 'keyword':
|
||
res = await apiCall('search_keyword', { query, offset: results.value.length, limit: 10 })
|
||
break
|
||
case 'semantic':
|
||
res = await apiCall('search_semantic', { query, offset: results.value.length, limit: 10 })
|
||
break
|
||
case 'visual':
|
||
res = await apiCall('search_vlm_scenes', { query, offset: results.value.length, limit: 10 })
|
||
break
|
||
case 'agent':
|
||
res = await apiCall('agents_search', { query, offset: results.value.length, limit: 10 })
|
||
break
|
||
case 'people':
|
||
res = await apiCall('search_people', { query, offset: results.value.length, limit: 10 })
|
||
break
|
||
}
|
||
|
||
if (res && res.length > 0) {
|
||
results.value.push(...res)
|
||
hasMore.value = res.length >= 10
|
||
totalResults.value = results.value.length
|
||
} else {
|
||
hasMore.value = false
|
||
}
|
||
} catch (err) {
|
||
console.error('Load more failed:', err)
|
||
} finally {
|
||
isLoadingMore.value = false
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 步骤 3: 修改 executeSearch(3 分钟)
|
||
|
||
在 `executeSearch` 函数末尾添加:
|
||
|
||
```typescript
|
||
async function executeSearch() {
|
||
// ... 现有代码 ...
|
||
|
||
results.value = res.results || res
|
||
|
||
// === 添加以下代码 ===
|
||
totalResults.value = results.value.length
|
||
hasMore.value = results.value.length >= pageSize
|
||
nextOffset.value = pageSize
|
||
sessionId.value = '' // 重置
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 步骤 4: 添加 UI(2 分钟)
|
||
|
||
在 `<template>` 结果列表后添加:
|
||
|
||
```vue
|
||
<!-- 在结果列表后添加 -->
|
||
<div v-if="hasMore && results.length > 0" class="load-more-container">
|
||
<button
|
||
class="load-more-btn"
|
||
@click="loadMore"
|
||
:disabled="isLoadingMore"
|
||
>
|
||
{{ isLoadingMore ? 'Loading...' : `More Results (${totalResults - results.length} remaining)` }}
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="!hasMore && results.length > 0" class="no-more-results">
|
||
<p>All {{ results.length }} results loaded</p>
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
### 步骤 5: 添加样式(1 分钟)
|
||
|
||
在 `<style scoped>` 中添加:
|
||
|
||
```css
|
||
/* More 按钮 */
|
||
.load-more-container {
|
||
margin-top: 24px;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.load-more-btn {
|
||
padding: 12px 24px;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 8px;
|
||
color: rgba(255, 255, 255, 0.7);
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.load-more-btn:hover:not(:disabled) {
|
||
background: rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.load-more-btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.no-more-results {
|
||
margin-top: 24px;
|
||
text-align: center;
|
||
color: rgba(255, 255, 255, 0.5);
|
||
font-size: 13px;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## ✅ 验证步骤
|
||
|
||
1. **打开 Studio**
|
||
2. **搜索查询**: "sunset"
|
||
3. **验证**:
|
||
- ✅ 显示 Top 5 结果
|
||
- ✅ More 按钮显示 "X remaining"
|
||
- ✅ 点击 More 追加 10 个结果
|
||
- ✅ 无更多结果时显示 "All X results loaded"
|
||
|
||
---
|
||
|
||
## 📊 效果对比
|
||
|
||
### 之前
|
||
|
||
```
|
||
搜索 → 显示 20 个结果(一次性加载)
|
||
```
|
||
|
||
### 之后
|
||
|
||
```
|
||
搜索 → 快速显示 5 个
|
||
↓
|
||
More → 追加 10 个
|
||
↓
|
||
More → 再追加 10 个
|
||
↓
|
||
完成 → "All X results loaded"
|
||
```
|
||
|
||
---
|
||
|
||
## 🎯 完整代码片段
|
||
|
||
### 复制即用的完整修改
|
||
|
||
```typescript
|
||
// === 1. 状态定义(script setup 开头) ===
|
||
const sessionId = ref('')
|
||
const nextOffset = ref(0)
|
||
const hasMore = ref(false)
|
||
const totalResults = ref(0)
|
||
const isLoadingMore = ref(false)
|
||
const pageSize = 5
|
||
|
||
// === 2. loadMore 函数 ===
|
||
async function loadMore() {
|
||
if (isLoadingMore.value || !hasMore.value) return
|
||
isLoadingMore.value = true
|
||
|
||
try {
|
||
const query = searchQuery.value.trim()
|
||
const mode = searchMode.value
|
||
const offset = results.value.length
|
||
|
||
let res: any
|
||
if (mode === 'keyword') {
|
||
res = await apiCall('search_keyword', { query, offset, limit: 10 })
|
||
} else if (mode === 'semantic') {
|
||
res = await apiCall('search_semantic', { query, offset, limit: 10 })
|
||
} else if (mode === 'visual') {
|
||
res = await apiCall('search_vlm_scenes', { query, offset, limit: 10 })
|
||
} else if (mode === 'agent') {
|
||
res = await apiCall('agents_search', { query, offset, limit: 10 })
|
||
} else if (mode === 'people') {
|
||
res = await apiCall('search_people', { query, offset, limit: 10 })
|
||
}
|
||
|
||
if (res?.length > 0) {
|
||
results.value.push(...res)
|
||
hasMore.value = res.length >= 10
|
||
} else {
|
||
hasMore.value = false
|
||
}
|
||
} catch (err) {
|
||
console.error('Load more failed:', err)
|
||
} finally {
|
||
isLoadingMore.value = false
|
||
}
|
||
}
|
||
|
||
// === 3. executeSearch 修改 ===
|
||
async function executeSearch() {
|
||
// ... 现有代码 ...
|
||
|
||
// 在 results.value = res 后添加
|
||
totalResults.value = results.value.length
|
||
hasMore.value = results.value.length >= pageSize
|
||
nextOffset.value = pageSize
|
||
}
|
||
```
|
||
|
||
```vue
|
||
<!-- === 4. Template 添加 === -->
|
||
<!-- 在结果列表后 -->
|
||
<div v-if="hasMore && results.length > 0" class="load-more-container">
|
||
<button class="load-more-btn" @click="loadMore" :disabled="isLoadingMore">
|
||
{{ isLoadingMore ? 'Loading...' : `More Results (${totalResults - results.length} remaining)` }}
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="!hasMore && results.length > 0" class="no-more-results">
|
||
<p>All {{ results.length }} results loaded</p>
|
||
</div>
|
||
```
|
||
|
||
```css
|
||
/* === 5. 样式添加 === */
|
||
.load-more-container {
|
||
margin-top: 24px;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.load-more-btn {
|
||
padding: 12px 24px;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 8px;
|
||
color: rgba(255, 255, 255, 0.7);
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.load-more-btn:hover:not(:disabled) {
|
||
background: rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.no-more-results {
|
||
margin-top: 24px;
|
||
text-align: center;
|
||
color: rgba(255, 255, 255, 0.5);
|
||
font-size: 13px;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## ⚠️ 注意事项
|
||
|
||
1. **API 兼容性**: 当前使用 offset 参数,如果后端支持 session_id,可以切换到 session 模式
|
||
2. **错误处理**: 已添加基本错误处理,如需更详细提示可扩展
|
||
3. **性能**: 初始加载 5 个结果,More 操作加载 10 个,避免一次性加载过多
|
||
|
||
---
|
||
|
||
## 🎉 完成标志
|
||
|
||
- [ ] 初始搜索显示 Top 5
|
||
- [ ] More 按钮显示正确
|
||
- [ ] 点击 More 追加结果
|
||
- [ ] 无更多结果时提示
|
||
- [ ] 样式正常显示
|
||
|
||
---
|
||
|
||
**快速实施完成!只需修改 SearchView.vue 一个文件。** |