Merge pull request #168 from nagisa77/2gqwih-codex

Fix filter dropdown preloading
This commit is contained in:
Tim
2025-07-10 13:53:47 +08:00
committed by GitHub
4 changed files with 91 additions and 23 deletions

View File

@@ -1,5 +1,5 @@
<template> <template>
<Dropdown v-model="selected" :fetch-options="fetchCategories" placeholder="选择分类"> <Dropdown v-model="selected" :fetch-options="fetchCategories" placeholder="选择分类" :initial-options="providedOptions">
<template #option="{ option }"> <template #option="{ option }">
<div class="option-container"> <div class="option-container">
<div class="option-main"> <div class="option-main">
@@ -17,7 +17,7 @@
</template> </template>
<script> <script>
import { computed } from 'vue' import { computed, ref, watch } from 'vue'
import { API_BASE_URL } from '../main' import { API_BASE_URL } from '../main'
import Dropdown from './Dropdown.vue' import Dropdown from './Dropdown.vue'
@@ -25,11 +25,24 @@ export default {
name: 'CategorySelect', name: 'CategorySelect',
components: { Dropdown }, components: { Dropdown },
props: { props: {
modelValue: { type: [String, Number], default: '' } modelValue: { type: [String, Number], default: '' },
options: { type: Array, default: () => [] }
}, },
emits: ['update:modelValue'], emits: ['update:modelValue'],
setup(props, { emit }) { setup(props, { emit }) {
const providedOptions = ref(Array.isArray(props.options) ? [...props.options] : [])
watch(
() => props.options,
val => {
providedOptions.value = Array.isArray(val) ? [...val] : []
}
)
const fetchCategories = async () => { const fetchCategories = async () => {
if (providedOptions.value.length) {
return [{ id: '', name: '无分类' }, ...providedOptions.value]
}
const res = await fetch(`${API_BASE_URL}/api/categories`) const res = await fetch(`${API_BASE_URL}/api/categories`)
if (!res.ok) return [] if (!res.ok) return []
const data = await res.json() const data = await res.json()
@@ -46,7 +59,7 @@ export default {
set: v => emit('update:modelValue', v) set: v => emit('update:modelValue', v)
}) })
return { fetchCategories, selected, isImageIcon } return { fetchCategories, selected, isImageIcon, providedOptions }
} }
} }
</script> </script>

View File

@@ -71,7 +71,8 @@ export default {
remote: { type: Boolean, default: false }, remote: { type: Boolean, default: false },
menuClass: { type: String, default: '' }, menuClass: { type: String, default: '' },
optionClass: { type: String, default: '' }, optionClass: { type: String, default: '' },
showSearch: { type: Boolean, default: true } showSearch: { type: Boolean, default: true },
initialOptions: { type: Array, default: () => [] }
}, },
emits: ['update:modelValue'], emits: ['update:modelValue'],
setup(props, { emit }) { setup(props, { emit }) {
@@ -80,7 +81,7 @@ export default {
const setSearch = (val) => { const setSearch = (val) => {
search.value = val search.value = val
} }
const options = ref([]) const options = ref(Array.isArray(props.initialOptions) ? [...props.initialOptions] : [])
const loaded = ref(false) const loaded = ref(false)
const loading = ref(false) const loading = ref(false)
const wrapper = ref(null) const wrapper = ref(null)
@@ -136,6 +137,15 @@ export default {
} }
} }
watch(
() => props.initialOptions,
val => {
if (Array.isArray(val)) {
options.value = [...val]
}
}
)
watch(open, async val => { watch(open, async val => {
if (val) { if (val) {
if (props.remote) { if (props.remote) {

View File

@@ -1,5 +1,5 @@
<template> <template>
<Dropdown v-model="selected" :fetch-options="fetchTags" multiple placeholder="选择标签" remote> <Dropdown v-model="selected" :fetch-options="fetchTags" multiple placeholder="选择标签" remote :initial-options="mergedOptions">
<template #option="{ option }"> <template #option="{ option }">
<div class="option-container"> <div class="option-container">
<div class="option-main"> <div class="option-main">
@@ -17,7 +17,7 @@
</template> </template>
<script> <script>
import { computed, ref } from 'vue' import { computed, ref, watch } from 'vue'
import { API_BASE_URL, toast } from '../main' import { API_BASE_URL, toast } from '../main'
import Dropdown from './Dropdown.vue' import Dropdown from './Dropdown.vue'
@@ -26,11 +26,25 @@ export default {
components: { Dropdown }, components: { Dropdown },
props: { props: {
modelValue: { type: Array, default: () => [] }, modelValue: { type: Array, default: () => [] },
creatable: { type: Boolean, default: false } creatable: { type: Boolean, default: false },
options: { type: Array, default: () => [] }
}, },
emits: ['update:modelValue'], emits: ['update:modelValue'],
setup(props, { emit }) { setup(props, { emit }) {
const localTags = ref([]) const localTags = ref([])
const providedTags = ref(Array.isArray(props.options) ? [...props.options] : [])
watch(
() => props.options,
val => {
providedTags.value = Array.isArray(val) ? [...val] : []
}
)
const mergedOptions = computed(() => {
const arr = [...providedTags.value, ...localTags.value]
return arr.filter((v, i, a) => a.findIndex(t => t.id === v.id) === i)
})
const isImageIcon = icon => { const isImageIcon = icon => {
if (!icon) return false if (!icon) return false
@@ -38,17 +52,21 @@ export default {
} }
const fetchTags = async (kw = '') => { const fetchTags = async (kw = '') => {
const url = new URL(`${API_BASE_URL}/api/tags`)
if (kw) url.searchParams.set('keyword', kw)
url.searchParams.set('limit', '10')
let data = [] let data = []
try { if (!kw && providedTags.value.length) {
const res = await fetch(url.toString()) data = [...providedTags.value]
if (res.ok) { } else {
data = await res.json() const url = new URL(`${API_BASE_URL}/api/tags`)
if (kw) url.searchParams.set('keyword', kw)
url.searchParams.set('limit', '10')
try {
const res = await fetch(url.toString())
if (res.ok) {
data = await res.json()
}
} catch {
toast.error('获取标签失败')
} }
} catch {
toast.error('获取标签失败')
} }
let options = [...data, ...localTags.value] let options = [...data, ...localTags.value]
@@ -91,7 +109,7 @@ export default {
} }
}) })
return { fetchTags, selected, isImageIcon } return { fetchTags, selected, isImageIcon, mergedOptions }
} }
} }
</script> </script>

View File

@@ -18,8 +18,8 @@
> >
{{ topic }} {{ topic }}
</div> </div>
<CategorySelect v-model="selectedCategory" /> <CategorySelect v-model="selectedCategory" :options="categoryOptions" />
<TagSelect v-model="selectedTags" /> <TagSelect v-model="selectedTags" :options="tagOptions" />
</div> </div>
</div> </div>
@@ -131,6 +131,8 @@ export default {
.map(v => decodeURIComponent(v)) .map(v => decodeURIComponent(v))
.map(v => (isNaN(v) ? v : Number(v))) .map(v => (isNaN(v) ? v : Number(v)))
} }
const tagOptions = ref([])
const categoryOptions = ref([])
const isLoadingPosts = ref(false) const isLoadingPosts = ref(false)
const topics = ref(['最新', '排行榜' /*, '热门', '类别'*/]) const topics = ref(['最新', '排行榜' /*, '热门', '类别'*/])
const selectedTopic = ref(route.query.view === 'ranking' ? '排行榜' : '最新') const selectedTopic = ref(route.query.view === 'ranking' ? '排行榜' : '最新')
@@ -140,6 +142,30 @@ export default {
const pageSize = 5 const pageSize = 5
const allLoaded = ref(false) const allLoaded = ref(false)
const loadOptions = async () => {
if (selectedCategory.value && !isNaN(selectedCategory.value)) {
try {
const res = await fetch(`${API_BASE_URL}/api/categories/${selectedCategory.value}`)
if (res.ok) {
categoryOptions.value = [await res.json()]
}
} catch (e) { /* ignore */ }
}
if (selectedTags.value.length) {
const arr = []
for (const t of selectedTags.value) {
if (!isNaN(t)) {
try {
const r = await fetch(`${API_BASE_URL}/api/tags/${t}`)
if (r.ok) arr.push(await r.json())
} catch (e) { /* ignore */ }
}
}
tagOptions.value = arr
}
}
const buildUrl = () => { const buildUrl = () => {
let url = `${API_BASE_URL}/api/posts?page=${page.value}&pageSize=${pageSize}` let url = `${API_BASE_URL}/api/posts?page=${page.value}&pageSize=${pageSize}`
if (selectedCategory.value) { if (selectedCategory.value) {
@@ -249,7 +275,8 @@ export default {
} }
} }
onMounted(() => { onMounted(async () => {
await loadOptions()
if (selectedTopic.value === '排行榜') { if (selectedTopic.value === '排行榜') {
fetchRanking() fetchRanking()
} else { } else {
@@ -275,7 +302,7 @@ export default {
const sanitizeDescription = (text) => stripMarkdown(text) const sanitizeDescription = (text) => stripMarkdown(text)
return { topics, selectedTopic, articles, sanitizeDescription, isLoadingPosts, handleScroll, selectedCategory, selectedTags } return { topics, selectedTopic, articles, sanitizeDescription, isLoadingPosts, handleScroll, selectedCategory, selectedTags, tagOptions, categoryOptions }
} }
} }
</script> </script>