Ver código fonte

Merge branch 'dev_20221112' into 'main'

Dev 20221112

See merge request kede/front!1
蒋文林 3 anos atrás
pai
commit
676cbb3ff9

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

@@ -1,38 +1,36 @@
-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: [
-          {
-            id: 1,
-            title: "标题",
-            authors: ["作者1"],
-            unit: "",
-            keywords: ["关键词1"],
-            summary: "摘要"
-          },
-          {
-            id: 2,
-            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)
+        );
+    }),
+];

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

@@ -1,13 +1,13 @@
-import type { Doc } from "@/types/doc.types";
-import type { Response } from "@/types/response.types";
-import type { AxiosResponse } from "axios";
-import httpClient from "./httpClient";
-
-/**
- * 根据文档ID查询文档详情
- * @param id 文档ID
- * @returns axios响应
- */
-export function fetch(id: number): Promise<AxiosResponse<Response<Doc>>> {
-  return httpClient.get<Response<Doc>>(`/doc/${id}`);
-}
+import type { Doc } from "@/types/doc.types";
+import type { Response } from "@/types/response.types";
+import type { AxiosResponse } from "axios";
+import httpClient from "./httpClient";
+
+/**
+ * 根据文档ID查询文档详情
+ * @param id 文档ID
+ * @returns axios响应
+ */
+export function fetch(id: Object): Promise<AxiosResponse<Response<Doc>>> {
+  return httpClient.get<Response<Doc>>("/gw/search/document/CN/" + id);
+}

+ 42 - 35
src/services/httpClient.ts

@@ -1,35 +1,42 @@
-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://192.168.100.234:8088/";
+
+
+const httpClient = axios.create({
+    baseURL: urlPrefix,
+    timeout: 60000,
+});
+
+
+// 添加请求拦截器
+httpClient.interceptors.request.use(function (config: AxiosRequestConfig) {
+    // 在发送请求之前做些什么
+    const token = localStorage.getItem('token');
+    if (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("请求参数错误!");
+    } else if (error) {
+        // @ts-ignore
+        message.error(error.response.data.msg);
+    }
+
+    return Promise.reject(error);
+});
+
+export default httpClient;

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

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

+ 13 - 0
src/services/user.service.ts

@@ -0,0 +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 login(param: Object): Promise<AxiosResponse<Response<Object>>> {
+  // const params = { query };
+  return httpClient.post("/gw/user/login", param);
+}
+
+export default {
+  login
+}

+ 21 - 21
src/stores/doc-detail.ts

@@ -1,21 +1,21 @@
-import type { Doc } from "@/types/doc.types";
-import type { Response } from "@/types/response.types";
-import { defineStore } from "pinia";
-import { ref, type Ref } from "vue";
-import * as docService from "@/services/doc.service";
-import type { AxiosResponse } from "axios";
-
-export const useDocDetailStore = defineStore("docDetail", () => {
-  const doc: Ref<Doc | null> = ref(null);
-
-  function fetch(id: number) {
-    return docService.fetch(id).then((resp: AxiosResponse<Response<Doc>>) => {
-      doc.value = resp.data.data;
-    });
-  }
-
-  return {
-    doc,
-    fetch,
-  }
-});
+import type { Doc } from "@/types/doc.types";
+import type { Response } from "@/types/response.types";
+import { defineStore } from "pinia";
+import { ref, type Ref } from "vue";
+import * as docService from "@/services/doc.service";
+import type { AxiosResponse } from "axios";
+
+export const useDocDetailStore = defineStore("docDetail", () => {
+  const doc: Ref<Doc | null> = ref(null);
+
+  function fetch(id: Object) {
+    return docService.fetch(id).then((resp: AxiosResponse<Response<Doc>>) => {
+      doc.value = resp.data.data;
+    });
+  }
+
+  return {
+    doc,
+    fetch,
+  }
+});

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

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

+ 119 - 101
src/views/LoginView.vue

@@ -1,124 +1,142 @@
-<script setup lang="ts">import { reactive } from 'vue';
+<script setup lang="ts">import {reactive} from 'vue';
+import {useRoute, useRouter} from "vue-router";
+import user from "@/services/user.service";
 
 interface FormState {
-  username: string;
-  password: string;
-  remember: boolean;
+    username: string;
+    password: string;
+    remember: boolean;
 }
 
+// 路由管理器
+const router = useRouter();
+// 当前路由对象
+const route = useRoute();
+
 const formState = reactive<FormState>({
-  username: '',
-  password: '',
-  remember: true,
+    username: '',
+    password: '',
+    remember: true,
 });
 const onFinish = (values: any) => {
-  console.log('Success:', values);
+    console.log(values);
+    let param = {
+        mobilePhone: values.username,
+        password: values.password,
+        type: 1
+    }
+    user.login(param).then((resp) => {
+        localStorage.setItem('token', resp.data.data.token);
+        router.push({name: "SearchIndex"});
+    }).catch((err) => {
+        console.log(err);
+    })
 };
 
 const onFinishFailed = (errorInfo: any) => {
-  console.log('Failed:', errorInfo);
+    console.log('Failed:', errorInfo);
 };
 
 </script>
 
 <template>
-  <div class="login-main">
-    <a-row type="flex">
-      <a-col class="left" :flex="1">
-        <h1 class="login-title">技淘平台智慧产权大脑</h1>
-        <p class="login-banner">
-          <img src="@/assets/imgs/login-banner@2x.png" />
-        </p>
-      </a-col>
-      <a-col class="right" :flex="1">
-        <h1>用户登录</h1>
-        <a-divider></a-divider>
-        <div class="login-form-wrapper">
-          <a-form
-            :model="formState"
-            name="basic"
-            :label-col="{ span: 4 }"
-            :wrapper-col="{ span: 20 }"
-            autocomplete="off"
-            @finish="onFinish"
-            @finishFailed="onFinishFailed"
-          >
-            <a-form-item
-              label="用户名"
-              name="username"
-              :rules="[{ required: true, message: '请输入用户名' }]"
-            >
-              <a-input v-model:value="formState.username" />
-            </a-form-item>
-
-            <a-form-item
-              label="密码"
-              name="password"
-              :rules="[{ required: true, message: '请输入密码' }]"
-            >
-              <a-input-password v-model:value="formState.password" />
-            </a-form-item>
-
-            <a-form-item :wrapper-col="{sm: {span: 20, offset: 4}, xs: {span: 24, offset: 0}}">
-              <a-button type="primary" html-type="submit" block>登&nbsp;&nbsp;&nbsp;&nbsp;录</a-button>
-            </a-form-item>
-          </a-form>
-          <p class="forgot-password-wrap">
-            <a href="#">忘记密码?</a>
-          </p>
-        </div>
-      </a-col>
-    </a-row>
-  </div>
+    <div class="login-main">
+        <a-row type="flex">
+            <a-col class="left" :flex="1">
+                <h1 class="login-title">技淘平台智慧产权大脑</h1>
+                <p class="login-banner">
+                    <img src="@/assets/imgs/login-banner@2x.png"/>
+                </p>
+            </a-col>
+            <a-col class="right" :flex="1">
+                <h1>用户登录</h1>
+                <a-divider></a-divider>
+                <div class="login-form-wrapper">
+                    <a-form
+                            :model="formState"
+                            name="basic"
+                            :label-col="{ span: 4 }"
+                            :wrapper-col="{ span: 20 }"
+                            autocomplete="off"
+                            @finish="onFinish"
+                            @finishFailed="onFinishFailed"
+                    >
+                        <a-form-item
+                                label="用户名"
+                                name="username"
+                                :rules="[{ required: true, message: '请输入用户名' }]"
+                        >
+                            <a-input v-model:value="formState.username"/>
+                        </a-form-item>
+
+                        <a-form-item
+                                label="密码"
+                                name="password"
+                                :rules="[{ required: true, message: '请输入密码' }]"
+                        >
+                            <a-input-password v-model:value="formState.password"/>
+                        </a-form-item>
+
+                        <a-form-item :wrapper-col="{sm: {span: 20, offset: 4}, xs: {span: 24, offset: 0}}">
+                            <a-button type="primary" html-type="submit" block>登&nbsp;&nbsp;&nbsp;&nbsp;录</a-button>
+                        </a-form-item>
+                    </a-form>
+                    <p class="forgot-password-wrap">
+                        <a href="#">忘记密码?</a>
+                    </p>
+                </div>
+            </a-col>
+        </a-row>
+    </div>
 </template>
 
 <style scoped lang="scss">
-.login-main {
-  height: 100vh;
-  background-color: #00A09A;
-
-  .left {
-    text-align: center;
-    padding-top: 100px;
-
-    .login-title {
-      color: #ffffff;
+    .login-main {
+        height: 100vh;
+        background-color: #00A09A;
+
+        .left {
+            text-align: center;
+            padding-top: 100px;
+
+            .login-title {
+                color: #ffffff;
+            }
+
+            .login-banner {
+                margin: 0 auto;
+                width: 300px;
+
+                img {
+                    margin-top: 50px;
+                    width: 100%;
+                }
+            }
+        }
+
+        .right {
+            height: 100vh;
+            padding-top: 100px;
+            padding-left: 60px;
+            border-top-left-radius: 1em;
+            border-bottom-left-radius: 1em;
+            background-color: #ffffff;
+
+            .login-form-wrapper {
+                width: 400px;
+            }
+        }
     }
 
-    .login-banner {
-      margin: 0 auto;
-      width: 300px;
+    .forgot-password-wrap {
+        text-align: right;
 
-      img {
-        margin-top: 50px;
-        width: 100%;
-      }
-    }
-  }
-
-  .right {
-    height: 100vh;
-    padding-top: 100px;
-    padding-left: 60px;
-    border-top-left-radius: 1em;
-    border-bottom-left-radius: 1em;
-    background-color: #ffffff;
-
-    .login-form-wrapper {
-      width: 400px;
-    }
-  }
-}
-
-.forgot-password-wrap {
-  text-align: right;
-
-  // a {
-  //   color: #ffffff;
+        // a {
+        //   color: #ffffff;
 
-  //   &:hover {
-  //     text-decoration: underline;
-  //   }
-  // }
-}
+        //   &:hover {
+        //     text-decoration: underline;
+        //   }
+        // }
+    }
 </style>

+ 50 - 49
src/views/detail/DetailView.vue

@@ -1,49 +1,50 @@
-<script setup lang="ts">
-import { useDocDetailStore } from '@/stores/doc-detail';
-import { useSideBarStore } from '@/stores/side-bar';
-import { onMounted, onUnmounted, computed } from 'vue';
-import { useRoute } from 'vue-router';
-import DocCatalog from './components/DocCatalog.vue';
-
-// 当前路由
-const route = useRoute();
-// 左侧边栏状态
-const sideBarStore = useSideBarStore();
-// 文档详情
-const docDetailStore = useDocDetailStore();
-
-const doc = computed(() => docDetailStore.doc);
-
-onMounted(() => {
-  // 收起左侧边栏
-  sideBarStore.setCollapse(true);
-  // 查询文档详情内容
-  const id = parseInt(route.params.id as string);
-  if (id > 0) {
-    docDetailStore.fetch(id);
-  }
-});
-
-onUnmounted(() => {
-  sideBarStore.setCollapse(false);
-});
-
-</script>
-
-<template>
-  <a-row type="flex" :gutter="24">
-    <a-col :span="4">
-      <h2>目录</h2>
-      <DocCatalog></DocCatalog>
-    </a-col>
-    <a-col :span="20">
-      <div v-if="doc" class="doc-wrap">
-        <h2 class="title">{{ doc.title }}</h2>
-        <p>文档详情</p>
-      </div>
-      <div v-else>
-        <a-empty />
-      </div>
-    </a-col>
-  </a-row>
-</template>
+<script setup lang="ts">
+import { useDocDetailStore } from '@/stores/doc-detail';
+import { useSideBarStore } from '@/stores/side-bar';
+import { onMounted, onUnmounted, computed } from 'vue';
+import { useRoute } from 'vue-router';
+import searchService from "@/services/search.service";
+import DocCatalog from './components/DocCatalog.vue';
+
+// 当前路由
+const route = useRoute();
+// 左侧边栏状态
+const sideBarStore = useSideBarStore();
+// 文档详情
+const docDetailStore = useDocDetailStore();
+
+const doc = computed(() => docDetailStore.doc);
+
+onMounted(() => {
+  // 收起左侧边栏
+  sideBarStore.setCollapse(true);
+  // 查询文档详情内容
+  const id = route.params.id;
+  if (id) {
+    docDetailStore.fetch(id);
+  }
+});
+
+onUnmounted(() => {
+  sideBarStore.setCollapse(false);
+});
+
+</script>
+
+<template>
+  <a-row type="flex" :gutter="24">
+    <a-col :span="4">
+      <h2>目录</h2>
+      <DocCatalog></DocCatalog>
+    </a-col>
+    <a-col :span="20">
+      <div v-if="doc" class="doc-wrap">
+        <h2 class="title">{{ doc.title }}</h2>
+        <p>文档详情</p>
+      </div>
+      <div v-else>
+        <a-empty />
+      </div>
+    </a-col>
+  </a-row>
+</template>

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

@@ -1,108 +1,282 @@
-<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 * as 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);
+
+    let authorChecked = ref(false);
+    let yearChecked = ref(false);
+
+    let yearValueArr: string[] = []
+    let authorValueArr: string[] = []
+
+    // 搜索结果
+    const searchResult: Ref<SearchResult | null> = ref(null);
+
+    // 搜索结果
+    const aggYearResult = ref(null);
+    // 搜索结果
+    const aggAuthorResult = ref(null);
+
+    // 根据年份聚合
+    function aggsYearSearch(query: string) {
+        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(query: string) {
+
+        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) {
+            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 += ')'
+                }
+            }
+            param.query += ' AND ' + auQuery + pyKw
+        }
+        searchResult.value = null
+        searchService.search(param).then((resp) => {
+            searchResult.value = resp.data.data;
+            if (yearValueArr && yearValueArr.length > 0) {
+                aggsAuthorsSearch(param.query)
+            }else {
+                aggsYearSearch(param.query)
+            }
+        }).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);
+        const query = `${field.value}=(${keyword.value})`
+        aggsYearSearch(query);
+        aggsAuthorsSearch(query);
+    });
+
+    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 = []
+        authorValueArr = []
+        for (let valueKey in yearChecked.value) {
+            yearValueArr.push(yearChecked.value[valueKey])
+        }
+        doSearch(field.value, keyword.value);
+    }
+
+    function changeAuthorChecked() {
+        authorValueArr = []
+        yearValueArr = []
+        for (let valueKey in authorChecked.value) {
+            authorValueArr.push(authorChecked.value[valueKey])
+        }
+        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" @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>

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

@@ -1,31 +1,31 @@
-<script setup lang="ts">
-import type { PropType } from "vue";
-import type { SearchResultDoc } from "@/types/search.types";
-
-defineProps({
-  data: {
-    type: Object as PropType<SearchResultDoc>,
-    required: true,
-  }
-});
-</script>
-
-<template>
-  <div class="item">
-    <h1>
-      <RouterLink :to="'/detail/' + data.id" target="_blank">
-        {{ data.title }}
-      </RouterLink>
-    </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 { SearchResultDoc } from "@/types/search.types";
+
+defineProps({
+  data: {
+    type: Object as PropType<SearchResultDoc>,
+    required: true,
+  }
+});
+</script>
+
+<template>
+  <div class="item">
+    <h1>
+      <RouterLink :to="'/detail/' + data._id" target="_blank">
+        {{ data.title }}
+      </RouterLink>
+    </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>