新增: 三实体列表查询维度补全(status/关键词/排序/分页下沉后端)
任务/项目/灵感三实体新增 list_by_query 动态 WHERE(累积式 where_clauses
+params_vec 收口)+ order_by 白名单防注入 + limit/offset 钳制。命令层
list_{tasks,projects,ideas} 吃 Option<XxxQuery> 双参向后兼容(旧无参/单参
路径等价全量)。前端 Tasks/Ideas status/keyword 筛选下沉后端 query。
F-260621-02
This commit is contained in:
@@ -126,8 +126,7 @@ import { useRouter, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { useProjectStore } from '../stores/project'
|
||||
import type { IdeaStatus } from '../api/types'
|
||||
import { parseTags } from '../stores/knowledge'
|
||||
import type { IdeaStatus, IdeaQuery } from '../api/types'
|
||||
import { formatDate } from '../utils/time'
|
||||
import { stripMd } from '../utils/markdown'
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
||||
@@ -181,38 +180,61 @@ function statusLabelKey(status: IdeaStatus): string {
|
||||
return statusOptions.find(s => s.value === status)?.labelKey ?? status
|
||||
}
|
||||
|
||||
// ── 后端查询构造(F-260621-02:status/keyword/order_by 下沉后端 WHERE) ──
|
||||
//
|
||||
// status 下沉:单一状态(all→不限 / promoted→promoted)走后端精确匹配;
|
||||
// hot(按 score≥80)与 pending(多状态 draft+pending_review)无法映射单一后端 status,
|
||||
// 仍在前端 filter(轻量,数据量小;后端 status 仅单值,扩多值属过度设计)。
|
||||
// keyword/title+description LIKE、order_by(排序)全下沉后端,避免前端全表扫描/重排。
|
||||
function buildIdeaQuery(): IdeaQuery {
|
||||
const q: IdeaQuery = {}
|
||||
// 排序下沉后端(score/time)
|
||||
q.order_by = sortMode.value === 'score' ? 'score' : 'created_at'
|
||||
// keyword 下沉后端(title/description LIKE)
|
||||
if (searchQuery.value.trim()) {
|
||||
q.keyword = searchQuery.value.trim()
|
||||
}
|
||||
// 单一状态映射:promoted → status=promoted;all/hot/pending 不走单一 status
|
||||
if (activeFilter.value === 'promoted') {
|
||||
q.status = 'promoted'
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
// 按当前筛选/搜索/排序触发后端加载(防抖仅对 keyword 输入,筛选/排序即时)。
|
||||
// 让 store.loading 驱动「加载中」空态显示(对齐 Tasks.vue 三态)。
|
||||
let keywordDebounce: ReturnType<typeof setTimeout> | null = null
|
||||
async function reloadIdeas(immediate = false) {
|
||||
if (keywordDebounce) {
|
||||
clearTimeout(keywordDebounce)
|
||||
keywordDebounce = null
|
||||
}
|
||||
const run = () => store.loadIdeas(buildIdeaQuery())
|
||||
if (immediate) {
|
||||
await run()
|
||||
} else {
|
||||
keywordDebounce = setTimeout(() => { void run() }, 300)
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选/排序变化即时重载;keyword 输入防抖重载
|
||||
watch(activeFilter, () => void reloadIdeas(true))
|
||||
watch(sortMode, () => void reloadIdeas(true))
|
||||
watch(searchQuery, () => void reloadIdeas(false))
|
||||
|
||||
const filteredIdeas = computed(() => {
|
||||
let ideas = store.ideas
|
||||
|
||||
// 应用状态过滤
|
||||
// hot(按 score≥80)/pending(多状态 draft+pending_review)保留前端 filter:
|
||||
// 后端 status 仅单值,无法表达 OR,扩多值属过度设计;数据量小,前端 filter 轻量。
|
||||
switch (activeFilter.value) {
|
||||
case 'hot': ideas = ideas.filter(i => (i.score ?? 0) >= 80); break
|
||||
case 'pending': ideas = ideas.filter(i => i.status === 'pending_review' || i.status === 'draft'); break
|
||||
case 'promoted': ideas = ideas.filter(i => i.status === 'promoted'); break
|
||||
default: break // 不过滤
|
||||
default: break // all / promoted:后端已过滤,前端不再 filter
|
||||
}
|
||||
|
||||
// 应用搜索过滤
|
||||
if (searchQuery.value.trim()) {
|
||||
const query = searchQuery.value.toLowerCase().trim()
|
||||
ideas = ideas.filter(i =>
|
||||
i.title.toLowerCase().includes(query) ||
|
||||
(i.description && i.description.toLowerCase().includes(query)) ||
|
||||
parseTags(i.tags).some((tag: string) => tag.toLowerCase().includes(query))
|
||||
)
|
||||
}
|
||||
|
||||
// 排序:在过滤+搜索之后、return 之前对最终数组排序
|
||||
// [...ideas] 浅拷贝避免改原数组(store.ideas)
|
||||
const sorted = [...ideas].sort((a, b) => {
|
||||
if (sortMode.value === 'score') {
|
||||
return (b.score ?? 0) - (a.score ?? 0) // 高分在前
|
||||
}
|
||||
// time:created_at 为毫秒字符串,字符串比较即可(同位数字典序等价数值序)
|
||||
return (b.created_at ?? '').localeCompare(a.created_at ?? '') // 新在前
|
||||
})
|
||||
|
||||
return sorted
|
||||
// 排序与 keyword 已下沉后端(buildIdeaQuery),前端不再重排/重过滤。
|
||||
return ideas
|
||||
})
|
||||
|
||||
const currentIdea = computed(() => {
|
||||
@@ -279,7 +301,7 @@ async function promoteToProject() {
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
await store.loadIdeas()
|
||||
await store.loadIdeas(buildIdeaQuery())
|
||||
}
|
||||
|
||||
const evaluating = ref(false)
|
||||
@@ -318,7 +340,7 @@ async function onUpdateRelated(ids: string[]) {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await store.loadIdeas()
|
||||
await store.loadIdeas(buildIdeaQuery())
|
||||
// 从路由参数恢复选中灵感(修复 /ideas/:id 直接访问空白页 B-260615-36)
|
||||
if (route.params.id) {
|
||||
const id = route.params.id as string
|
||||
|
||||
Reference in New Issue
Block a user