Browse Source

重构搜索结果

1. 解析url到状态,支持相同url得到相同结果
2. 搜索过程使用search store执行与保存
3. 添加高级搜索表单
Kevin Jiang 3 years ago
parent
commit
d64b06b9a1

+ 28 - 0
.dockerignore

@@ -0,0 +1,28 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 10 - 1
src/App.vue

@@ -1,7 +1,16 @@
 <script setup lang="ts">
 import { RouterView } from "vue-router";
+import zhCN from 'ant-design-vue/es/locale/zh_CN';
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+import { ref } from "vue";
+dayjs.locale('zh-cn');
+
+const locale = ref(zhCN);
 </script>
 
 <template>
-  <RouterView />
+  <a-config-provider :locale="locale">
+    <RouterView />
+  </a-config-provider>
 </template>

+ 3 - 0
src/components/NavMenu.vue

@@ -65,6 +65,9 @@ function getOpenKeys(path: string): string[] {
       <a-menu-item key="/search/index">
         <RouterLink to="/search/index">智能搜索</RouterLink>
       </a-menu-item>
+      <a-menu-item key="/search/advanced/index">
+        <RouterLink to="/search/advanced/index">高级搜索</RouterLink>
+      </a-menu-item>
     </a-sub-menu>
     <a-sub-menu key="/knowledge-graph">
       <template #icon>

+ 5 - 0
src/router/index.ts

@@ -37,6 +37,11 @@ const router = createRouter({
           component: () => import("../views/search/SearchView.vue")
         },
         {
+          path: "advanced/index",
+          name: "AdvancedSearchIndex",
+          component: () => import("../views/search/AdvancedSearchView.vue"),
+        },
+        {
           path: "result",
           name: "SearchResult",
           component: () => import("../views/search/SearchResultView.vue")

+ 44 - 0
src/stores/search.store.ts

@@ -0,0 +1,44 @@
+import { ref, type Ref } from "vue";
+import { defineStore } from "pinia";
+import type { AggItem, SearchResult } from "@/types/search.types";
+import * as searchService from "@/services/search.service";
+import type { SearchRequest } from "@/types/request.types";
+
+export const useSearchStore = defineStore('search', () => {
+  const result: Ref<SearchResult | null> = ref(null);
+  const yearAggResult: Ref<AggItem[] | null> = ref(null);
+  const authorAggResult: Ref<AggItem[] | null> = ref(null);
+
+  function search(request: SearchRequest) {
+    return searchService.search(request).then((resp) => {
+      result.value = resp.data.data;
+    });
+  }
+
+  async function aggYear(request: SearchRequest) {
+    try {
+      const resp = await searchService.search(request);
+      yearAggResult.value = resp.data.data.aggs.year || [];
+    } catch (err) {
+      console.log(err);
+    }
+  }
+
+  async function aggAuthor(request: SearchRequest) {
+    try {
+      const resp = await searchService.search(request);
+      authorAggResult.value = resp.data.data.aggs.AU || [];
+    } catch (err) {
+      console.log(err);
+    }
+  }
+
+  return {
+    result,
+    yearAggResult,
+    authorAggResult,
+    search,
+    aggYear,
+    aggAuthor,
+  }
+});

+ 1 - 0
src/types/request.types.ts

@@ -1,6 +1,7 @@
 export interface SearchRequest {
   dataSources?: string[];
   query: string;
+  filter?: string;
   from?: number;
   size?: number;
   aggs?: SearchAggRequest;

+ 9 - 9
src/views/knowledge-grapn/KnowLedgeGraphView.vue

@@ -2,7 +2,7 @@
 import { onMounted, ref, computed, type Ref } from "vue";
 import G6 from '@antv/g6';
 
-import * as searchService from "@/services/search.service";
+// import * as searchService from "@/services/search.service";
 import type { SearchResult } from "@/types/search.types";
 import type { GraphData } from "@/types/knowledgeGraph.types";
 import SearchResultList from "../search/components/SearchResultList.vue";
@@ -88,14 +88,14 @@ onMounted(() => {
   });
 });
 
-function searchSubject(name: string) {
-  searchService.search({ "query": `TP=(${name})` }).then((resp) => {
-    searchResult.value = resp.data.data;
-  }).catch((err) => {
-    console.error(err);
-    message.warning('搜索学科分类遇到问题,请稍后再试');
-  })
-}
+// function searchSubject(name: string) {
+//   searchService.search({ "query": `TP=(${name})` }).then((resp) => {
+//     searchResult.value = resp.data.data;
+//   }).catch((err) => {
+//     console.error(err);
+//     message.warning('搜索学科分类遇到问题,请稍后再试');
+//   })
+// }
 </script>
 
 <template>

+ 59 - 0
src/views/search/AdvancedSearchView.vue

@@ -0,0 +1,59 @@
+<script setup lang="ts">
+import { message } from "ant-design-vue";
+import { ref } from "vue";
+import { routeToSearch } from "./search.helper";
+
+const query = ref('');
+
+function onSearch() {
+  const q = query.value.trim();
+  if (q.length == 0) {
+    message.warning("请输入查询关键词");
+    return;
+  }
+
+  routeToSearch({ type: 'advanced', query: query.value });
+}
+</script>
+
+<template>
+  <div class="advanced-search-wrap">
+    <h1 class="title">高级搜索</h1>
+    <a-divider></a-divider>
+    <div class="search-box-wrap">
+      <div>查询表达式:</div>
+      <div>
+        <a-textarea v-model:value="query" placeholder="请输入查询表达式" :rows="4" />
+      </div>
+      <div class="operation-wrap">
+        <a-space>
+          <a-button>清除</a-button>
+          <a-button type="primary" @click="onSearch">搜索</a-button>
+        </a-space>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.advanced-search-wrap {
+  width: 80%;
+  margin: 0 auto;
+  margin-top: 10%;
+
+  h1.title {
+    font-size: 2rem;
+    text-align: center;
+  }
+
+  .search-box-wrap {
+    > div {
+      margin-top: 10px;
+    }
+
+    .operation-wrap {
+      text-align: right;
+    }
+  }
+}
+</style>

+ 182 - 155
src/views/search/SearchResultView.vue

@@ -1,17 +1,19 @@
 <script setup lang="ts">
-import { onUnmounted, ref, type Ref, onMounted } from "vue";
-import { useRoute, useRouter } from "vue-router";
+import { onUnmounted, ref, computed, type Ref, onMounted, reactive } from "vue";
+import { useRoute } from "vue-router";
 import { useSideBarStore } from "@/stores/side-bar";
-import * as searchService from "@/services/search.service";
-import type { SearchResult, AggItem } from "@/types/search.types";
 import SearchBox from "./components/SearchBox.vue";
 import SearchResultToolbar from "./components/SearchResultToolbar.vue";
 import SearchResultList from "./components/SearchResultList.vue";
 import type { SearchRequest } from "@/types/request.types";
+import { useSearchStore } from "@/stores/search.store";
+import { routeToSearch, type SearchRouteParam } from "./search.helper";
+import type { Sort } from "./search-component.type";
 
 const sideBarStore = useSideBarStore();
-// 路由管理器
-const router = useRouter();
+
+const searchStore = useSearchStore();
+
 // 当前路由对象
 const route = useRoute();
 
@@ -20,206 +22,225 @@ const keyword = ref("");
 // 字段
 const field = ref("TP");
 // 排序
-const sort = ref("Relevance");
+const sort: Ref<Sort> = ref("Relevance");
 
-const page = ref(1);
+const pagination = reactive({
+  page: 1,
+  size: 10,
+})
 
-const size = ref(10);
+let authorChecked: Ref<string[]> = ref([]);
+let yearChecked: Ref<string[]> = ref([]);
 
-let authorChecked = ref([]);
-let yearChecked = ref([]);
+const searchResult = computed(() => searchStore.result);
+const yearAggResult = computed(() => searchStore.yearAggResult);
+const authorAggResult = computed(() => searchStore.authorAggResult);
 
-let yearValueArr: string[] = []
-let authorValueArr: string[] = []
+const searchType: Ref<'simple' | 'advanced'> = ref('simple');
 
-// 搜索结果
-const searchResult: Ref<SearchResult | null> = ref(null);
+const query = ref('');
+
+interface SearchParams {
+  query: string;
+  yearSelected?: string[];
+  authorSelected?: string[];
+  sort?: Sort;
+  page?: number;
+  size?: number;
+}
 
-// 搜索结果
-const aggYearResult: Ref<AggItem[] | null> = ref(null);
-// 搜索结果
-const aggAuthorResult: Ref<AggItem[] | null> = ref(null);
+function performSearch(searchParams: SearchParams) {
+  let sortParam: Object = { "Relevance": "Relevance" };
+  if (searchParams.sort) {
+    const sort = searchParams.sort;
+    if (sort === 'DateDesc') {
+      sortParam = { "year": "DESC" }
+    } else if (sort === 'DateAsc') {
+      sortParam = { "year": "ASC" }
+    }
+  }
 
-// 根据年份聚合
-function aggsYearSearch(query: string) {
-  const aggYear = {
-    name: 'year',
-    field: 'year'
+  let filters: string[] = []
+  if (searchParams.yearSelected && searchParams.yearSelected.length > 0) {
+    filters.push(`PY=(${searchParams.yearSelected.join(" ")})`);
   }
-  const aggs: { [key: string]: any } = {}
-  aggs.year = aggYear;
-  const param = {
-    query: query,
-    aggs: aggs
-  };
-  searchService.search(param).then((resp) => {
-    aggYearResult.value = resp.data.data.aggs.year ? resp.data.data.aggs.year : []
-  }).catch((err) => {
-    console.log(err);
-  })
-}
+  if (searchParams.authorSelected && searchParams.authorSelected.length > 0) {
+    filters.push(`AU=(${searchParams.authorSelected.join(" OR ")})`);
+  }
+  const filter = filters.join(" AND ");
 
-// 根据作者聚合
-function aggsAuthorsSearch(query: string) {
+  // size
+  const size = Math.max(searchParams.size || 10, 10);
+  // from
+  const from = (Math.max(searchParams.page || 1, 1) - 1) * size;
 
-  const aggAuthors = {
-    name: 'authors',
-    field: 'authors.keyword'
+  const commonParams = {
+    query: searchParams.query,
+    filter,
   }
-  const aggs: { [key: string]: any } = {}
-  aggs.authors = aggAuthors;
-  const param = {
-    query: query,
-    aggs: aggs
+
+  const param: SearchRequest = {
+    ...commonParams,
+    from,
+    size,
+    sort: sortParam,
   };
-  searchService.search(param).then((resp) => {
-    aggAuthorResult.value = resp.data.data.aggs.authors ? resp.data.data.aggs.authors : []
-  }).catch((err) => {
-    console.log(err);
-  })
+
+  searchStore.search(param);
+  searchStore.aggYear({
+    ...commonParams,
+    aggs: { year: {} },
+    size: 0,
+  });
+  searchStore.aggAuthor({
+    ...commonParams,
+    aggs: { AU: {} },
+    size: 0,
+  });
 }
 
-function doSearch(field: string, keyword: string) {
-  if (keyword && keyword.length == 0) {
-    router.push({ name: "SearchIndex" });
-    return;
+/**
+ * 执行搜索,同时修改url
+ */
+function doSearch() {
+  let params: SearchRouteParam = {
+    type: searchType.value,
   }
-  router.push({ name: "SearchResult", query: { kw: keyword } });
-  const query = `${field}=(${keyword})`;
-  let sortParam = null;
-  if (sort.value === 'Relevance') {
-    sortParam = { "Relevance": "Relevance" }
-  } else if (sort.value === 'DateDesc') {
-    sortParam = { "year": "DESC" }
-  } else if (sort.value === 'DateAsc') {
-    sortParam = { "year": "ASC" }
-  } else if (sort.value === 'CitedDesc') {
-    sortParam = { "cited": "ASC" }
+  if (searchType.value == 'simple') {
+    params['field'] = field.value;
+    params['kw'] = keyword.value;
+  } else {
+    params['query'] = query.value;
   }
 
-  const param: SearchRequest = {
-    query: query,
-    from: page.value,
-    size: size.value,
-    sort: sortParam
+  const extras = {
+    page: pagination.page,
+    size: pagination.size,
+    yearSelected: yearChecked.value,
+    authorSelected: authorChecked.value,
+    sort: sort.value,
+  }
+  routeToSearch(params, extras);
+
+  const searchParams: SearchParams = {
+    query: query.value,
+    yearSelected: yearChecked.value,
+    authorSelected: authorChecked.value,
+    sort: sort.value,
   };
-  const aggs: { [key: string]: any } = {}
-  if (yearValueArr && yearValueArr.length > 0) {
-    var yearQuery: string = "PY=";
-    var pyKw = '('
-    for (let i = 0; i < yearValueArr.length; i++) {
-      if (i == 0) {
-        pyKw += yearValueArr[i]
-      } else {
-        pyKw += " " + yearValueArr[i]
-      }
-
-      console.log(pyKw);
-      if (i == yearValueArr.length - 1) {
-        pyKw += ')'
-      }
+  performSearch(searchParams);
+}
+
+function onSimpleSearch() {
+  routeToSearch({type: 'simple', field: field.value, kw: keyword.value});
+  doSearch();
+}
+
+/**
+ * 根据url包含的信息,初始化状态信息
+ */
+function setupState() {
+  const queryParam = route.query;
+  if (queryParam.type == 'advanced') {
+    searchType.value = 'advanced';
+    query.value = queryParam.query as string;
+  } else {
+    searchType.value = 'simple';
+    // 初始化关键词
+    if (route.query.kw) {
+      keyword.value = (route.query.kw as string).trim();
+    }
+    if (route.query.field) {
+      field.value = (route.query.field as string).trim();
     }
-    param.query += ' AND ' + yearQuery + pyKw
+    query.value = `${field.value}=(${keyword.value})`
   }
 
-  if (authorValueArr && authorValueArr.length > 0) {
-    var auQuery: string = "AU=";
-    var pyKw = '('
-    for (let i = 0; i < authorValueArr.length; i++) {
-      if (i == 0) {
-        pyKw += authorValueArr[i]
-      } else {
-        pyKw += " " + authorValueArr[i]
-      }
-
-      console.log(pyKw);
-      if (i == authorValueArr.length - 1) {
-        pyKw += ')'
-      }
+  if (queryParam.yearSelected) {
+    let yearSelected: string[];
+    if (Array.isArray(queryParam.yearSelected)) {
+      yearSelected = queryParam.yearSelected as string[];
+    } else {
+      yearSelected = [queryParam.yearSelected];
     }
-    param.query += ' AND ' + auQuery + pyKw
+    yearChecked.value = yearSelected;
   }
-  searchResult.value = null
-  searchService.search(param).then((resp) => {
-    searchResult.value = resp.data.data;
-    if (yearValueArr && yearValueArr.length > 0) {
-      aggsAuthorsSearch(param.query)
+
+  if (queryParam.authorSelected) {
+    let authorSelected: string[];
+    if (Array.isArray(queryParam.authorSelected)) {
+      authorSelected = queryParam.authorSelected as string[];
     } else {
-      aggsYearSearch(param.query)
+      authorSelected = [queryParam.authorSelected];
     }
-  }).catch((err) => {
-    console.log(err);
-  })
+    authorChecked.value = authorSelected;
+  }
+  if (queryParam.sort) {
+    sort.value = queryParam.sort as Sort;
+  }
+  if (queryParam.page) {
+    pagination.page = parseInt(queryParam.page as string);
+  }
+  if (queryParam.size) {
+    pagination.size = parseInt(queryParam.size as string);
+  }
 }
 
 onMounted(() => {
   // 收起左侧边栏
   sideBarStore.setCollapse(true);
-  // 初始化关键词
-  if (route.query.kw) {
-    keyword.value = (route.query.kw as string).trim();
-  }
-  if (route.query.field) {
-    field.value = (route.query.field as string).trim();
-  }
+  // 初始化状态
+  setupState();
   // 搜索
-  doSearch(field.value, keyword.value);
-  const query = `${field.value}=(${keyword.value})`
-  aggsYearSearch(query);
-  aggsAuthorsSearch(query);
+  doSearch();
 });
 
-function changeFile(val: string) {
-  // 搜索
-  field.value = val
-  doSearch(field.value, keyword.value);
+function changeField(value: string) {
+  field.value = value;
+  query.value = `${field.value}=(${keyword.value})`;
 }
 
-function changKeyword(val: string) {
-  keyword.value = val
-  doSearch(field.value, keyword.value);
+function changKeyword(value: string) {
+  keyword.value = value;
+  query.value = `${field.value}=(${keyword.value})`;
 }
 
 function changeSort(val: any) {
-  sort.value = val
-  doSearch(field.value, keyword.value);
+  sort.value = val;
+  doSearch();
 }
 
-function changePagination(p: any, s: any) {
-  page.value = p
-  size.value = s
-  doSearch(field.value, keyword.value);
+function onPaginationChange(page: any, pageSize: any) {
+  pagination.page = page;
+  pagination.size = pageSize;
+  doSearch();
 }
 
 function changeYearChecked() {
-  yearValueArr = []
-  authorValueArr = []
-  for (let valueKey in yearChecked.value) {
-    yearValueArr.push(yearChecked.value[valueKey])
-  }
-  doSearch(field.value, keyword.value);
+  pagination.page = 1;
+  doSearch();
 }
 
 function changeAuthorChecked() {
-  authorValueArr = []
-  yearValueArr = []
-  for (let valueKey in authorChecked.value) {
-    authorValueArr.push(authorChecked.value[valueKey])
-  }
-  doSearch(field.value, keyword.value);
+  pagination.page = 1;
+  doSearch();
 }
 
 onUnmounted(() => {
   sideBarStore.setCollapse(false);
 });
-
 </script>
 
 <template>
   <div class="search-result-wrap">
     <div class="search-box-wrap">
-      <SearchBox v-model:keyword="keyword" v-model:field="field" @search="doSearch" @update:field="changeFile"
-        @update:keyword="changKeyword" />
+      <SearchBox
+        v-model:keyword="keyword"
+        v-model:field="field"
+        @search="onSimpleSearch"
+        @update:field="changeField"
+        @update:keyword="changKeyword"
+      />
     </div>
     <a-row type="flex" class="search-result-main" :gutter="10">
       <a-col :span="5">
@@ -229,8 +250,8 @@ onUnmounted(() => {
         <a-space direction="vertical" style="width: 100%" v-if="searchResult">
           <a-card title="年份">
             <a-checkbox-group v-model:value="yearChecked" style="width: 100%" @change="changeYearChecked"
-              v-if="aggYearResult && aggYearResult.length > 0">
-              <a-row v-for="(item, index) in aggYearResult" :key="index">
+              v-if="yearAggResult && yearAggResult.length > 0">
+              <a-row v-for="(item, index) in yearAggResult" :key="index">
                 <a-col :span="20">
                   <a-checkbox :value="item.key">{{ item.key }}</a-checkbox>
                 </a-col>
@@ -240,7 +261,7 @@ onUnmounted(() => {
           </a-card>
           <a-card title="作者">
             <a-checkbox-group v-model:value="authorChecked" style="width: 100%" @change="changeAuthorChecked">
-              <a-row v-for="(item, index) in aggAuthorResult" :key="index">
+              <a-row v-for="(item, index) in authorAggResult" :key="index">
                 <a-col :span="20">
                   <a-checkbox :value="item.key">{{ item.key }}</a-checkbox>
                 </a-col>
@@ -251,8 +272,14 @@ onUnmounted(() => {
         </a-space>
       </a-col>
       <a-col :span="19">
-        <SearchResultToolbar :data="searchResult" v-model:sort="sort" v-if="searchResult" @update:sort="changeSort"
-          @update:pagination="changePagination" />
+        <SearchResultToolbar
+          v-if="searchResult"
+          @update:sort="changeSort"
+          :data="searchResult"
+          v-model:sort="sort"
+          v-model:pagination="pagination"
+          @paginationChange="onPaginationChange"
+        />
         <SearchResultList :data="searchResult" v-if="searchResult" />
       </a-col>
     </a-row>

+ 2 - 4
src/views/search/SearchView.vue

@@ -1,21 +1,19 @@
 <script setup lang="ts">
 import { message, type SelectProps } from "ant-design-vue";
 import { ref } from "vue";
-import { useRouter } from "vue-router";
+import { routeToSearch } from "./search.helper";
 
 const keyword = ref("");
 
 const field = ref("TP");
 
-const router = useRouter();
-
 const onSearch = () => {
   const kwd = keyword.value.trim();
   if (kwd.length == 0) {
     message.warning("请输入查询关键词");
     return;
   }
-  router.push({name: 'SearchResult', query: { kw: kwd, field: field.value }});
+  routeToSearch({ type: 'simple', kw: kwd, field: field.value });
 }
 
 const fieldOptions = ref<SelectProps['options']>([

+ 28 - 12
src/views/search/components/SearchResultToolbar.vue

@@ -3,6 +3,7 @@ import type { SelectProps } from "ant-design-vue";
 import { ref } from "vue";
 import type { PropType } from "vue";
 import type { SearchResult } from "@/types/search.types";
+import type { Pagination } from "../search-component.type";
 
 // 排序选择项列表
 const sortOptions = ref<SelectProps["options"]>([
@@ -23,11 +24,17 @@ defineProps({
     validator(value: string) {
       return ["Relevance", "DateDesc", "DateAsc", "CitedDesc"].includes(value);
     }
+  },
+  pagination: {
+    type: Object as PropType<Pagination>,
+    default() {
+      return {page: 1, size: 10} as Pagination
+    }
   }
 });
 
 const emits = defineEmits<{
-  (e: "update:pagination", field: string, keyword: string): void;
+  (e: "paginationChange", field: string, keyword: string): void;
   (e: "update:sort", sort: string): void;
 }>();
 
@@ -39,12 +46,9 @@ function handleSortChange(value: string) {
   emits("update:sort", value);
 }
 
-function onChange(size: string, page: string) {
-  emits("update:pagination", size, page);
+function onPageChange(size: string, page: string) {
+  emits("paginationChange", size, page);
 }
-
-const currentPage = ref(1);
-
 </script>
 
 <template>
@@ -52,19 +56,31 @@ const currentPage = ref(1);
     <a-space>
       <span>搜索结果:共 {{ data.total }} 条</span>
       <a-divider type="vertical"></a-divider>
-      <!-- <span>
+      <span>
         <span>显示:</span>
         <a-space>
         <a href="#">简洁</a>
         <a href="#">详细</a>
         </a-space>
       </span>
-      <a-divider type="vertical"></a-divider> -->
-      <a-select ref="select" :value="sort" style="width: 160px" size="small" :options="sortOptions"
-        @change="handleSortChange"></a-select>
+      <a-divider type="vertical"></a-divider>
+      <a-select
+        ref="select"
+        :value="sort"
+        style="width: 160px"
+        size="small"
+        :options="sortOptions"
+        @change="handleSortChange"
+      ></a-select>
     </a-space>
-    <a-pagination v-model:current="data.page" v-model:pageSize="data.size" :total="data.total" show-less-items
-      size="small" @change="onChange" />
+    <a-pagination
+      :current="pagination.page"
+      :pageSize="pagination.size"
+      :total="data.total"
+      show-size-changer
+      size="small"
+      @change="onPageChange"
+    />
   </div>
 </template>
 

+ 7 - 0
src/views/search/search-component.type.ts

@@ -0,0 +1,7 @@
+
+export interface Pagination {
+  page: number;
+  size: number;
+}
+
+export type Sort = 'Relevance' | 'DateDesc' | 'DateAsc';

+ 105 - 0
src/views/search/search.helper.ts

@@ -0,0 +1,105 @@
+import router from "@/router/index";
+import type { LocationQuery } from "vue-router";
+import type { Sort } from "./search-component.type";
+
+export type SearchType = 'simple' | 'advanced';
+
+export interface SearchRouteParam {
+  type: SearchType;
+  query?: string;
+  field?: string;
+  kw?: string;
+  yearSelected?: string[];
+  authorSelected?: string[];
+  sort?: Sort;
+  page?: number;
+  size?: number;
+}
+
+/**
+ * 路由到搜索结果页面,把查询表达式传入到搜索结果页面
+ * @param param queryString参数
+ */
+export function routeToSearch(param: SearchRouteParam, extras: {[index: string]: any} = {}) {
+  const queryStrParam: {[index: string]: any} = { ...param, ...extras };
+
+  router.push({ name: 'SearchResult', query: queryStrParam });
+}
+
+/**
+ * 解析url query string为SearchRouteParam对象
+ * @param queryParam parsed url query string
+ * @returns SearchRouteParam
+ */
+export function parseUrlQuery(queryParam: LocationQuery): SearchRouteParam {
+  let type: SearchType;
+  let query: string = '';
+  let field: string = '';
+  let kw: string = '';
+  if (queryParam.type == 'advanced') {
+    type = 'advanced';
+    query = queryParam.query as string;
+  } else {
+    type = 'simple';
+    // 初始化关键词
+    if (queryParam.kw) {
+      kw = (queryParam.kw as string).trim();
+    }
+    if (queryParam.field) {
+      field = (queryParam.field as string).trim();
+    }
+    query = `${field}=(${kw})`
+  }
+
+  let yearSelected: string[] = [];
+  if (queryParam.yearSelected) {
+    if (Array.isArray(queryParam.yearSelected)) {
+      yearSelected = queryParam.yearSelected as string[];
+    } else {
+      yearSelected = [queryParam.yearSelected];
+    }
+  }
+
+  let authorSelected: string[] = [];
+  if (queryParam.authorSelected) {
+    if (Array.isArray(queryParam.authorSelected)) {
+      authorSelected = queryParam.authorSelected as string[];
+    } else {
+      authorSelected = [queryParam.authorSelected];
+    }
+  }
+
+  let sort: Sort = 'Relevance';
+  if (queryParam.sort) {
+    sort = queryParam.sort as Sort;
+  }
+
+  let page: number | null = null;
+  if (queryParam.page) {
+    page = parseInt(queryParam.page as string);
+  }
+
+  let size: number | null = null;
+  if (queryParam.size) {
+    size = parseInt(queryParam.size as string);
+  }
+
+  const result: SearchRouteParam = {
+    type,
+    query,
+    field,
+    kw,
+    yearSelected,
+    authorSelected,
+    sort,
+  };
+
+  if (page) {
+    result['page'] = page;
+  }
+  if (size) {
+    result['size'] = size;
+  }
+
+  return result;
+}