dev01 5 miesięcy temu
commit
75ab55568b
100 zmienionych plików z 9392 dodań i 0 usunięć
  1. 28 0
      .dockerignore
  2. 20 0
      .eslintrc.cjs
  3. 29 0
      .gitignore
  4. 3 0
      .vscode/extensions.json
  5. 14 0
      .vscode/settings.json
  6. 7 0
      Dockerfile
  7. 15 0
      Dockerfile.bak
  8. 59 0
      README.md
  9. 8 0
      cypress.config.ts
  10. 8 0
      cypress/e2e/example.cy.ts
  11. 10 0
      cypress/e2e/tsconfig.json
  12. 5 0
      cypress/fixtures/example.json
  13. 39 0
      cypress/support/commands.ts
  14. 20 0
      cypress/support/e2e.ts
  15. 1 0
      env.d.ts
  16. 13 0
      index.html
  17. 11 0
      nginx/conf.d/default.conf
  18. 31 0
      nginx/nginx.conf
  19. 6653 0
      package-lock.json
  20. 57 0
      package.json
  21. BIN
      public/favicon.ico
  22. 303 0
      public/mockServiceWorker.js
  23. 28 0
      src/App.vue
  24. 87 0
      src/assets/base.scss
  25. BIN
      src/assets/imgs/login-banner.png
  26. BIN
      src/assets/imgs/login-banner@1x.png
  27. BIN
      src/assets/imgs/login-banner@2x.png
  28. 7 0
      src/assets/logo-banner.svg
  29. 13 0
      src/assets/logo-small.svg
  30. BIN
      src/assets/logo.png
  31. 7 0
      src/assets/logo.svg
  32. 1 0
      src/assets/main.scss
  33. 4 0
      src/assets/variables.scss
  34. 11 0
      src/client/ai.client.ts
  35. 25 0
      src/client/browsingHistory.client.ts
  36. 11 0
      src/client/chapterSearch.client.ts
  37. 37 0
      src/client/collection.client.ts
  38. 27 0
      src/client/company.client.ts
  39. 11 0
      src/client/duplicateCheck.client.ts
  40. 11 0
      src/client/knowledgeGraph.client.ts
  41. 25 0
      src/client/logo.client.ts
  42. 19 0
      src/client/paperDetail.client.ts
  43. 20 0
      src/client/paperSection.client.ts
  44. 39 0
      src/client/report.client.ts
  45. 9 0
      src/client/reportExport.client.ts
  46. 32 0
      src/client/reportTemplate.client.ts
  47. 25 0
      src/client/reportTemplateLogo.client.ts
  48. 35 0
      src/client/reports.client.ts
  49. 11 0
      src/client/search.client.ts
  50. 11 0
      src/client/searchHistory.client.ts
  51. 41 0
      src/components/HelloWorld.vue
  52. 9 0
      src/components/LogoComponent.vue
  53. 132 0
      src/components/NavMenu.vue
  54. 30 0
      src/components/PageHeader.vue
  55. 26 0
      src/components/SpinComponent.vue
  56. 121 0
      src/components/TheWelcome.vue
  57. 86 0
      src/components/WelcomeItem.vue
  58. 11 0
      src/components/__tests__/HelloWorld.spec.ts
  59. 12 0
      src/components/icons/IconCommunity.vue
  60. 10 0
      src/components/icons/IconDetailList.vue
  61. 12 0
      src/components/icons/IconDocumentation.vue
  62. 12 0
      src/components/icons/IconEcosystem.vue
  63. 10 0
      src/components/icons/IconSimpleList.vue
  64. 12 0
      src/components/icons/IconSupport.vue
  65. 19 0
      src/components/icons/IconTooling.vue
  66. 21 0
      src/libs/clipboard.lib.ts
  67. 6 0
      src/libs/image.lib.ts
  68. 18 0
      src/libs/log.lib.ts
  69. 51 0
      src/libs/reportExport.lib.ts
  70. 10 0
      src/libs/url.lib.ts
  71. 18 0
      src/main.ts
  72. 64 0
      src/models/knowledgeGraph.model.ts
  73. 85 0
      src/models/report.model.ts
  74. 176 0
      src/router/index.ts
  75. 5 0
      src/services/ai.service.ts
  76. 14 0
      src/services/browsingHistory.service.ts
  77. 6 0
      src/services/chapterSearch.service.ts
  78. 18 0
      src/services/collection.service.ts
  79. 17 0
      src/services/company.service.ts
  80. 51 0
      src/services/httpClient.ts
  81. 18 0
      src/services/knowledgeGraph.service.ts
  82. 17 0
      src/services/logo.service.ts
  83. 9 0
      src/services/paperDetail.service.ts
  84. 13 0
      src/services/paperSection.service.ts
  85. 32 0
      src/services/report.service.ts
  86. 5 0
      src/services/reportExport.service.ts
  87. 26 0
      src/services/reportTemplate.service.ts
  88. 17 0
      src/services/reportTemplateLogo.service.ts
  89. 9 0
      src/services/reports.service.ts
  90. 18 0
      src/services/search.service.ts
  91. 6 0
      src/services/searchHistory.service.ts
  92. 58 0
      src/services/user.service.ts
  93. 42 0
      src/stores/auth.store.ts
  94. 12 0
      src/stores/counter.ts
  95. 29 0
      src/stores/doc-detail.ts
  96. 15 0
      src/stores/history.store.ts
  97. 49 0
      src/stores/knowledge-graph.ts
  98. 59 0
      src/stores/search.store.ts
  99. 15 0
      src/stores/side-bar.ts
  100. 0 0
      src/types/browsingHistory.types.ts

+ 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?

+ 20 - 0
.eslintrc.cjs

@@ -0,0 +1,20 @@
+/* eslint-env node */
+require("@rushstack/eslint-patch/modern-module-resolution");
+
+module.exports = {
+  root: true,
+  extends: [
+    "plugin:vue/vue3-essential",
+    "eslint:recommended",
+    "@vue/eslint-config-typescript",
+  ],
+  overrides: [
+    {
+      files: ["cypress/e2e/**.{cy,spec}.{js,ts,jsx,tsx}"],
+      extends: ["plugin:cypress/recommended"],
+    },
+  ],
+  parserOptions: {
+    ecmaVersion: "latest",
+  },
+};

+ 29 - 0
.gitignore

@@ -0,0 +1,29 @@
+# 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
+!.vscode/settings.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 3 - 0
.vscode/extensions.json

@@ -0,0 +1,3 @@
+{
+  "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
+}

+ 14 - 0
.vscode/settings.json

@@ -0,0 +1,14 @@
+{
+  "[typescript]": {
+    "editor.tabSize": 2
+  },
+  "[vue]": {
+    "editor.tabSize": 2
+  },
+  "[sass]": {
+    "editor.tabSize": 2
+  },
+  "[html]": {
+    "editor.tabSize": 2
+  }
+}

+ 7 - 0
Dockerfile

@@ -0,0 +1,7 @@
+FROM nginx:alpine
+WORKDIR /app
+COPY dist /app
+COPY nginx/nginx.conf /etc/nginx/
+COPY nginx/conf.d/default.conf /etc/nginx/conf.d
+
+EXPOSE 8080

+ 15 - 0
Dockerfile.bak

@@ -0,0 +1,15 @@
+FROM node:lts-alpine3.14 as build-stage
+
+WORKDIR /app
+COPY package.json /app/
+RUN npm install --registry=https://repo.huaweicloud.com/repository/npm/
+COPY . /app/
+RUN npm run build
+
+FROM nginx:alpine
+WORKDIR /app
+COPY --from=build-stage /app/dist /app
+COPY nginx/nginx.conf /etc/nginx/
+COPY nginx/conf.d/default.conf /etc/nginx/conf.d
+
+EXPOSE 8080

+ 59 - 0
README.md

@@ -0,0 +1,59 @@
+# front
+
+This template should help get you started developing with Vue 3 in Vite.
+
+## Recommended IDE Setup
+
+[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
+
+## Type Support for `.vue` Imports in TS
+
+TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
+
+If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
+
+1. Disable the built-in TypeScript Extension
+    1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
+    2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
+2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
+
+## Customize configuration
+
+See [Vite Configuration Reference](https://vitejs.dev/config/).
+
+## Project Setup
+
+```sh
+npm install
+```
+
+### Compile and Hot-Reload for Development
+
+```sh
+npm run dev
+```
+
+### Type-Check, Compile and Minify for Production
+
+```sh
+npm run build
+```
+
+### Run Unit Tests with [Vitest](https://vitest.dev/)
+
+```sh
+npm run test:unit
+```
+
+### Run End-to-End Tests with [Cypress](https://www.cypress.io/)
+
+```sh
+npm run build
+npm run test:e2e # or `npm run test:e2e:ci` for headless testing
+```
+
+### Lint with [ESLint](https://eslint.org/)
+
+```sh
+npm run lint
+```

+ 8 - 0
cypress.config.ts

@@ -0,0 +1,8 @@
+import { defineConfig } from "cypress";
+
+export default defineConfig({
+  e2e: {
+    specPattern: "cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}",
+    baseUrl: "http://localhost:4173",
+  },
+});

+ 8 - 0
cypress/e2e/example.cy.ts

@@ -0,0 +1,8 @@
+// https://docs.cypress.io/api/introduction/api.html
+
+describe("My First Test", () => {
+  it("visits the app root url", () => {
+    cy.visit("/");
+    cy.contains("h1", "You did it!");
+  });
+});

+ 10 - 0
cypress/e2e/tsconfig.json

@@ -0,0 +1,10 @@
+{
+  "extends": "@vue/tsconfig/tsconfig.web.json",
+  "include": ["./**/*", "../support/**/*"],
+  "compilerOptions": {
+    "isolatedModules": false,
+    "target": "es5",
+    "lib": ["es5", "dom"],
+    "types": ["cypress"]
+  }
+}

+ 5 - 0
cypress/fixtures/example.json

@@ -0,0 +1,5 @@
+{
+  "name": "Using fixtures to represent data",
+  "email": "hello@cypress.io",
+  "body": "Fixtures are a great way to mock data for responses to routes"
+}

+ 39 - 0
cypress/support/commands.ts

@@ -0,0 +1,39 @@
+/// <reference types="cypress" />
+// ***********************************************
+// This example commands.ts shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add('login', (email, password) => { ... })
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This will overwrite an existing command --
+// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
+//
+// declare global {
+//   namespace Cypress {
+//     interface Chainable {
+//       login(email: string, password: string): Chainable<void>
+//       drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
+//       dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
+//       visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
+//     }
+//   }
+// }
+
+export {};

+ 20 - 0
cypress/support/e2e.ts

@@ -0,0 +1,20 @@
+// ***********************************************************
+// This example support/index.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import "./commands";
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')

+ 1 - 0
env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>技淘平台智慧产权大脑</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 11 - 0
nginx/conf.d/default.conf

@@ -0,0 +1,11 @@
+server {
+    listen       8080;
+    listen  [::]:8080;
+    server_name  _;
+
+    location / {
+        root   /app;
+        index  index.html index.htm;
+        try_files $uri $uri/ /index.html;
+    }
+}

+ 31 - 0
nginx/nginx.conf

@@ -0,0 +1,31 @@
+user  nginx;
+worker_processes  2;
+
+error_log  /var/log/nginx/error.log notice;
+pid        /var/run/nginx.pid;
+
+
+events {
+    worker_connections  1024;
+}
+
+
+http {
+    include       /etc/nginx/mime.types;
+    default_type  application/octet-stream;
+
+    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
+                      '$status $body_bytes_sent "$http_referer" '
+                      '"$http_user_agent" "$http_x_forwarded_for"';
+
+    access_log  /var/log/nginx/access.log  main;
+
+    sendfile        on;
+    #tcp_nopush     on;
+
+    keepalive_timeout  65;
+
+    gzip  on;
+
+    include /etc/nginx/conf.d/*.conf;
+}

Plik diff jest za duży
+ 6653 - 0
package-lock.json


+ 57 - 0
package.json

@@ -0,0 +1,57 @@
+{
+  "name": "front",
+  "version": "0.0.0",
+  "scripts": {
+    "dev": "vite",
+    "build": "run-p type-check build-only",
+    "preview": "vite preview --port 4173",
+    "test:unit": "vitest --environment jsdom",
+    "test:e2e": "start-server-and-test preview http://localhost:4173/ 'cypress open --e2e'",
+    "test:e2e:ci": "start-server-and-test preview http://localhost:4173/ 'cypress run --e2e'",
+    "build-only": "vite build",
+    "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
+    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
+  },
+  "dependencies": {
+    "@antv/g6": "^4.8.7",
+    "@tinymce/tinymce-vue": "^5.0.0",
+    "@types/lodash": "^4.14.188",
+    "ant-design-vue": "^3.2.13",
+    "axios": "^1.1.3",
+    "marked": "^5.0.1",
+    "nzh": "^1.0.8",
+    "pinia": "^2.0.21",
+    "tinymce": "^6.3.1",
+    "vue": "^3.2.38",
+    "vue-router": "^4.1.5",
+    "vuedraggable": "^4.1.0"
+  },
+  "devDependencies": {
+    "@rushstack/eslint-patch": "^1.1.4",
+    "@types/jsdom": "^20.0.0",
+    "@types/marked": "^4.3.0",
+    "@types/node": "^16.11.56",
+    "@vitejs/plugin-vue": "^3.0.3",
+    "@vitejs/plugin-vue-jsx": "^2.0.1",
+    "@vue/eslint-config-typescript": "^11.0.0",
+    "@vue/test-utils": "^2.0.2",
+    "@vue/tsconfig": "^0.1.3",
+    "cypress": "^10.7.0",
+    "eslint": "^8.22.0",
+    "eslint-plugin-cypress": "^2.12.1",
+    "eslint-plugin-vue": "^9.3.0",
+    "jsdom": "^20.0.0",
+    "less": "^4.1.3",
+    "msw": "^0.48.1",
+    "npm-run-all": "^4.1.5",
+    "sass": "^1.56.0",
+    "start-server-and-test": "^1.14.0",
+    "typescript": "~4.7.4",
+    "vite": "^3.0.9",
+    "vitest": "^0.23.0",
+    "vue-tsc": "^0.40.7"
+  },
+  "msw": {
+    "workerDirectory": "public"
+  }
+}

BIN
public/favicon.ico


+ 303 - 0
public/mockServiceWorker.js

@@ -0,0 +1,303 @@
+/* eslint-disable */
+/* tslint:disable */
+
+/**
+ * Mock Service Worker (0.48.3).
+ * @see https://github.com/mswjs/msw
+ * - Please do NOT modify this file.
+ * - Please do NOT serve this file on production.
+ */
+
+const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70'
+const activeClientIds = new Set()
+
+self.addEventListener('install', function () {
+  self.skipWaiting()
+})
+
+self.addEventListener('activate', function (event) {
+  event.waitUntil(self.clients.claim())
+})
+
+self.addEventListener('message', async function (event) {
+  const clientId = event.source.id
+
+  if (!clientId || !self.clients) {
+    return
+  }
+
+  const client = await self.clients.get(clientId)
+
+  if (!client) {
+    return
+  }
+
+  const allClients = await self.clients.matchAll({
+    type: 'window',
+  })
+
+  switch (event.data) {
+    case 'KEEPALIVE_REQUEST': {
+      sendToClient(client, {
+        type: 'KEEPALIVE_RESPONSE',
+      })
+      break
+    }
+
+    case 'INTEGRITY_CHECK_REQUEST': {
+      sendToClient(client, {
+        type: 'INTEGRITY_CHECK_RESPONSE',
+        payload: INTEGRITY_CHECKSUM,
+      })
+      break
+    }
+
+    case 'MOCK_ACTIVATE': {
+      activeClientIds.add(clientId)
+
+      sendToClient(client, {
+        type: 'MOCKING_ENABLED',
+        payload: true,
+      })
+      break
+    }
+
+    case 'MOCK_DEACTIVATE': {
+      activeClientIds.delete(clientId)
+      break
+    }
+
+    case 'CLIENT_CLOSED': {
+      activeClientIds.delete(clientId)
+
+      const remainingClients = allClients.filter((client) => {
+        return client.id !== clientId
+      })
+
+      // Unregister itself when there are no more clients
+      if (remainingClients.length === 0) {
+        self.registration.unregister()
+      }
+
+      break
+    }
+  }
+})
+
+self.addEventListener('fetch', function (event) {
+  const { request } = event
+  const accept = request.headers.get('accept') || ''
+
+  // Bypass server-sent events.
+  if (accept.includes('text/event-stream')) {
+    return
+  }
+
+  // Bypass navigation requests.
+  if (request.mode === 'navigate') {
+    return
+  }
+
+  // Opening the DevTools triggers the "only-if-cached" request
+  // that cannot be handled by the worker. Bypass such requests.
+  if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
+    return
+  }
+
+  // Bypass all requests when there are no active clients.
+  // Prevents the self-unregistered worked from handling requests
+  // after it's been deleted (still remains active until the next reload).
+  if (activeClientIds.size === 0) {
+    return
+  }
+
+  // Generate unique request ID.
+  const requestId = Math.random().toString(16).slice(2)
+
+  event.respondWith(
+    handleRequest(event, requestId).catch((error) => {
+      if (error.name === 'NetworkError') {
+        console.warn(
+          '[MSW] Successfully emulated a network error for the "%s %s" request.',
+          request.method,
+          request.url,
+        )
+        return
+      }
+
+      // At this point, any exception indicates an issue with the original request/response.
+      console.error(
+        `\
+[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
+        request.method,
+        request.url,
+        `${error.name}: ${error.message}`,
+      )
+    }),
+  )
+})
+
+async function handleRequest(event, requestId) {
+  const client = await resolveMainClient(event)
+  const response = await getResponse(event, client, requestId)
+
+  // Send back the response clone for the "response:*" life-cycle events.
+  // Ensure MSW is active and ready to handle the message, otherwise
+  // this message will pend indefinitely.
+  if (client && activeClientIds.has(client.id)) {
+    ;(async function () {
+      const clonedResponse = response.clone()
+      sendToClient(client, {
+        type: 'RESPONSE',
+        payload: {
+          requestId,
+          type: clonedResponse.type,
+          ok: clonedResponse.ok,
+          status: clonedResponse.status,
+          statusText: clonedResponse.statusText,
+          body:
+            clonedResponse.body === null ? null : await clonedResponse.text(),
+          headers: Object.fromEntries(clonedResponse.headers.entries()),
+          redirected: clonedResponse.redirected,
+        },
+      })
+    })()
+  }
+
+  return response
+}
+
+// Resolve the main client for the given event.
+// Client that issues a request doesn't necessarily equal the client
+// that registered the worker. It's with the latter the worker should
+// communicate with during the response resolving phase.
+async function resolveMainClient(event) {
+  const client = await self.clients.get(event.clientId)
+
+  if (client?.frameType === 'top-level') {
+    return client
+  }
+
+  const allClients = await self.clients.matchAll({
+    type: 'window',
+  })
+
+  return allClients
+    .filter((client) => {
+      // Get only those clients that are currently visible.
+      return client.visibilityState === 'visible'
+    })
+    .find((client) => {
+      // Find the client ID that's recorded in the
+      // set of clients that have registered the worker.
+      return activeClientIds.has(client.id)
+    })
+}
+
+async function getResponse(event, client, requestId) {
+  const { request } = event
+  const clonedRequest = request.clone()
+
+  function passthrough() {
+    // Clone the request because it might've been already used
+    // (i.e. its body has been read and sent to the client).
+    const headers = Object.fromEntries(clonedRequest.headers.entries())
+
+    // Remove MSW-specific request headers so the bypassed requests
+    // comply with the server's CORS preflight check.
+    // Operate with the headers as an object because request "Headers"
+    // are immutable.
+    delete headers['x-msw-bypass']
+
+    return fetch(clonedRequest, { headers })
+  }
+
+  // Bypass mocking when the client is not active.
+  if (!client) {
+    return passthrough()
+  }
+
+  // Bypass initial page load requests (i.e. static assets).
+  // The absence of the immediate/parent client in the map of the active clients
+  // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
+  // and is not ready to handle requests.
+  if (!activeClientIds.has(client.id)) {
+    return passthrough()
+  }
+
+  // Bypass requests with the explicit bypass header.
+  // Such requests can be issued by "ctx.fetch()".
+  if (request.headers.get('x-msw-bypass') === 'true') {
+    return passthrough()
+  }
+
+  // Notify the client that a request has been intercepted.
+  const clientMessage = await sendToClient(client, {
+    type: 'REQUEST',
+    payload: {
+      id: requestId,
+      url: request.url,
+      method: request.method,
+      headers: Object.fromEntries(request.headers.entries()),
+      cache: request.cache,
+      mode: request.mode,
+      credentials: request.credentials,
+      destination: request.destination,
+      integrity: request.integrity,
+      redirect: request.redirect,
+      referrer: request.referrer,
+      referrerPolicy: request.referrerPolicy,
+      body: await request.text(),
+      bodyUsed: request.bodyUsed,
+      keepalive: request.keepalive,
+    },
+  })
+
+  switch (clientMessage.type) {
+    case 'MOCK_RESPONSE': {
+      return respondWithMock(clientMessage.data)
+    }
+
+    case 'MOCK_NOT_FOUND': {
+      return passthrough()
+    }
+
+    case 'NETWORK_ERROR': {
+      const { name, message } = clientMessage.data
+      const networkError = new Error(message)
+      networkError.name = name
+
+      // Rejecting a "respondWith" promise emulates a network error.
+      throw networkError
+    }
+  }
+
+  return passthrough()
+}
+
+function sendToClient(client, message) {
+  return new Promise((resolve, reject) => {
+    const channel = new MessageChannel()
+
+    channel.port1.onmessage = (event) => {
+      if (event.data && event.data.error) {
+        return reject(event.data.error)
+      }
+
+      resolve(event.data)
+    }
+
+    client.postMessage(message, [channel.port2])
+  })
+}
+
+function sleep(timeMs) {
+  return new Promise((resolve) => {
+    setTimeout(resolve, timeMs)
+  })
+}
+
+async function respondWithMock(response) {
+  await sleep(response.delay)
+  return new Response(response.body, response)
+}

+ 28 - 0
src/App.vue

@@ -0,0 +1,28 @@
+<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 { onMounted, ref } from "vue";
+import { useAuthStore } from "./stores/auth.store";
+import { routeToLogin } from "./router";
+dayjs.locale('zh-cn');
+
+const locale = ref(zhCN);
+
+const authStore = useAuthStore();
+
+onMounted(() => {
+  authStore.checkLogin().then((isLogin) => {
+    if (!isLogin) {
+      routeToLogin();
+    }
+  });
+});
+</script>
+
+<template>
+  <a-config-provider :locale="locale">
+    <RouterView />
+  </a-config-provider>
+</template>

+ 87 - 0
src/assets/base.scss

@@ -0,0 +1,87 @@
+/* color palette from <https://github.com/vuejs/theme> */
+/* :root {
+  --vt-c-white: #ffffff;
+  --vt-c-white-soft: #f8f8f8;
+  --vt-c-white-mute: #f2f2f2;
+
+  --vt-c-black: #181818;
+  --vt-c-black-soft: #222222;
+  --vt-c-black-mute: #282828;
+
+  --vt-c-indigo: #2c3e50;
+
+  --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
+  --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
+  --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
+  --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
+
+  --vt-c-text-light-1: var(--vt-c-indigo);
+  --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
+  --vt-c-text-dark-1: var(--vt-c-white);
+  --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
+} */
+
+/* semantic color variables for this project */
+/* :root {
+  --color-background: var(--vt-c-white);
+  --color-background-soft: var(--vt-c-white-soft);
+  --color-background-mute: var(--vt-c-white-mute);
+
+  --color-border: var(--vt-c-divider-light-2);
+  --color-border-hover: var(--vt-c-divider-light-1);
+
+  --color-heading: var(--vt-c-text-light-1);
+  --color-text: var(--vt-c-text-light-1);
+
+  --section-gap: 160px;
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --color-background: var(--vt-c-black);
+    --color-background-soft: var(--vt-c-black-soft);
+    --color-background-mute: var(--vt-c-black-mute);
+
+    --color-border: var(--vt-c-divider-dark-2);
+    --color-border-hover: var(--vt-c-divider-dark-1);
+
+    --color-heading: var(--vt-c-text-dark-1);
+    --color-text: var(--vt-c-text-dark-2);
+  }
+}
+
+*,
+*::before,
+*::after {
+  box-sizing: border-box;
+  margin: 0;
+  position: relative;
+  font-weight: normal;
+}
+
+body {
+  min-height: 100vh;
+  color: var(--color-text);
+  background: var(--color-background);
+  transition: color 0.5s, background-color 0.5s;
+  line-height: 1.6;
+  font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
+    Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
+  font-size: 15px;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+} */
+
+$border-color-primary: #f0f0f0;
+$border-color-secondary: #e0e0e0;
+
+$desc-color: #4f4f4f;
+
+h1.title {
+  font-size: 1.75em;
+}
+
+.text-red {
+  color: red;
+}

BIN
src/assets/imgs/login-banner.png


BIN
src/assets/imgs/login-banner@1x.png


BIN
src/assets/imgs/login-banner@2x.png


Plik diff jest za duży
+ 7 - 0
src/assets/logo-banner.svg


Plik diff jest za duży
+ 13 - 0
src/assets/logo-small.svg


BIN
src/assets/logo.png


Plik diff jest za duży
+ 7 - 0
src/assets/logo.svg


+ 1 - 0
src/assets/main.scss

@@ -0,0 +1 @@
+@import "./base.scss";

+ 4 - 0
src/assets/variables.scss

@@ -0,0 +1,4 @@
+
+$border-color: #d9d9d9;
+
+$bg-color-gray-light: #eee;

+ 11 - 0
src/client/ai.client.ts

@@ -0,0 +1,11 @@
+import httpClient from "@/services/httpClient";
+import type { Response } from "@/types/response.types";
+import * as urlHelper from "@/libs/url.lib"
+
+const _url = urlHelper.withPrefix("/gw/user")
+
+export async function generate(projectName: string, keywords: string, chapterTitle: string) {
+  const params = { projectName, keywords, chapterTitle }
+  const resp = await httpClient.post<Response<string>>(_url("ai/generate"), params);
+  return resp.data.data;
+}

+ 25 - 0
src/client/browsingHistory.client.ts

@@ -0,0 +1,25 @@
+import httpClient from "@/services/httpClient";
+import type { Response } from "@/types/response.types";
+import * as urlHelper from "@/libs/url.lib"
+import type { BrowsingHistories } from "@/types/browsingHistory.types";
+
+const _url = urlHelper.withPrefix("/gw/user/browsing");
+
+export async function fetch(page: number, dateNum?: number): Promise<BrowsingHistories> {
+  const params: {[index: string]: number} = { page }
+  if (dateNum) {
+    params['dateNum'] = dateNum
+  }
+  const resp = await httpClient.get<Response<BrowsingHistories>>(_url("/getPage"), { params })
+  return resp.data.data
+}
+
+export async function remove(id: number): Promise<boolean> {
+  const resp = await httpClient.delete<Response<boolean>>(_url(`/delById/${id}`))
+  return resp.data.data
+}
+
+export async function removeAll(): Promise<boolean> {
+  const resp = await httpClient.delete<Response<boolean>>(_url("/delByUserId"))
+  return resp.data.data
+}

+ 11 - 0
src/client/chapterSearch.client.ts

@@ -0,0 +1,11 @@
+import httpClient from "@/services/httpClient";
+import type { Response } from "@/types/response.types"
+import type { ChapterSearchRequest, ChapterSearchResponse } from "@/types/search.types";
+import * as urlHelper from "@/libs/url.lib"
+
+const _url = urlHelper.withPrefix("/gw/search/chapter/search")
+
+export async function search(request: ChapterSearchRequest): Promise<ChapterSearchResponse> {
+  const resp = await httpClient.post<Response<ChapterSearchResponse>>(_url(), request);
+  return resp.data.data;
+}

+ 37 - 0
src/client/collection.client.ts

@@ -0,0 +1,37 @@
+import httpClient from "@/services/httpClient";
+import type { Response } from "@/types/response.types";
+import * as urlHelper from "@/libs/url.lib"
+import type { Collections } from "@/types/collection.types";
+
+const _url = urlHelper.withPrefix("/gw/user/collect");
+
+/**
+ * 收藏文献
+ * @param paperId 文献ID
+ * @param title 文献标题
+ * @returns 收藏的ID
+ */
+export async function add(paperId: number, title: string, authors: string, units: string): Promise<number> {
+  const resp = await httpClient.post<Response<number>>(_url("/add"), {paperId, title, authors, units})
+  return resp.data.data
+}
+
+export async function remove(id: number): Promise<boolean> {
+  const resp = await httpClient.delete<Response<boolean>>(_url(`/del/${id}`))
+  return resp.data.data
+}
+
+/**
+ * 文献是否已经收藏
+ * @param paperId 文献ID
+ * @returns 收藏ID
+ */
+export async function isCollected(paperId: number): Promise<number> {
+  const resp = await httpClient.get<Response<number>>(_url("/isCollected"), { params: { paperId } })
+  return resp.data.data
+}
+
+export async function list(page: number): Promise<Collections> {
+  const resp = await httpClient.get<Response<Collections>>(_url("/getPage"), { params: { page } })
+  return resp.data.data
+}

+ 27 - 0
src/client/company.client.ts

@@ -0,0 +1,27 @@
+import httpClient from "@/services/httpClient";
+import type { Company, CompanyAgg } from "@/types/company.types";
+import type { Response } from "@/types/response.types";
+import * as urlHelper from "@/libs/url.lib"
+
+const _url = urlHelper.withPrefix("/gw/user");
+
+export async function get(id: number): Promise<Company> {
+  const resp = await httpClient.get<Response<Company>>(_url(`/company/${id}`));
+  return resp.data.data;
+}
+
+export async function create(name: string): Promise<Company> {
+  const resp = await httpClient.put<Response<Company>>(_url("/company"), { name });
+  return resp.data.data;
+}
+
+export async function query(name: string, size: number = 10): Promise<Company[]> {
+  const params = { name, size };
+  const resp = await httpClient.get<Response<Company[]>>(_url("/company/query"), { params });
+  return resp.data.data;
+}
+
+export async function agg(): Promise<CompanyAgg[]> {
+  const resp = await httpClient.get<Response<CompanyAgg[]>>(_url("/company/agg"))
+  return resp.data.data;
+}

+ 11 - 0
src/client/duplicateCheck.client.ts

@@ -0,0 +1,11 @@
+import httpClient from "@/services/httpClient";
+import type { DuplicateCheckResponse , DuplicateCheckRequest} from "@/types/report.types";
+import type { Response } from "@/types/response.types";
+import * as urlHelper from "@/libs/url.lib"
+
+const _url = urlHelper.withPrefix('/gw/user/duplicateCheck');
+
+export async function duplicateCheck(duplicateCheckRequest: DuplicateCheckRequest){
+  const resp = await httpClient.post<Response<DuplicateCheckResponse>>(_url(), duplicateCheckRequest);
+  return resp.data.data;
+}

+ 11 - 0
src/client/knowledgeGraph.client.ts

@@ -0,0 +1,11 @@
+import httpClient from "@/services/httpClient";
+import type { Response } from "@/types/response.types";
+import * as urlHelper from "@/libs/url.lib"
+import type { SubjectNode, Request } from "@/types/knowledgeGraph.types";
+
+const _url = urlHelper.withPrefix("/gw/search/knowledgeGraph");
+
+export async function find(request: Request): Promise<SubjectNode[]> {
+  const resp = await httpClient.post<Response<SubjectNode[]>>(_url("/find"), request)
+  return resp.data.data
+}

+ 25 - 0
src/client/logo.client.ts

@@ -0,0 +1,25 @@
+import httpClient from "@/services/httpClient"
+import type { Response } from "@/types/response.types"
+import * as urlHelper from "@/libs/url.lib"
+
+const _prefix = "/gw/user/logo"
+const _url = urlHelper.withPrefix(_prefix);
+
+export async function upload(formData: FormData): Promise<string> {
+  const resp = await httpClient.post<Response<string>>(_url(), formData)
+  return resp.data.data;
+}
+
+export async function fetch(reportId: number, token: string): Promise<Blob> {
+  const resp = await httpClient.get<File>(_url(`${reportId}?token=${token}`));
+  return resp.data;
+}
+
+export async function remove(reportId: number) {
+  const resp = await httpClient.delete<Response<boolean>>(_url(`${reportId}`));
+  return resp.data.data
+}
+
+export function link(reportId: number, token: string): string {
+  return `${_prefix}/${reportId}?token=${token}&t=${Date.now()}`
+}

+ 19 - 0
src/client/paperDetail.client.ts

@@ -0,0 +1,19 @@
+/**
+ * 文献客户端
+ */
+import httpClient from "@/services/httpClient";
+import * as urlHelper from "@/libs/url.lib"
+import type { Response } from "@/types/response.types"
+import type { PaperDetail } from "@/types/doc.types";
+
+const _url = urlHelper.withPrefix("/gw/user/detail")
+
+export async function get(paperId: number): Promise<PaperDetail> {
+  const resp = await httpClient.get<Response<PaperDetail>>(_url(paperId))
+  return resp.data.data
+}
+
+export function pdfUrl(pdfPath: string, token: string) {
+  return `${_url("/pdf")}?path=${pdfPath}&token=${token}`;
+}
+

+ 20 - 0
src/client/paperSection.client.ts

@@ -0,0 +1,20 @@
+import httpClient from "@/services/httpClient";
+import * as urlHelper from "@/libs/url.lib"
+import type { Response } from "@/types/response.types"
+
+const _url = urlHelper.withPrefix("/gw/user/paperSection")
+
+export async function record(sectionId: number): Promise<boolean> {
+  const resp = await httpClient.post<Response<boolean>>(_url(sectionId))
+  return resp.data.data;
+}
+
+export async function cancel(sectionId: number): Promise<boolean> {
+  const resp = await httpClient.delete<Response<boolean>>(_url(sectionId))
+  return resp.data.data
+}
+
+export async function query(sectionIds: number[]): Promise<Record<number, boolean>> {
+  const resp = await httpClient.post<Response<Record<number, boolean>>>(_url("/query"), sectionIds)
+  return resp.data.data
+}

+ 39 - 0
src/client/report.client.ts

@@ -0,0 +1,39 @@
+import httpClient from "@/services/httpClient";
+import type { ReportCreateRequest, ReportSaveRequest } from "@/types/report.types";
+import type { Response } from "@/types/response.types";
+import * as urlHelper from "@/libs/url.lib"
+
+const _url = urlHelper.withPrefix('/gw/user/report');
+
+/**
+ * 通过报告ID获取报告数据
+ * @param id 报告ID
+ * @returns Promise
+ */
+export async function get(id: number): Promise<ReportSaveRequest> {
+  const resp = await httpClient.get<Response<ReportSaveRequest>>(_url(`/${id}`))
+  return resp.data.data;
+}
+
+/**
+ * 请求后端服务创建一个新报告
+ */
+export async function create(request: ReportCreateRequest): Promise<number> {
+  const resp = await httpClient.put<Response<number>>(_url(), request);
+  return resp.data.data;
+}
+
+export async function save(reportSaveRequest: ReportSaveRequest) {
+  const resp = await httpClient.post<Response<ReportSaveRequest>>(_url(), reportSaveRequest);
+  return resp.data.data;
+}
+
+export async function updateStatus(id: number) {
+  const resp = await httpClient.put<Response<number>>(_url(`/updateStatus/${id}`));
+  return resp.data.data;
+}
+
+export async function remove(id: number) {
+  const resp = await httpClient.delete<Response<boolean>>(_url(`${id}`))
+  return resp.data.data;
+}

+ 9 - 0
src/client/reportExport.client.ts

@@ -0,0 +1,9 @@
+import httpClient from "@/services/httpClient";
+import * as urlHelper from "@/libs/url.lib"
+
+const _url = urlHelper.withPrefix('/gw/user/reportExport');
+
+export async function download(id: number) {
+  const resp = await httpClient.get<any>(_url(id))
+  return resp.data
+}

+ 32 - 0
src/client/reportTemplate.client.ts

@@ -0,0 +1,32 @@
+import httpClient from "@/services/httpClient";
+import type { ReportTemplateSave, ReportTemplatesResponse } from "@/types/report.types";
+import type { Response } from "@/types/response.types";
+import * as urlHelper from "@/libs/url.lib"
+
+const _url = urlHelper.withPrefix('/gw/user/reportTemplate')
+
+export async function save(reportTemplateSave: ReportTemplateSave): Promise<ReportTemplateSave> {
+  const resp = await httpClient.put<Response<ReportTemplateSave>>(_url(""), reportTemplateSave);
+  return resp.data.data
+}
+
+export async function tags(): Promise<string[]> {
+  const resp = await httpClient.get<Response<string[]>>(_url("/tags"))
+  return resp.data.data
+}
+
+export async function list(field?: string, keyword?: string, page?: number): Promise<ReportTemplatesResponse> {
+  const params = { field, keyword, page }
+  const resp = await httpClient.get<Response<ReportTemplatesResponse>>(_url(), { params });
+  return resp.data.data
+}
+
+export async function remove(id: number): Promise<boolean> {
+  const resp = await httpClient.delete<Response<boolean>>(_url(id))
+  return resp.data.data
+}
+
+export async function get(id: number): Promise<ReportTemplateSave> {
+  const resp = await httpClient.get<Response<ReportTemplateSave>>(_url(id));
+  return resp.data.data;
+}

+ 25 - 0
src/client/reportTemplateLogo.client.ts

@@ -0,0 +1,25 @@
+import httpClient from "@/services/httpClient"
+import type { Response } from "@/types/response.types"
+import * as urlHelper from "@/libs/url.lib"
+
+const _prefix = "/gw/user/reportTemplate/logo"
+const _url = urlHelper.withPrefix(_prefix);
+
+export async function upload(formData: FormData): Promise<string> {
+  const resp = await httpClient.post<Response<string>>(_url(), formData)
+  return resp.data.data;
+}
+
+export async function fetch(templateId: number, token: string): Promise<Blob> {
+  const resp = await httpClient.get<File>(_url(`${templateId}?token=${token}`));
+  return resp.data;
+}
+
+export async function remove(templateId: number) {
+  const resp = await httpClient.delete<Response<boolean>>(_url(`${templateId}`));
+  return resp.data.data
+}
+
+export function link(templateId: number, token: string): string {
+  return `${_prefix}/${templateId}?token=${token}&t=${Date.now()}`
+}

+ 35 - 0
src/client/reports.client.ts

@@ -0,0 +1,35 @@
+/// 报告列表请求客户端
+
+import { withPrefix } from "@/libs/url.lib";
+import httpClient from "@/services/httpClient";
+import type { CompanyAgg } from "@/types/company.types";
+import type { ReportsResponse } from "@/types/report.types";
+import type { Response } from "@/types/response.types";
+
+const _url = withPrefix("/gw/user")
+
+export async function query(page: number, companyIds?: number[], field?: string, keyword?: string, status?: number): Promise<ReportsResponse> {
+  //const params: any = { page, companyIds, field, keyword }
+  // if (companyId) {
+  //   params['companyId'] = companyId;
+  // }
+
+  let param = ''
+  if (companyIds && companyIds.length > 0) {
+    param = companyIds.map((id: number)=>`companyIds=${id}`).join('&');
+  }
+  if (field && keyword && keyword.length > 0 && field.length > 0) {
+    param += `&field=${field}&keyword=${keyword}`
+  }
+  if (status || status == 0) {
+     param += `&status=${status}`
+  }
+
+  const resp = await httpClient.get<Response<ReportsResponse>>(_url(`/reports/?page=${page}&${param}`))
+  return resp.data.data
+}
+
+export async function agg(): Promise<CompanyAgg[]> {
+  const resp = await httpClient.get<Response<CompanyAgg[]>>(_url("/company/agg"))
+  return resp.data.data;
+}

+ 11 - 0
src/client/search.client.ts

@@ -0,0 +1,11 @@
+import type { SearchRequest } from "@/types/request.types";
+import httpClient from "@/services/httpClient";
+import * as urlHelper from "@/libs/url.lib"
+import type { SearchResult } from "@/types/search.types";
+
+const _url = urlHelper.withPrefix("/gw/search/search")
+
+export async function search(query: SearchRequest): Promise<SearchResult> {
+  const resp = await httpClient.post(_url(), query)
+  return resp.data.data
+}

+ 11 - 0
src/client/searchHistory.client.ts

@@ -0,0 +1,11 @@
+import httpClient from "@/services/httpClient";
+import type { Response } from "@/types/response.types";
+import * as urlHelper from "@/libs/url.lib"
+import type { HotKeyword } from "@/types/searchHistory.types";
+
+const _url = urlHelper.withPrefix("/gw/user/searchHistory");
+
+export async function getHoyKeyword(size: number): Promise<HotKeyword[]> {
+  const resp = await httpClient.get<Response<HotKeyword[]>>(_url(`/getHotKeyword?size=${size}`))
+  return resp.data.data
+}

+ 41 - 0
src/components/HelloWorld.vue

@@ -0,0 +1,41 @@
+<script setup lang="ts">
+defineProps<{
+  msg: string;
+}>();
+</script>
+
+<template>
+  <div class="greetings">
+    <h1 class="green">{{ msg }}</h1>
+    <h3>
+      You’ve successfully created a project with
+      <a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
+      <a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
+      What's next?
+    </h3>
+  </div>
+</template>
+
+<style scoped>
+h1 {
+  font-weight: 500;
+  font-size: 2.6rem;
+  top: -10px;
+}
+
+h3 {
+  font-size: 1.2rem;
+}
+
+.greetings h1,
+.greetings h3 {
+  text-align: center;
+}
+
+@media (min-width: 1024px) {
+  .greetings h1,
+  .greetings h3 {
+    text-align: left;
+  }
+}
+</style>

Plik diff jest za duży
+ 9 - 0
src/components/LogoComponent.vue


+ 132 - 0
src/components/NavMenu.vue

@@ -0,0 +1,132 @@
+<script setup lang="ts">
+import {
+  FilePptOutlined,
+  FileSearchOutlined,
+  NodeIndexOutlined,
+  LogoutOutlined,
+  HistoryOutlined,
+  StarOutlined,
+  ExclamationCircleOutlined,
+} from '@ant-design/icons-vue';
+import { createVNode, onMounted, ref, watch } from 'vue';
+import { useRoute, RouterLink } from "vue-router";
+import _ from "lodash";
+import { useAuthStore } from '@/stores/auth.store';
+import { routeToLogin } from "@/router";
+import { Modal } from 'ant-design-vue';
+
+const selectedKeys = ref<string[]>(['/']);
+const openKeys = ref<string[]>(['/']);
+// 当前路由
+const route = useRoute();
+// 授权store
+const authStore = useAuthStore();
+
+onMounted(() => {
+  openKeys.value = getOpenKeys(route.path);
+  selectedKeys.value = [route.path];
+
+  watch(
+    () => route.path,
+    (path: string) => {
+      const currentOpenkeys = getOpenKeys(path);
+      openKeys.value = _.uniq(_.concat(openKeys.value, currentOpenkeys));
+      selectedKeys.value = openKeys.value;
+    }
+  );
+});
+
+function getOpenKeys(path: string): string[] {
+  const parts = _.dropRight(path.split('/').filter(x => x != '').map(x => '/'+x), 1);
+  if (parts.length == 0) {
+    return ["/"];
+  }
+  let openKeys = parts.reduce((acc, item) => {
+    if (acc.length == 0) {
+      return acc.concat([item]);
+    }
+    return acc.concat([acc[acc.length-1] + item]);
+  }, [] as string[]);
+  openKeys.unshift("/");
+  return openKeys;
+}
+
+function logout(e: MouseEvent) {
+  authStore.logout();
+  routeToLogin();
+  e.stopPropagation();
+}
+
+function logoutConfirm(e: MouseEvent) {
+  Modal.confirm({
+    title: '正在退出系统,退出请点击确认。',
+    icon: createVNode(ExclamationCircleOutlined),
+    okText: '确认',
+    okType: 'danger',
+    cancelText: '取消',
+    onOk() {
+      logout(e);
+    }
+  });
+}
+</script>
+
+<template>
+  <a-menu
+    v-model:openKeys="openKeys"
+    v-model:selectedKeys="selectedKeys"
+    mode="inline"
+  >
+    <a-sub-menu key="/report">
+      <template #icon>
+        <FilePptOutlined />
+      </template>
+      <template #title>报告管理</template>
+      <a-menu-item key="/report/index">
+        <RouterLink to="/report/index">我的报告</RouterLink>
+      </a-menu-item>
+      <a-menu-item key="/report/template">
+        <RouterLink to="/report/template">模板管理</RouterLink>
+      </a-menu-item>
+    </a-sub-menu>
+    <a-sub-menu key="/search">
+      <template #icon>
+        <FileSearchOutlined />
+      </template>
+      <template #title>搜索</template>
+      <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="/knowledgeGraph">
+      <template #icon>
+        <NodeIndexOutlined />
+      </template>
+      <template #title>知识导航</template>
+      <a-menu-item key="/knowledgeGraph/search">
+        <RouterLink to="/knowledgeGraph/search">建筑工程及工程设计</RouterLink>
+      </a-menu-item>
+    </a-sub-menu>
+    <a-menu-item key="/history">
+      <template #icon>
+        <history-outlined />
+      </template>
+      <RouterLink to="/history">历史记录</RouterLink>
+    </a-menu-item>
+    <a-menu-item key="/favorite">
+      <template #icon>
+        <star-outlined />
+      </template>
+      <RouterLink to="/favorite">我的收藏</RouterLink>
+    </a-menu-item>
+    <a-menu-item key="/logout" @click="logoutConfirm">
+      <template #icon>
+        <logout-outlined />
+      </template>
+      退出系统
+    </a-menu-item>
+  </a-menu>
+</template>

+ 30 - 0
src/components/PageHeader.vue

@@ -0,0 +1,30 @@
+<script setup lang="ts">
+defineProps({
+  title: {
+    type: String,
+    required: true,
+  }
+});
+</script>
+
+<template>
+  <a-row type="flex" justify="space-between" :align="'middle'">
+    <a-col>
+      <h1 class="title">{{ title }}</h1>
+    </a-col>
+    <a-col>
+      <slot></slot>
+    </a-col>
+  </a-row>
+  <a-divider class="divider" />
+</template>
+
+<style scoped lang="scss">
+h1.title {
+  font-size: 1.75em;
+}
+
+.divider {
+  margin-top: 1em;
+}
+</style>

+ 26 - 0
src/components/SpinComponent.vue

@@ -0,0 +1,26 @@
+<script setup lang="ts">
+import { LoadingOutlined } from '@ant-design/icons-vue';
+
+defineProps({
+  spinning: {
+    type: Boolean,
+    default: false,
+  }
+});
+</script>
+
+<template>
+  <a-spin :spinning="spinning" class="spinning" size="large">
+    <template #indicator>
+      <loading-outlined :spin="true" />
+    </template>
+    <slot></slot>
+  </a-spin>
+</template>
+
+<style scoped lang="scss">
+.spinning {
+  font-size: 24px;
+  width: 100%;
+}
+</style>

+ 121 - 0
src/components/TheWelcome.vue

@@ -0,0 +1,121 @@
+<script setup lang="ts">
+import WelcomeItem from "./WelcomeItem.vue";
+import DocumentationIcon from "./icons/IconDocumentation.vue";
+import ToolingIcon from "./icons/IconTooling.vue";
+import EcosystemIcon from "./icons/IconEcosystem.vue";
+import CommunityIcon from "./icons/IconCommunity.vue";
+import SupportIcon from "./icons/IconSupport.vue";
+</script>
+
+<template>
+  <WelcomeItem>
+    <template #icon>
+      <DocumentationIcon />
+    </template>
+    <template #heading>Documentation</template>
+
+    Vue’s
+    <a href="https://vuejs.org/" target="_blank" rel="noopener"
+      >official documentation</a
+    >
+    provides you with all information you need to get started.
+  </WelcomeItem>
+
+  <WelcomeItem>
+    <template #icon>
+      <ToolingIcon />
+    </template>
+    <template #heading>Tooling</template>
+
+    This project is served and bundled with
+    <a
+      href="https://vitejs.dev/guide/features.html"
+      target="_blank"
+      rel="noopener"
+      >Vite</a
+    >. The recommended IDE setup is
+    <a href="https://code.visualstudio.com/" target="_blank" rel="noopener"
+      >VSCode</a
+    >
+    +
+    <a
+      href="https://github.com/johnsoncodehk/volar"
+      target="_blank"
+      rel="noopener"
+      >Volar</a
+    >. If you need to test your components and web pages, check out
+    <a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
+    and
+    <a href="https://on.cypress.io/component" target="_blank"
+      >Cypress Component Testing</a
+    >.
+
+    <br />
+
+    More instructions are available in <code>README.md</code>.
+  </WelcomeItem>
+
+  <WelcomeItem>
+    <template #icon>
+      <EcosystemIcon />
+    </template>
+    <template #heading>Ecosystem</template>
+
+    Get official tools and libraries for your project:
+    <a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
+    <a href="https://router.vuejs.org/" target="_blank" rel="noopener"
+      >Vue Router</a
+    >,
+    <a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener"
+      >Vue Test Utils</a
+    >, and
+    <a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener"
+      >Vue Dev Tools</a
+    >. If you need more resources, we suggest paying
+    <a
+      href="https://github.com/vuejs/awesome-vue"
+      target="_blank"
+      rel="noopener"
+      >Awesome Vue</a
+    >
+    a visit.
+  </WelcomeItem>
+
+  <WelcomeItem>
+    <template #icon>
+      <CommunityIcon />
+    </template>
+    <template #heading>Community</template>
+
+    Got stuck? Ask your question on
+    <a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a
+    >, our official Discord server, or
+    <a
+      href="https://stackoverflow.com/questions/tagged/vue.js"
+      target="_blank"
+      rel="noopener"
+      >StackOverflow</a
+    >. You should also subscribe to
+    <a href="https://news.vuejs.org" target="_blank" rel="noopener"
+      >our mailing list</a
+    >
+    and follow the official
+    <a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
+      >@vuejs</a
+    >
+    twitter account for latest news in the Vue world.
+  </WelcomeItem>
+
+  <WelcomeItem>
+    <template #icon>
+      <SupportIcon />
+    </template>
+    <template #heading>Support Vue</template>
+
+    As an independent project, Vue relies on community backing for its
+    sustainability. You can help us by
+    <a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener"
+      >becoming a sponsor</a
+    >.
+  </WelcomeItem>
+</template>

+ 86 - 0
src/components/WelcomeItem.vue

@@ -0,0 +1,86 @@
+<template>
+  <div class="item">
+    <i>
+      <slot name="icon"></slot>
+    </i>
+    <div class="details">
+      <h3>
+        <slot name="heading"></slot>
+      </h3>
+      <slot></slot>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.item {
+  margin-top: 2rem;
+  display: flex;
+}
+
+.details {
+  flex: 1;
+  margin-left: 1rem;
+}
+
+i {
+  display: flex;
+  place-items: center;
+  place-content: center;
+  width: 32px;
+  height: 32px;
+
+  color: var(--color-text);
+}
+
+h3 {
+  font-size: 1.2rem;
+  font-weight: 500;
+  margin-bottom: 0.4rem;
+  color: var(--color-heading);
+}
+
+@media (min-width: 1024px) {
+  .item {
+    margin-top: 0;
+    padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
+  }
+
+  i {
+    top: calc(50% - 25px);
+    left: -26px;
+    position: absolute;
+    border: 1px solid var(--color-border);
+    background: var(--color-background);
+    border-radius: 8px;
+    width: 50px;
+    height: 50px;
+  }
+
+  .item:before {
+    content: " ";
+    border-left: 1px solid var(--color-border);
+    position: absolute;
+    left: 0;
+    bottom: calc(50% + 25px);
+    height: calc(50% - 25px);
+  }
+
+  .item:after {
+    content: " ";
+    border-left: 1px solid var(--color-border);
+    position: absolute;
+    left: 0;
+    top: calc(50% + 25px);
+    height: calc(50% - 25px);
+  }
+
+  .item:first-of-type:before {
+    display: none;
+  }
+
+  .item:last-of-type:after {
+    display: none;
+  }
+}
+</style>

+ 11 - 0
src/components/__tests__/HelloWorld.spec.ts

@@ -0,0 +1,11 @@
+import { describe, it, expect } from "vitest";
+
+import { mount } from "@vue/test-utils";
+import HelloWorld from "../HelloWorld.vue";
+
+describe("HelloWorld", () => {
+  it("renders properly", () => {
+    const wrapper = mount(HelloWorld, { props: { msg: "Hello Vitest" } });
+    expect(wrapper.text()).toContain("Hello Vitest");
+  });
+});

Plik diff jest za duży
+ 12 - 0
src/components/icons/IconCommunity.vue


Plik diff jest za duży
+ 10 - 0
src/components/icons/IconDetailList.vue


Plik diff jest za duży
+ 12 - 0
src/components/icons/IconDocumentation.vue


Plik diff jest za duży
+ 12 - 0
src/components/icons/IconEcosystem.vue


Plik diff jest za duży
+ 10 - 0
src/components/icons/IconSimpleList.vue


+ 12 - 0
src/components/icons/IconSupport.vue

@@ -0,0 +1,12 @@
+<template>
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    width="20"
+    height="20"
+    fill="currentColor"
+  >
+    <path
+      d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
+    />
+  </svg>
+</template>

+ 19 - 0
src/components/icons/IconTooling.vue

@@ -0,0 +1,19 @@
+<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
+<template>
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    xmlns:xlink="http://www.w3.org/1999/xlink"
+    aria-hidden="true"
+    role="img"
+    class="iconify iconify--mdi"
+    width="24"
+    height="24"
+    preserveAspectRatio="xMidYMid meet"
+    viewBox="0 0 24 24"
+  >
+    <path
+      d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
+      fill="currentColor"
+    ></path>
+  </svg>
+</template>

+ 21 - 0
src/libs/clipboard.lib.ts

@@ -0,0 +1,21 @@
+
+export function useClipboard() {
+  function copy(text: string) {
+    if (navigator.clipboard && window.isSecureContext) {
+      return navigator.clipboard.writeText(text)
+    } else {
+      const textArea = document.createElement("textarea");
+      textArea.value = text
+      textArea.style.position = "absolute";
+      textArea.style.opacity = '0';
+      document.body.appendChild(textArea);
+      textArea.select();
+      return new Promise((resolve, reject) => {
+        document.execCommand("copy") ? resolve('') : reject(new Error("出错了"));
+        textArea.remove()
+      })
+    }
+  }
+  
+  return { copy }
+}

+ 6 - 0
src/libs/image.lib.ts

@@ -0,0 +1,6 @@
+
+export function getBase64(img: Blob, callback: (base64Url: string) => void) {
+  const reader = new FileReader();
+  reader.addEventListener('load', () => callback(reader.result as string));
+  reader.readAsDataURL(img);
+}

+ 18 - 0
src/libs/log.lib.ts

@@ -0,0 +1,18 @@
+
+export class CompLog {
+  /**
+   * 输出错误信息,带上tag信息
+   * @param args 输出数据
+   */
+  public static logErr(tag: string) {
+    return (...args: any) => {
+      console.error(tag, ...args);
+    };
+  }
+
+  public static log(tag: string) {
+    return (...args: any) => {
+      console.log(tag, ...args);
+    };
+  }
+}

+ 51 - 0
src/libs/reportExport.lib.ts

@@ -0,0 +1,51 @@
+import { useAuthStore } from "@/stores/auth.store"
+
+export function useReportExport() {
+  const authStore = useAuthStore()
+
+  // async function download(name: string, id: number) {
+  //   return reportExportService.download(id).then((resp) => {
+  //     if (!resp) {
+  //       return
+  //     }
+  //     const url = window.URL.createObjectURL(new Blob([resp]))
+  //     const a = document.createElement('a')
+  //     a.style.display = 'none'
+  //     a.setAttribute('download', name + '.docx')
+  //     a.href = url
+  //     document.body.appendChild(a)
+  //     a.click()
+  //     window.URL.revokeObjectURL(a.href)
+  //     document.body.removeChild(a)
+  //   })
+  // }
+
+  function download(name: string, id: number) {
+    const href = link(id, authStore.token)
+    if (!href) {
+      return
+    }
+    const a = document.createElement("a")
+    a.setAttribute("download", name + '.docx')
+    a.setAttribute("href", href)
+    a.style.display = 'none'
+    document.body.appendChild(a)
+    a.click()
+    document.body.removeChild(a)
+  }
+
+  /**
+   * 生成报告下载链接
+   * @param id 报告ID
+   * @param token 用户token
+   * @returns 报告下载的链接字符串或者null
+   */
+  function link(id: number, token: string | null) {
+    if (!token || !id) {
+      return null
+    }
+    return `/gw/user/reportExport/${id}?token=${token}`
+  }
+
+  return { download, link }
+}

+ 10 - 0
src/libs/url.lib.ts

@@ -0,0 +1,10 @@
+import _ from "lodash";
+
+export function withPrefix(prefix: string) {
+  return (path?: string | number) => {
+    if (path) {
+      return _.trimEnd(prefix, "/") + "/" + _.trimStart(`${path}`, "/")
+    }
+    return prefix;
+  }
+}

+ 18 - 0
src/main.ts

@@ -0,0 +1,18 @@
+import { createApp } from "vue";
+import { createPinia } from "pinia";
+import Antd from "ant-design-vue";
+
+import App from "./App.vue";
+import { router } from "./router";
+
+import "ant-design-vue/dist/antd.less";
+
+import "./assets/main.scss";
+
+const app = createApp(App);
+
+app.use(Antd);
+app.use(createPinia());
+app.use(router);
+
+app.mount("#app");

+ 64 - 0
src/models/knowledgeGraph.model.ts

@@ -0,0 +1,64 @@
+import type { SubjectNode } from "@/types/knowledgeGraph.types";
+import type { GraphData } from "@antv/g6";
+
+export class GraphDataManager {
+  private graphData: GraphData = {nodes: [], edges: []};
+  private nodeIds: Set<string> = new Set();
+  private edgeNames: Set<string> = new Set();
+
+  constructor() {}
+
+  public addSubjectNodes(nodes: SubjectNode[]) {
+    for (const node of nodes) {
+      this.addSubjectNode(node)
+    }
+  }
+
+  public addSubjectNode(node: SubjectNode) {
+    if (!this.nodeIds.has(node.fieldID)) {
+      this.graphData.nodes?.push({
+        id: node.fieldID,
+        nodeId: node.id,
+        label: node.name,
+      })
+      this.nodeIds.add(node.fieldID);
+    }
+    if (node.children && node.children.length > 0) {
+      this.addEdge(node);
+      this.addSubjectNodes(node.children)
+    }
+  }
+
+  private addEdge(node: SubjectNode, deepth: number = 0) {
+    if (!node.children || node.children.length <= 0 || deepth > 2) {
+      return;
+    }
+    for (const child of node.children) {
+      const key = `${node.fieldID}_${child.fieldID}`;
+      if (this.edgeNames.has(key)) {
+        continue;
+      }
+      this.graphData.edges?.push({
+        source: node.fieldID,
+        target: child.fieldID,
+        labelCfg: {
+          style: {
+            fontSize: 10,
+            opacity: 0.5,
+          }
+        }
+      })
+      this.edgeNames.add(key);
+    }
+  }
+
+  public pruge() {
+    this.graphData = {nodes: [], edges: []};
+    this.nodeIds = new Set();
+    this.edgeNames = new Set();
+  }
+
+  public getGraphData() {
+    return this.graphData;
+  }
+}

+ 85 - 0
src/models/report.model.ts

@@ -0,0 +1,85 @@
+import { ChapterTypeKeywords, ChapterTypeLabels, type Chapter, type ChapterType } from "@/types/report.types";
+import Nzh from "nzh";
+
+/**
+ * 章节管理器
+ */
+export class ChapterManager {
+  private chapters: Chapter[];
+
+  constructor() {
+    this.chapters = [];
+  }
+
+  public addChapter(chapterType: ChapterType) {
+    const item = ChapterTypeLabels.find((item) => item.key == chapterType);
+    const keywordsItem = ChapterTypeKeywords.find((item) => item.key == chapterType);
+    if (item) {
+      this.chapters.push({
+        title: item.label,
+        fullTitle: this.getTitleWithChineseNum(this.chapters.length, item.label),
+        content: '',
+        chapterType: chapterType,
+        keywords: keywordsItem?.keywords || [],
+      });
+    }
+  }
+
+  public addChapters(chapterTypes: ChapterType[]) {
+    for (const type of chapterTypes) {
+      this.addChapter(type);
+    }
+  }
+
+  public getChapter(index: number) {
+    return this.chapters[index];
+  }
+
+  private getTitleWithChineseNum(index: number, title: string) {
+    return Nzh.cn.encodeS(index+1) + '、' + title
+  }
+
+  public setChapter(index: number, data: Chapter) {
+    const chapter = this.chapters[index]
+    this.chapters[index] = {
+      ...chapter,
+      ...data,
+      fullTitle: this.getTitleWithChineseNum(index, chapter.title)
+    };
+  }
+
+  public setChapters(chapters: Chapter[]) {
+    this.chapters = chapters;
+  }
+
+  public getChapters(): Chapter[] {
+    return this.chapters.map((chapter, index) => {
+      const fullTitle = this.getTitleWithChineseNum(index, chapter.title)
+      return { ...chapter, fullTitle };
+    });
+  }
+
+  public moveUp(index: number) {
+    if (index < 1) {
+      return;
+    }
+    const temp = this.chapters[index-1];
+    this.chapters[index-1] = this.chapters[index]
+    this.chapters[index] = temp
+  }
+
+  public moveDown(index: number) {
+    if (index >= this.chapters.length-1) {
+      return;
+    }
+    const temp = this.chapters[index+1];
+    this.chapters[index+1] = this.chapters[index]
+    this.chapters[index] = temp
+  }
+
+  public remove(index: number) {
+    if (index >= 0 && index < this.chapters.length) {
+      this.chapters.splice(index, 1)
+    }
+  }
+}

+ 176 - 0
src/router/index.ts

@@ -0,0 +1,176 @@
+import { useAuthStore } from "@/stores/auth.store";
+import type { SearchRouteParam } from "@/types/route.type";
+import { createRouter, createWebHistory } from "vue-router";
+import RouteView from "../views/RouteView.vue";
+
+export const router = createRouter({
+  history: createWebHistory(import.meta.env.BASE_URL),
+  routes: [
+    {
+      path: "/",
+      name: "Home",
+      redirect: "/report/index"
+    },
+    {
+      path: "/report",
+      name: "Report",
+      component: RouteView,
+      children: [
+        {
+          path: "index",
+          name: "ReportList",
+          component: () => import("../views/report/ReportListView.vue"),
+        },
+        {
+          path: "edit/:id",
+          name: "ReportEditor",
+          component: () => import("../views/report/ReportEditorView.vue"),
+        },
+        {
+          path: "preview/:id",
+          name: "ReportPreview",
+          component: () => import("../views/reportPreview/ReportPreviewView.vue"),
+        },
+        {
+          path: "template",
+          name: "ReportTemplate",
+          component: () => import("../views/reportTemplate/ReportTemplateListView.vue"),
+        },
+        {
+          path: "template/edit/:id",
+          name: "ReportTemplateEditor",
+          component: () => import("../views/reportTemplate/ReportTemplateEditorView.vue"),
+        },
+      ]
+    },
+    {
+      path: "/search",
+      name: "Search",
+      component: RouteView,
+      children: [
+        {
+          path: "index",
+          name: "SearchIndex",
+          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")
+        }
+      ]
+    },
+    {
+      path: "/detail",
+      name: "DetailRoot",
+      component: RouteView,
+      children: [
+        {
+          path: ":id",
+          name: "Detail",
+          component: () => import("../views/detail/DetailView.vue"),
+        }
+      ]
+    },
+    {
+      path: "/knowledgeGraph",
+      name: "KnowledgeGraph",
+      component: RouteView,
+      children: [
+        {
+          path: "search",
+          name: "KnowledgeGraph",
+          component: () => import("../views/knowledgeGraph/KnowledgeGraphView.vue")
+        }
+      ]
+    },
+    {
+      path: "/login",
+      name: "Login",
+      // route level code-splitting
+      // this generates a separate chunk (About.[hash].js) for this route
+      // which is lazy-loaded when the route is visited.
+      component: () => import("../views/LoginView.vue"),
+    },
+    {
+      path: "/history",
+      name: "History",
+      component: RouteView,
+      children: [
+        {
+          path: "",
+          name: "HistoryIndex",
+          component: () => import("../views/history/HistoryView.vue"),
+        }
+      ]
+    },
+    {
+      path: "/favorite",
+      name: "Favorite",
+      component: RouteView,
+      children: [
+        {
+          path: "",
+          name: "FavoriteIndex",
+          component: () => import("../views/favorite/FavoriteView.vue"),
+        }
+      ]
+    },
+    {
+      path: '/:pathMatch(.*)*',
+      name: 'NotFound',
+      component: () => import("../views/NotFound.vue"),
+    },
+  ],
+});
+
+router.beforeResolve(async to => {
+  const authStore = useAuthStore();
+
+  if (to.name != 'Login' && !authStore.isLoginIn()) {
+    return { name: "Login" };
+  }
+});
+
+export function routeToHome() {
+  router.push({ name: "Home" })
+}
+
+export function routeToLogin() {
+  router.push({ name: "Login" });
+}
+
+export function routeToSearchPage() {
+  router.push({ name: "SearchIndex"})
+}
+
+/**
+ * 路由到报告编辑界面
+ * @param id 报告ID
+ */
+export function routeToReportEditor(id: number) {
+  router.push({ name: "ReportEditor", params: { id }});
+}
+
+/**
+ * 路由到搜索结果页面,把查询表达式传入到搜索结果页面
+ * @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 });
+}
+
+/**
+ * 路由到报告预览界面
+ * @param id 报告ID
+ */
+export function routeToReportPreview(id: number) {
+  router.push({ name: 'ReportPreview', params: { id } })
+}

+ 5 - 0
src/services/ai.service.ts

@@ -0,0 +1,5 @@
+import * as aiClient from "@/client/ai.client";
+
+export async function generate(projectName: string, keywords: string, chapterTitle: string) {
+  return aiClient.generate(projectName, keywords, chapterTitle)
+}

+ 14 - 0
src/services/browsingHistory.service.ts

@@ -0,0 +1,14 @@
+import * as browsingHistoryClient from "@/client/browsingHistory.client"
+import type { BrowsingHistories } from "@/types/browsingHistory.types"
+
+export async function fetch(page: number, dateNum?: number): Promise<BrowsingHistories> {
+  return browsingHistoryClient.fetch(page, dateNum)
+}
+
+export async function remove(id: number) {
+  return browsingHistoryClient.remove(id)
+}
+
+export async function removeAll() {
+  return browsingHistoryClient.removeAll()
+}

+ 6 - 0
src/services/chapterSearch.service.ts

@@ -0,0 +1,6 @@
+import * as chapterSearchClient from "@/client/chapterSearch.client"
+import type { ChapterSearchRequest, ChapterSearchResponse } from "@/types/search.types"
+
+export async function search(request: ChapterSearchRequest): Promise<ChapterSearchResponse> {
+  return chapterSearchClient.search(request);
+}

+ 18 - 0
src/services/collection.service.ts

@@ -0,0 +1,18 @@
+import * as collectionClient from "@/client/collection.client"
+import type { Collections } from "@/types/collection.types"
+
+export async function add(paperId: number, title: string, authors: string, units: string): Promise<number> {
+  return collectionClient.add(paperId, title, authors, units)
+}
+
+export async function remove(id: number): Promise<boolean> {
+  return collectionClient.remove(id)
+}
+
+export async function isCollected(paperId: number): Promise<number> {
+  return collectionClient.isCollected(paperId)
+}
+
+export async function list(page: number): Promise<Collections> {
+  return collectionClient.list(page)
+}

+ 17 - 0
src/services/company.service.ts

@@ -0,0 +1,17 @@
+import * as companyClient from "@/client/company.client";
+
+export async function get(id: number) {
+  return companyClient.get(id);
+}
+
+export async function create(name: string) {
+  return companyClient.create(name);
+}
+
+export async function query(name: string, size: number = 0) {
+  return companyClient.query(name, size);
+}
+
+export async function agg() {
+  return companyClient.agg();
+}

+ 51 - 0
src/services/httpClient.ts

@@ -0,0 +1,51 @@
+import { message } from "ant-design-vue";
+import axios, { AxiosError, type AxiosRequestConfig } from "axios";
+import { routeToLogin } from "@/router";
+import type { MessageType } from "ant-design-vue/lib/message";
+
+const urlPrefix = "/";
+
+const httpClient = axios.create({
+  baseURL: urlPrefix,
+  timeout: 300000,
+});
+
+
+// 添加请求拦截器
+httpClient.interceptors.request.use(function (config: AxiosRequestConfig) {
+  // 在发送请求之前做些什么
+  const token = localStorage.getItem('token');
+  if (token && config.headers) {
+    config.headers.Authorization = token;
+  }
+  return config;
+}, function (error) {
+  // 对请求错误做些什么
+  return Promise.reject(error);
+});
+
+let errorMsgHandler: MessageType | null = null;
+
+// 添加响应拦截器
+httpClient.interceptors.response.use(function (response) {
+  // 对响应数据做点什么
+  return response;
+}, function (error: AxiosError) {
+  if (error.response?.status == 404) {
+    message.error("请求参数错误!");
+  } else if (error.response?.status == 401) {
+    routeToLogin();
+  } else if (error.response?.status == 403) {
+    message.error("账号没有权限!");
+  } else if (error) {
+    if (errorMsgHandler != null) {
+      errorMsgHandler()
+    }
+    errorMsgHandler = message.error(error.message);
+    // routeToLogin();
+  }
+
+  return Promise.reject(error);
+});
+
+export default httpClient;

+ 18 - 0
src/services/knowledgeGraph.service.ts

@@ -0,0 +1,18 @@
+import type { AxiosResponse } from "axios";
+import httpClient from "./httpClient";
+import * as knowledgeGraphClient from "@/client/knowledgeGraph.client"
+import type { Response } from "@/types/response.types";
+import type { SubjectNode, Property, Request } from "@/types/knowledgeGraph.types";
+
+/**
+ * 根据学科ID查询学科属性
+ * @param id 学科ID
+ * @returns 学科属性
+ */
+export function fetchProperty(id: number): Promise<AxiosResponse<Response<Property>>> {
+  return httpClient.get(`/knowledgeGraph/property/${id}`);
+}
+
+export async function find(request: Request): Promise<SubjectNode[]> {
+  return knowledgeGraphClient.find(request)
+}

+ 17 - 0
src/services/logo.service.ts

@@ -0,0 +1,17 @@
+import * as logoClient from "@/client/logo.client"
+
+export async function upload(formData: FormData): Promise<string> {
+  return logoClient.upload(formData)
+}
+
+export async function fetch(reportId: number, token: string): Promise<Blob> {
+  return logoClient.fetch(reportId, token)
+}
+
+export async function remove(reportId: number) {
+  return logoClient.remove(reportId)
+}
+
+export function link(reportId: number, token: string): string {
+  return logoClient.link(reportId, token)
+}

+ 9 - 0
src/services/paperDetail.service.ts

@@ -0,0 +1,9 @@
+import * as paperDetailClient from "@/client/paperDetail.client"
+
+export async function get(paperId: number) {
+  return paperDetailClient.get(paperId)
+}
+
+export function pdfUrl(pdfPath: string, token: string) {
+  return paperDetailClient.pdfUrl(pdfPath, token)
+}

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

@@ -0,0 +1,13 @@
+import * as paperSectionClient from "@/client/paperSection.client"
+
+export async function record(sectionId: number) {
+  return paperSectionClient.record(sectionId)
+}
+
+export async function cancel(sectionId: number) {
+  return paperSectionClient.cancel(sectionId)
+}
+
+export async function query(sectionIds: number[]): Promise<Record<number, boolean>> {
+  return paperSectionClient.query(sectionIds)
+}

+ 32 - 0
src/services/report.service.ts

@@ -0,0 +1,32 @@
+import * as reportClient from "@/client/report.client";
+import * as duplicateCheckClient from "@/client/duplicateCheck.client";
+import type { ReportCreateRequest, ReportSaveRequest, DuplicateCheckRequest } from "@/types/report.types";
+
+export async function get(id: number): Promise<ReportSaveRequest> {
+  const resp = await reportClient.get(id);
+  if (!resp.metadata) {
+    resp.metadata = [];
+  }
+  return resp;
+}
+
+export async function create(request: ReportCreateRequest): Promise<number> {
+  const id = await reportClient.create(request);
+  return id;
+}
+
+export async function save(reportSaveRequest: ReportSaveRequest) {
+  return reportClient.save(reportSaveRequest);
+}
+
+export async function updateStatus(id: number){
+  return reportClient.updateStatus(id);
+}
+
+export async function remove(id: number) {
+  return reportClient.remove(id);
+}
+
+export function duplicateCheck(duplicateCheckRequest: DuplicateCheckRequest){
+  return duplicateCheckClient.duplicateCheck(duplicateCheckRequest);
+}

+ 5 - 0
src/services/reportExport.service.ts

@@ -0,0 +1,5 @@
+import * as reportExportClient from "@/client/reportExport.client"
+
+export async function download(id: number) {
+  return reportExportClient.download(id)
+}

+ 26 - 0
src/services/reportTemplate.service.ts

@@ -0,0 +1,26 @@
+import * as reportTemplateClient from "@/client/reportTemplate.client"
+import type { ReportTemplateSave } from "@/types/report.types";
+
+export async function save(reportTemplateSave: ReportTemplateSave) {
+  return reportTemplateClient.save(reportTemplateSave);
+}
+
+export async function tags() {
+  return reportTemplateClient.tags()
+}
+
+export async function list(field?: string, keyword?: string, page?: number) {
+  return reportTemplateClient.list(field, keyword, page);
+}
+
+export async function remove(id: number) {
+  return reportTemplateClient.remove(id)
+}
+
+/**
+ * 通过报告模板ID获取报告模板内容
+ * @param id 报告模板ID
+ */
+export async function get(id: number) {
+  return reportTemplateClient.get(id)
+}

+ 17 - 0
src/services/reportTemplateLogo.service.ts

@@ -0,0 +1,17 @@
+import * as logoClient from "@/client/reportTemplateLogo.client"
+
+export async function upload(formData: FormData): Promise<string> {
+  return logoClient.upload(formData)
+}
+
+export async function fetch(reportId: number, token: string): Promise<Blob> {
+  return logoClient.fetch(reportId, token)
+}
+
+export async function remove(reportId: number) {
+  return logoClient.remove(reportId)
+}
+
+export function link(reportId: number, token: string): string {
+  return logoClient.link(reportId, token)
+}

+ 9 - 0
src/services/reports.service.ts

@@ -0,0 +1,9 @@
+import * as reportsClient from "@/client/reports.client"
+
+export async function query(page?: number, companyIds?: number[], field?: string, keyword?: string, status?: number) {
+  return reportsClient.query(page || 1, companyIds, field, keyword, status);
+}
+
+export async function agg() {
+  return reportsClient.agg();
+}

+ 18 - 0
src/services/search.service.ts

@@ -0,0 +1,18 @@
+import type {SearchResult} from "@/types/search.types";
+import type { SearchRequest } from "@/types/request.types";
+import * as searchClient from "@/client/search.client"
+
+export async function search(query: SearchRequest): Promise<SearchResult> {
+  if (!query.dataSources) {
+    query["dataSources"] = ["CN"];
+  }
+  const result = await searchClient.search(query)
+  if (result.docs && result.docs.length > 0) {
+    for (const doc of result.docs) {
+      if (doc.keywords == null) {
+        doc.keywords = []
+      }
+    }
+  }
+  return result
+}

+ 6 - 0
src/services/searchHistory.service.ts

@@ -0,0 +1,6 @@
+import * as SearchHistoryClient from "@/client/searchHistory.client"
+import type { HotKeyword } from "@/types/searchHistory.types";
+
+export async function getHoyKeyword(size: number): Promise<HotKeyword[]> {
+  return SearchHistoryClient.getHoyKeyword(size)
+}

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

@@ -0,0 +1,58 @@
+import type { SearchHistoryRecord, UserLoginRequest, CurrentUser } from "@/types/user.types";
+import type { Page } from "@/types/pagination.types"
+import type { Response } from "@/types/response.types";
+import type { AxiosResponse } from "axios";
+import httpClient from "./httpClient";
+
+const urlPrefix = "/gw/user";
+
+function _url(path: string): string {
+  return `${urlPrefix}${path}`;
+}
+
+function login(param: UserLoginRequest): Promise<AxiosResponse<Response<CurrentUser>>> {
+  return httpClient.post<Response<CurrentUser>>(_url("/login"), param);
+}
+
+async function checkLogin(): Promise<boolean> {
+  const resp = await httpClient.get("/gw/user/user/checkLogin");
+  if (resp) {
+    return true;
+  }
+  return false;
+}
+
+/**
+ * 获取指定页码和数量的搜索历史记录
+ * @param currentUser user对象的json字符串
+ * @param page 页码
+ * @param size 每页数量
+ * @returns Promise
+ */
+export function fetchHistory(currentUser: string, page: number, size: number, dateNum: number) {
+  const url = _url("/searchHistory/getMySearchHistory");
+  currentUser =  encodeURIComponent(currentUser);
+  const headers = { currentUser };
+  const params = { page, size, dateNum };
+  return httpClient.get<Response<Page<SearchHistoryRecord>>>(url, { params, headers });
+}
+
+export function delHistory(currentUser: string, id: number) {
+  const url = _url("/searchHistory/delById/" + id);
+  const headers = { currentUser };
+  return httpClient.delete<Response<Object>>(url, { headers });
+}
+
+export function delAllHistory(currentUser: string) {
+  const url = _url("/searchHistory/delAllByUserId");
+  const headers = { currentUser };
+  return httpClient.delete<Response<Object>>(url, { headers });
+}
+
+export default {
+  login,
+  checkLogin,
+  fetchHistory,
+  delHistory,
+  delAllHistory,
+}

+ 42 - 0
src/stores/auth.store.ts

@@ -0,0 +1,42 @@
+import { ref, type Ref } from "vue";
+import { defineStore } from "pinia";
+import type { CurrentUser } from "@/types/user.types";
+import userService from "@/services/user.service";
+
+const tokenKey = "token";
+
+export const useAuthStore = defineStore('auth', () => {
+  const token = ref(localStorage.getItem(tokenKey));
+  const currentUser: Ref<CurrentUser | null> = ref(null);
+
+  function setToken(_token: string) {
+    token.value = _token;
+    localStorage.setItem(tokenKey, _token);
+  }
+
+  function setCurrentUser(user: CurrentUser) {
+    currentUser.value = user;
+  }
+
+  function isLoginIn() {
+    return token.value && token.value.trim() != "";
+  }
+
+  function checkLogin() {
+    return userService.checkLogin();
+  }
+
+  function logout() {
+    setToken("");
+  }
+
+  return {
+    token,
+    currentUser,
+    setToken,
+    setCurrentUser,
+    isLoginIn,
+    logout,
+    checkLogin,
+  }
+});

+ 12 - 0
src/stores/counter.ts

@@ -0,0 +1,12 @@
+import { ref, computed } from "vue";
+import { defineStore } from "pinia";
+
+export const useCounterStore = defineStore("counter", () => {
+  const count = ref(0);
+  const doubleCount = computed(() => count.value * 2);
+  function increment() {
+    count.value++;
+  }
+
+  return { count, doubleCount, increment };
+});

+ 29 - 0
src/stores/doc-detail.ts

@@ -0,0 +1,29 @@
+import type { PaperDetail } from "@/types/doc.types";
+import { defineStore } from "pinia";
+import { ref, type Ref } from "vue";
+import * as paperDetailService from "@/services/paperDetail.service"
+import { useAuthStore } from "./auth.store";
+
+export const useDocDetailStore = defineStore("docDetail", () => {
+  const detail: Ref<PaperDetail | null> = ref(null);
+
+  const pdfUrl: Ref<string | null> = ref(null);
+
+  const authStore = useAuthStore();
+
+  function fetch(id: number) {
+    return paperDetailService.get(id).then((resp) => {
+      detail.value = resp;
+      const token = authStore.token;
+      if (token) {
+        pdfUrl.value = paperDetailService.pdfUrl(resp.filePath, token)
+      }
+    })
+  }
+
+  return {
+    detail,
+    pdfUrl,
+    fetch,
+  }
+});

+ 15 - 0
src/stores/history.store.ts

@@ -0,0 +1,15 @@
+import { ref } from "vue";
+import { defineStore } from "pinia";
+
+export const useHistoryStore = defineStore('history', () => {
+  const searchHistory = ref<any>([]);
+
+  function setSearchHistory(histories: any[]) {
+    searchHistory.value = histories;
+  }
+
+  return {
+    searchHistory,
+    setSearchHistory
+  }
+});

+ 49 - 0
src/stores/knowledge-graph.ts

@@ -0,0 +1,49 @@
+import { ref, type Ref } from "vue";
+import { defineStore } from "pinia";
+import * as kgService from "@/services/knowledgeGraph.service";
+import * as searchService from "@/services/search.service";
+import type { Property } from "@/types/knowledgeGraph.types";
+import type { SearchResult } from "@/types/search.types";
+import { message } from "ant-design-vue";
+
+export const useKnowledgeGraphStore = defineStore('knowledgeGraph', () => {
+  const property: Ref<Property> = ref({} as Property);
+
+  const searchResult: Ref<SearchResult | null> = ref(null);
+
+  function fetchGraph(query?: string) {
+    // TODO
+    message.info('根据查询条件' + query + '查询图数据');
+    return new Promise((resolve) => {
+      resolve('TOTO');
+    });
+  }
+
+  /**
+   * 根据学科ID查询学科属性
+   * @param id 学科ID
+   */
+  function fetchProperty(id: number) {
+    kgService.fetchProperty(id).then((resp) => {
+      property.value = resp.data.data;
+    })
+  }
+
+  /**
+   * 根据学科名称搜索文献
+   * @param name 学科名称
+   */
+  function search(name: string) {
+    searchService.search({query: `TP=("${name}")`}).then((resp) => {
+      searchResult.value = resp;
+    });
+  }
+
+  return {
+    property,
+    searchResult,
+    fetchGraph,
+    fetchProperty,
+    search,
+  }
+});

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

@@ -0,0 +1,59 @@
+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);
+  const unitAggResult: Ref<AggItem[] | null> = ref(null);
+
+  async function search(request: SearchRequest) {
+    return searchService.search(request).then((resp) => {
+      result.value = resp;
+    })
+  }
+
+  async function aggYear(request: SearchRequest) {
+    try {
+      const params = { ...request, aggs: { year: { sort: "key/desc" } }, size: 0 }
+      const resp = await searchService.search(params);
+      yearAggResult.value = resp.aggs.year || [];
+    } catch (err) {
+      console.log(err);
+    }
+  }
+
+  async function aggAuthor(request: SearchRequest) {
+    try {
+      const params = { ...request, aggs: { AU: { sort: "count/desc" } }, size: 0 }
+      const resp = await searchService.search(params);
+      authorAggResult.value = resp.aggs.AU || [];
+    } catch (err) {
+      console.log(err);
+    }
+  }
+
+  async function aggUnit(request: SearchRequest) {
+    try {
+      const params = { ...request, aggs: { UN: { sort: "count/desc" } }, size: 0 }
+      const resp = await searchService.search(params);
+      unitAggResult.value = resp.aggs.UN || [];
+    } catch (err) {
+      console.error(err)
+    }
+  }
+
+  return {
+    result,
+    yearAggResult,
+    authorAggResult,
+    unitAggResult,
+    search,
+    aggYear,
+    aggAuthor,
+    aggUnit,
+  }
+});

+ 15 - 0
src/stores/side-bar.ts

@@ -0,0 +1,15 @@
+import { defineStore } from "pinia";
+import { ref } from "vue";
+
+export const useSideBarStore = defineStore("sideBar", () => {
+  const collapsed = ref(false);
+
+  function setCollapse(collapse: boolean) {
+    collapsed.value = collapse
+  }
+
+  return {
+    collapsed,
+    setCollapse,
+  }
+});

+ 0 - 0
src/types/browsingHistory.types.ts


Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików