瀏覽代碼

完成智能搜索,左侧根据作者聚合还有点问题,待修改

唐钦 3 年之前
父節點
當前提交
281ff1794e

+ 36 - 35
src/mocks/search.handler.ts

@@ -1,35 +1,36 @@
-import { rest } from 'msw';
-import { urlPrefix } from './mock.settings';
-import type { Response, SearchResult } from '@/types/search.types';
-
-export const handlers = [
-  rest.post(`${urlPrefix}/search`, (req, res, ctx) => {
-    const response: Response<SearchResult> = {
-      status: 200,
-      msg: '',
-      data: {
-        total: 12,
-        docs: [
-          {
-            title: "标题",
-            authors: ["作者1"],
-            unit: "",
-            keywords: ["关键词1"],
-            summary: "摘要"
-          },
-          {
-            title: "标题",
-            authors: ["作者1"],
-            unit: "",
-            keywords: ["关键词1"],
-            summary: "摘要"
-          }
-        ]
-      }
-    };
-    return res(
-      ctx.status(200),
-      ctx.json(response)
-    );
-  }),
-];
+import {rest} from 'msw';
+import {urlPrefix} from './mock.settings';
+import type {Response} from '@/types/response.types';
+import type {SearchResult} from '@/types/search.types';
+
+export const handlers = [
+    rest.post(`${urlPrefix}/search`, (req, res, ctx) => {
+        const response: Response<SearchResult> = {
+            status: 200,
+            msg: '',
+            data: {
+                total: 12,
+                docs: [
+                    {
+                        title: "标题",
+                        authors: ["作者1"],
+                        unit: "",
+                        keywords: ["关键词1"],
+                        summary: "摘要"
+                    },
+                    {
+                        title: "标题",
+                        authors: ["作者1"],
+                        unit: "",
+                        keywords: ["关键词1"],
+                        summary: "摘要"
+                    }
+                ]
+            }
+        };
+        return res(
+            ctx.status(200),
+            ctx.json(response)
+        );
+    }),
+];

+ 35 - 35
src/services/httpClient.ts

@@ -1,35 +1,35 @@
-import { message } from "ant-design-vue";
-import axios, { AxiosError, type AxiosRequestConfig } from "axios";
-
-const urlPrefix = "/api";
-
-const httpClient = axios.create({
-  baseURL: urlPrefix,
-  timeout: 60000,
-});
-
-// 添加请求拦截器
-httpClient.interceptors.request.use(function (config: AxiosRequestConfig) {
-  // 在发送请求之前做些什么
-  // const token = localStorage.getItem('token');
-  // config.headers.Authorization = token;
-  return config;
-}, function (error) {
-  // 对请求错误做些什么
-  return Promise.reject(error);
-});
-
-// 添加响应拦截器
-httpClient.interceptors.response.use(function (response) {
-  // 对响应数据做点什么
-  return response;
-}, function (error: AxiosError) {
-  // 对响应错误做点什么
-  if (error.response?.status == 404) {
-    message.error("请求参数错误!");
-  }
-
-  return Promise.reject(error);
-});
-
-export default httpClient;
+import { message } from "ant-design-vue";
+import axios, { AxiosError, type AxiosRequestConfig } from "axios";
+
+const urlPrefix = "http://127.0.0.1:8082/v1";
+
+const httpClient = axios.create({
+  baseURL: urlPrefix,
+  timeout: 60000,
+});
+
+// 添加请求拦截器
+httpClient.interceptors.request.use(function (config: AxiosRequestConfig) {
+  // 在发送请求之前做些什么
+  // const token = localStorage.getItem('token');
+  // config.headers.Authorization = token;
+  return config;
+}, function (error) {
+  // 对请求错误做些什么
+  return Promise.reject(error);
+});
+
+// 添加响应拦截器
+httpClient.interceptors.response.use(function (response) {
+  // 对响应数据做点什么
+  return response;
+}, function (error: AxiosError) {
+  // 对响应错误做点什么
+  if (error.response?.status == 404) {
+    message.error("请求参数错误!");
+  }
+
+  return Promise.reject(error);
+});
+
+export default httpClient;

+ 13 - 13
src/services/search.service.ts

@@ -1,13 +1,13 @@
-import type { SearchResult } from "@/types/search.types";
-import type { Response } from "@/types/response.types";
-import type { AxiosResponse } from "axios";
-import httpClient from "./httpClient";
-
-function search(query: string): Promise<AxiosResponse<Response<SearchResult>>> {
-  const params = { query };
-  return httpClient.post("/search", params);
-}
-
-export default {
-  search,
-}
+import type { SearchResult } from "@/types/search.types";
+import type { Response } from "@/types/response.types";
+import type { AxiosResponse } from "axios";
+import httpClient from "./httpClient";
+
+function search(query: Object): Promise<AxiosResponse<Response<SearchResult>>> {
+  // const params = { query };
+  return httpClient.post("/search", query);
+}
+
+export default {
+  search,
+}

+ 13 - 12
src/types/search.types.ts

@@ -1,12 +1,13 @@
-export interface Doc {
-  title: string;
-  authors: string[];
-  unit: string;
-  keywords: string[];
-  summary: string;
-}
-
-export interface SearchResult {
-  total: number;
-  docs: Doc[];
-}
+export interface Doc {
+  title: string;
+  authors: string[];
+  unit: string;
+  keywords: string[];
+  summary: string;
+}
+
+export interface SearchResult {
+  aggs: any;
+  total: number;
+  docs: Doc[];
+}

+ 285 - 108
src/views/search/SearchResultView.vue

@@ -1,108 +1,285 @@
-<script setup lang="ts">
-import { onUnmounted, ref, type Ref, onMounted } from "vue";
-import { useRoute, useRouter } from "vue-router";
-import { useSideBarStore } from "@/stores/side-bar";
-import searchService from "@/services/search.service";
-import type { SearchResult } from "@/types/search.types";
-import SearchBox from "./components/SearchBox.vue";
-import SearchResultToolbar from "./components/SearchResultToolbar.vue";
-import SearchResultList from "./components/SearchResultList.vue";
-
-const sideBarStore = useSideBarStore();
-// 路由管理器
-const router = useRouter();
-// 当前路由对象
-const route = useRoute();
-
-// 关键词
-const keyword = ref("");
-// 字段
-const field = ref("TP");
-// 排序
-const sort = ref("Relevance");
-
-// 搜索结果
-const searchResult: Ref<SearchResult | null> = ref(null);
-
-function doSearch(field: string, keyword: string) {
-  if (keyword && keyword.length == 0) {
-    router.push({name: "SearchIndex"});
-    return;
-  }
-  router.push({name: "SearchResult", query: { kw: keyword }});
-  const query = `${field}=(${keyword})`;
-  searchService.search(query).then((resp) => {
-    searchResult.value = resp.data.data;
-  }).catch((err) => {
-    console.log(err);
-  })
-}
-
-onMounted(() => {
-  // 收起左侧边栏
-  sideBarStore.setCollapse(true);
-  // 初始化关键词
-  if (route.query.kw) {
-    keyword.value = (route.query.kw as string).trim();
-  }
-  // 搜索
-  doSearch(field.value, keyword.value);
-});
-
-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" />
-    </div>
-    <a-row type="flex" class="search-result-main" :gutter="10">
-      <a-col :span="5">
-        <div class="left-filter-header">
-          筛选
-        </div>
-        <a-space direction="vertical" style="width: 100%">
-          <a-card title="Default size card">
-            <p>card content</p>
-            <p>card content</p>
-            <p>card content</p>
-          </a-card>
-          <a-card title="Small size card">
-            <p>card content</p>
-            <p>card content</p>
-            <p>card content</p>
-          </a-card>
-        </a-space>
-      </a-col>
-      <a-col :span="19">
-        <SearchResultToolbar :data="searchResult" v-model:sort="sort" v-if="searchResult" />
-        <SearchResultList :data="searchResult" v-if="searchResult" />
-      </a-col>
-    </a-row>
-  </div>
-</template>
-
-<style scoped lang="scss">
-.search-result-wrap {
-  margin-top: 20px;
-
-  .search-box-wrap {
-    padding: 0 120px;
-  }
-
-  .search-result-main {
-    margin-top: 20px;
-  }
-
-  @media (max-width: 1024px) {
-    .search-box-wrap {
-      padding: 0 20px;
-    }
-  }
-}
-
-</style>
+<script setup lang="ts">
+    import {onUnmounted, ref, type Ref, onMounted} from "vue";
+    import {useRoute, useRouter} from "vue-router";
+    import {useSideBarStore} from "@/stores/side-bar";
+    import searchService from "@/services/search.service";
+    import type {SearchResult} from "@/types/search.types";
+    import SearchBox from "./components/SearchBox.vue";
+    import SearchResultToolbar from "./components/SearchResultToolbar.vue";
+    import SearchResultList from "./components/SearchResultList.vue";
+
+    const sideBarStore = useSideBarStore();
+    // 路由管理器
+    const router = useRouter();
+    // 当前路由对象
+    const route = useRoute();
+
+    // 关键词
+    const keyword = ref("");
+    // 字段
+    const field = ref("TP");
+    // 排序
+    const sort = ref("Relevance");
+
+    const page = ref(1);
+
+    const size = ref(10);
+
+    const authorChecked = ref(false);
+    const yearChecked = ref(false);
+
+    let yearValueArr: string[] = []
+    const authorValueArr: string[] = []
+
+    // 搜索结果
+    const searchResult: Ref<SearchResult | null> = ref(null);
+
+    // 搜索结果
+    const aggYearResult = ref(null);
+    // 搜索结果
+    const aggAuthorResult = ref(null);
+
+    function aggsYearSearch(field: string, keyword: string) {
+        if (keyword && keyword.length == 0) {
+            router.push({name: "SearchIndex"});
+            return;
+        }
+        const query = `${field}=(${keyword})`;
+        const aggYear = {
+            name: 'year',
+            field: 'year'
+        }
+        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);
+        })
+    }
+
+    function aggsAuthorsSearch(field: string, keyword: string) {
+        if (keyword && keyword.length == 0) {
+            router.push({name: "SearchIndex"});
+            return;
+        }
+        const query = `${field}=(${keyword})`;
+        const aggAuthors = {
+            name: 'authors',
+            field: 'authors.keyword'
+        }
+        const aggs: { [key: string]: any } = {}
+        aggs.authors = aggAuthors;
+        const param = {
+            query: query,
+            aggs: aggs
+        };
+        searchService.search(param).then((resp) => {
+            aggAuthorResult.value = resp.data.data.aggs.authors ? resp.data.data.aggs.authors : []
+        }).catch((err) => {
+            console.log(err);
+        })
+    }
+
+    function doSearch(field: string, keyword: string) {
+        if (keyword && keyword.length == 0) {
+            router.push({name: "SearchIndex"});
+            return;
+        }
+        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"}
+        }
+
+
+        const param: { [key: string]: any } = {
+            query: query,
+            from: page.value,
+            size: size.value,
+            sort: sortParam
+        };
+        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 += ')'
+                }
+            }
+            param.query += ' AND ' + yearQuery + pyKw
+        }
+
+        if (!authorValueArr || authorValueArr.length <= 0) {
+
+        } else {
+            // var yearQuery: string = "PY=";
+            // console.log(yearValueArr);
+            // 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 += ')'
+            //     }
+            // }
+            // param.query += ' AND ' + yearQuery + pyKw
+        }
+        searchResult.value = null
+        searchService.search(param).then((resp) => {
+            searchResult.value = resp.data.data;
+            if (!yearValueArr || yearValueArr.length <= 0) {
+
+
+            }
+        }).catch((err) => {
+            console.log(err);
+        })
+    }
+
+
+    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();
+        }
+        // 搜索
+        doSearch(field.value, keyword.value);
+        aggsYearSearch(field.value, keyword.value);
+        aggsAuthorsSearch(field.value, keyword.value);
+    });
+
+    function changeFile(val) {
+        // 搜索
+        field.value = val
+        doSearch(field.value, keyword.value);
+    }
+
+    function changKeyword(val) {
+        keyword.value = val
+        doSearch(field.value, keyword.value);
+    }
+
+    function changeSort(val) {
+        sort.value = val
+        doSearch(field.value, keyword.value);
+    }
+
+    function changePagination(p, s) {
+        page.value = p
+        size.value = s
+        doSearch(field.value, keyword.value);
+    }
+
+    function changeYearChecked() {
+        yearValueArr = []
+        for (let valueKey in yearChecked.value) {
+            yearValueArr.push(yearChecked.value[valueKey])
+        }
+        doSearch(field.value, keyword.value);
+    }
+
+    function changeAuthorChecked() {
+        console.log(authorChecked.value);
+    }
+
+    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"/>
+        </div>
+        <a-row type="flex" class="search-result-main" :gutter="10">
+            <a-col :span="5">
+                <div class="left-filter-header">
+                    筛选
+                </div>
+                <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">
+                                <a-col :span="20">
+                                    <a-checkbox :value="item.key">{{item.key}}</a-checkbox>
+                                </a-col>
+                                <a-col :span="4"> {{item.count}}</a-col>
+                            </a-row>
+                        </a-checkbox-group>
+                    </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-col :span="20">
+                                    <a-checkbox :value="item.key">{{item.key}}</a-checkbox>
+                                </a-col>
+                                <a-col :span="4"> {{item.count}}</a-col>
+                            </a-row>
+                        </a-checkbox-group>
+                    </a-card>
+                </a-space>
+            </a-col>
+            <a-col :span="19">
+                <SearchResultToolbar :data="searchResult" v-model:sort="sort" v-if="searchResult"
+                                     @update:sort="changeSort" @update:pagination="changePagination"/>
+                <SearchResultList :data="searchResult" v-if="searchResult"/>
+            </a-col>
+        </a-row>
+    </div>
+</template>
+
+<style scoped lang="scss">
+    .search-result-wrap {
+        margin-top: 20px;
+
+        .search-box-wrap {
+            padding: 0 120px;
+        }
+
+        .search-result-main {
+            margin-top: 20px;
+        }
+
+        @media (max-width: 1024px) {
+            .search-box-wrap {
+                padding: 0 20px;
+            }
+        }
+    }
+
+</style>

+ 1 - 1
src/views/search/SearchView.vue

@@ -15,7 +15,7 @@ const onSearch = () => {
     message.warning("请输入查询关键词");
     return;
   }
-  router.push({name: 'SearchResult', query: { kw: kwd }});
+  router.push({name: 'SearchResult', query: { kw: kwd, field: field.value }});
 }
 
 const fieldOptions = ref<SelectProps['options']>([

+ 73 - 72
src/views/search/components/SearchBox.vue

@@ -1,72 +1,73 @@
-<script setup lang="ts">
-import { message, type SelectProps } from "ant-design-vue";
-import { ref } from "vue";
-
-const props = defineProps({
-  keyword: {
-    type: String,
-    required: true,
-  },
-  field: {
-    type: String,
-    required: true,
-  }
-});
-
-const emits = defineEmits<{
-  (e: "search", field: string, keyword: string): void;
-  (e: "update:keyword", keyword: string): void;
-  (e: "update:field", field: string): void;
-}>();
-
-// 搜索框字段选择项列表
-const fieldOptions = ref<SelectProps['options']>([
-  { value: 'TP', label: '主题', },
-  { value: 'TI', label: '标题', },
-  { value: 'AU', label: '作者', },
-  { value: 'KW', label: '关键词', },
-]);
-
-function onSearch() {
-  const kwd = props.keyword.trim();
-  if (kwd.length == 0) {
-    message.warning("请输入查询关键词");
-    return;
-  }
-  emits("search", props.field, props.keyword);
-}
-
-function onKeywordChange(e: InputEvent) {
-  emits("update:keyword", (e.target as HTMLInputElement).value);
-}
-
-function onFieldChange(value: string) {
-  emits("update:field", value);
-}
-
-</script>
-
-<template>
-  <a-row type="flex">
-    <a-col>
-      <a-select
-        ref="select"
-        :value="field"
-        style="width: 120px"
-        :options="fieldOptions"
-        @change="onFieldChange"
-        size="large"
-      ></a-select>
-    </a-col>
-    <a-col flex="1">
-      <a-input-search
-        :value="keyword"
-        placeholder="请输入关键词进行搜索"
-        enter-button="搜&nbsp;&nbsp;索"
-        size="large"
-        @change="onKeywordChange"
-        @search="onSearch"
-      />
-    </a-col>
-  </a-row>
-</template>
+<script setup lang="ts">
+import { message, type SelectProps } from "ant-design-vue";
+import { ref } from "vue";
+
+const props = defineProps({
+  keyword: {
+    type: String,
+    required: true,
+  },
+  field: {
+    type: String,
+    required: true,
+  }
+});
+
+const emits = defineEmits<{
+  (e: "search", field: string, keyword: string): void;
+  (e: "update:keyword", keyword: string): void;
+  (e: "update:field", field: string): void;
+}>();
+
+// 搜索框字段选择项列表
+const fieldOptions = ref<SelectProps['options']>([
+  { value: 'TP', label: '主题', },
+  { value: 'TI', label: '标题', },
+  { value: 'AU', label: '作者', },
+  { value: 'KW', label: '关键词', },
+]);
+
+function onSearch() {
+  const kwd = props.keyword.trim();
+  if (kwd.length == 0) {
+    message.warning("请输入查询关键词");
+    return;
+  }
+  emits("search", props.field, props.keyword);
+}
+
+function onKeywordChange(e: InputEvent) {
+  emits("update:keyword", (e.target as HTMLInputElement).value);
+}
+
+function onFieldChange(value: string) {
+  emits("update:field", value);
+}
+
+
+</script>
+
+<template>
+  <a-row type="flex">
+    <a-col>
+      <a-select
+        ref="select"
+        :value="field"
+        style="width: 120px"
+        :options="fieldOptions"
+        @change="onFieldChange"
+        size="large"
+      ></a-select>
+    </a-col>
+    <a-col flex="1">
+      <a-input-search
+        :value="keyword"
+        placeholder="请输入关键词进行搜索"
+        enter-button="搜&nbsp;&nbsp;索"
+        size="large"
+        @change="onKeywordChange"
+        @search="onSearch"
+      />
+    </a-col>
+  </a-row>
+</template>

+ 27 - 27
src/views/search/components/SearchResultItem.vue

@@ -1,27 +1,27 @@
-<script setup lang="ts">
-import type { PropType } from "vue";
-import type { Doc } from "@/types/search.types";
-
-defineProps({
-  data: {
-    type: Object as PropType<Doc>,
-    required: true,
-  }
-});
-</script>
-
-<template>
-  <div class="item">
-    <h1>{{ data.title }}</h1>
-    <p>作者:{{ data.authors }}</p>
-    <p>机构:{{ data.unit }}</p>
-    <p>关键词:{{ data.keywords }}</p>
-    <p>摘要:{{ data.summary }}</p>
-  </div>
-</template>
-
-<style scoped lang="scss">
-.item {
-  padding: 5px;
-}
-</style>
+<script setup lang="ts">
+import type { PropType } from "vue";
+import type { Doc } from "@/types/search.types";
+
+defineProps({
+  data: {
+    type: Object as PropType<Doc>,
+    required: true,
+  }
+});
+</script>
+
+<template>
+  <div class="item">
+    <h1>{{ data.title }}</h1>
+    <p>作者:{{ data.authors.join(",") }}</p>
+    <p>机构:{{ data.unit }}</p>
+    <p>关键词:{{ data.keywords.join(",") }}</p>
+    <p>摘要:<span v-html="data.abstract.replace('摘要:', '')"></span></p>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.item {
+  padding: 5px;
+}
+</style>

+ 84 - 78
src/views/search/components/SearchResultToolbar.vue

@@ -1,78 +1,84 @@
-<script setup lang="ts">
-import type { SelectProps } from "ant-design-vue";
-import { ref } from "vue";
-import type { PropType } from "vue";
-import type { SearchResult } from "@/types/search.types";
-
-// 排序选择项列表
-const sortOptions = ref<SelectProps["options"]>([
-  { value: "Relevance", label: "按相关度排序" },
-  { value: "DateDesc", label: "按时间由近到远排序" },
-  { value: "DateAsc", label: "按时间由远到近排序" },
-  { value: "CitedDesc", label: "按被引量排序" },
-]);
-
-defineProps({
-  data: {
-    type: Object as PropType<SearchResult>,
-    required: true,
-  },
-  sort: {
-    type: String,
-    required: true,
-    validator(value: string) {
-      return [ "Relevance", "DateDesc", "DateAsc", "CitedDesc" ].includes(value);
-    }
-  }
-});
-
-const emits = defineEmits<{
-  (e: "update:sort", sort: string): void
-}>();
-
-/**
- * 排序方式改变监听函数
- * @param value 排序方式
- */
- function handleSortChange(value: string) {
-  emits("update:sort", value);
-}
-
-const currentPage = ref(1);
-
-</script>
-
-<template>
-  <div class="search-result-toolbar">
-    <a-space>
-      <span>搜索结果:共 {{ data.total }} 条</span>
-      <a-divider type="vertical"></a-divider>
-      <!-- <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-space>
-    <a-pagination v-model:current="currentPage" :total="data.total" show-less-items size="small" />
-  </div>
-</template>
-
-<style scoped lang="scss">
-
-.search-result-toolbar {
-  display: flex;
-  padding-top: 5px;
-  justify-content: space-between;
-}
-</style>
+<script setup lang="ts">
+    import type {SelectProps} from "ant-design-vue";
+    import {ref} from "vue";
+    import type {PropType} from "vue";
+    import type {SearchResult} from "@/types/search.types";
+
+    // 排序选择项列表
+    const sortOptions = ref<SelectProps["options"]>([
+        {value: "Relevance", label: "按相关度排序"},
+        {value: "DateDesc", label: "按时间由近到远排序"},
+        {value: "DateAsc", label: "按时间由远到近排序"},
+        {value: "CitedDesc", label: "按被引量排序"},
+    ]);
+
+    defineProps({
+        data: {
+            type: Object as PropType<SearchResult>,
+            required: true,
+        },
+        sort: {
+            type: String,
+            required: true,
+            validator(value: string) {
+                return ["Relevance", "DateDesc", "DateAsc", "CitedDesc"].includes(value);
+            }
+        }
+    });
+
+    const emits = defineEmits<{
+        (e: "update:pagination", field: string, keyword: string): void;
+        (e: "update:sort", sort: string): void;
+    }>();
+
+    /**
+     * 排序方式改变监听函数
+     * @param value 排序方式
+     */
+    function handleSortChange(value: string) {
+        emits("update:sort", value);
+    }
+
+    function onChange(size, page) {
+        emits("update:pagination", size, page);
+    }
+
+    const currentPage = ref(1);
+
+</script>
+
+<template>
+    <div class="search-result-toolbar">
+        <a-space>
+            <span>搜索结果:共 {{ data.total }} 条</span>
+            <a-divider type="vertical"></a-divider>
+            <!-- <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-space>
+        <a-pagination v-model:current="data.page" v-model:pageSize = "data.size" :total="data.total" show-less-items size="small"
+                      @change="onChange"/>
+    </div>
+</template>
+
+<style scoped lang="scss">
+
+    .search-result-toolbar {
+        display: flex;
+        padding-top: 5px;
+        justify-content: space-between;
+    }
+</style>