Browse Source

完善搜索结果页面

Kevin Jiang 3 years ago
parent
commit
dc348b6c61

+ 2 - 13
src/mocks/handlers.ts

@@ -1,16 +1,5 @@
-import { rest } from 'msw';
-
-const urlPrefix = "/api";
+import { handlers as searchHandlers } from "./search.handler";
 
 export const handlers = [
-  rest.post(`${urlPrefix}/search`, (req, res, ctx) => {
-    return res(
-      ctx.status(200),
-      ctx.json({
-        status: 200,
-        msg: '',
-        data: [1, 2, 3]
-      })
-    );
-  }),
+  ...searchHandlers
 ];

+ 2 - 0
src/mocks/mock.settings.ts

@@ -0,0 +1,2 @@
+
+export const urlPrefix = "/api";

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

@@ -0,0 +1,35 @@
+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)
+    );
+  }),
+];

+ 0 - 2
src/services/httpClient.ts

@@ -1,6 +1,5 @@
 import { message } from "ant-design-vue";
 import axios, { AxiosError, type AxiosRequestConfig } from "axios";
-import _ from "lodash";
 
 const urlPrefix = "/api";
 
@@ -14,7 +13,6 @@ httpClient.interceptors.request.use(function (config: AxiosRequestConfig) {
   // 在发送请求之前做些什么
   // const token = localStorage.getItem('token');
   // config.headers.Authorization = token;
-  // config.url = urlPrefix + "/" + _.trimStart(config.url, '/');
   return config;
 }, function (error) {
   // 对请求错误做些什么

+ 2 - 1
src/services/search.service.ts

@@ -1,8 +1,9 @@
 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<SearchResult>> {
+function search(query: string): Promise<AxiosResponse<Response<SearchResult>>> {
   const params = { query };
   return httpClient.post("/search", params);
 }

+ 5 - 0
src/types/response.types.ts

@@ -0,0 +1,5 @@
+export interface Response<T> {
+  status: number;
+  msg: string;
+  data: T;
+}

+ 10 - 1
src/types/search.types.ts

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

+ 61 - 140
src/views/search/SearchResultView.vue

@@ -1,75 +1,52 @@
 <script setup lang="ts">
-import { onMounted, onUnmounted, ref, watch } from "vue";
+import { onUnmounted, ref, type Ref, onMounted } from "vue";
 import { useRoute, useRouter } from "vue-router";
-import { message, type SelectProps } from "ant-design-vue";
 import { useSideBarStore } from "@/stores/side-bar";
-import SearchResultItem from "./components/SearchResultItem.vue";
 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 router = useRouter();
+// 排序
+const sort = ref("Relevance");
 
-function doSearch() {
-  if (keyword.value.length == 0) {
-    keyword.value = (route.query.kw as string) || "";
-    if (keyword.value.length == 0) {
-      router.push({name: "SearchIndex"});
-      return;
-    }
+// 搜索结果
+const searchResult: Ref<SearchResult | null> = ref(null);
+
+function doSearch(field: string, keyword: string) {
+  if (keyword && keyword.length == 0) {
+    router.push({name: "SearchIndex"});
+    return;
   }
-  searchService.search("query").then((resp) => {
-    console.log(resp.data);
+  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);
   })
 }
 
-// 搜索事件监听
-const onSearch = () => {
-  router.push({name: 'SearchResult', query: { kw: keyword.value }});
-
-  doSearch();
-}
-// 搜索框字段选择项列表
-const fieldOptions = ref<SelectProps['options']>([
-  { value: 'TP', label: '主题', },
-  { value: 'TI', label: '标题', },
-  { value: 'AU', label: '作者', },
-  { value: 'KW', label: '关键词', },
-]);
-// 排序方式
-const sortOption = ref('Relevance');
-// 排序选择项列表
-const sortOptions = ref<SelectProps['options']>([
-  { value: 'Relevance', label: '按相关度排序' },
-  { value: 'DateDesc', label: '按时间由近到远排序' },
-  { value: 'DateAsc', label: '按时间由远到近排序' },
-  { value: 'CitedDesc', label: '按被引量排序' },
-]);
-
-/**
- * 排序方式改变监听函数
- * @param event 排序方式改变事件
- */
-function handleSortChange(event: any) {
-  console.log('handleSortChange', event);
-}
-
-const currentPage = ref(1);
-
 onMounted(() => {
   // 收起左侧边栏
   sideBarStore.setCollapse(true);
-
-  doSearch();
+  // 初始化关键词
+  if (route.query.kw) {
+    keyword.value = (route.query.kw as string).trim();
+  }
+  // 搜索
+  doSearch(field.value, keyword.value);
 });
 
 onUnmounted(() => {
@@ -79,109 +56,53 @@ onUnmounted(() => {
 </script>
 
 <template>
-  <div class="search-box-wrap">
-    <a-row type="flex">
-      <a-col>
-        <a-select
-          ref="select"
-          v-model:value="field"
-          style="width: 120px"
-          :options="fieldOptions"
-          size="large"
-        ></a-select>
+  <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 flex="1">
-        <a-input-search
-          v-model:value="keyword"
-          placeholder="请输入关键词进行搜索"
-          enter-button="搜&nbsp;&nbsp;索"
-          size="large"
-          @search="onSearch"
-        />
+      <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>
-  <a-row type="flex" class="search-result-main" :gutter="10">
-    <a-col :span="5" class="left-filter-wrap">
-      <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" class="right-content-wrap">
-      <div class="right-header">
-        <a-space>
-          <span>搜索结果:共 XX 条</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"
-            v-model:value="sortOption"
-            style="width: 160px"
-            size="small"
-            :options="sortOptions"
-            @change="handleSortChange"
-          ></a-select>
-        </a-space>
-        <a-pagination v-model:current="currentPage" :total="50" show-less-items size="small" />
-      </div>
-      <div class="search-result-content">
-        <SearchResultItem />
-        <a-divider />
-        <SearchResultItem />
-      </div>
-    </a-col>
-  </a-row>
 </template>
 
 <style scoped lang="scss">
-.search-box-wrap {
-  padding: 0 120px;
-}
-
-.search-result-main {
+.search-result-wrap {
   margin-top: 20px;
 
-  .left-filter-header {
-    font-size: 1.25rem;
+  .search-box-wrap {
+    padding: 0 120px;
   }
 
-  .right-content-wrap {
-    .right-header {
-      display: flex;
-      padding-top: 5px;
-      justify-content: space-between;
-    }
+  .search-result-main {
+    margin-top: 20px;
+  }
 
-    .search-result-content {
-      background-color: #ffffff;
-      margin-top: 18px;
-      padding: 10px;
+  @media (max-width: 1024px) {
+    .search-box-wrap {
+      padding: 0 20px;
     }
   }
 }
 
-@media (max-width: 1024px) {
-  .search-box-wrap {
-    padding: 0 20px;
-  }
-}
 </style>

+ 12 - 0
src/views/search/components/EmptySearchResult.vue

@@ -0,0 +1,12 @@
+<script setup lang="ts">
+defineProps({
+  keyword: {
+    type: String,
+    required: true
+  }
+});
+</script>
+
+<template>
+  <div>没有找到关键词 {{ keyword }} 的搜索结果</div>
+</template>

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

@@ -0,0 +1,72 @@
+<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>

+ 17 - 5
src/views/search/components/SearchResultItem.vue

@@ -1,10 +1,22 @@
+<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>标题</h1>
-    <p>作者:作者1,作者2</p>
-    <p>机构:机构</p>
-    <p>关键词:关键词1,关键词2</p>
-    <p>摘要:作者1,作者2</p>
+    <h1>{{ data.title }}</h1>
+    <p>作者:{{ data.authors }}</p>
+    <p>机构:{{ data.unit }}</p>
+    <p>关键词:{{ data.keywords }}</p>
+    <p>摘要:{{ data.summary }}</p>
   </div>
 </template>
 

+ 31 - 0
src/views/search/components/SearchResultList.vue

@@ -0,0 +1,31 @@
+<script setup lang="ts">
+import { computed, type PropType } from "vue";
+import SearchResultItem from "./SearchResultItem.vue";
+import type { SearchResult } from "@/types/search.types";
+
+const props = defineProps({
+  data: {
+    type: Object as PropType<SearchResult>,
+    required: true,
+  }
+});
+
+const docSize = computed(() => props.data.docs.length);
+</script>
+
+<template>
+  <div class="search-result-content">
+    <div v-for="(doc, index) in data.docs" :key="index">
+      <SearchResultItem :data="doc" />
+      <a-divider v-if="index < docSize - 1" />
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.search-result-content {
+  background-color: #ffffff;
+  margin-top: 8px;
+  padding: 10px;
+}
+</style>

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

@@ -0,0 +1,78 @@
+<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>