Skip to content

Commit

Permalink
【feat】改进书籍搜索体验 (#163)
Browse files Browse the repository at this point in the history
* feat: 加载中的书籍展示加载中而不是奇怪的n年前

* doc: 添加镜像选项避免每次google

* feat: search-input单独暴露精确搜索与否信息,方便外部单独处理

* feat: 添加搜索结果为空的提示
  • Loading branch information
Inori-Lover authored Feb 2, 2023
1 parent b0ed235 commit 9a5db03
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 65 deletions.
4 changes: 4 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 去除注释可以使用代理进行安装
# proxy=http://127.0.0.1:1080
# https_proxy=http://127.0.0.1:1080
# npm镜像
# registry=http://mirrors.cloud.tencent.com/npm/
# electron镜像
# ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
8 changes: 7 additions & 1 deletion src/components/BookCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,13 @@ const { generalSetting } = settingStore // 引入setting用于控制图片自定
const $q = useQuasar()
const props = defineProps<{ book: BookInList }>()
const cover = computed(() => props.book.Cover)
const updateTime = useToNow(computed(() => props.book.LastUpdateTime))
const updateTime = computed(() => {
if (+props.book.LastUpdateTime <= 0) {
return ''
}
return useToNow(computed(() => props.book.LastUpdateTime)).value
})
const visible = ref(false)
function onIntersection(entry: IntersectionObserverEntry) {
visible.value = entry.isIntersecting
Expand Down
11 changes: 6 additions & 5 deletions src/components/SearchInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ const props = withDefaults(
modelValue: ''
}
)
const emits = defineEmits<{ (e: 'search', val: string): void; (e: 'update:modelValue', val: string): void }>()
const emits = defineEmits<{
(e: 'search', val: string, exact: boolean): void
(e: 'update:modelValue', val: string): void
}>()
const inputEleRef = ref<HTMLInputElement | null>(null)
Expand Down Expand Up @@ -80,10 +83,8 @@ function syncHandle(evt: string | number | null) {
}
function searchHandle(exact = false) {
const key = exact ? `"${keyword.value}"` : keyword.value
emits('update:modelValue', key)
emits('search', key)
emits('update:modelValue', keyword.value)
emits('search', keyword.value, !!exact)
// 因为点menu的话一定会blur没法避免,所以这里统一blur(即使是按回车触发的search)
inputEleRef.value?.blur()
Expand Down
6 changes: 3 additions & 3 deletions src/components/app/Header.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
v-model="searchKey"
:width="searchInputWidth"
max-width="unset"
@search="search"
@search="onSearch"
/>

<q-space />
Expand Down Expand Up @@ -238,8 +238,8 @@ const userInfoMenuOptions: Array<Record<string, any>> = [
}
]
function search() {
router.push({ name: 'Search', params: { keyWords: searchKey.value } })
function onSearch(keyWords: string, exact: boolean) {
router.push({ name: 'Search', params: { keyWords: keyWords }, query: { exact: exact ? '1' : '' } })
searchKey.value = ''
}
function changAppName() {
Expand Down
2 changes: 1 addition & 1 deletion src/stores/bookListData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class EMPTY_BOOK implements BookInList {
public readonly Title = ''
// public readonly Cover = '/img/bg-paper-dark.jpeg'
public readonly Cover = ''
public readonly LastUpdateTime = new Date(1)
public readonly LastUpdateTime = new Date(-1)
public readonly UserName = ''
public readonly Level = 0
public readonly InteriorLevel = 0
Expand Down
162 changes: 107 additions & 55 deletions src/views/Search.vue
Original file line number Diff line number Diff line change
@@ -1,43 +1,46 @@
<template>
<q-page padding>
<!-- todo 不懂他为什么不能放在q-tab-panel里面 -->
<q-infinite-scroll @load="requestBook" :offset="100" ref="scroll">
<div class="q-gutter-y-md">
<div class="row flex-center">
<!-- <q-input rounded outlined dense v-model="searchKey" @keyup.enter="search" /> -->
<search-input
outlined
dense
:width="searchInputWidth"
max-width="600px"
v-model="searchKey"
@search="search"
/>
</div>
<q-infinite-scroll @load="requestBook" :offset="100" ref="scrollEleInstanceRef">
<template #default>
<div class="q-gutter-y-md">
<q-tabs dense v-model="tab" class="text-teal">
<template v-for="option in tabOptions" :key="option.key">
<q-tab :disable="option.disable" :name="option.name" :icon="option.icon" :label="option.label" />
</template>
</q-tabs>
<q-tab-panels v-model="tab" animated>
<q-tab-panel name="Book">
<q-grid :x-gap="12" :y-gap="8" cols="6" xs="3" sm="4" md="5" xl="6" lg="6" style="margin-top: 12px">
<q-grid-item v-for="book in bookData" :key="book['Id']">
<book-card :book="book"></book-card>
</q-grid-item>
</q-grid>
</q-tab-panel>

<q-tab-panel name="Form">
<div class="q-pa-md">
<div>Form</div>
</div>
</q-tab-panel>
</q-tab-panels>
<div class="row flex-center">
<!-- <q-input rounded outlined dense v-model="searchKey" @keyup.enter="search" /> -->
<search-input
outlined
dense
:width="searchInputWidth"
max-width="600px"
v-model="searchKeyInInput"
@search="onSearch"
/>
</div>
<div class="q-gutter-y-md">
<q-tabs dense v-model="tab" class="text-teal">
<template v-for="option in tabOptions" :key="option.key">
<q-tab :disable="option.disable" :name="option.name" :icon="option.icon" :label="option.label" />
</template>
</q-tabs>
<q-tab-panels v-model="tab" animated>
<q-tab-panel name="Book">
<template v-if="bookData.length">
<q-grid :x-gap="12" :y-gap="8" cols="6" xs="3" sm="4" md="5" xl="6" lg="6" style="margin-top: 12px">
<q-grid-item v-for="book in bookData" :key="book['Id']">
<book-card :book="book"></book-card>
</q-grid-item>
</q-grid>
</template>
<template v-else-if="!loading">
<div class="row justify-center q-my-md text-center text-h5"
>无<template v-if="isExactInRoute">精确</template>搜索结果</div
>
</template>
</q-tab-panel>
</q-tab-panels>
</div>
</div>
</div>
<template v-slot:loading>
</template>
<template #loading>
<div class="row justify-center q-my-md">
<q-spinner-dots color="primary" size="40px" />
</div>
Expand All @@ -47,7 +50,7 @@
</template>

<script setup lang="ts">
import { defineComponent, ref, reactive, watch } from 'vue'
import { ref, reactive, watch } from 'vue'
import { getBookList } from 'src/services/book'
import { icon } from 'src/assets/icon'
import { QGrid, QGridItem } from 'components/grid'
Expand All @@ -56,38 +59,87 @@ import { BookInList } from 'src/services/book/types'
import { useRouter } from 'vue-router'
import SearchInput from 'components/SearchInput.vue'
const router = useRouter()
const route = useRoute()
const scrollEleInstanceRef = ref<null | {
stop(): void
reset(): void
resume(): void
poll(): void
}>(null)
const loading = ref(false)
/** 移除精确搜索的双引号 */
function getTrimmedKeyword(str: string) {
return str.replace(/^"(.+)"$/, '$1')
}
defineComponent({ QGrid, QGridItem, BookCard })
const props = defineProps<{ keyWords: string }>()
const router = useRouter()
const scroll = ref()
/** 数组的最后一个,如果不是数组就返回输入值 */
function last(param: string | string[]): string {
if (Array.isArray(param)) {
return param[param.length - 1]
}
return param
}
const searchKey = ref(getTrimmedKeyword(props.keyWords))
const isExactInRoute = computed(() => {
const exact = !!last(route.query?.exact ?? '')
return exact
})
/**
* 路由上指定的关键词
*
* @desc
* 这里就组装好数据是为了简化watch逻辑:keyword和extra变了都要初始化,
*
* 但初始化逻辑中体现不了对 exact 的使用,所以退而求其次放这里来了,简化维护是心智负担
*/
const searchKeyInRoute = computed(() => {
const keyword = last(route.params?.keyWords ?? '')
return isExactInRoute.value ? `"${keyword}"` : keyword
})
/** 仅用作search-input的受控记录 */
const searchKeyInInput = ref('')
const searchInputWidth = () => {
return '60vw'
}
const requestBook = async (index, done) => {
let res = await getBookList({ Page: index, Size: 24, KeyWords: props.keyWords })
bookData.push(...res.Data)
if (res.TotalPages === index || res.TotalPages === 0) scroll.value.stop()
else done()
const requestBook = async (index: number, done: (stop?: boolean) => void) => {
loading.value = true
try {
let res = await getBookList({ Page: index, Size: 24, KeyWords: searchKeyInRoute.value })
bookData.push(...res.Data)
if (res.TotalPages === index || res.TotalPages === 0) scrollEleInstanceRef.value.stop()
else done()
} finally {
loading.value = false
}
}
function search() {
router.push({ name: 'Search', params: { keyWords: searchKey.value } })
function onSearch(val: string, exact: boolean) {
router.push({ name: 'Search', params: { keyWords: val }, query: { exact: exact ? '1' : '' } })
}
// 同步路由的值到input中并触发容器初始化
watch(
() => props.keyWords,
() => {
searchKey.value = getTrimmedKeyword(props.keyWords)
scroll.value.reset()
scroll.value.resume()
scroll.value.poll()
[searchKeyInRoute, scrollEleInstanceRef],
([nextSearchKey, instance]) => {
if (!instance) {
return
}
searchKeyInInput.value = getTrimmedKeyword(nextSearchKey)
instance.reset()
instance.resume()
instance.poll()
// 数组在这重置还有一层用意:触发滚动容器回调;poll调用后理应就会触发回调,但实际情况并非如此
// TODO:探明滚动容器触发条件
bookData.length = 0
}
},
{ immediate: true }
)
const tabOptions: Array<Record<string, any>> = [
{
Expand Down

0 comments on commit 9a5db03

Please sign in to comment.