├── .editorconfig ├── .env.development ├── .env.production ├── .gitattributes ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc.mjs ├── .stylelintignore ├── .stylelintrc.mjs ├── .vscode └── extensions.json ├── README.md ├── auto-imports.d.ts ├── components.d.ts ├── env.d.ts ├── eslint.config.ts ├── index.html ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── images │ ├── chat_preview.png │ ├── fastapi.png │ ├── markdown_theme.png │ ├── pre_code.png │ ├── preview.mp4 │ ├── preview_1.gif │ ├── preview_2.gif │ ├── preview_3.gif │ └── preview_chat_history.mp4 ├── src ├── App.vue ├── api │ ├── chat │ │ ├── index.ts │ │ └── types.ts │ ├── chatSession │ │ ├── index.ts │ │ └── types.ts │ └── documents │ │ ├── index.ts │ │ └── types.ts ├── assets │ └── logo.svg ├── components │ ├── Dialog │ │ ├── BaseDialog │ │ │ └── index.vue │ │ └── index.ts │ ├── Icon │ │ ├── AssistantIcon.vue │ │ ├── ErrorIcon.vue │ │ ├── HumanIcon.vue │ │ ├── SendIcon.vue │ │ └── StopIcon.vue │ └── Loading │ │ ├── ChatLoading │ │ └── index.vue │ │ └── index.ts ├── enums │ └── httpEnum.ts ├── http │ ├── axios │ │ └── config.ts │ ├── fetch │ │ ├── InterceptorManager.ts │ │ └── config.ts │ ├── helper │ │ ├── abortController.ts │ │ ├── checkStatus.ts │ │ └── httpError.ts │ ├── index.ts │ └── types │ │ └── index.ts ├── layout │ ├── LayoutClassic.vue │ └── components │ │ └── base │ │ ├── LayoutAside.vue │ │ ├── LayoutFooter.vue │ │ └── LayoutHeader.vue ├── main.ts ├── router │ └── index.ts ├── stores │ └── app.ts ├── styles │ ├── element │ │ ├── element.scss │ │ └── index.scss │ ├── index.scss │ ├── markdown │ │ ├── mdmdt-light.scss │ │ └── plugins.scss │ ├── reset.scss │ └── var.scss ├── utils │ ├── common.ts │ ├── markdownit │ │ ├── codeCopyPlugins.ts │ │ ├── hljsConfig.ts │ │ └── index.ts │ └── proxy.ts └── views │ ├── chat │ ├── components │ │ ├── AssistantChat.vue │ │ ├── ChatHistory.vue │ │ └── HumanChat.vue │ └── index.vue │ ├── documents │ ├── index.vue │ └── writeForm.vue │ └── test │ └── index.vue ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.vitest.json ├── vite.config.ts └── vitest.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}] 2 | charset = utf-8 3 | indent_size = 2 4 | indent_style = space 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | end_of_line = lf 9 | max_line_length = 100 10 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 本地环境 2 | NODE_ENV = development 3 | 4 | # 标题 5 | VITE_APP_TITLE = 聊天平台 6 | 7 | # vite本地端口 8 | VITE_PORT = 8081 9 | 10 | # 后端axios接口地址 11 | VITE_API_BASEURL = /api 12 | 13 | # 开发环境跨域代理,支持配置多个 14 | VITE_PROXY = [["/api","http://localhost:8082"]] -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 生产环境 2 | NODE_ENV = production 3 | 4 | # 标题 5 | VITE_APP_TITLE = 聊天平台 6 | 7 | # 本地端口 8 | VITE_PORT = 8081 9 | 10 | # VITE_API_BASEURL = /api 11 | # 线上环境接口地址 12 | VITE_API_BASEURL = "https://127.0.0.1:8082" 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | **/.eslintcache 17 | 18 | /cypress/videos/ 19 | /cypress/screenshots/ 20 | 21 | # Editor directories and files 22 | .vscode/* 23 | !.vscode/extensions.json 24 | .idea 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | 31 | *.tsbuildinfo 32 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | # pre-commit 2 | 3 | #!/usr/bin/env sh 4 | . "$(dirname -- "$0")/_/husky.sh" 5 | 6 | # Run the pre-commit hook 7 | npx --no-install -- lint-staged 8 | 9 | 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | build/ 4 | *.min.js 5 | *.bundle.js 6 | *.log 7 | src/styles/markdown/ 8 | **/*.md -------------------------------------------------------------------------------- /.prettierrc.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://prettier.io/docs/en/configuration.html 3 | * @type {import("prettier").Config} 4 | */ 5 | export default { 6 | // 不尾随分号 7 | semi: false, 8 | // 使用单引号 9 | singleQuote: true, 10 | // 多行逗号分割的语法中,最后一行不加逗号 11 | trailingComma: 'none', 12 | // 行尾风格,设置为auto 13 | endOfLine: 'auto', 14 | // 指定最大换行长度 15 | printWidth: 140, 16 | // 使用 2 个空格缩进 17 | tabWidth: 2, 18 | // 不使用缩进符,而使用空格 19 | useTabs: false, 20 | // 单个参数的箭头函数不加括号 x => x 21 | arrowParens: 'avoid', 22 | // 对象大括号内两边是否加空格 { a:0 } 23 | bracketSpacing: true, 24 | // 将 > 多行元素放在最后一行的末尾,而不是单独放在下一行 (true:放末尾,false:单独一行) 25 | bracketSameLine: false 26 | } 27 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src/uni_modules/ 3 | src/static/ 4 | dist/ 5 | 6 | src/styles/markdown/ 7 | **/*.svg -------------------------------------------------------------------------------- /.stylelintrc.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('stylelint').Config} */ 2 | export default { 3 | extends: [ 4 | 'stylelint-config-standard', 5 | 'stylelint-config-standard-scss', 6 | 'stylelint-config-standard-vue', 7 | 'stylelint-config-recess-order' 8 | ], 9 | rules: { 10 | 'no-empty-source': null 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "vitest.explorer", 5 | "dbaeumer.vscode-eslint", 6 | "EditorConfig.EditorConfig", 7 | "esbenp.prettier-vscode" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📃 **关于vue-doc-qa-chat** 2 | 3 | 基于 [🦜️🔗 LangChain](https://github.com/hwchase17/langchain) 与 DeepSeek R1 语言模型的本地知识库问答。 4 | 5 | 本项目是本地知识库问答应用的 web 前端模块,使用 vue3 + typescript + vite + elementplus 框架。 6 | 7 | 目前实现前端chat聊天网页,上传文档主要功能。后续会系统学习 langchain ,逐步添加更多的功能。 8 | 9 | > 为了直观的体现API交互,可搭配下面的服务端结合使用。 10 | > 11 | > 服务端 **py-doc-qa-deepseek-server** 链接: 12 | 13 | ## 快速上手 14 | 15 | * **确保已安装 18.0 或更高版本的 Node.js** 16 | 17 | ```shell 18 | # 拉取项目 19 | $ git clone https://github.com/YuiGod/vue-doc-qa-chat.git 20 | 21 | # 进去项目 22 | $ cd vue-doc-qa-chat 23 | 24 | $ npm install 25 | # 启动服务 26 | $ npm run dev 27 | ``` 28 | 29 | ## 项目预览 30 | > 因为git压缩了帧率,看起来不够流畅。可点击这里下载预览视频观看:[预览视频](https://raw.githubusercontent.com/YuiGod/vue-doc-qa-chat/refs/heads/main/public/images/preview.mp4) 31 | 32 | 33 | ![项目预览](./public/images/chat_preview.png) 34 | ![preview_1](./public/images/preview_1.gif) 35 | ![preview_2](./public/images/preview_2.gif) 36 | ![preview_3](./public/images/preview_3.gif) 37 | 38 | ## 系列文章 39 | 40 | 1. [《从零开始DeepSeek R1搭建本地知识库问答系统》一:利用WSL2搭建Linux子系统并设置运行环境前言](https://juejin.cn/post/7470334881735196684) 41 | 2. [《从零开始DeepSeek R1搭建本地知识库问答系统》二:Ollama 部署 DeepSeek R1 蒸馏模型及Api测试](https://juejin.cn/post/7470345587309854774) 42 | 3. [《从零开始DeepSeek R1搭建本地知识库问答系统》三:基于LangChain构建本地知识库问答RAG应用](https://juejin.cn/post/7470807715898212406) 43 | 4. [《从零开始DeepSeek R1搭建本地知识库问答系统》四:FastApi 框架搭建本地知识库问答Web Server端](https://juejin.cn/post/7478991058870747170) 44 | 5. [《从零开始DeepSeek R1搭建本地知识库问答系统》五:实现问答系统前端 UI 框架,基于 vue3 + typescript + ElementPlus](https://juejin.cn/post/7480009518175567907) 45 | 46 | 47 | ## 项目功能 48 | 49 | 1. 文档管理,文档上传,预览和向量化。 50 | 51 | 2. 聊天采用流式响应,并使用 markdown-it 渲染文本。样式使用 [Mdmdt 主题](https://github.com/cayxc/Mdmdt),自定义code 代码块样式。 52 | 53 | 3. 聊天对话历史管理,可重命名对话标题,删除对话。 54 | 55 | ## 造轮子 56 | 57 | 5. 封装 markdown-it,高亮代码,代码块样式套一个漂亮的壳子。 58 | 6. 封装整合 Axios 与 Fetch,添加请求拦截器和响应拦截器,统一取消请求功能,两者可以搭配使用。 59 | 60 | > 我发现网站关于封装 Fetch 的文章比较少,而且封装的不够完美,所以造了个封装整合 Axios 与 Fetch 的轮子。 61 | > 62 | > 如果彦祖亦菲们有需求,该http模块可直接拿去用,作为项目的请求模块。记得请求拦截器带上 token 。 63 | 64 | ## src 目录树 65 | 66 | ``` 67 | src 68 | ├─api # api接口 69 | │ ├─chat # 聊天接口 70 | │ ├─chatSession # 聊天历史管理接口 71 | │ └─documents # 文档管理接口,包含向量化api 72 | ├─assets # 静态资源文件 73 | ├─components # 公共组件 74 | │ ├─Dialog # 表单弹窗 75 | │ │ └─BaseDialog 76 | │ ├─Icon # 图标扩展 77 | │ └─Loading # 加载样式 78 | │ └─ChatLoading 79 | ├─enums # 常用枚举 80 | ├─http # http 封装 81 | │ ├─axios # axios 封装,拦截器处理 82 | │ ├─fetch # fetch 封装,拦截器处理 83 | │ ├─helper # 内有取消请求封装,状态检查,错误处理 84 | │ └─types # http ts 声明 85 | ├─layout # 框架布局模块 86 | │ └─components 87 | │ └─base 88 | ├─router # 路由管理 89 | ├─stores # pinia store 90 | ├─styles # 全局样式 91 | │ ├─element # elementplus 样式 92 | │ └─markdown # markdown 样式 93 | ├─utils # 公共 utils 94 | │ └─markdownit # markdown-it 封装,内有高亮代码,代码块样式美化 95 | └─views # 项目所有页面 96 | ├─chat # 对话聊天 97 | │ └─components # 对话聊天子组件 98 | ├─documents # 文档管理 99 | └─test # markdown 样式预览 100 | ``` 101 | 102 | ## 关于 markdown 样式预览 103 | 104 | 在 `src/styles/markdown` 中可以找到样式,`src/utils/markdownit` 中对代码块高亮显示。 105 | 106 | 主题预览: 107 | 108 | ![Mdmdt主题预览转存失败,建议直接上传图片文件](./public/images/markdown_theme.png) 109 | 110 | 代码块预览: 111 | 112 | ![代码块预览转存失败,建议直接上传图片文件](./public/images/pre_code.png) 113 | 114 | ## Api 接口 115 | 116 | ![fastApi转存失败,建议直接上传图片文件](./public/images/fastapi.png) 117 | 118 | ### 1. 聊天 119 | 120 | #### `/chat` 121 | 122 | - 请求类型:***POST*** 123 | - Request data 请求体: 124 | 125 | ``` 126 | { 127 | "model": "deepseek-r1:7b", // 模型名称 128 | "stream": true, // 开启流式响应 129 | "messages": { 130 | "role": "user", // 角色 131 | "content": "FFF团会长是谁?" // 内容 132 | } 133 | } 134 | ``` 135 | 136 | - Responses 响应体:返回 JSON 对象流。`content-type: application/x-ndjson` 137 | 138 | ``` 139 | // json 流未完成时 140 | { 141 | "model": "deepseek-r1:7b", // 模型名称 142 | "created_at": 1741384731918, // 时间戳 143 | "message": { 144 | "role": "assistant", // 角色 145 | "content": "首先" // 内容 146 | }, 147 | "done": false // 流式未完成标记 148 | } 149 | {……} 150 | ... 151 | 152 | // json 流完成时 153 | { 154 | "model": "deepseek-r1:7b", // 模型名称 155 | "created_at": 1741384734349, // 时间戳 156 | "message": { 157 | "role": "assistant", // 角色 158 | "content": "" // 内容,为空 159 | }, 160 | "done": true, // 流式是已完成标记 161 | "done_reason": "stop" // 完成信息 162 | } 163 | ``` 164 | --- 165 | #### `/chat/history` 166 | 167 | - 请求类型:***GET*** 168 | - Request params 参数: 169 | 170 | ``` 171 | { 172 | "id": "cae1e775-31b2-44a8-b5d3-873bbabfff4c" // 必填,会话 id 173 | "title": "标题" // 可选,会话标题 174 | } 175 | ``` 176 | - Responses 响应体:`application/json` 177 | 178 | ``` 179 | { 180 | "code": 200, 181 | "message": "响应成功!", 182 | "data": [ 183 | { 184 | "id": "43339654-d5ce-4ace-ab98-399741558b32", 185 | "role": "user", 186 | "content": "FFF团会长是谁?", 187 | "think": null, 188 | "chat_session_id": "cae1e775-31b2-44a8-b5d3-873bbabfff4c", // 会话id 189 | "date": "2025-03-08 00:44:35" 190 | }, 191 | { 192 | "id": "dc05a6ce-b093-47de-869a-62f9e2efcb0a", 193 | "role": "assistant", 194 | "content": "\n\n根据文档内容,FFF团的会长是大靓仔。", 195 | "think": "\n嗯,用户问的是“FFF团会长是谁………………", 196 | "chat_session_id": "cae1e775-31b2-44a8-b5d3-873bbabfff4c", // 会话id 197 | "date": "2025-03-08 00:44:38" 198 | } 199 | ] 200 | } 201 | ``` 202 | --- 203 | 204 | ### 2. 会话管理 205 | 206 | #### `/session/list` 207 | 208 | - 请求类型:***GET*** 209 | - Request params 参数:无 210 | - Responses 响应体:`application/json` 211 | ``` 212 | { 213 | "code": 200, 214 | "message": "响应成功!", 215 | "data": [ 216 | { 217 | "id": "cae1e775-31b2-44a8-b5d3-873bbabfff4c", 218 | "title": "FFF团会长是谁?", 219 | "date": "2025-03-08 00:44:35" 220 | }, 221 | { 222 | "id": "3eed0670-2c68-4b09-942a-e1b5b9a02bf8", 223 | "title": "小芳最喜欢的电影是什么?", 224 | "date": "2025-03-07 00:40:20" 225 | } 226 | ] 227 | } 228 | ``` 229 | --- 230 | 231 | #### `/session/add` 232 | 233 | - 请求类型:***POST*** 234 | - Request data 请求体: 235 | 236 | ``` 237 | { 238 | "title": "标题" // 必填,会话标题 239 | } 240 | ``` 241 | 242 | - Responses 响应体:`application/json` 243 | 244 | ``` 245 | { 246 | "code": 200, 247 | "message": "响应成功!", 248 | "data": { 249 | "id": "cae1e775-31b2-44a8-b5d3-873bbabfff4c", 250 | "title": "FFF团会长是谁?", 251 | "date": "2025-03-08 00:44:35" 252 | } 253 | } 254 | ``` 255 | 256 | --- 257 | #### `/session/update` 258 | 259 | - 请求类型:***PUT*** 260 | - Request data 请求体: 261 | 262 | ``` 263 | { 264 | "id": "cae1e775-31b2-44a8-b5d3-873bbabfff4c", // 必填,会话 id 265 | "title": "标题" // 必填,会话标题 266 | } 267 | ``` 268 | 269 | - Responses 响应体:`application/json` 270 | 271 | ``` 272 | { 273 | "code": 200, 274 | "message": "响应成功!", 275 | "data": { 276 | "id": "cae1e775-31b2-44a8-b5d3-873bbabfff4c", 277 | "title": "FFF团会长是谁?", 278 | "date": "2025-03-08 00:44:35" 279 | } 280 | } 281 | ``` 282 | --- 283 | 284 | #### `/session/delete` 285 | 286 | - 请求类型:***DELETE*** 287 | - Request data 请求体: 288 | 289 | ``` 290 | { 291 | "id": "cae1e775-31b2-44a8-b5d3-873bbabfff4c" // 必填,会话 id 292 | } 293 | ``` 294 | 295 | - Responses 响应体:`application/json` 296 | 297 | ``` 298 | { 299 | "code": 200, 300 | "message": "响应成功!", 301 | "data": null 302 | } 303 | ``` 304 | -- 305 | ### 2. 文档管理 306 | 307 | #### `/documents/page` 308 | 309 | - 请求类型:***GET*** 310 | - Request params 参数: 311 | 312 | ``` 313 | { 314 | "page_num": 1 315 | "page_size": 10, 316 | // 以下可选 317 | "id": "", // 文档 id 318 | "name": "", // 文档名称 319 | "file_name": "", // 文档服务器名称,uuid 一般用不到 320 | "file_path": "", // 文档服务器保存路径 321 | "suffix": "", // 文档后缀类型 322 | "vector": "", // 是否已经向量化 323 | "date": "", // 创建时间 324 | } 325 | ``` 326 | 327 | - Responses 响应体:`application/json` 328 | 329 | ``` 330 | { 331 | "code": 200, 332 | "message": "响应成功!", 333 | "data": { 334 | "total": 1, 335 | "page_num": 1, 336 | "page_size": 10, 337 | "list": [ 338 | { 339 | "id": "6b364b00-b7d7-408b-95f3-646ca226133f", 340 | "name": "FFF团", 341 | "file_name": "b0f5c29a-7caa-4fcf-bd10-b1bd7ec6687d.txt", 342 | "file_path": "/fileStorage/b0f5c29a-7caa-4fcf-bd10-b1bd7ec6687d.txt", 343 | "suffix": ".txt", 344 | "vector": "yes", // yes/no 345 | "date": "2025-03-08 00:44:26" 346 | } 347 | ] 348 | } 349 | } 350 | ``` 351 | --- 352 | 353 | #### `/documents/add` 354 | 355 | - 请求类型:***POST*** 356 | - Request FormData 请求体:表单数据 357 | 358 | ``` 359 | { 360 | "name": "FFF团", // 必填,文档名称 361 | "flie": "blob" // 必带,二进制文件 362 | } 363 | ``` 364 | 365 | - Responses 响应体:`application/json` 366 | 367 | ``` 368 | { 369 | "code": 200, 370 | "message": "添加成功!", 371 | "data": null 372 | } 373 | ``` 374 | --- 375 | 376 | #### `/documents/update` 377 | 378 | - 请求类型:***PUT*** 379 | - Request FormData 请求体:表单数据 380 | 381 | ``` 382 | { 383 | "name": "FFF团", 384 | "flie": "blob" // 二进制文件 385 | } 386 | ``` 387 | - Responses 响应体:`application/json` 388 | 389 | ``` 390 | { 391 | "code": 200, 392 | "message": "更新成功!", 393 | "data": null 394 | } 395 | ``` 396 | --- 397 | 398 | #### `/documents/delete` 399 | 400 | - 请求类型:***DELETE*** 401 | - Request data 请求体: 402 | 403 | ``` 404 | { 405 | "id": "6b364b00-b7d7-408b-95f3-646ca226133f" // 文档 id 406 | } 407 | ``` 408 | - Responses 响应体:`application/json` 409 | 410 | ``` 411 | { 412 | "code": 200, 413 | "message": "删除成功!", 414 | "data": null 415 | } 416 | ``` 417 | --- 418 | 419 | #### `/documents/read` 420 | 421 | - 请求类型:***GET*** 422 | - Request data 请求体: 423 | 424 | ``` 425 | { 426 | "id": "6b364b00-b7d7-408b-95f3-646ca226133f" // 文档 id 427 | } 428 | ``` 429 | Responses 响应体:根据不同文件后缀,返回不同的请求头 430 | 431 | ``` 432 | Blob 433 | ``` 434 | --- 435 | 436 | ### 3. 向量化 437 | #### `/documents/vector-all` 438 | 439 | - 请求类型:***GET*** 440 | - Request data 请求体:无 441 | - Responses 响应体:`application/json` 442 | 443 | ``` 444 | { 445 | "code": 200, 446 | "message": "删除成功!", 447 | "data": null 448 | } 449 | ``` 450 | 451 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | // biome-ignore lint: disable 7 | export {} 8 | declare global { 9 | const ElMessage: typeof import('element-plus/es')['ElMessage'] 10 | const ElMessageBox: typeof import('element-plus/es')['ElMessageBox'] 11 | } 12 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {} 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | AssistantIcon: typeof import('./src/components/Icon/AssistantIcon.vue')['default'] 11 | BaseDialog: typeof import('./src/components/Dialog/BaseDialog/index.vue')['default'] 12 | ChatLoading: typeof import('./src/components/Loading/ChatLoading/index.vue')['default'] 13 | ElAside: typeof import('element-plus/es')['ElAside'] 14 | ElBacktop: typeof import('element-plus/es')['ElBacktop'] 15 | ElButton: typeof import('element-plus/es')['ElButton'] 16 | ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup'] 17 | ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] 18 | ElContainer: typeof import('element-plus/es')['ElContainer'] 19 | ElDialog: typeof import('element-plus/es')['ElDialog'] 20 | ElDropdown: typeof import('element-plus/es')['ElDropdown'] 21 | ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] 22 | ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] 23 | ElEmpty: typeof import('element-plus/es')['ElEmpty'] 24 | ElForm: typeof import('element-plus/es')['ElForm'] 25 | ElFormItem: typeof import('element-plus/es')['ElFormItem'] 26 | ElHeader: typeof import('element-plus/es')['ElHeader'] 27 | ElIcon: typeof import('element-plus/es')['ElIcon'] 28 | ElImage: typeof import('element-plus/es')['ElImage'] 29 | ElInput: typeof import('element-plus/es')['ElInput'] 30 | ElLink: typeof import('element-plus/es')['ElLink'] 31 | ElMain: typeof import('element-plus/es')['ElMain'] 32 | ElMenu: typeof import('element-plus/es')['ElMenu'] 33 | ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] 34 | ElPagination: typeof import('element-plus/es')['ElPagination'] 35 | ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm'] 36 | ElPopover: typeof import('element-plus/es')['ElPopover'] 37 | ElProgress: typeof import('element-plus/es')['ElProgress'] 38 | ElRow: typeof import('element-plus/es')['ElRow'] 39 | ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] 40 | ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] 41 | ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] 42 | ElTable: typeof import('element-plus/es')['ElTable'] 43 | ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] 44 | ElTag: typeof import('element-plus/es')['ElTag'] 45 | ElText: typeof import('element-plus/es')['ElText'] 46 | ElTooltip: typeof import('element-plus/es')['ElTooltip'] 47 | ElUpload: typeof import('element-plus/es')['ElUpload'] 48 | ErrorIcon: typeof import('./src/components/Icon/ErrorIcon.vue')['default'] 49 | HumanIcon: typeof import('./src/components/Icon/HumanIcon.vue')['default'] 50 | RouterLink: typeof import('vue-router')['RouterLink'] 51 | RouterView: typeof import('vue-router')['RouterView'] 52 | SendIcon: typeof import('./src/components/Icon/SendIcon.vue')['default'] 53 | StopIcon: typeof import('./src/components/Icon/StopIcon.vue')['default'] 54 | } 55 | export interface ComponentCustomProperties { 56 | vLoading: typeof import('element-plus/es')['ElLoadingDirective'] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly NODE_ENV: 'development' | 'production' | 'test' 5 | /** vite_title 标题 */ 6 | readonly VITE_TITLE: string 7 | /** 后端axios接口地址 */ 8 | readonly VITE_API_BASEURL: string 9 | /** vite本地端口 */ 10 | readonly VITE_PORT: number 11 | /** 开发环境跨域代理,支持配置多个 */ 12 | readonly VITE_PROXY: string[] 13 | } 14 | 15 | interface ImportMeta { 16 | readonly env: ImportMetaEnv 17 | } 18 | -------------------------------------------------------------------------------- /eslint.config.ts: -------------------------------------------------------------------------------- 1 | import pluginVue from 'eslint-plugin-vue' 2 | import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript' 3 | import pluginVitest from '@vitest/eslint-plugin' 4 | import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' 5 | 6 | // 要允许在 '.vue' 文件中使用除 'ts' 以外的更多语言,请取消以下行的注释: 7 | // import { configureVueProject } from '@vue/eslint-config-typescript' 8 | // configureVueProject({ scriptLangs: ['ts', 'tsx'] }) 9 | // 更多信息请访问 https://github.com/vuejs/eslint-config-typescript/#advanced-setup 10 | 11 | export default defineConfigWithVueTs( 12 | { 13 | name: 'app/files-to-lint', 14 | files: ['**/*.{ts,mts,tsx,vue}'] 15 | }, 16 | 17 | { 18 | name: 'app/files-to-ignore', 19 | ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'] 20 | }, 21 | 22 | pluginVue.configs['flat/essential'], 23 | vueTsConfigs.recommended, 24 | 25 | { 26 | ...pluginVitest.configs.recommended, 27 | files: ['src/**/__tests__/*'] 28 | }, 29 | skipFormatting, 30 | { 31 | rules: { 32 | 'vue/multi-word-component-names': 'off', 33 | '@typescript-eslint/no-explicit-any': 'off', 34 | 'no-unused-expressions': 'off', 35 | '@typescript-eslint/no-unused-expressions': 'off' 36 | } 37 | } 38 | ) 39 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-doc-qa-chat", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "run-p type-check \"build-only {@}\" --", 9 | "preview": "vite preview", 10 | "test:unit": "vitest", 11 | "build-only": "vite build", 12 | "type-check": "vue-tsc --build", 13 | "lint": "eslint --fix --ext .js,.ts,.vue ./src", 14 | "lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,less,scss,vue,html,md}\"", 15 | "lint:stylelint": "stylelint --cache --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/", 16 | "format": "prettier --write src/", 17 | "prepare": "husky" 18 | }, 19 | "lint-staged": { 20 | "**/*.{vue,js,ts,jsx,tsx,mjs,cjs,html,json,md}": [ 21 | "prettier --write" 22 | ], 23 | "**/*.{vue,js,ts,jsx,tsx,mjs,cjs}": [ 24 | "eslint --cache --fix" 25 | ], 26 | "**/*.{vue,css,scss,html}": [ 27 | "stylelint --fix" 28 | ] 29 | }, 30 | "dependencies": { 31 | "@element-plus/icons-vue": "^2.3.1", 32 | "axios": "^1.7.9", 33 | "dompurify": "^3.2.4", 34 | "element-plus": "^2.9.4", 35 | "highlight.js": "^11.11.1", 36 | "markdown-it": "^14.1.0", 37 | "pinia": "^3.0.1", 38 | "qs": "^6.14.0", 39 | "vue": "^3.5.13", 40 | "vue-router": "^4.5.0" 41 | }, 42 | "devDependencies": { 43 | "@tsconfig/node22": "^22.0.0", 44 | "@types/jsdom": "^21.1.7", 45 | "@types/lodash-es": "^4.17.12", 46 | "@types/markdown-it": "^14.1.2", 47 | "@types/node": "^22.13.4", 48 | "@types/qs": "^6.9.18", 49 | "@vitejs/plugin-vue": "^5.2.1", 50 | "@vitest/eslint-plugin": "1.1.31", 51 | "@vue/eslint-config-prettier": "^10.2.0", 52 | "@vue/eslint-config-typescript": "^14.4.0", 53 | "@vue/test-utils": "^2.4.6", 54 | "@vue/tsconfig": "^0.7.0", 55 | "clipboard": "^2.0.11", 56 | "eslint": "^9.20.1", 57 | "eslint-plugin-vue": "^9.32.0", 58 | "husky": "^9.1.7", 59 | "jiti": "^2.4.2", 60 | "jsdom": "^26.0.0", 61 | "lint-staged": "^15.4.3", 62 | "lodash-es": "^4.17.21", 63 | "npm-run-all2": "^7.0.2", 64 | "prettier": "^3.5.1", 65 | "sass-embedded": "^1.85.0", 66 | "stylelint": "^16.14.1", 67 | "stylelint-config-recess-order": "^6.0.0", 68 | "stylelint-config-standard": "^37.0.0", 69 | "stylelint-config-standard-scss": "^14.0.0", 70 | "stylelint-config-standard-vue": "^1.0.0", 71 | "typescript": "~5.7.3", 72 | "unplugin-auto-import": "^19.1.0", 73 | "unplugin-vue-components": "^28.2.0", 74 | "vite": "^6.1.0", 75 | "vite-plugin-vue-devtools": "^7.7.2", 76 | "vitest": "^3.0.5", 77 | "vue-tsc": "^2.2.2" 78 | } 79 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuiGod/vue-doc-qa-chat/5ba48ad61e62ddf94b13677ac782b72d8c76951a/public/favicon.ico -------------------------------------------------------------------------------- /public/images/chat_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuiGod/vue-doc-qa-chat/5ba48ad61e62ddf94b13677ac782b72d8c76951a/public/images/chat_preview.png -------------------------------------------------------------------------------- /public/images/fastapi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuiGod/vue-doc-qa-chat/5ba48ad61e62ddf94b13677ac782b72d8c76951a/public/images/fastapi.png -------------------------------------------------------------------------------- /public/images/markdown_theme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuiGod/vue-doc-qa-chat/5ba48ad61e62ddf94b13677ac782b72d8c76951a/public/images/markdown_theme.png -------------------------------------------------------------------------------- /public/images/pre_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuiGod/vue-doc-qa-chat/5ba48ad61e62ddf94b13677ac782b72d8c76951a/public/images/pre_code.png -------------------------------------------------------------------------------- /public/images/preview.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuiGod/vue-doc-qa-chat/5ba48ad61e62ddf94b13677ac782b72d8c76951a/public/images/preview.mp4 -------------------------------------------------------------------------------- /public/images/preview_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuiGod/vue-doc-qa-chat/5ba48ad61e62ddf94b13677ac782b72d8c76951a/public/images/preview_1.gif -------------------------------------------------------------------------------- /public/images/preview_2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuiGod/vue-doc-qa-chat/5ba48ad61e62ddf94b13677ac782b72d8c76951a/public/images/preview_2.gif -------------------------------------------------------------------------------- /public/images/preview_3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuiGod/vue-doc-qa-chat/5ba48ad61e62ddf94b13677ac782b72d8c76951a/public/images/preview_3.gif -------------------------------------------------------------------------------- /public/images/preview_chat_history.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuiGod/vue-doc-qa-chat/5ba48ad61e62ddf94b13677ac782b72d8c76951a/public/images/preview_chat_history.mp4 -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/api/chat/index.ts: -------------------------------------------------------------------------------- 1 | import http from '@/http' 2 | import type { ChatHistoryResponseType, ChatRequestType, ChatResponseType } from './types' 3 | import type { OnReady, OnStream } from '@/http/types' 4 | 5 | /** 6 | * Fetch 请求,chat对话内容 7 | * @param data data 8 | * @param onReady 回调函数,请求响应成功,准备流式输出 9 | * @param onStream 回调函数,开启 stream 流式响应并回调函数 10 | * @returns `Promise` 11 | */ 12 | const chatApi = (data: ChatRequestType, onReady: OnReady, onStream: OnStream): Promise => { 13 | return http.fetchPostChat('/chat', data, { onReady, onStream }) 14 | } 15 | 16 | /** 17 | * 根据会话 id 获取聊天历史记录 18 | * @param id 会话id 19 | * @returns 历史记录列表 20 | */ 21 | const chatHistoryApi = (id: string) => { 22 | return http.get('/chat/history', { id: id }) 23 | } 24 | 25 | /** 26 | * 取消请求 27 | * @returns 28 | */ 29 | function chatCancelRequest() { 30 | return http.cancelRequest('/chat') 31 | } 32 | 33 | export { chatApi, chatCancelRequest, chatHistoryApi } 34 | -------------------------------------------------------------------------------- /src/api/chat/types.ts: -------------------------------------------------------------------------------- 1 | export interface ChatRequestType { 2 | model?: string 3 | stream?: boolean 4 | messages: { 5 | role: string 6 | content: string 7 | } 8 | } 9 | 10 | export interface ChatResponseType { 11 | code: number 12 | message: string 13 | } 14 | 15 | export interface ChatHistoryResponseType { 16 | id: string 17 | role: string 18 | content: string 19 | think: string 20 | chat_session_id: string 21 | date: string 22 | } 23 | -------------------------------------------------------------------------------- /src/api/chatSession/index.ts: -------------------------------------------------------------------------------- 1 | import http from '@/http' 2 | import type { ChatSessionRequestType, ChatSessionResponseType } from './types' 3 | import type { CustomAxiosConfig } from '@/http/types' 4 | 5 | /** 6 | * 获取会话列表 7 | * @returns list 8 | */ 9 | const chatSessionsApi = () => { 10 | return http.get('/session/list') 11 | } 12 | 13 | /** 14 | * 添加会话记录,post 15 | * @param data data 参数 16 | * @returns 记录信息 17 | */ 18 | const chatSessionsAddApi = (data: ChatSessionRequestType) => { 19 | return http.post('/session/add', data) 20 | } 21 | 22 | /** 23 | * 修改会话记录,put 24 | * @param data data 参数 25 | * @param config axios config配置 26 | * @returns 记录信息 27 | */ 28 | const chatSessionsEditApi = (data: ChatSessionRequestType, config?: CustomAxiosConfig) => { 29 | return http.put('/session/update', data, config) 30 | } 31 | 32 | /** 33 | * 删除会话记录,delete 34 | * @param id 会话id 35 | */ 36 | const chatSessionsDeleteApi = (id: string) => { 37 | return http.delete('/session/delete', { id: id }) 38 | } 39 | 40 | export { chatSessionsApi, chatSessionsAddApi, chatSessionsEditApi, chatSessionsDeleteApi } 41 | -------------------------------------------------------------------------------- /src/api/chatSession/types.ts: -------------------------------------------------------------------------------- 1 | export interface ChatSessionRequestType { 2 | id?: string 3 | title?: string 4 | date?: string 5 | } 6 | 7 | export interface ChatSessionResponseType { 8 | id: string 9 | title: string 10 | date: string 11 | } 12 | -------------------------------------------------------------------------------- /src/api/documents/index.ts: -------------------------------------------------------------------------------- 1 | import type { CustomAxiosConfig } from '@/http/types' 2 | import type { DocParamsType, ResponseDocPageType } from './types' 3 | import http from '@/http' 4 | 5 | const docPageApi = (params: DocParamsType) => { 6 | return http.get('/documents/page', params) 7 | } 8 | 9 | const docAddApi = (data: FormData, config?: CustomAxiosConfig) => { 10 | return http.post('/documents/add', data, config) 11 | } 12 | 13 | const docEditApi = (data: FormData, config?: CustomAxiosConfig) => { 14 | return http.put('/documents/update', data, config) 15 | } 16 | 17 | const docDeleteApi = (id: string) => { 18 | return http.delete('/documents/delete', { id: id }) 19 | } 20 | 21 | const docDownloadApi = (id: string) => { 22 | return http.download('/documents/download', { id: id }) 23 | } 24 | 25 | /** 26 | * 文档全部向量化 Api 27 | * @param config axios config配置 28 | */ 29 | const docVectorAllApi = (config?: CustomAxiosConfig) => { 30 | return http.get('/documents/vector-all', undefined, config) 31 | } 32 | 33 | export { docPageApi, docAddApi, docEditApi, docDeleteApi, docDownloadApi, docVectorAllApi } 34 | -------------------------------------------------------------------------------- /src/api/documents/types.ts: -------------------------------------------------------------------------------- 1 | interface DocParamsType { 2 | page_num: number 3 | page_size: number 4 | id?: string 5 | name?: string 6 | } 7 | 8 | interface ResponseDocPageType { 9 | page_num: number 10 | page_size: number 11 | total: number 12 | list: DocTableType[] 13 | } 14 | 15 | interface DocTableType { 16 | id: string 17 | name: string 18 | file_name: string 19 | file_path: string 20 | suffix: string 21 | vector: string 22 | date: string 23 | } 24 | 25 | export type { DocParamsType, ResponseDocPageType, DocTableType } 26 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/Dialog/BaseDialog/index.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 124 | 125 | 150 | -------------------------------------------------------------------------------- /src/components/Dialog/index.ts: -------------------------------------------------------------------------------- 1 | import BaseDialog from './BaseDialog/index.vue' 2 | 3 | export { BaseDialog } 4 | -------------------------------------------------------------------------------- /src/components/Icon/AssistantIcon.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | -------------------------------------------------------------------------------- /src/components/Icon/ErrorIcon.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 31 | -------------------------------------------------------------------------------- /src/components/Icon/HumanIcon.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 37 | -------------------------------------------------------------------------------- /src/components/Icon/SendIcon.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /src/components/Icon/StopIcon.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 29 | -------------------------------------------------------------------------------- /src/components/Loading/ChatLoading/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 58 | -------------------------------------------------------------------------------- /src/components/Loading/index.ts: -------------------------------------------------------------------------------- 1 | import ChatLoading from './ChatLoading/index.vue' 2 | 3 | export { ChatLoading } 4 | -------------------------------------------------------------------------------- /src/enums/httpEnum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description:请求配置 3 | */ 4 | export enum ResultEnum { 5 | SUCCESS = 200, 6 | ERROR = 500, 7 | OVERDUE = 401, 8 | TIMEOUT = 30000, 9 | TYPE = 'success' 10 | } 11 | 12 | /** 13 | * @description:常用的 contentTyp 类型 14 | */ 15 | export enum ContentTypeEnum { 16 | // json 17 | JSON = 'application/json;charset=UTF-8', 18 | // text 19 | TEXT = 'text/plain;charset=UTF-8', 20 | // form-data 一般配合qs 21 | FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8', 22 | // form-data 上传 23 | FORM_DATA = 'multipart/form-data;charset=UTF-8' 24 | } 25 | -------------------------------------------------------------------------------- /src/http/axios/config.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError, type AxiosResponse } from 'axios' 2 | import type { CustomAxiosConfig, CustomAxiosInterceptorsConfig } from '../types' 3 | import { addPending, removePending } from '../helper/abortController' 4 | import { ResultEnum } from '../../enums/httpEnum' 5 | import { checkStatus } from '../helper/checkStatus' 6 | import router from '@/router' 7 | 8 | /** 默认baseUrl */ 9 | const PATH_URL = import.meta.env.VITE_API_BASEURL 10 | 11 | /** 12 | * 默认配置 13 | */ 14 | const defaultConfig: CustomAxiosConfig = { 15 | /** 基本路径 */ 16 | baseURL: PATH_URL, 17 | /** 请求超时时间 */ 18 | timeout: ResultEnum.TIMEOUT as number 19 | } 20 | 21 | /** 22 | * axios 请求实例 23 | */ 24 | const axiosInstance = axios.create(defaultConfig) 25 | 26 | /** 27 | * @description 请求拦截器 28 | * 客户端发送请求 -> [请求拦截器] -> 服务器 29 | */ 30 | axiosInstance.interceptors.request.use( 31 | (config: CustomAxiosInterceptorsConfig) => { 32 | // 重复请求不需要取消,在 api 服务中通过指定的第三个参数: { cancel: false } 来控制 33 | config.cancel ??= true 34 | config.cancel && addPending(config) 35 | 36 | return config 37 | }, 38 | (error: AxiosError) => { 39 | return Promise.reject(error) 40 | } 41 | ) 42 | 43 | /** 44 | * @description 响应拦截器 45 | * 服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息 46 | */ 47 | axiosInstance.interceptors.response.use( 48 | (response: AxiosResponse & { config: CustomAxiosConfig }) => { 49 | const { data, config } = response 50 | 51 | removePending(config) 52 | // 全局错误信息拦截(防止下载文件的时候返回数据流,没有 code 直接报错) 53 | if (data.code && data.code !== ResultEnum.SUCCESS) { 54 | checkStatus(data.code, data.message) 55 | return Promise.reject(data) 56 | } 57 | // 成功请求(在页面上除非特殊情况,否则不用处理失败逻辑) 58 | return data 59 | }, 60 | async (error: AxiosError) => { 61 | const { response } = error 62 | // 请求超时,没有 response 63 | if (error.message.indexOf('timeout') !== -1) ElMessage.error('请求超时!请您稍后重试') 64 | // 网络错误单独判断,没有 response 65 | if (error.message.indexOf('Network Error') !== -1) ElMessage.error('网络错误!请您稍后重试') 66 | // 根据服务器响应的错误状态码,做不同的处理 67 | if (response) checkStatus(response.status) 68 | // 服务器结果都没有返回(可能服务器错误可能客户端断网),断网处理:可以跳转到断网页面 69 | if (!window.navigator.onLine) router.replace('/500') 70 | return Promise.reject(error) 71 | } 72 | ) 73 | 74 | export default axiosInstance 75 | -------------------------------------------------------------------------------- /src/http/fetch/InterceptorManager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 定义拦截器的接口 3 | */ 4 | interface Interceptor { 5 | onFulfilled?: (value: T) => T | Promise 6 | onRejected?: (error: any) => any 7 | } 8 | 9 | /** 10 | * 拦截器管理类,用于管理多个拦截器 11 | */ 12 | class InterceptorManager { 13 | private handlers: Array | null> 14 | 15 | constructor() { 16 | this.handlers = [] 17 | } 18 | 19 | /** 20 | * 向管理器列表添加新的拦截器 21 | * @param interceptor 拦截器 22 | * @returns 23 | */ 24 | use(interceptor: Interceptor) { 25 | this.handlers.push(interceptor) 26 | return this.handlers.length - 1 27 | } 28 | 29 | /** 30 | * 向管理器列表删除拦截器 31 | * @param id use() 返回的id 32 | */ 33 | eject(id: number) { 34 | if (this.handlers[id]) { 35 | this.handlers[id] = null 36 | } 37 | } 38 | 39 | /** 40 | * 删除所有的拦截器 41 | */ 42 | clear() { 43 | if (this.handlers) { 44 | this.handlers = [] 45 | } 46 | } 47 | 48 | /** 49 | * 遍历并执行所有的拦截器 50 | * @param fn 执行函数,参数为拦截器 51 | */ 52 | forEach(fn: (interceptor: Interceptor) => void) { 53 | for (const interceptor of this.handlers) { 54 | if (interceptor) { 55 | fn(interceptor) 56 | } 57 | } 58 | } 59 | } 60 | 61 | export default InterceptorManager 62 | -------------------------------------------------------------------------------- /src/http/fetch/config.ts: -------------------------------------------------------------------------------- 1 | import qs from 'qs' 2 | import type { FetchConfig, FetchResponse } from '../types' 3 | import InterceptorManager from './InterceptorManager' 4 | import { checkStatus } from '../helper/checkStatus' 5 | import { addPending, removePending } from '../helper/abortController' 6 | import { ContentTypeEnum, ResultEnum } from '@/enums/httpEnum' 7 | import HttpError from '../helper/httpError' 8 | 9 | /** 默认baseUrl */ 10 | const PATH_URL = import.meta.env.VITE_API_BASEURL 11 | 12 | const defaultConfig: FetchConfig = { 13 | method: 'GET', 14 | /** 基本路径 */ 15 | baseURL: PATH_URL, 16 | /** 请求超时时间 */ 17 | timeout: ResultEnum.TIMEOUT as number, 18 | headers: { 19 | 'Content-Type': ContentTypeEnum.JSON 20 | } 21 | } 22 | 23 | /** 24 | * 请求拦截器 25 | * @returns 请求拦截器管理 26 | */ 27 | function requestInterceptor(interceptors: InterceptorManager>) { 28 | // 添加请求拦截器 29 | interceptors.use({ 30 | onFulfilled: config => { 31 | // 取消重复的请求,需要当前url请求完成后,才会重新请求。 32 | config.cancel ??= true 33 | // 请求开始,在 AbortController 管理中添加该请求 34 | config.cancel && addPending(config) 35 | return config 36 | }, 37 | onRejected: error => { 38 | return Promise.reject(new HttpError(400, error.message)) 39 | } 40 | }) 41 | 42 | return interceptors 43 | } 44 | 45 | /** 46 | * 响应拦截器 47 | * @returns 响应拦截器管理 48 | */ 49 | function responseInterceptor(interceptors: InterceptorManager>) { 50 | let fetchConfig: FetchConfig 51 | // 添加响应拦截器,处理 Fetch 返回的数据,此时 response 还需要进一步处理 52 | interceptors.use({ 53 | onFulfilled: response => { 54 | if (!response.ok) { 55 | // 想要获取后端返回的错误信息,还需要 then 一次 response 56 | // 与后端协商好返回的错误信息。 57 | // 默认服务器返回错误对象必须含有 code 和 message 属性,如: {code: 500,message: '响应失败!'} 58 | return Promise.reject(response.json()) 59 | 60 | // 如果不需要处理服务器返回的错误信息 61 | // return Promise.reject(new HttpError(response.status, '')) 62 | } 63 | 64 | const { config } = response 65 | config && (fetchConfig = config) 66 | 67 | // 文本流式响应单独处理 68 | if (config?.onStream) { 69 | return handleStream(response, config) 70 | } 71 | 72 | const contentType = response.headers.get('content-type') || '' 73 | if (contentType.includes('application/json')) { 74 | return response.json() 75 | } else if (contentType.startsWith('text/')) { 76 | return response.text() 77 | } else if (contentType.includes('image/')) { 78 | return response.blob() 79 | } else if (contentType.includes('multipart/form-data')) { 80 | return response.formData() 81 | } 82 | // 其他类型默认返回文本 83 | return response.text() 84 | }, 85 | onRejected: error => { 86 | // 处理除了 2xx 和 5xx 状态码的错误信息。 87 | return Promise.reject(new HttpError(error.code || 400, error.message)) 88 | } 89 | }) 90 | 91 | /** 92 | * 添加响应拦截器,处理最终的数据和错误信息。 93 | */ 94 | interceptors.use({ 95 | onFulfilled: response => { 96 | // 请求响应完成,在 AbortController 管理中移除该请求 97 | removePending(fetchConfig) 98 | return response 99 | }, 100 | onRejected: async error => { 101 | // 处理服务器返回 5xx 的错误信息 102 | const response = await error 103 | // 统一处理 promise 链的 reject 错误。 104 | return Promise.reject(checkStatus(response.code, response.message)) 105 | } 106 | }) 107 | 108 | return interceptors 109 | } 110 | 111 | /** 112 | * 处理流式响应 113 | * @param Response response fetch返回的响应对象 114 | * @param Function onChunk 处理每个数据块的函数 115 | */ 116 | async function handleStream(response: FetchResponse, config: FetchConfig) { 117 | if (!config.onStream) { 118 | return Promise.reject(checkStatus(701, false)) 119 | } 120 | 121 | if (!response.body) { 122 | return Promise.reject(checkStatus(702, false)) 123 | } 124 | const reader = response.body.getReader() 125 | // 用于文本流解码 126 | const decoder = new TextDecoder() 127 | config.onReady && config.onReady(response) 128 | 129 | while (true) { 130 | const { done, value } = await reader.read() 131 | if (done) break 132 | 133 | const chunk = decoder.decode(value, { stream: true }) 134 | config.onStream(value, chunk) 135 | } 136 | 137 | return Promise.resolve({ code: 700, message: '流式响应完成!' }) 138 | } 139 | 140 | /** 141 | * Fecth 请求 142 | * @param url url路径,可以是完整的 url。如果不是完整的,则会从 PATH_URL 中拼接 143 | * @param config 请求参数配置 144 | * @returns 返回响应数据 Promise 145 | */ 146 | async function fetchRequest(url: string, config: FetchConfig = {}): Promise { 147 | let requestInterceptors = new InterceptorManager>() 148 | let responseInterceptors = new InterceptorManager>() 149 | 150 | requestInterceptors = requestInterceptor(requestInterceptors) 151 | responseInterceptors = responseInterceptor(responseInterceptors) 152 | 153 | // 合并基础配置 154 | const mergedConfig: FetchConfig = { 155 | ...defaultConfig, 156 | ...config 157 | } 158 | 159 | // 处理URL 160 | let finalURL = url 161 | if (PATH_URL && !url.startsWith('http')) { 162 | finalURL = PATH_URL + url 163 | } 164 | // 处理查询参数 165 | if (mergedConfig.params) { 166 | const params = qs.stringify(mergedConfig.params) 167 | finalURL += `?${params.toString()}` 168 | } 169 | // 处理请求数据 170 | if (mergedConfig.data) { 171 | mergedConfig.body = JSON.stringify(mergedConfig.data) 172 | } 173 | mergedConfig.url = finalURL 174 | 175 | // 创建 Fetch Promise 链,流程:(请求拦截器 → Fetch请求 → 响应拦截器) 176 | let promise = Promise.resolve(mergedConfig) 177 | 178 | const requestInterceptorChain: any[] = [] 179 | requestInterceptors.forEach(interceptor => { 180 | requestInterceptorChain.push(interceptor.onFulfilled, interceptor.onRejected) 181 | }) 182 | 183 | const responseInterceptorChain: any[] = [] 184 | responseInterceptors.forEach(interceptor => { 185 | responseInterceptorChain.push(interceptor.onFulfilled, interceptor.onRejected) 186 | }) 187 | 188 | // 将请求拦截器依次添加到 promise 链 189 | let i = 0 190 | while (i < requestInterceptorChain.length) { 191 | promise = promise.then(requestInterceptorChain[i++], requestInterceptorChain[i++]) 192 | } 193 | 194 | // Fetch 请求添加到 promise 链 195 | promise = promise.then(async newConfig => { 196 | const response = (await fetch(finalURL, newConfig)) as FetchResponse 197 | // 将 config 添加到 FetchResponse 中,响应拦截器需要用到 confog 198 | response.config = newConfig 199 | return response 200 | }) 201 | 202 | // 响应拦截器依次添加到 promise 链 203 | i = 0 204 | while (i < responseInterceptorChain.length) { 205 | promise = promise.then(responseInterceptorChain[i++], responseInterceptorChain[i++]) 206 | } 207 | 208 | return promise as Promise 209 | } 210 | 211 | export { fetchRequest } 212 | -------------------------------------------------------------------------------- /src/http/helper/abortController.ts: -------------------------------------------------------------------------------- 1 | import type { CustomAbortRequestConfig } from '../types' 2 | 3 | /** 4 | * AbortController 请求管理,用于取消请求操作 5 | */ 6 | 7 | // 声明一个 Map 用于存储每个 请求的标识 和对应 AbortController 8 | const pendingMap = new Map() 9 | 10 | // 获取请求的标识,url,method,data,params 值都一样即为相同请求 11 | const getPendingUrl = (config: CustomAbortRequestConfig) => { 12 | return [config.method, config.url].join('&') 13 | } 14 | 15 | /** 16 | * @description: 添加请求到 pendingMap 中 17 | * @param config 请求参数配置 18 | * @return void 19 | */ 20 | function addPending(config: CustomAbortRequestConfig) { 21 | // 在请求开始前,对之前的请求做检查取消操作 22 | removePending(config) 23 | const url = getPendingUrl(config) 24 | const controller = new AbortController() 25 | config.signal = controller.signal 26 | pendingMap.set(url, controller) 27 | } 28 | 29 | /** 30 | * @description: 从 pendingMap 中移除请求 31 | * @param config 请求参数配置 32 | */ 33 | function removePending(config: CustomAbortRequestConfig) { 34 | const url = getPendingUrl(config) 35 | // 如果在 pending 中存在当前请求标识,需要取消当前请求并删除条目 36 | const controller = pendingMap.get(url) 37 | if (controller) { 38 | controller.abort() 39 | pendingMap.delete(url) 40 | } 41 | } 42 | 43 | /** 44 | * @description: 清空所有 pending 45 | */ 46 | function removeAllPending() { 47 | pendingMap.forEach(controller => { 48 | if (controller) { 49 | controller.abort() 50 | } 51 | }) 52 | pendingMap.clear() 53 | } 54 | 55 | /** 56 | * 匹配 url 取消请求 57 | * @param url 字符串或字符串数组 58 | */ 59 | function cancelRequest(url: string | string[]) { 60 | const urlList = Array.isArray(url) ? url : [url] 61 | const keys = pendingMap.keys() 62 | for (const _url of urlList) { 63 | const mapKeys = keys.filter(item => item.indexOf(_url) > -1) 64 | for (const key of mapKeys) { 65 | pendingMap.get(key)?.abort() 66 | pendingMap.delete(key) 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * 取消所有请求 73 | * @param params 74 | */ 75 | function cancelAllRequest() { 76 | removeAllPending() 77 | } 78 | 79 | function getPendingMap() { 80 | return pendingMap 81 | } 82 | 83 | export { addPending, removePending, removeAllPending, cancelRequest, cancelAllRequest, getPendingMap } 84 | -------------------------------------------------------------------------------- /src/http/helper/checkStatus.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 校验网络请求状态码 3 | * @param status 状态码 4 | * @param msg `类型为 string`:添加额外的信息。`类型为 boolean`:是否开启消息提示 5 | * @param isAlert 是否开启消息提示 6 | * @returns 7 | */ 8 | export const checkStatus = (status: number, msg?: string | boolean, isAlert: boolean = true) => { 9 | let message = '' 10 | switch (status) { 11 | case 0: 12 | case 20: 13 | message = '已取消请求。' 14 | ElMessage.warning(message) 15 | return { code: status, message: message } 16 | case 400: 17 | message = '请求失败!请求未提交到服务器。' 18 | break 19 | case 401: 20 | message = '登录失效!用户没有权限(令牌、用户名、密码错误)。' 21 | break 22 | case 403: 23 | message = '当前账号无权限访问!' 24 | break 25 | case 404: 26 | message = '您所访问的资源不存在!' 27 | break 28 | case 405: 29 | message = '请求方式错误!' 30 | break 31 | case 406: 32 | message = '请求的格式不可得。' 33 | break 34 | case 408: 35 | message = '请求超时!请您稍后重试。' 36 | break 37 | case 422: 38 | message = '请求失败!无法处理的内容。' 39 | break 40 | case 500: 41 | message = '服务器发生错误,请检查服务器。' 42 | break 43 | case 502: 44 | message = '网关错误!' 45 | break 46 | case 503: 47 | message = '服务不可用,服务器暂时过载或维护。' 48 | break 49 | case 504: 50 | message = '网关超时!' 51 | break 52 | case 701: 53 | message = '流式响应失败,没有对应的回调处理!' 54 | break 55 | case 702: 56 | message = '流式响应失败,无响应数据!' 57 | break 58 | default: 59 | message = '请求失败!' 60 | } 61 | 62 | if (typeof msg === 'string') { 63 | message += msg 64 | } 65 | 66 | if ((typeof msg === 'boolean' && msg === true && isAlert) || (typeof msg !== 'boolean' && isAlert)) { 67 | ElMessage.error(`请求失败!状态码:${status},${message}`) 68 | } 69 | 70 | return { 71 | code: status, 72 | message: message 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/http/helper/httpError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 自定义 http 请求 Error 3 | * @param code 状态码 4 | * @param message 错误消息 5 | */ 6 | class HttpError extends Error { 7 | /** 状态码 */ 8 | code: number 9 | /** 错误消息 */ 10 | message: string 11 | /** 12 | * 自定义 http 请求 Error 13 | * @param code 状态码 14 | * @param message 错误消息 15 | */ 16 | constructor(code: number, message: string) { 17 | super(message) 18 | this.code = code 19 | this.message = message 20 | } 21 | } 22 | 23 | export default HttpError 24 | -------------------------------------------------------------------------------- /src/http/index.ts: -------------------------------------------------------------------------------- 1 | import axiosRequest from './axios/config' 2 | import { fetchRequest } from './fetch/config' 3 | import { cancelAllRequest, cancelRequest } from './helper/abortController' 4 | import type { CustomAxiosConfig, FetchConfig, ResultData } from './types' 5 | 6 | /** 7 | * @description 常用 http 请求,默认 Axios,Fetch前缀的函数使用 Fetch 8 | */ 9 | export default { 10 | /** 11 | * Axios get 请求 12 | * @param url url 13 | * @param params param 请求参数 14 | * @param config Axios config 配置 15 | * @returns `Promise>` 16 | */ 17 | get(url: string, params?: object, config: CustomAxiosConfig = {}): Promise> { 18 | return axiosRequest.get(url, { params, ...config }) 19 | }, 20 | /** 21 | * Axios post 请求 22 | * @param url url 23 | * @param data data 参数 24 | * @param config Axios config 配置 25 | * @returns `Promise>` 26 | */ 27 | post(url: string, data?: object | string, config: CustomAxiosConfig = {}): Promise> { 28 | return axiosRequest.post(url, data, config) 29 | }, 30 | /** 31 | * Axios put 请求 32 | * @param url url 33 | * @param data data参数 34 | * @param config Axios config 配置 35 | * @returns `Promise>` 36 | */ 37 | put(url: string, data?: object, config: CustomAxiosConfig = {}): Promise> { 38 | return axiosRequest.put(url, data, config) 39 | }, 40 | /** 41 | * Axios delete 请求 42 | * @param url url 43 | * @param data data 请求参数,通常是id 44 | * @param config Axios config 配置 45 | * @returns `Promise>` 46 | */ 47 | delete(url: string, data?: any, config: CustomAxiosConfig = {}): Promise> { 48 | return axiosRequest.delete(url, { data, ...config }) 49 | }, 50 | /** 51 | * Axios download 下载请求,responseType: blob 52 | * @param url url 53 | * @param data data 请求参数 54 | * @param config Axios config 配置 55 | * @returns `Promise` 56 | */ 57 | download(url: string, data?: object, config: CustomAxiosConfig = {}): Promise { 58 | return axiosRequest.post(url, data, { ...config, responseType: 'blob' }) 59 | }, 60 | /** 61 | * Fetch get 请求 62 | * @param url url 63 | * @param params param 请求参数 64 | * @param config Fetch config 配置 65 | * @returns Promise 66 | */ 67 | fetchGet(url: string, params?: object, config: FetchConfig = {}): Promise> { 68 | return fetchRequest>(url, { params, ...config }) 69 | }, 70 | /** 71 | * Fetch get 请求 72 | * @param url url 73 | * @param params param 请求参数 74 | * @param config Fetch config 配置 75 | * @returns Promise 76 | */ 77 | fetchPost(url: string, data?: object, config: FetchConfig = {}): Promise> { 78 | return fetchRequest>(url, { method: 'POST', data, ...config }) 79 | }, 80 | /** 81 | * Fetch 对话 Post 请求,专门为 `Chat` 配置,启动流式响应 82 | * @param url url 83 | * @param params param 请求参数 84 | * @param config Fetch config 配置 85 | * @returns Promise 86 | */ 87 | fetchPostChat(url: string, data?: object, config: FetchConfig = {}): Promise { 88 | return fetchRequest(url, { method: 'POST', data, ...config }) 89 | }, 90 | /** 91 | * 匹配 url 取消请求 92 | * @param url 字符串或字符串数组 93 | * @returns 94 | */ 95 | cancelRequest: (url: string | string[]) => { 96 | cancelRequest(url) 97 | }, 98 | /** 99 | * 取消所有请求 100 | * @returns 101 | */ 102 | cancelAllRequest: () => { 103 | cancelAllRequest() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/http/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosRequestConfig, GenericAbortSignal, InternalAxiosRequestConfig } from 'axios' 2 | import type { ValueOf } from 'element-plus/es/components/table/src/table-column/defaults.mjs' 3 | 4 | /** 5 | * AbortController 自定义中断请求配置 6 | */ 7 | export interface CustomAbortRequestConfig { 8 | url?: string 9 | method?: string 10 | params?: any 11 | data?: any 12 | signal?: GenericAbortSignal | AbortSignal | null 13 | } 14 | 15 | /** 16 | * axios 扩展配置参数,基本参数 17 | */ 18 | export interface CustomAxiosBaseConfig { 19 | /** 20 | * 控制是否取消重复的请求,默认为true。 21 | * true:重复请求需要取消,对于不小心连续触发多个相同的请求的情况,取消重复的请求 22 | * false:重复请求不需要取消 23 | */ 24 | cancel?: boolean 25 | } 26 | /** axios 扩展参数配置 */ 27 | export interface CustomAxiosConfig extends CustomAxiosBaseConfig, AxiosRequestConfig {} 28 | /** axios 拦截器扩展参数配置 */ 29 | export interface CustomAxiosInterceptorsConfig extends CustomAxiosBaseConfig, InternalAxiosRequestConfig {} 30 | 31 | /** 32 | * fetch 扩展配置参数,继承 fetch 原有 config 33 | */ 34 | export interface FetchConfig extends RequestInit { 35 | /** `baseURL` 将自动加在 `url` 前面,如果 `url` 是一个绝对 URL,则会忽略 `baseURL`。 */ 36 | baseURL?: string 37 | /** `url` 是用于请求的服务器 URL */ 38 | url?: string 39 | /** `method` 是创建请求时使用的方法 */ 40 | method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' 41 | /** `params` 是与请求一起发送的 URL 参数,必须是一个简单对象或 URLSearchParams 对象 */ 42 | params?: object 43 | /** `data` 是作为请求体被发送的数据,仅适用 'PUT', 'POST', 'DELETE 和 'PATCH' 请求方法 */ 44 | data?: D 45 | /** `timeout` 指定请求超时的毫秒数。如果请求时间超过 `timeout` 的值,则请求会被中断 */ 46 | timeout?: number 47 | /** 48 | * `cancel` 控制是否取消重复的请求,默认为true。 49 | * @description `true`:取消重复请求,对于不小心连续触发多个相同的请求的情况,可以取消重复的请求。 50 | * @description `false`:不取消重复请求。 51 | */ 52 | cancel?: boolean 53 | /** 54 | * `onReady()`请求响应成功,准备流式输出 55 | * @param response 响应值 response 56 | * @returns 57 | */ 58 | onReady?: (response: FetchResponse) => void 59 | /** 60 | * `onChunk()` 开启 stream 流式响应并回调函数 61 | * @param reader 二进制字节流,一般用于下载文件流 62 | * @param chunk TextDecoder()解码后的文本流,一般用于文字流式输出 63 | * @returns 64 | */ 65 | onStream?: (reader: Uint8Array, chunk: string) => void 66 | } 67 | 68 | /** 69 | * 请求响应成功,准备流式输出,类型提取自 `FetchConfig.onReady` 70 | * @param response 响应值 response 71 | * @returns 72 | */ 73 | export type OnReady = ValueOf, 'onReady'>> 74 | /** 75 | * 开启 stream 流式响应并回调函数,类型提取自 `FetchConfig.onStream` 76 | * @param reader 二进制字节流,一般用于下载文件流 77 | * @param chunk TextDecoder()解码后的文本流,一般用于文字流式输出 78 | * @returns 79 | */ 80 | export type OnStream = ValueOf> 81 | 82 | /** 83 | * fetch 扩展响应参数,继承 fetch 原有 Response 84 | */ 85 | export interface FetchResponse extends Response { 86 | config?: FetchConfig 87 | } 88 | 89 | /** 90 | * 请求响应参数(不包含data) 91 | */ 92 | export interface Result { 93 | code: number 94 | message: string 95 | } 96 | 97 | /** 98 | * 请求响应参数(包含data),继承 Result 99 | */ 100 | export interface ResultData extends Result { 101 | data: T 102 | } 103 | -------------------------------------------------------------------------------- /src/layout/LayoutClassic.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 34 | 35 | 65 | -------------------------------------------------------------------------------- /src/layout/components/base/LayoutAside.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 55 | 56 | 73 | -------------------------------------------------------------------------------- /src/layout/components/base/LayoutFooter.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /src/layout/components/base/LayoutHeader.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | 20 | 44 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createPinia } from 'pinia' 3 | 4 | import App from './App.vue' 5 | import router from './router' 6 | 7 | // 重置默认样式 8 | import '@/styles/reset.scss' 9 | // markdown 样式 10 | import '@/styles/markdown/mdmdt-light.scss' 11 | import '@/styles/markdown/plugins.scss' 12 | // elementplus 自定义样式 13 | import '@/styles/index.scss' 14 | // elementplus 图标 15 | import * as ElementPlusIconsVue from '@element-plus/icons-vue' 16 | 17 | const app = createApp(App) 18 | 19 | app.use(createPinia()) 20 | app.use(router) 21 | 22 | // elementplus 图标注册 23 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 24 | app.component(key, component) 25 | } 26 | 27 | app.mount('#app') 28 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import LayoutClassic from '@/layout/LayoutClassic.vue' 3 | 4 | const router = createRouter({ 5 | history: createWebHistory(import.meta.env.BASE_URL), 6 | routes: [ 7 | { 8 | path: '/', 9 | redirect: '/chat' 10 | }, 11 | { 12 | path: '/', 13 | component: LayoutClassic, 14 | children: [ 15 | { 16 | path: '/chat', 17 | name: 'Chat', 18 | meta: { 19 | title: '聊天', 20 | icon: 'ChatDotRound', 21 | keepAlive: true 22 | }, 23 | component: () => import('@/views/chat/index.vue') 24 | }, 25 | { 26 | path: '/documents', 27 | name: 'Documents', 28 | meta: { 29 | title: '文档管理', 30 | icon: 'FolderOpened' 31 | }, 32 | component: () => import('@/views/documents/index.vue') 33 | }, 34 | { 35 | path: '/md-preview', 36 | name: 'MdPreview', 37 | meta: { 38 | title: 'md主题预览', 39 | icon: 'Tickets' 40 | }, 41 | component: () => import('@/views/test/index.vue') 42 | } 43 | ] 44 | } 45 | ] 46 | }) 47 | 48 | export default router 49 | -------------------------------------------------------------------------------- /src/stores/app.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | 4 | /** 5 | * 项目 全局状态 6 | */ 7 | export const useAppStore = defineStore('app', () => { 8 | // 标题 9 | const title = ref(import.meta.env.VITE_APP_TITLE) 10 | // 折叠菜单 11 | const collapse = ref(false) 12 | 13 | return { title, collapse } 14 | }) 15 | -------------------------------------------------------------------------------- /src/styles/element/element.scss: -------------------------------------------------------------------------------- 1 | // el-table 表格样式 2 | .el-table { 3 | flex: 1; 4 | 5 | // 修复 safari 浏览器表格错位 https://github.com/HalseySpicy/Geeker-Admin/issues/83 6 | table { 7 | width: 100%; 8 | } 9 | 10 | /* stylelint-disable-next-line selector-class-pattern */ 11 | .el-table__header th { 12 | font-size: 15px; 13 | font-weight: bold; 14 | } 15 | 16 | /* stylelint-disable-next-line selector-class-pattern */ 17 | .el-table__row { 18 | font-size: 14px; 19 | 20 | .move { 21 | cursor: move; 22 | 23 | .el-icon { 24 | cursor: move; 25 | } 26 | } 27 | } 28 | 29 | // 设置 el-table 中 header 文字不换行,并省略 30 | /* stylelint-disable-next-line selector-class-pattern */ 31 | .el-table__header .el-table__cell > .cell { 32 | white-space: wrap; 33 | } 34 | 35 | // 解决表格数据为空时样式不居中问题(仅在element-plus中) 36 | /* stylelint-disable-next-line selector-class-pattern */ 37 | .el-table__empty-block { 38 | position: absolute; 39 | top: 50%; 40 | left: 50%; 41 | transform: translate(-50%, -50%); 42 | 43 | .table-empty { 44 | line-height: 30px; 45 | } 46 | } 47 | 48 | // table 中 image 图片样式 49 | .table-image { 50 | width: 50px; 51 | height: 50px; 52 | border-radius: 50%; 53 | } 54 | } 55 | 56 | // el-dialog 自定义弹窗样式 57 | .el-dialog { 58 | &__body { 59 | padding: 0 !important; 60 | } 61 | 62 | &__footer { 63 | border-top: 1px solid var(--el-border-color); 64 | } 65 | 66 | &__headerbtn { 67 | top: 0; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/styles/element/index.scss: -------------------------------------------------------------------------------- 1 | @forward 'element-plus/theme-chalk/src/common/var.scss' with ( 2 | $colors: ( 3 | 'primary': ( 4 | 'base': #80752c 5 | ) 6 | ), 7 | $header: ( 8 | 'height': 60px 9 | ), 10 | $footer: ( 11 | 'height': 30px 12 | ), 13 | $table: ( 14 | 'header-bg-color': var(--el-fill-color-light), 15 | 'header-text-color': var(--el-text-color-primary) 16 | ), 17 | $table-padding: ( 18 | 'large': 16px 0, 19 | 'default': 12px 0, 20 | 'small': 4px 0 21 | ), 22 | $scrollbar: ( 23 | 'bg-color': var(--el-text-color-primary), 24 | 'hover-bg-color': var(--el-text-color-primary) 25 | ) 26 | ); 27 | 28 | // 自定义覆盖 elementplus 组件样式,补充上面所没有的样式定义 29 | @use './element.scss' as *; 30 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @use './var'; 2 | 3 | /* 全局滚动条 */ 4 | ::-webkit-scrollbar { 5 | width: 6px; 6 | } 7 | 8 | ::-webkit-scrollbar-thumb { 9 | background-color: rgb(from var(--el-text-color-primary) r g b / 30%); 10 | border-radius: 6px; 11 | transition: all 0.2s ease-in-out; 12 | 13 | &:hover { 14 | cursor: pointer; 15 | background-color: rgb(from var(--el-text-color-primary) r g b / 50%); 16 | } 17 | } 18 | 19 | ::-webkit-scrollbar-track { 20 | border-radius: 6px; 21 | } 22 | 23 | .mainl-white { 24 | width: 100%; 25 | min-height: calc(100vh - var(--el-header-height) - var(--el-footer-height)); 26 | background-color: white; 27 | } 28 | 29 | .main-full { 30 | width: 100%; 31 | height: calc(100vh - var(--el-header-height) - var(--el-footer-height) - var(--app-content-padding) * 2); 32 | } 33 | 34 | .main-full-white { 35 | width: 100%; 36 | height: calc(100vh - var(--el-header-height) - var(--el-footer-height) - var(--app-content-padding) * 2); 37 | background-color: white; 38 | } 39 | 40 | // 卡片 41 | .card { 42 | padding: 20px; 43 | overflow-x: hidden; 44 | background-color: var(--el-bg-color); 45 | border: 1px solid var(--el-border-color-light); 46 | border-radius: 6px; 47 | box-shadow: 0 0 12px rgb(0 0 0 / 5%); 48 | } 49 | 50 | .pagination-right { 51 | display: flex; 52 | justify-content: flex-end; 53 | margin-top: 20px; 54 | } 55 | -------------------------------------------------------------------------------- /src/styles/markdown/mdmdt-light.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable */ 2 | :root { 3 | --mdmdt-bg-color: #fff; 4 | --mdmdt-bg-color2: #e3c7c0; 5 | --mdmdt-table-header-bg: #f4b5ad; 6 | --mdmdt-table-tr-bg: #f0f0f0; 7 | --mdmdt-text-color: #000; 8 | --mdmdt-text-grey: #666; 9 | --mdmdt-text-code: #2f479f; 10 | --mdmdt-title-color: #070909; 11 | --mdmdt-border-color: #d2d2d2; 12 | --mdmdt-color-1: #3e69d7; 13 | --mdmdt-color-1-0-a: rgb(62 105 215 / 15%); 14 | --mdmdt-color-1-0-b: rgb(62 105 215 / 6%); 15 | --mdmdt-color-2: #f59102; 16 | --mdmdt-color-2-0-a: rgb(245 145 2 / 15%); 17 | --mdmdt-color-2-0-b: rgb(245 145 2 / 6%); 18 | --mdmdt-color-2-0-c: rgb(245 145 2 / 30%); 19 | --mdmdt-color-3: #03b736; 20 | --mdmdt-color-3-0-a: rgb(3 183 54 / 15%); 21 | --mdmdt-color-3-0-b: rgb(3 183 54 / 6%); 22 | --mdmdt-color-4: #8250df; 23 | --mdmdt-color-4-0-a: rgb(130 80 223 / 15%); 24 | --mdmdt-color-4-0-b: rgb(130 80 223 / 6%); 25 | --mdmdt-color-5: #e30f2e; 26 | --mdmdt-color-5-0-a: rgb(227 15 46 / 15%); 27 | --mdmdt-color-5-0-b: rgb(227 15 46 / 6%); 28 | --mdmdt-md-char-color: rgb(72 93 108 / 75%); 29 | --monospace: 'JetBrains Mono', 'Source Code Pro', 'Fira Code', consolas, inconsolata, 'Cascadia Code', monaco, 'Ubuntu Mono', monospace; 30 | } 31 | 32 | .mdmdt { 33 | line-height: 1.6; 34 | 35 | /* 36 | * ------------------------ 37 | * scroll style 38 | * ------------------------ 39 | */ 40 | ::-webkit-scrollbar { 41 | width: 8px !important; 42 | height: 8px !important; 43 | } 44 | ::-webkit-scrollbar-thumb { 45 | background: #e3c7c0; 46 | border-radius: 4px !important; 47 | } 48 | ::-webkit-scrollbar-track { 49 | border-radius: 10px; 50 | } 51 | 52 | /* 53 | * ----------------------------------- 54 | * h1 ~ h6 55 | * p, strong, dl, em, u, kbd, hr, mark 56 | * ----------------------------------- 57 | */ 58 | h1, 59 | h2, 60 | h3, 61 | h4, 62 | h5, 63 | h6 { 64 | position: relative; 65 | margin: 32px 0 18px; 66 | line-height: 1.5; 67 | color: var(--mdmdt-title-color); 68 | letter-spacing: 2px; 69 | } 70 | 71 | h1 { 72 | font-size: 32px; 73 | border-bottom: 1px solid var(--mdmdt-border-color); 74 | } 75 | 76 | h2 { 77 | font-size: 28px; 78 | } 79 | 80 | h3 { 81 | font-size: 24px; 82 | } 83 | 84 | h4 { 85 | font-size: 20px; 86 | } 87 | 88 | h5 { 89 | font-size: 18px; 90 | } 91 | 92 | h6 { 93 | font-size: 16px; 94 | } 95 | 96 | h2.md-focus::before, 97 | h3.md-focus::before, 98 | h4.md-focus::before, 99 | h5.md-focus::before, 100 | h6.md-focus::before, 101 | h2::before, 102 | h3::before, 103 | h4::before, 104 | h5::before, 105 | h6::before { 106 | position: absolute; 107 | left: -36px; 108 | float: left; 109 | display: none; 110 | height: 20px; 111 | padding-right: 3px; 112 | padding-left: 6px; 113 | font-size: 12px; 114 | font-weight: 400; 115 | color: var(--mdmdt-color-1); 116 | border: 1px solid; 117 | border-radius: 3px; 118 | } 119 | 120 | h2::before, 121 | h2.md-focus::before { 122 | top: 10px; 123 | content: 'h2'; 124 | } 125 | 126 | h3::before, 127 | h3.md-focus::before { 128 | top: 7px; 129 | content: 'h3'; 130 | } 131 | 132 | h4::before, 133 | h4.md-focus::before { 134 | top: 5px; 135 | content: 'h4'; 136 | } 137 | 138 | h5::before, 139 | h5.md-focus::before { 140 | top: 4px; 141 | content: 'h5'; 142 | } 143 | 144 | h6::before, 145 | h6.md-focus::before { 146 | top: 2px; 147 | content: 'h6'; 148 | } 149 | 150 | h2:hover::before, 151 | h3:hover::before, 152 | h4:hover::before, 153 | h5:hover::before, 154 | h6:hover::before { 155 | display: block; 156 | } 157 | 158 | hr { 159 | box-sizing: border-box; 160 | height: 1px; 161 | margin: 16px 0; 162 | background: var(--mdmdt-border-color); 163 | border: none; 164 | } 165 | 166 | p { 167 | margin: 16px 0; 168 | } 169 | 170 | strong { 171 | font-weight: 900; 172 | color: var(--mdmdt-title-color); 173 | } 174 | 175 | u { 176 | text-decoration-thickness: 1.5px; 177 | text-decoration-color: var(--mdmdt-title-color); 178 | text-underline-offset: 4px; 179 | } 180 | 181 | em { 182 | font-weight: 400; 183 | } 184 | 185 | del { 186 | color: var(--mdmdt-text-grey); 187 | text-decoration: line-through; 188 | text-decoration-color: var(--mdmdt-color-5); 189 | } 190 | 191 | mark { 192 | padding: 0 4px; 193 | color: var(--mdmdt-bg-color); 194 | background-color: var(--mdmdt-color-1); 195 | border-radius: 5px; 196 | box-decoration-break: clone; 197 | } 198 | 199 | /* 200 | * ------------------------ 201 | * image 202 | * ------------------------ 203 | */ 204 | img { 205 | display: inline-block; 206 | max-width: 100%; 207 | margin: 0 auto 14px; 208 | color: var(--mdmdt-md-char-color); 209 | border-radius: 8px; 210 | } 211 | 212 | span.md-image, 213 | span.md-image span.md-content, 214 | span.md-image span.md-image-src-span, 215 | span.md-image span.md-image-before-src, 216 | span.md-image span.md-image-after-src, 217 | span.md-image span.md-image-input-src-btn, 218 | span.md-image span.md-image-pick-file-btn, 219 | span.md-image span.md-before::before, 220 | span.md-image span.md-image-input-src-btn::before, 221 | span.md-image span.md-image-pick-file-btn::before { 222 | color: var(--mdmdt-color-1); 223 | } 224 | 225 | span.md-image-btn { 226 | background: var(--mdmdt-bg-color2); 227 | } 228 | 229 | span.md-image-btn:hover::before { 230 | color: var(--mdmdt-bg-color) !important; 231 | } 232 | 233 | span.md-image span.md-image-pick-file-btn { 234 | border-left-color: var(--mdmdt-border-color); 235 | } 236 | 237 | /* 238 | * ------------------------ 239 | * a, link 240 | * ------------------------ 241 | */ 242 | a { 243 | font-size: 16px; 244 | font-weight: 500; 245 | color: var(--mdmdt-color-1); 246 | text-decoration: none; 247 | text-underline-offset: 4px; 248 | } 249 | 250 | a:hover, 251 | .md-link a:hover, 252 | .footnotes a:hover { 253 | color: var(--mdmdt-color-1); 254 | text-decoration: underline; 255 | cursor: pointer !important; 256 | } 257 | 258 | #write a::before, 259 | .cm-link::before { 260 | display: inline-block; 261 | font-size: 12px; 262 | content: '\0460'; 263 | transform: rotate(45deg); 264 | } 265 | 266 | .unibody-window a:hover, 267 | content a:hover { 268 | color: var(--mdmdt-color-2) !important; 269 | } 270 | 271 | .footnotes, 272 | .footnotes a { 273 | font-size: 14px; 274 | text-decoration: none; 275 | } 276 | 277 | .footnotes .md-def-name::before, 278 | .footnotes .md-def-name::after { 279 | color: var(--mdmdt-text-color); 280 | } 281 | 282 | .footnotes .md-def-url, 283 | .md-link .md-url { 284 | color: var(--mdmdt-color-1); 285 | text-decoration: none; 286 | } 287 | 288 | /* 289 | * ------------------------ 290 | * ul, ol 291 | * ------------------------ 292 | */ 293 | ul { 294 | padding-left: 36px; 295 | margin-top: 4px !important; 296 | margin-bottom: 4px; 297 | margin-left: 0; 298 | } 299 | 300 | ol { 301 | padding-left: 40px; 302 | margin-top: 4px !important; 303 | margin-bottom: 4px; 304 | margin-left: 0; 305 | } 306 | 307 | p:has(+ ul), 308 | p:has(+ ol) { 309 | margin-bottom: 0; 310 | } 311 | 312 | ul li, 313 | ol li { 314 | margin-bottom: 4px; 315 | } 316 | 317 | ul > li > p { 318 | margin: 0 0 0 -2px; 319 | } 320 | 321 | ol > li > p { 322 | margin: 0 0 0 -6px; 323 | } 324 | 325 | ul > .task-list-item > input { 326 | margin-left: -22px; 327 | } 328 | 329 | ul ul { 330 | margin-left: -2px; 331 | } 332 | 333 | ol ol { 334 | margin-left: -7px; 335 | } 336 | 337 | ol > li > ul { 338 | margin-left: -7px; 339 | } 340 | 341 | ul > li > ol { 342 | margin-left: -2px; 343 | } 344 | 345 | /* 346 | * ------------------------ 347 | * code 348 | * ------------------------ 349 | */ 350 | code:not(.hljs), 351 | tt { 352 | box-sizing: border-box; 353 | padding: 2px 10px; 354 | font-family: Consolas, Menlo, 'Helvetica Neue', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; 355 | font-weight: 600; 356 | border-radius: 5px; 357 | background: var(--mdmdt-bg-color2); 358 | box-decoration-break: clone; 359 | } 360 | 361 | p code, 362 | p tt, 363 | li code, 364 | li tt, 365 | blockquote code, 366 | blockquote tt, 367 | .md-alert code, 368 | .md-alert tt, 369 | table code, 370 | table tt::not(.hljs) { 371 | padding: 3px 6px; 372 | background: var(--mdmdt-bg-color2); 373 | box-decoration-break: clone; 374 | } 375 | 376 | h1 code, 377 | h2 code, 378 | h3 code, 379 | h4 code, 380 | h5 code, 381 | h6 code { 382 | vertical-align: middle; 383 | } 384 | 385 | h1 code { 386 | font-size: 18px; 387 | } 388 | 389 | h2 code { 390 | font-size: 16px; 391 | } 392 | 393 | .outline-content .outline-item code { 394 | padding: 2px 6px !important; 395 | font-size: 11px; 396 | } 397 | 398 | /* 399 | * ------------------------ 400 | * pre 401 | * ------------------------ 402 | */ 403 | 404 | pre code { 405 | font-family: Consolas, Menlo, 'Helvetica Neue', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; 406 | font-size: 14px; 407 | font-weight: normal; 408 | } 409 | 410 | p + pre { 411 | margin-top: -8px; 412 | } 413 | 414 | pre pre { 415 | padding: 5px 10px; 416 | background: transparent !important; 417 | border: none; 418 | } 419 | 420 | pre .CodeMirror-sizer { 421 | padding-left: 4px; 422 | } 423 | 424 | pre .CodeMirror-gutters { 425 | background: var(--mdmdt-bg-color2); 426 | border-color: var(--mdmdt-border-color); 427 | } 428 | 429 | /* 430 | * ------------------------ 431 | * blockquote 432 | * ------------------------ 433 | */ 434 | blockquote, 435 | .md-alert { 436 | box-sizing: border-box; 437 | padding: 16px; 438 | margin: 14px 0; 439 | background: var(--mdmdt-color-2-0-b); 440 | border-top: 0.1px solid transparent; 441 | border-right: 0.1px solid transparent; 442 | border-bottom: 0.1px solid transparent; 443 | border-left: 4px solid var(--mdmdt-color-2); 444 | border-radius: 8px; 445 | } 446 | 447 | blockquote blockquote { 448 | background: var(--mdmdt-color-1-0-b); 449 | } 450 | 451 | blockquote > blockquote, 452 | blockquote > .md-alert, 453 | .md-alert > .md-alert, 454 | .md-alert > blockquote { 455 | margin-bottom: 0; 456 | } 457 | 458 | p + blockquote, 459 | p + .md-alert { 460 | margin-top: -8px; 461 | } 462 | 463 | .md-alert.md-alert-note > *:first-child, 464 | .md-alert.md-alert-caution > *:first-child, 465 | .md-alert.md-alert-tip > *:first-child, 466 | .md-alert.md-alert-important > *:first-child, 467 | .md-alert.md-alert-warning > *:first-child { 468 | margin-top: 0; 469 | margin-bottom: -4px; 470 | line-height: 1; 471 | } 472 | 473 | .md-alert.md-alert-note > *:last-child, 474 | .md-alert.md-alert-caution > *:last-child, 475 | .md-alert.md-alert-tip > *:last-child, 476 | .md-alert.md-alert-important > *:last-child, 477 | .md-alert.md-alert-warning > *:last-child { 478 | margin-bottom: 0; 479 | } 480 | 481 | .md-alert.md-alert-note { 482 | color: var(--mdmdt-color-1); 483 | background: var(--mdmdt-color-1-0-b); 484 | border-left-color: var(--mdmdt-color-1); 485 | } 486 | 487 | .md-alert.md-alert-note .md-alert-text-note { 488 | color: var(--mdmdt-color-1); 489 | } 490 | 491 | .md-alert.md-alert-caution { 492 | color: var(--mdmdt-color-2); 493 | background: var(--mdmdt-color-2-0-b); 494 | border-left-color: var(--mdmdt-color-2); 495 | } 496 | 497 | .md-alert.md-alert-caution .md-alert-text-caution { 498 | color: var(--mdmdt-color-2); 499 | } 500 | 501 | .md-alert.md-alert-tip { 502 | color: var(--mdmdt-color-3); 503 | background: var(--mdmdt-color-3-0-b); 504 | border-left-color: var(--mdmdt-color-3); 505 | } 506 | 507 | .md-alert.md-alert-tip .md-alert-text-tip { 508 | color: var(--mdmdt-color-3); 509 | } 510 | 511 | .md-alert.md-alert-important { 512 | color: var(--mdmdt-color-4); 513 | background: var(--mdmdt-color-4-0-b); 514 | border-left-color: var(--mdmdt-color-4); 515 | } 516 | 517 | .md-alert.md-alert-important .md-alert-text-important { 518 | color: var(--mdmdt-color-4); 519 | } 520 | 521 | .md-alert.md-alert-warning { 522 | color: var(--mdmdt-color-5); 523 | background: var(--mdmdt-color-5-0-b); 524 | border-left-color: var(--mdmdt-color-5); 525 | } 526 | 527 | .md-alert.md-alert-warning .md-alert-text-warning { 528 | color: var(--mdmdt-color-5); 529 | } 530 | 531 | /* 532 | * ------------------------ 533 | * sup, sub, kbd 534 | * ------------------------ 535 | */ 536 | sup, 537 | sub { 538 | font-size: 12px; 539 | } 540 | 541 | sup.md-footnote { 542 | padding: 0 2px; 543 | color: var(--mdmdt-color-1); 544 | background: transparent; 545 | } 546 | 547 | sup.md-footnote:hover { 548 | color: var(--mdmdt-color-2); 549 | cursor: pointer; 550 | } 551 | 552 | kbd { 553 | display: inline-block; 554 | padding: 0 6px; 555 | font-size: 14px; 556 | color: var(--mdmdt-bg-color); 557 | background: var(--mdmdt-title-color); 558 | border: none; 559 | border-radius: 5px; 560 | border-image: none; 561 | box-shadow: none; 562 | box-decoration-break: clone; 563 | } 564 | 565 | ::selection { 566 | background: var(--mdmdt-color-2-0-c); 567 | box-decoration-break: clone; 568 | } 569 | 570 | /* 571 | * ------------------------ 572 | * table 573 | * ------------------------ 574 | */ 575 | figure.table-figure { 576 | margin-top: -5px; 577 | } 578 | 579 | p + .table-figure { 580 | margin-top: -8px; 581 | } 582 | 583 | table { 584 | width: 100%; 585 | padding: 0 !important; 586 | margin-top: 16px; 587 | overflow: hidden; 588 | text-align: left; 589 | border-spacing: 0; 590 | border-collapse: separate; 591 | border: 1px solid var(--mdmdt-border-color) !important; 592 | border-radius: 8px; 593 | } 594 | 595 | table thead tr { 596 | background: var(--mdmdt-table-header-bg); 597 | } 598 | 599 | table tbody tr:nth-child(even) { 600 | background: var(--mdmdt-table-tr-bg); 601 | } 602 | 603 | table tbody tr:nth-child(odd) { 604 | background: var(--mdmdt-bg-color); 605 | } 606 | 607 | table tr th, 608 | table tr td { 609 | padding: 10px !important; 610 | } 611 | 612 | table tr td, 613 | table thead tr th { 614 | border-left: 1px solid var(--mdmdt-border-color) !important; 615 | } 616 | 617 | table tbody tr td:first-of-type, 618 | table thead tr th:first-of-type { 619 | border-left: none !important; 620 | } 621 | 622 | .md-grid-board-wrap table { 623 | border: none !important; 624 | border-radius: 0; 625 | } 626 | 627 | .md-grid-board-wrap table tr th, 628 | .md-grid-board-wrap table tr td { 629 | padding: 0 !important; 630 | } 631 | 632 | #md-grid-width, 633 | #md-grid-height { 634 | padding: 1px 0; 635 | text-align: center !important; 636 | } 637 | 638 | button#md-resize-grid { 639 | display: block !important; 640 | width: 100%; 641 | margin: 5px auto 0; 642 | color: var(--mdmdt-bg-color); 643 | } 644 | 645 | button#md-resize-grid:hover { 646 | color: var(--mdmdt-color-1); 647 | border-color: var(--mdmdt-color-1); 648 | } 649 | 650 | .md-tooltip-remove { 651 | position: absolute; 652 | z-index: 200; 653 | margin-top: -27px !important; 654 | background: var(--mdmdt-bg-color); 655 | } 656 | 657 | .md-table-resize-popover .md-reset > a::before { 658 | display: none !important; 659 | } 660 | 661 | /* 662 | * ------------------------ 663 | * btn 664 | * ------------------------ 665 | */ 666 | .btn, 667 | .btn-default, 668 | .long-btn { 669 | outline: none !important; 670 | background: var(--mdmdt-bg-color2) !important; 671 | border: 1px solid var(--mdmdt-border-color) !important; 672 | border-radius: 5px !important; 673 | } 674 | 675 | .btn-primary { 676 | color: #fff; 677 | background: var(--mdmdt-color-1) !important; 678 | border-color: var(--mdmdt-color-1) !important; 679 | } 680 | 681 | .btn:hover, 682 | .long-btn:hover { 683 | color: var(--mdmdt-color-1); 684 | background: var(--mdmdt-color-1-0-b) !important; 685 | border-color: var(--mdmdt-color-1) !important; 686 | } 687 | 688 | .btn:focus, 689 | .long-btn:focus { 690 | outline: none !important; 691 | } 692 | 693 | .btn { 694 | margin-right: 5px !important; 695 | } 696 | 697 | .btn:last-of-type { 698 | margin-right: 0; 699 | } 700 | 701 | .dropdown-toggle::after { 702 | font-size: 14px; 703 | } 704 | 705 | .dropdown-toggle:hover::after { 706 | color: var(--mdmdt-color-1); 707 | } 708 | 709 | /* 710 | * ------------------------ 711 | * select 712 | * ------------------------ 713 | */ 714 | select { 715 | padding: 6px !important; 716 | border: 1px solid var(--mdmdt-border-color) !important; 717 | border-radius: 5px !important; 718 | } 719 | 720 | option { 721 | background: var(--mdmdt-bg-color); 722 | } 723 | 724 | /* 725 | * ------------------------ 726 | * textarea 727 | * ------------------------ 728 | */ 729 | textarea { 730 | padding: 5px; 731 | color: var(--mdmdt-text-color) !important; 732 | outline: none; 733 | background: var(--mdmdt-bg-color) !important; 734 | border-color: var(--mdmdt-border-color); 735 | border-radius: 5px; 736 | } 737 | 738 | /* 739 | * ------------------------ 740 | * input 741 | * ------------------------ 742 | */ 743 | html input, 744 | input { 745 | position: relative; 746 | padding: 0 6px; 747 | line-height: 1.2; 748 | outline: none !important; 749 | border: 1px solid var(--mdmdt-border-color); 750 | border-radius: 5px !important; 751 | } 752 | 753 | html input:focus, 754 | html textarea:focus, 755 | html input[type='number']:focus, 756 | html input[type='search']:focus, 757 | html input[type='text']:focus { 758 | outline: none !important; 759 | border-color: var(--mdmdt-color-1) !important; 760 | box-shadow: none !important; 761 | } 762 | 763 | input::placeholder { 764 | font-size: 14px; 765 | color: var(--mdmdt-text-grey) !important; 766 | } 767 | 768 | /* input checkbox */ 769 | input[type='checkbox'], 770 | input[type='radio'] { 771 | box-sizing: border-box; 772 | width: 14px !important; 773 | height: 14px !important; 774 | padding: 0 !important; 775 | margin-right: 10px !important; 776 | margin-bottom: -2px !important; 777 | appearance: none; 778 | outline: none; 779 | list-style: none; 780 | background: transparent !important; 781 | border: 1px solid var(--mdmdt-border-color) !important; 782 | border-radius: 3px !important; 783 | } 784 | 785 | input[type='checkbox']:checked, 786 | input[type='radio']:checked { 787 | background: var(--mdmdt-color-1) !important; 788 | border-color: transparent !important; 789 | } 790 | 791 | input[type='checkbox']:checked::after { 792 | position: absolute; 793 | bottom: 2.5px; 794 | left: 3.5px; 795 | box-sizing: border-box; 796 | display: block; 797 | width: 5px; 798 | height: 9px; 799 | content: ''; 800 | background: transparent; 801 | border: 2px solid #fff; 802 | border-top: none; 803 | border-left: none; 804 | transform: rotate(40deg); 805 | } 806 | 807 | input[type='radio'] { 808 | border: 1px solid var(--mdmdt-border-color) !important; 809 | border-radius: 50% !important; 810 | } 811 | 812 | input[type='radio']:checked::after { 813 | box-sizing: border-box; 814 | display: block; 815 | width: 5px; 816 | height: 5px; 817 | margin: 3.5px auto; 818 | content: ''; 819 | background: #fff; 820 | border-radius: 50%; 821 | } 822 | 823 | span.md-comment { 824 | color: var(--mdmdt-md-char-color); 825 | opacity: 1; 826 | } 827 | 828 | .md-image-btn.selected, 829 | .md-image-btn:hover { 830 | background: var(--mdmdt-color-1); 831 | } 832 | 833 | #write pre.md-meta-block:empty::before { 834 | color: var(--mdmdt-text-code); 835 | } 836 | 837 | /* md-htmlblock */ 838 | .md-htmlblock-panel, 839 | .md-htmlblock-container, 840 | .md-htmlblock-container .md-htmlblock-panel-placeholder { 841 | background: transparent; 842 | } 843 | 844 | .md-htmlblock-container, 845 | .md-math-container { 846 | background: var(--mdmdt-bg-color); 847 | border-radius: 8px; 848 | } 849 | 850 | .md-math-container { 851 | width: 100%; 852 | padding: 18px; 853 | } 854 | 855 | .md-rawblock-on-edit .md-htmlblock-panel, 856 | .md-rawblock-on-edit .md-mathblock-panel { 857 | padding: 18px; 858 | background: var(--mdmdt-bg-color2); 859 | border-radius: 8px; 860 | } 861 | 862 | .md-mathblock-panel .md-rawblock-control { 863 | background: transparent; 864 | } 865 | 866 | .md-mathblock-panel .code-tooltip { 867 | margin: 16px 6px 0; 868 | border-top: 1px solid var(--mdmdt-border-color); 869 | border-radius: 0; 870 | box-shadow: none; 871 | } 872 | 873 | .code-tooltip { 874 | box-shadow: none; 875 | } 876 | 877 | .code-tooltip .ty-input { 878 | border-color: var(--mdmdt-border-color); 879 | } 880 | 881 | .md-rawblock-on-edit .md-rawblock-input { 882 | padding: 0; 883 | background: transparent; 884 | } 885 | 886 | .md-rawblock .md-rawblock-tooltip { 887 | float: right; 888 | height: auto; 889 | margin-top: -10px; 890 | background: var(--mdmdt-bg-color2); 891 | border-radius: 5px; 892 | animation: none !important; 893 | } 894 | 895 | .md-rawblock .md-rawblock-tooltip span { 896 | padding-top: 4px; 897 | padding-bottom: 4px; 898 | } 899 | 900 | .md-rawblock .md-rawblock-tooltip-edit-btn:hover { 901 | background: transparent; 902 | } 903 | 904 | p:has(+ .md-math-block) { 905 | margin-bottom: -5px; 906 | } 907 | 908 | .md-htmlblock:hover .md-htmlblock-container, 909 | .md-htmlblock:hover .md-rawblock-tooltip, 910 | .md-math-block:hover .md-math-container, 911 | .md-math-block:hover .md-rawblock-tooltip, 912 | .md-rawblock-on-edit:hover .md-rawblock-tooltip { 913 | background: var(--mdmdt-bg-color2); 914 | } 915 | 916 | .md-inline-math script { 917 | color: #f9007c; 918 | } 919 | 920 | /* #write */ 921 | #write { 922 | width: 100%; 923 | max-width: 1440px; 924 | padding: 32px 96px; 925 | margin: 0 auto; 926 | overflow-x: auto; 927 | scroll-padding: 10px; 928 | scroll-behavior: smooth; 929 | } 930 | 931 | #write > *:first-child { 932 | margin-top: 0; 933 | } 934 | 935 | #typora-sidebar { 936 | border-right-color: var(--mdmdt-border-color); 937 | } 938 | 939 | /* 940 | * ------------------------ 941 | * typora-source 942 | * ------------------------ 943 | */ 944 | #typora-source pre { 945 | white-space: pre-wrap !important; 946 | } 947 | 948 | #typora-source .CodeMirror-sizer { 949 | padding-right: 0 !important; 950 | } 951 | 952 | #typora-source .CodeMirror-lines { 953 | box-sizing: border-box; 954 | width: 100%; 955 | max-width: 1400px; 956 | padding: 32px 100px; 957 | margin: 0 auto; 958 | } 959 | 960 | #typora-source .CodeMirror-lines pre { 961 | padding: 8px 16px !important; 962 | font-size: 16px !important; 963 | background: var(--mdmdt-bg-color) !important; 964 | border: none !important; 965 | } 966 | 967 | #typora-source .CodeMirror-activeline pre { 968 | background: var(--mdmdt-bg-color2) !important; 969 | border-radius: 6px; 970 | } 971 | 972 | #typora-source .CodeMirror-activeline .CodeMirror-linebackground { 973 | background: transparent !important; 974 | } 975 | 976 | #typora-source .CodeMirror-lines .cm-header { 977 | color: var(--mdmdt-color-2); 978 | } 979 | 980 | .CodeMirror.cm-s-typora-default div.CodeMirror-cursor { 981 | border-left: 3px solid var(--mdmdt-color-2); 982 | } 983 | 984 | .cm-s-typora-default .cm-header, 985 | .cm-s-typora-default .cm-property, 986 | .cm-s-typora-default .cm-link { 987 | color: var(--mdmdt-color-2); 988 | } 989 | 990 | .cm-s-inner .cm-comment, 991 | .cm-s-inner.cm-comment, 992 | .cm-overlay { 993 | color: var(--mdmdt-md-char-color); 994 | } 995 | 996 | .cm-s-typora-default .cm-string { 997 | color: var(--mdmdt-color-1); 998 | } 999 | 1000 | .cm-s-typora-default .cm-code, 1001 | .cm-s-typora-default .cm-comment { 1002 | color: var(--mdmdt-text-code); 1003 | } 1004 | 1005 | .cm-s-typora-default .cm-tag { 1006 | color: #e31570; 1007 | } 1008 | 1009 | .cm-attribute { 1010 | color: var(--mdmdt-color-2); 1011 | } 1012 | 1013 | /* 1014 | * ------------------------ 1015 | * #toc-dropmenu 1016 | * ------------------------ 1017 | */ 1018 | #toc-dropmenu { 1019 | top: calc(var(--title-bar-height) + 8px) !important; 1020 | right: 18px; 1021 | border-top-left-radius: 8px; 1022 | border-bottom-left-radius: 8px; 1023 | } 1024 | 1025 | #toc-dropmenu #pin-outline-btn { 1026 | top: 10px; 1027 | display: inline-block; 1028 | } 1029 | 1030 | #toc-dropmenu .divider { 1031 | margin-bottom: 0; 1032 | } 1033 | 1034 | /* 1035 | * ------------------------ 1036 | * sidebar 1037 | * toc-content 1038 | * ------------------------ 1039 | */ 1040 | .outline-content { 1041 | padding: 16px; 1042 | font-size: 14px !important; 1043 | } 1044 | 1045 | .outline-content li { 1046 | position: relative; 1047 | z-index: 30; 1048 | margin-bottom: 0; 1049 | line-height: 1.3 !important; 1050 | } 1051 | 1052 | .outline-content li::before { 1053 | position: absolute; 1054 | top: 0; 1055 | left: 0; 1056 | z-index: 30; 1057 | width: 1px; 1058 | height: calc(100% + 5px); 1059 | content: ''; 1060 | border-left: 1px solid var(--mdmdt-border-color); 1061 | } 1062 | 1063 | .outline-content > li:first-of-type::before { 1064 | top: 0; 1065 | } 1066 | 1067 | .outline-content li a { 1068 | font-size: 14px !important; 1069 | } 1070 | 1071 | .outline-content li ul { 1072 | position: relative; 1073 | z-index: 48; 1074 | margin-top: 0 !important; 1075 | margin-left: 20px; 1076 | } 1077 | 1078 | .outline-content ul li::before { 1079 | top: -10px; 1080 | width: 1px; 1081 | height: calc(100% + 10px); 1082 | content: ''; 1083 | border-left: 1px solid var(--mdmdt-border-color); 1084 | } 1085 | 1086 | .outline-content ul > li:last-of-type::before { 1087 | top: -10px; 1088 | left: 0; 1089 | width: 1px; 1090 | height: 10px; 1091 | content: ''; 1092 | border-left: 1px solid var(--mdmdt-border-color); 1093 | } 1094 | 1095 | .outline-content li .outline-item { 1096 | position: relative; 1097 | z-index: 50; 1098 | padding: 4px 8px; 1099 | margin: 0 0 3px 7px; 1100 | border: 1px solid transparent; 1101 | border-radius: 5px; 1102 | } 1103 | 1104 | .outline-item > .outline-label { 1105 | text-decoration: none; 1106 | } 1107 | 1108 | .outline-content li .outline-item:hover, 1109 | .outline-content li .outline-item-active { 1110 | z-index: 70; 1111 | } 1112 | 1113 | .outline-content li .outline-item::before { 1114 | position: absolute; 1115 | top: -11.5px; 1116 | left: -8px; 1117 | z-index: 38; 1118 | width: 12px; 1119 | height: calc(50% + 12px); 1120 | content: ''; 1121 | background: transparent; 1122 | border-bottom: 1px solid var(--mdmdt-border-color); 1123 | border-left: 1px solid var(--mdmdt-border-color); 1124 | } 1125 | 1126 | .outline-content > li:first-of-type > .outline-item::before { 1127 | top: calc(50% - 1px) !important; 1128 | border-top: 1px solid var(--mdmdt-border-color); 1129 | border-bottom: none; 1130 | border-top-left-radius: 3px; 1131 | border-bottom-left-radius: 0; 1132 | } 1133 | 1134 | .outline-content > li:first-of-type > .outline-item::after { 1135 | position: absolute; 1136 | top: -2px; 1137 | left: -8px; 1138 | z-index: 35; 1139 | width: 1px; 1140 | height: 100%; 1141 | content: ''; 1142 | border-left: 1px solid var(--mdmdt-bg-color); 1143 | } 1144 | 1145 | .outline-content > li:last-of-type > .outline-item::before, 1146 | .outline-content > li ul > li:last-of-type > .outline-item::before { 1147 | z-index: 90; 1148 | background: transparent; 1149 | border-top: none !important; 1150 | border-bottom: 1px solid var(--mdmdt-border-color); 1151 | border-left: 1px solid var(--mdmdt-border-color); 1152 | border-top-left-radius: 0; 1153 | border-bottom-left-radius: 3px; 1154 | } 1155 | 1156 | .outline-content > li:only-of-type > .outline-item::before, 1157 | .outline-content > li:last-of-type::before { 1158 | display: none; 1159 | } 1160 | 1161 | .outline-content > li:only-of-type { 1162 | margin-left: -6px; 1163 | } 1164 | 1165 | .outline-expander { 1166 | width: auto; 1167 | height: 8px; 1168 | padding-left: 0; 1169 | background: transparent; 1170 | } 1171 | 1172 | .outline-expander::before { 1173 | padding: 0 4px 0 2px; 1174 | margin-top: -2px; 1175 | margin-left: -2px; 1176 | font-size: 10px; 1177 | background: transparent; 1178 | } 1179 | 1180 | .outline-item:hover, 1181 | .outline-item:hover > .outline-expander::before, 1182 | .outline-item:hover > .outline-expander { 1183 | color: var(--mdmdt-text-color) !important; 1184 | background: var(--mdmdt-bg-color2) !important; 1185 | } 1186 | 1187 | li > .outline-item-active { 1188 | color: var(--mdmdt-color-1); 1189 | background: var(--mdmdt-bg-color2); 1190 | } 1191 | 1192 | .outline-item:hover::before, 1193 | .outline-item-active::before { 1194 | width: 7px !important; 1195 | } 1196 | 1197 | .outline-item-active .outline-expander { 1198 | font-weight: 900 !important; 1199 | } 1200 | 1201 | /* ty-on-outline-filter(catalog search style) */ 1202 | .ty-on-outline-filter .outline-content li::before, 1203 | .ty-on-outline-filter .outline-content .outline-item::before, 1204 | .ty-on-outline-filter .outline-content .outline-item::after { 1205 | display: none; 1206 | } 1207 | 1208 | .ty-on-outline-filter .outline-content > li > ul, 1209 | .ty-on-outline-filter .outline-content .outline-item { 1210 | margin-left: 0; 1211 | } 1212 | 1213 | /* 1214 | * -------------------------- 1215 | * file-library-list 1216 | * file-library-search-result 1217 | * -------------------------- 1218 | */ 1219 | #file-library-list, 1220 | #file-library-search-result { 1221 | padding: 12px; 1222 | } 1223 | 1224 | /* ty-search-item */ 1225 | #file-library-list .file-list-item, 1226 | #file-library-search-result .ty-search-item { 1227 | margin-bottom: 8px; 1228 | border: 1px solid transparent; 1229 | border-bottom-color: var(--mdmdt-border-color); 1230 | } 1231 | 1232 | #file-library-list .file-list-item { 1233 | padding-right: 12px; 1234 | padding-left: 12px; 1235 | } 1236 | 1237 | #file-library-list .file-list-item.active, 1238 | #file-library-search-result .ty-search-item.active { 1239 | border: 1px solid var(--mdmdt-color-1); 1240 | border-radius: 5px; 1241 | } 1242 | 1243 | #file-library-list .file-list-item.active { 1244 | padding: 6px 12px; 1245 | } 1246 | 1247 | #file-library-search-result .ty-search-item.active { 1248 | padding: 6px 12px 6px 0; 1249 | } 1250 | 1251 | #file-library-list .file-list-item:first-of-type.active, 1252 | #file-library-search-result .ty-search-item:first-of-type.active { 1253 | margin-top: 0; 1254 | } 1255 | 1256 | #file-library-list .file-list-item:hover, 1257 | #file-library-search-result .ty-search-item:hover { 1258 | background: var(--mdmdt-bg-color2); 1259 | border-color: var(--mdmdt-border-color); 1260 | border-radius: 5px; 1261 | } 1262 | 1263 | #file-library-list .file-list-item:hover { 1264 | color: var(--mdmdt-color-1); 1265 | } 1266 | 1267 | #file-library-search-result .ty-search-item.active { 1268 | color: var(--mdmdt-text-color); 1269 | } 1270 | 1271 | #file-library-search-result .ty-search-item.active .ty-search-item-line { 1272 | word-break: break-word; 1273 | white-space: normal; 1274 | } 1275 | 1276 | .file-list-item-time { 1277 | padding-right: 0; 1278 | margin-right: 0; 1279 | } 1280 | 1281 | .file-list-item-count { 1282 | padding: 0 6px; 1283 | margin-right: 0; 1284 | border-radius: 3px; 1285 | } 1286 | 1287 | #file-library-search-result .ty-search-item.active .file-list-item-count { 1288 | padding-right: 0; 1289 | } 1290 | 1291 | #sidebar-loading-template { 1292 | display: none; 1293 | padding: 8px; 1294 | margin-bottom: 5px; 1295 | border-radius: 5px; 1296 | } 1297 | 1298 | .ty-file-search-match-text { 1299 | background-color: var(--mdmdt-color-2-0-c); 1300 | } 1301 | 1302 | /* 1303 | * ------------------------ 1304 | * file-library-tree 1305 | * ------------------------ 1306 | */ 1307 | #file-library-tree { 1308 | box-sizing: border-box; 1309 | padding-top: 8px; 1310 | padding-right: 12px; 1311 | padding-left: 12px; 1312 | } 1313 | 1314 | .sidebar-tab-btn { 1315 | margin-top: 12px !important; 1316 | font-size: 16px !important; 1317 | line-height: 20px !important; 1318 | vertical-align: text-top; 1319 | color: var(--mdmdt-title-color); 1320 | } 1321 | 1322 | .sidebar-tab-btn:hover { 1323 | color: var(--mdmdt-color-1); 1324 | } 1325 | 1326 | .ty-sidebar-search-panel .searchpanel-search-option-btn { 1327 | top: 8px; 1328 | } 1329 | 1330 | .file-tree-node { 1331 | position: relative; 1332 | padding-left: 0; 1333 | margin-left: 0; 1334 | } 1335 | 1336 | .file-library-file-node:hover .file-node-background { 1337 | background: var(--mdmdt-bg-color2); 1338 | } 1339 | 1340 | .file-library-file-node:hover .file-node-content { 1341 | cursor: pointer; 1342 | } 1343 | 1344 | .file-library-file-node:hover .file-node-icon::after { 1345 | width: 8px !important; 1346 | } 1347 | 1348 | .file-node-expanded > .file-node-children { 1349 | margin-left: 29.5px; 1350 | } 1351 | 1352 | .file-tree-node > .file-node-background { 1353 | margin-left: -12px; 1354 | border: none !important; 1355 | border-radius: 5px; 1356 | } 1357 | 1358 | #file-library-tree .file-node-root > .file-node-content { 1359 | margin-left: -3px !important; 1360 | } 1361 | 1362 | #file-library-tree .file-tree-node > .file-node-content { 1363 | margin-left: -15px; 1364 | } 1365 | 1366 | #file-library-tree .file-node-collapsed > .file-node-content, 1367 | #file-library-tree .file-node-expanded > .file-node-content { 1368 | margin-left: -9px; 1369 | } 1370 | 1371 | .file-library-node:not(.file-node-root):focus > .file-node-content { 1372 | outline: none; 1373 | } 1374 | 1375 | .os-windows #file-library-tree .file-node-content { 1376 | padding-right: 37px; 1377 | margin-bottom: 3px; 1378 | } 1379 | 1380 | .file-library-node:not(.file-node-root):focus > .file-node-background { 1381 | border: 1px dashed var(--mdmdt-text-color) !important; 1382 | } 1383 | 1384 | .file-library-node.active:not(.file-node-root):focus > .file-node-background { 1385 | border: 1px dashed var(--mdmdt-color-1) !important; 1386 | } 1387 | 1388 | .fa-folder::before, 1389 | .fa-folder::after, 1390 | .fa-caret-right, 1391 | .fa-caret-down { 1392 | color: var(--mdmdt-text-grey); 1393 | } 1394 | 1395 | .file-node-content .file-node-title { 1396 | padding-right: 9px; 1397 | } 1398 | 1399 | .file-library-file-node.active .file-node-title { 1400 | font-weight: 700; 1401 | } 1402 | 1403 | #file-library-tree .file-node-root > .file-node-children { 1404 | margin-left: 26.5px; 1405 | } 1406 | 1407 | #file-library-tree .file-tree-node { 1408 | position: relative; 1409 | } 1410 | 1411 | .file-node-expanded > .file-node-content .fa-caret-down { 1412 | margin-right: 1px; 1413 | } 1414 | 1415 | #file-library-tree .file-tree-node .file-node-icon { 1416 | padding-left: 3px; 1417 | } 1418 | 1419 | #file-library-tree .file-node-open-state i { 1420 | position: relative; 1421 | z-index: 30; 1422 | padding-left: 4px; 1423 | } 1424 | 1425 | #file-library-tree .file-node-content::before { 1426 | position: absolute; 1427 | top: -8px; 1428 | left: -20px; 1429 | z-index: 20; 1430 | display: block; 1431 | width: 1px; 1432 | height: calc(100% + 5px); 1433 | content: ''; 1434 | border-left: 1px solid var(--mdmdt-border-color); 1435 | } 1436 | 1437 | #file-library-tree .file-node-content > .file-node-icon::before { 1438 | position: relative; 1439 | z-index: 35; 1440 | } 1441 | 1442 | #file-library-tree .file-node-content > .file-node-icon::after { 1443 | position: absolute; 1444 | top: 14px; 1445 | left: -20px; 1446 | z-index: 20; 1447 | width: 12px; 1448 | height: 16px; 1449 | content: ''; 1450 | background: transparent; 1451 | border-top: 1px solid var(--mdmdt-border-color); 1452 | border-left: 1px solid var(--mdmdt-border-color); 1453 | } 1454 | 1455 | #file-library-tree .active > .file-node-content > .file-node-icon::after { 1456 | width: 8px; 1457 | } 1458 | 1459 | #file-library-tree .file-node-expanded > .file-node-content > .file-node-icon::after { 1460 | top: 15px; 1461 | left: -20px; 1462 | width: 8px; 1463 | } 1464 | 1465 | #file-library-tree .file-node-collapsed > .file-node-content > .file-node-icon::after { 1466 | top: 14px; 1467 | left: -20px; 1468 | width: 12px; 1469 | } 1470 | 1471 | .os-windows #file-library-tree .file-node-collapsed > .file-node-content > .file-node-icon::after { 1472 | top: 15px; 1473 | } 1474 | 1475 | #file-library-tree .file-node-children > div:last-of-type > .file-node-content > .file-node-icon::after { 1476 | top: -1px; 1477 | border-top: none; 1478 | border-bottom: 1px solid var(--mdmdt-border-color); 1479 | border-bottom-left-radius: 3px; 1480 | } 1481 | 1482 | #file-library-tree .file-node-children > div:last-of-type > .file-node-content::before { 1483 | top: -9px; 1484 | height: 10px; 1485 | } 1486 | 1487 | .file-node-root > .file-node-content > .file-node-icon::after { 1488 | display: none; 1489 | } 1490 | 1491 | /* 1492 | * ------------------------ 1493 | * md-notification-container 1494 | * ------------------------ 1495 | */ 1496 | .os-windows .md-notification-container { 1497 | padding-top: 10px; 1498 | background: var(--mdmdt-bg-color); 1499 | box-shadow: 0 1px 3px 0 rgb(0 0 0 / 15%); 1500 | } 1501 | 1502 | #md-notification { 1503 | padding-top: calc(var(--title-bar-height) + 5px) !important; 1504 | padding-bottom: 8px; 1505 | } 1506 | 1507 | .typora-export-spinner { 1508 | margin-top: calc(var(--title-bar-height) + 5px) !important; 1509 | } 1510 | 1511 | #md-notification::before { 1512 | top: 13px; 1513 | font-size: 18px; 1514 | } 1515 | 1516 | .os-windows #md-notification::before { 1517 | top: 33px; 1518 | } 1519 | 1520 | #md-notification p { 1521 | margin: 5px 0 0; 1522 | font-size: 14px; 1523 | } 1524 | 1525 | #md-notification .btn { 1526 | margin: 0; 1527 | } 1528 | 1529 | #md-notification-content span { 1530 | margin-bottom: 4px; 1531 | margin-left: 8px; 1532 | } 1533 | 1534 | .typora-search-spinner > div { 1535 | background: var(--mdmdt-color-1); 1536 | } 1537 | 1538 | /* 1539 | * ------------------------ 1540 | * TOC md-toc 1541 | * ------------------------ 1542 | */ 1543 | .md-toc { 1544 | padding: 26px 32px; 1545 | margin: 14px 0 0; 1546 | font-size: 14px; 1547 | background: var(--mdmdt-bg-color2); 1548 | } 1549 | 1550 | p + .md-toc { 1551 | margin-top: -8px; 1552 | } 1553 | 1554 | .md-toc-content { 1555 | padding: 0; 1556 | margin: 0; 1557 | } 1558 | 1559 | .md-toc:focus .md-toc-content { 1560 | margin: 0 !important; 1561 | border: none !important; 1562 | } 1563 | 1564 | .md-toc-content .md-toc-item { 1565 | position: relative; 1566 | padding-bottom: 8px; 1567 | } 1568 | 1569 | .md-toc-content .md-toc-item a::before { 1570 | display: none !important; 1571 | } 1572 | 1573 | #write div.md-toc-tooltip { 1574 | position: relative; 1575 | padding-bottom: 12px; 1576 | margin: 14px 0 0; 1577 | background: transparent; 1578 | border-top: none; 1579 | border-bottom: 1px solid var(--mdmdt-border-color); 1580 | } 1581 | 1582 | #write div.md-toc-tooltip .md-delete-toc { 1583 | padding: 0 10px; 1584 | margin: 0 !important; 1585 | background: var(--mdmdt-bg-color); 1586 | } 1587 | 1588 | #write div.md-toc-tooltip .md-delete-toc:hover { 1589 | color: var(--mdmdt-color-1); 1590 | background: var(--mdmdt-bg-color2); 1591 | } 1592 | 1593 | /* 1594 | * ------------------------ 1595 | * code colors 1596 | * ------------------------ 1597 | */ 1598 | .cm-s-inner { 1599 | color: var(--mdmdt-text-code); 1600 | background-color: transparent !important; 1601 | } 1602 | 1603 | .cm-s-inner .CodeMirror-gutters { 1604 | color: var(--mdmdt-text-code); 1605 | border-right-color: var(--mdmdt-border-color); 1606 | } 1607 | 1608 | .cm-s-inner .CodeMirror-guttermarker, 1609 | .cm-s-inner .CodeMirror-guttermarker-subtle, 1610 | .cm-s-inner .CodeMirror-linenumber { 1611 | color: var(--mdmdt-md-char-color); 1612 | } 1613 | 1614 | .cm-s-inner .CodeMirror-cursor { 1615 | border-left: 1px solid var(--mdmdt-border-color); 1616 | } 1617 | 1618 | .cm-s-inner div.CodeMirror-selected, 1619 | .cm-s-inner.CodeMirror-focused div.CodeMirror-selected, 1620 | .cm-s-inner .CodeMirror-line::selection, 1621 | .cm-s-inner .CodeMirror-line > span::selection, 1622 | .cm-s-inner .CodeMirror-line > span > span::selection, 1623 | .cm-s-inner .CodeMirror-line::selection, 1624 | .cm-s-inner .CodeMirror-line > span::selection, 1625 | .cm-s-inner .CodeMirror-line > span > span::selection { 1626 | background: var(--mdmdt-color-2-0-c); 1627 | } 1628 | 1629 | .cm-s-inner .CodeMirror-activeline-background { 1630 | background: transparent; 1631 | } 1632 | 1633 | .cm-s-inner .cm-keyword { 1634 | color: #bb59fd; 1635 | } 1636 | 1637 | .cm-s-inner .cm-operator { 1638 | color: #40d7ec; 1639 | } 1640 | 1641 | .cm-s-inner .cm-variable { 1642 | color: #f4395dff; 1643 | } 1644 | 1645 | .cm-s-inner .cm-variable-2 { 1646 | color: #e2785f; 1647 | } 1648 | 1649 | .cm-s-inner .cm-variable-3 { 1650 | color: #6083ff; 1651 | } 1652 | 1653 | .cm-s-inner .cm-builtin { 1654 | color: #f61d78; 1655 | } 1656 | 1657 | .cm-s-inner .cm-atom { 1658 | color: #fa5336; 1659 | } 1660 | 1661 | .cm-s-inner .cm-number { 1662 | color: #f59102; 1663 | } 1664 | 1665 | .cm-s-inner .cm-def { 1666 | color: #3876eb; 1667 | } 1668 | 1669 | .cm-s-inner .cm-string { 1670 | color: #02be74; 1671 | } 1672 | 1673 | .cm-s-inner .cm-string-2 { 1674 | color: #0a790a; 1675 | } 1676 | 1677 | .cm-s-inner .cm-comment, 1678 | .cm-s-inner .cm-meta { 1679 | color: var(--mdmdt-md-char-color); 1680 | } 1681 | 1682 | .cm-s-inner .cm-attribute { 1683 | color: #c08b01; 1684 | } 1685 | 1686 | .cm-s-inner .cm-property { 1687 | color: #1b9f72; 1688 | } 1689 | 1690 | .cm-s-inner .cm-qualifier { 1691 | color: #dc7b45; 1692 | } 1693 | 1694 | .cm-s-inner .cm-tag { 1695 | color: #e32e73; 1696 | } 1697 | 1698 | .cm-s-inner .cm-tag.cm-bracket { 1699 | color: #0c9bd3; 1700 | } 1701 | 1702 | .cm-s-inner .cm-header, 1703 | .cm-s-inner.cm-header { 1704 | color: #401df1; 1705 | } 1706 | 1707 | .cm-s-inner .CodeMirror-matchingbracket { 1708 | color: var(--mdmdt-text-code) !important; 1709 | text-decoration: underline; 1710 | } 1711 | 1712 | /* apply to code fences with plan text */ 1713 | .md-fences { 1714 | color: var(--mdmdt-text-grey); 1715 | background-color: var(--mdmdt-bg-color2); 1716 | } 1717 | 1718 | .md-fences .code-tooltip { 1719 | right: 0 !important; 1720 | bottom: -28px !important; 1721 | z-index: 50; 1722 | padding: 0; 1723 | color: var(--mdmdt-text-color); 1724 | background-color: var(--mdmdt-bg-color2); 1725 | border-radius: 5px; 1726 | } 1727 | 1728 | .md-fences .code-tooltip input, 1729 | .md-fences .code-tooltip span { 1730 | padding: 3px; 1731 | margin: 0; 1732 | border-radius: 5px; 1733 | } 1734 | 1735 | /* 1736 | * ------------------------ 1737 | * export html 1738 | * ------------------------ 1739 | */ 1740 | body.typora-export { 1741 | padding-right: 0; 1742 | padding-left: 0; 1743 | } 1744 | 1745 | .typora-export-content, 1746 | .typora-export-show-outline .typora-export-content { 1747 | width: 100vw; 1748 | max-width: 2560px; 1749 | height: 100vh; 1750 | margin: 0 auto; 1751 | } 1752 | 1753 | .typora-export-sidebar { 1754 | margin-top: 0; 1755 | margin-right: 0; 1756 | border-right: 1px solid var(--mdmdt-border-color); 1757 | } 1758 | 1759 | .typora-export-sidebar .outline-content { 1760 | padding-top: 18px; 1761 | } 1762 | 1763 | .typora-export-sidebar .outline-content li ul { 1764 | z-index: 55; 1765 | margin-left: 21px; 1766 | } 1767 | 1768 | .typora-export-sidebar .outline-item-active > .outline-item { 1769 | position: relative; 1770 | z-index: 70; 1771 | } 1772 | 1773 | .typora-export .outline-content li a { 1774 | padding: 2px 0; 1775 | font-weight: normal; 1776 | } 1777 | 1778 | .typora-export .outline-item-active > .outline-item { 1779 | color: var(--mdmdt-color-1); 1780 | background: var(--mdmdt-bg-color2); 1781 | } 1782 | 1783 | .typora-export-sidebar .outline-content ul li::before, 1784 | .typora-export-sidebar .outline-content ul > li:last-of-type::before { 1785 | top: 0; 1786 | } 1787 | 1788 | .typora-export .outline-expander::before { 1789 | box-sizing: border-box; 1790 | width: 7px; 1791 | height: 7px; 1792 | margin-top: 1px; 1793 | margin-right: 5px; 1794 | content: ''; 1795 | background: transparent; 1796 | border-top: 1px solid var(--mdmdt-text-color); 1797 | border-right: 1px solid var(--mdmdt-text-color); 1798 | transform: rotate(45deg); 1799 | } 1800 | 1801 | .typora-export .outline-expander:hover::before { 1802 | box-sizing: border-box; 1803 | width: 7px; 1804 | height: 7px; 1805 | margin-top: -2px; 1806 | margin-right: 5px; 1807 | margin-left: 2px; 1808 | content: ''; 1809 | background: transparent; 1810 | border-top: 1px solid var(--mdmdt-text-color); 1811 | border-right: 1px solid var(--mdmdt-text-color); 1812 | transform: rotate(135deg); 1813 | } 1814 | 1815 | .typora-export .outline-item-open > .outline-item > .outline-expander::before { 1816 | box-sizing: border-box; 1817 | width: 7px; 1818 | height: 7px; 1819 | margin-top: -2px; 1820 | margin-right: 5px; 1821 | margin-left: 2px; 1822 | content: ''; 1823 | background: transparent; 1824 | border-top: 1px solid var(--mdmdt-text-color); 1825 | border-right: 1px solid var(--mdmdt-text-color); 1826 | transform: rotate(135deg); 1827 | } 1828 | 1829 | .typora-export .outline-item:hover { 1830 | margin-right: 0; 1831 | } 1832 | 1833 | .typora-export .outline-item:hover .outline-label { 1834 | color: var(--mdmdt-text-color); 1835 | } 1836 | 1837 | .typora-export-sidebar .outline-content li > .outline-item:hover > .outline-expander::before { 1838 | border-color: var(--mdmdt-text-color); 1839 | } 1840 | 1841 | .typora-export-sidebar .outline-item-active > .outline-item::before { 1842 | width: 7px; 1843 | } 1844 | 1845 | .typora-export-sidebar .outline-item-active::before { 1846 | width: 1px !important; 1847 | } 1848 | 1849 | .typora-export-sidebar .outline-item-active > .outline-item > .outline-expander::before { 1850 | border-color: var(--mdmdt-color-1); 1851 | border-width: 2px !important; 1852 | } 1853 | 1854 | .typora-export-no-collapse-outline .outline-expander { 1855 | display: none; 1856 | } 1857 | 1858 | /** 1859 | * -------------------------------------- 1860 | * unibody-window 1861 | * Control UI on Windows/Linux (optional) 1862 | * -------------------------------------- 1863 | */ 1864 | 1865 | .unibody-window #write ul, 1866 | .typora-export #write ul { 1867 | padding-left: 34px; 1868 | } 1869 | 1870 | .unibody-window #write ol, 1871 | .typora-export #write ol { 1872 | padding-left: 36px; 1873 | } 1874 | 1875 | .unibody-window #write ul ul, 1876 | .unibody-window #write ol ol, 1877 | .unibody-window #write ul > li > ol, 1878 | .unibody-window #write ol > li > ul, 1879 | .typora-export #write ul ul, 1880 | .typora-export #write ol ol, 1881 | .typora-export #write ul > li > ol, 1882 | .typora-export #write ol > li > ul { 1883 | margin-left: 0; 1884 | } 1885 | 1886 | .unibody-window #write ul > li > p, 1887 | .typora-export #write ul > li > p { 1888 | margin: 0; 1889 | } 1890 | 1891 | .unibody-window #write ol > li > p, 1892 | .typora-export #write ol > li > p { 1893 | margin: 0 0 0 -2px; 1894 | } 1895 | 1896 | .unibody-window #write ul > .task-list-item > input, 1897 | .typora-export #write ul > .task-list-item > input { 1898 | margin-left: -22px; 1899 | } 1900 | 1901 | .unibody-window #megamenu-menu-sidebar { 1902 | --mdmdt-bg-color: #1b1b1f; 1903 | --mdmdt-bg-color2: rgb(40 42 50); 1904 | --mdmdt-border-color: rgb(60 62 70); 1905 | 1906 | overflow: hidden; 1907 | } 1908 | 1909 | #megamenu-menu-sidebar .megamenu-menu-list { 1910 | border: none; 1911 | } 1912 | 1913 | #recent-file-panel-search-input, 1914 | .megamenu-menu-panel .btn, 1915 | .dropdown-menu li a { 1916 | border-radius: 5px !important; 1917 | } 1918 | 1919 | .unibody-window .long-btn { 1920 | border-radius: 8px !important; 1921 | } 1922 | 1923 | /* .dropdown-menu */ 1924 | .dropdown-menu { 1925 | padding: 6px !important; 1926 | border: 1px solid var(--mdmdt-border-color); 1927 | border-radius: 8px !important; 1928 | } 1929 | 1930 | .dropdown-menu li { 1931 | border-radius: 5px !important; 1932 | } 1933 | 1934 | .ty-spell-check-panel-item { 1935 | margin-bottom: 4px; 1936 | border-radius: 5px; 1937 | } 1938 | 1939 | .btn-split-group .dropdown-menu { 1940 | margin-top: 0; 1941 | } 1942 | 1943 | .input-group-content .dropdown-menu li:hover { 1944 | background: var(--mdmdt-bg-color2); 1945 | border-radius: 5px; 1946 | } 1947 | 1948 | .dropdown-menu li:hover a { 1949 | color: var(--mdmdt-color-1); 1950 | } 1951 | 1952 | .dropdown-menu li a { 1953 | font-size: 14px; 1954 | } 1955 | 1956 | .dropdown-menu table th, 1957 | .dropdown-menu table td { 1958 | padding: 5px !important; 1959 | } 1960 | 1961 | #sidebar-files-menu li:hover a { 1962 | color: var(--mdmdt-color-1) !important; 1963 | } 1964 | 1965 | #close-sidebar-menu-btn:hover { 1966 | color: var(--mdmdt-color-1); 1967 | } 1968 | 1969 | .unibody-window .long-btn:hover span, 1970 | .unibody-window .long-btn:hover i, 1971 | .megamenu-menu-panel .btn:hover, 1972 | #megamenu-menu-header-title:hover, 1973 | .toolbar-icon:hover, 1974 | #megamenu-back-btn:hover i { 1975 | color: var(--mdmdt-color-1) !important; 1976 | text-decoration: none; 1977 | } 1978 | 1979 | .unibody-window .long-btn:hover, 1980 | .megamenu-menu-panel .btn:hover, 1981 | #megamenu-back-btn:hover { 1982 | background: var(--mdmdt-bg-color2); 1983 | border-color: var(--mdmdt-color-1); 1984 | } 1985 | 1986 | #w-traffic-lights span { 1987 | background: transparent !important; 1988 | border: none !important; 1989 | border-radius: 0 !important; 1990 | } 1991 | 1992 | #w-traffic-lights span:hover { 1993 | background: var(--mdmdt-color-1-0-b) !important; 1994 | } 1995 | 1996 | #w-traffic-lights #w-close:hover { 1997 | color: #fff !important; 1998 | background: var(--mdmdt-color-5) !important; 1999 | } 2000 | 2001 | .os-windows > header, 2002 | .os-windows > header .toolbar-icon.btn { 2003 | height: 26px; 2004 | } 2005 | 2006 | #top-titlebar .btn { 2007 | margin: 0 0 -2px !important; 2008 | } 2009 | 2010 | /* .megamenu-men */ 2011 | .megamenu-menu-panel .btn { 2012 | padding: 6px 12px; 2013 | } 2014 | 2015 | .megamenu-content { 2016 | padding-top: 26px; 2017 | padding-right: 30px; 2018 | } 2019 | 2020 | #open-theme-folder-btn { 2021 | margin-top: 48px; 2022 | } 2023 | 2024 | .megamenu-menu { 2025 | background: var(--mdmdt-bg-color); 2026 | } 2027 | 2028 | .megamenu-menu-header { 2029 | border-color: var(--mdmdt-bg-color2); 2030 | } 2031 | 2032 | #megamenu-back-btn { 2033 | margin-left: 10px; 2034 | border-color: var(--mdmdt-border-color); 2035 | border-radius: 5px; 2036 | } 2037 | 2038 | #megamenu-menu-list { 2039 | padding: 0 8px; 2040 | } 2041 | 2042 | .megamenu-menu-list li { 2043 | margin-top: 6px; 2044 | } 2045 | 2046 | .megamenu-menu-list li a { 2047 | display: block; 2048 | font-size: 16px; 2049 | line-height: 34px; 2050 | border: 1px solid transparent; 2051 | border-radius: 5px; 2052 | } 2053 | 2054 | .megamenu-menu-list li a .fa { 2055 | display: inline-block; 2056 | margin-left: 0; 2057 | font-size: 18px; 2058 | line-height: 38px; 2059 | opacity: 1; 2060 | } 2061 | 2062 | .megamenu-menu-list li #m-saved .fa { 2063 | font-size: 20px; 2064 | line-height: 36px !important; 2065 | color: var(--mdmdt-color-2); 2066 | } 2067 | 2068 | .megamenu-menu-list li a:hover, 2069 | .megamenu-menu-list li a.active:hover { 2070 | color: var(--mdmdt-color-1) !important; 2071 | background: var(--mdmdt-bg-color2) !important; 2072 | border: 1px solid var(--mdmdt-color-1); 2073 | } 2074 | 2075 | .megamenu-menu-list li a:hover, 2076 | .megamenu-menu-list li a.active { 2077 | background: var(--mdmdt-bg-color2); 2078 | } 2079 | 2080 | .megamenu-menu-list li a.active { 2081 | color: var(--mdmdt-color-1) !important; 2082 | } 2083 | 2084 | @media (width <= 530px) { 2085 | #megamenu-back-btn { 2086 | margin-left: 0; 2087 | } 2088 | 2089 | .megamenu-menu-list li a i { 2090 | margin-left: 10px; 2091 | } 2092 | } 2093 | 2094 | #recent-file-panel-action-btn-container { 2095 | margin-right: 0; 2096 | } 2097 | 2098 | #recent-file-panel-search-input { 2099 | width: calc(100% - 55px); 2100 | } 2101 | 2102 | #megamenu-clear-recet-documents { 2103 | padding: 5px 10px; 2104 | font-size: 14px; 2105 | background: var(--mdmdt-bg-color2); 2106 | border-radius: 5px; 2107 | } 2108 | 2109 | #megamenu-clear-recet-documents:hover { 2110 | color: var(--mdmdt-color-1); 2111 | } 2112 | 2113 | .ty-show-search #info-panel-tab-search .info-panel-tab-border, 2114 | .ty-show-search #info-panel-tab-search .info-panel-tab-border, 2115 | .active-tab-files #info-panel-tab-file .info-panel-tab-border, 2116 | .active-tab-outline #info-panel-tab-outline .info-panel-tab-border { 2117 | height: 3px; 2118 | border-radius: 2px; 2119 | } 2120 | 2121 | #file-library-search-input { 2122 | top: 4px; 2123 | border-radius: 0 !important; 2124 | } 2125 | 2126 | #close-outline-filter-btn { 2127 | top: 8px; 2128 | background: transparent !important; 2129 | border: none !important; 2130 | } 2131 | 2132 | .unibody-window .sidebar-content { 2133 | top: 58px; 2134 | } 2135 | 2136 | .native-window #file-library-search-input, 2137 | .unibody-window #file-library-search-input { 2138 | height: 28px; 2139 | border-radius: 5px !important; 2140 | } 2141 | 2142 | .megamenu-menu-panel { 2143 | scrollbar-width: none; 2144 | } 2145 | 2146 | #theme-preview-grid { 2147 | display: grid; 2148 | grid-template-columns: 1fr 1fr 1fr 1fr 1fr; 2149 | grid-gap: 32px; 2150 | justify-content: space-between; 2151 | max-width: 100%; 2152 | } 2153 | 2154 | @media (width <= 1200px) { 2155 | #theme-preview-grid { 2156 | grid-template-columns: 1fr 1fr 1fr; 2157 | } 2158 | } 2159 | 2160 | .theme-preview-div { 2161 | box-sizing: border-box; 2162 | padding: 4px; 2163 | margin: 0; 2164 | overflow: hidden; 2165 | color: var(--mdmdt-color-1); 2166 | border: 2px solid var(--mdmdt-border-color); 2167 | border-radius: 8px; 2168 | } 2169 | 2170 | .theme-preview-div:hover { 2171 | border-color: var(--mdmdt-color-1); 2172 | } 2173 | 2174 | .theme-preview-div.active { 2175 | border-color: var(--mdmdt-color-2); 2176 | } 2177 | 2178 | .theme-preview-div .fa { 2179 | color: var(--mdmdt-color-2); 2180 | } 2181 | 2182 | .theme-preview-content { 2183 | width: 100%; 2184 | height: 100%; 2185 | border-radius: 5px; 2186 | } 2187 | 2188 | #outline-btn:hover { 2189 | color: var(--mdmdt-color-1) !important; 2190 | } 2191 | 2192 | /* context menu */ 2193 | .context-menu { 2194 | padding: 5px !important; 2195 | border: 1px solid var(--mdmdt-border-color); 2196 | border-radius: 5px !important; 2197 | } 2198 | 2199 | .context-menu li a { 2200 | display: inline-block; 2201 | width: 100%; 2202 | border-radius: 5px !important; 2203 | } 2204 | 2205 | .context-menu > .active a { 2206 | color: var(--mdmdt-color-1) !important; 2207 | } 2208 | 2209 | .tb43e-d6bd-dbe4y { 2210 | background: var(--mdmdt-bg-color2); 2211 | } 2212 | 2213 | /* 2214 | * ------------------------ 2215 | * UI-controls 2216 | * dialogs 2217 | * ------------------------ 2218 | */ 2219 | #md-searchpanel input, 2220 | #md-replace-type-label, 2221 | #search-panel-replace-btn, 2222 | .modal-content, 2223 | .modal-dialog, 2224 | .modal-title, 2225 | .modal-content { 2226 | border-radius: 5px; 2227 | } 2228 | 2229 | .modal-content { 2230 | padding: 6px; 2231 | } 2232 | 2233 | #sidebar-files-menu li a { 2234 | padding-top: 5px; 2235 | padding-bottom: 5px; 2236 | } 2237 | 2238 | #sidebar-files-menu .file-action-item { 2239 | line-height: 30px; 2240 | } 2241 | 2242 | /* footer */ 2243 | #footer-word-count-info { 2244 | padding: 6px; 2245 | border: 1px solid var(--mdmdt-border-color); 2246 | border-radius: 5px; 2247 | } 2248 | 2249 | #footer-word-count-info table { 2250 | border-radius: 0; 2251 | } 2252 | 2253 | #footer-word-count-info table tr > td:first-of-type { 2254 | border-top-left-radius: 5px; 2255 | border-bottom-left-radius: 5px; 2256 | } 2257 | 2258 | #footer-word-count-info table tr > td:last-of-type { 2259 | border-top-right-radius: 5px; 2260 | border-bottom-right-radius: 5px; 2261 | } 2262 | 2263 | /** 2264 | * --------------------------------- 2265 | * Control UI on Mac (optional) 2266 | * --------------------------------- 2267 | */ 2268 | #typora-quick-open { 2269 | padding: 12px; 2270 | background-color: var(--mdmdt-bg-color); 2271 | border: 1px solid var(--mdmdt-border-color); 2272 | border-radius: 8px; 2273 | } 2274 | 2275 | .typora-quick-open-item, 2276 | .md-hover-tip .code-tooltip-content { 2277 | overflow-x: auto; 2278 | border-radius: 5px; 2279 | } 2280 | 2281 | .code-tooltip.md-hover-tip, 2282 | .md-hover-tip .code-tooltip-content, 2283 | .md-arrow::after { 2284 | background: var(--mdmdt-color-1) !important; 2285 | border-bottom-color: var(--mdmdt-color-1) !important; 2286 | box-shadow: 0 1px 4px var(--mdmdt-color-1) !important; 2287 | } 2288 | 2289 | .md-hover-tip .code-tooltip-content:hover a { 2290 | color: var(--mdmdt-color-2) !important; 2291 | } 2292 | 2293 | /* 2294 | * ------------------------ 2295 | * #root 2296 | * .ty-preferences 2297 | * ------------------------ 2298 | */ 2299 | .sidebar { 2300 | position: relative; 2301 | } 2302 | 2303 | .sidebar::after { 2304 | position: absolute; 2305 | top: 0; 2306 | right: -24px; 2307 | display: block; 2308 | width: 1px; 2309 | height: 100%; 2310 | content: ''; 2311 | border-left: 1px solid var(--mdmdt-border-color); 2312 | } 2313 | 2314 | .ty-preferences a { 2315 | font-size: 12px !important; 2316 | color: var(--mdmdt-color-1); 2317 | text-decoration: none; 2318 | } 2319 | 2320 | .ty-preferences a:hover { 2321 | color: var(--mdmdt-color-2); 2322 | text-decoration: underline; 2323 | } 2324 | 2325 | .export-item.active, 2326 | .export-items-list-control { 2327 | border-radius: 3px !important; 2328 | } 2329 | 2330 | .nav-group-item { 2331 | border-radius: 5px !important; 2332 | } 2333 | 2334 | .input-group table, 2335 | .export-detail { 2336 | border-radius: 5px !important; 2337 | } 2338 | 2339 | .export-detail .file-input input { 2340 | height: 30px !important; 2341 | padding-left: 8px !important; 2342 | border-color: var(--mdmdt-border-color) !important; 2343 | } 2344 | 2345 | .search-input { 2346 | padding: 6px 12px !important; 2347 | border-color: var(--mdmdt-border-color) !important; 2348 | } 2349 | 2350 | .label-input-group div { 2351 | padding: 0 !important; 2352 | margin: 0 !important; 2353 | border: none !important; 2354 | } 2355 | 2356 | .label-input-group div pre { 2357 | font-size: 14px !important; 2358 | border-radius: 5px !important; 2359 | } 2360 | 2361 | .input-group-content { 2362 | border-radius: 5px !important; 2363 | } 2364 | 2365 | .input-group-content table { 2366 | margin-top: 4px; 2367 | } 2368 | 2369 | .label-hint svg { 2370 | margin-right: 3px; 2371 | } 2372 | 2373 | .export-detail .input-group-content > .row { 2374 | margin-right: 16px; 2375 | } 2376 | 2377 | .export-detail, 2378 | .export-item.active { 2379 | color: var(--mdmdt-text-color) !important; 2380 | } 2381 | 2382 | .export-item.active { 2383 | color: var(--mdmdt-color-1) !important; 2384 | } 2385 | 2386 | .md-show-hint::after { 2387 | background: var(--mdmdt-color-1-0-b) !important; 2388 | border-radius: 3px; 2389 | box-shadow: 0 0 5px var(--mdmdt-color-1) !important; 2390 | } 2391 | 2392 | /* search style */ 2393 | #searchpanel-search-group .ty-search-panel-row { 2394 | padding-top: 0; 2395 | } 2396 | 2397 | #searchpanel-search-group .ty-search-panel-row input { 2398 | margin-top: 2px; 2399 | } 2400 | 2401 | .ty-search-panel-row .ion-close-round, 2402 | .ty-search-panel-row .ty-upload { 2403 | padding-right: 8px !important; 2404 | padding-left: 4px !important; 2405 | } 2406 | 2407 | #search-panel-next, 2408 | #search-panel-replace-btn { 2409 | left: 2px; 2410 | } 2411 | 2412 | #search-panel-replaceall-btn, 2413 | #search-panel-replace-btn { 2414 | padding-right: 4px !important; 2415 | padding-left: 4px !important; 2416 | text-align: center !important; 2417 | } 2418 | 2419 | .ty-on-outline-filter .ty-outline-hit { 2420 | color: var(--mdmdt-color-2); 2421 | } 2422 | 2423 | .md-search-hit { 2424 | color: #070909 !important; 2425 | background: var(--mdmdt-color-2-0-c) !important; 2426 | } 2427 | 2428 | .md-search-select { 2429 | color: #070909 !important; 2430 | background: var(--mdmdt-color-2) !important; 2431 | } 2432 | 2433 | /* 2434 | * ------------------------ 2435 | * diagram 2436 | * ------------------------ 2437 | */ 2438 | .md-diagram-panel { 2439 | width: 100% !important; 2440 | max-width: 100% !important; 2441 | margin-top: 20px; 2442 | background: var(--mdmdt-bg-color2) !important; 2443 | border-color: var(--mdmdt-border-color) !important; 2444 | border-radius: 8px; 2445 | } 2446 | 2447 | .md-diagram { 2448 | width: 100% !important; 2449 | max-width: 100% !important; 2450 | margin-right: 0 !important; 2451 | margin-left: 0 !important; 2452 | } 2453 | 2454 | .md-diagram + .md-diagram { 2455 | margin-top: -7px; 2456 | } 2457 | 2458 | .md-diagram-panel > svg { 2459 | width: 100% !important; 2460 | max-width: 100% !important; 2461 | } 2462 | 2463 | p:has(+ .md-diagram) { 2464 | margin-bottom: -12px; 2465 | } 2466 | 2467 | /* 2468 | * ------------------------ 2469 | * @media screen 2470 | * ------------------------ 2471 | */ 2472 | @media screen and (width <= 768px) { 2473 | #write { 2474 | padding: 32px; 2475 | } 2476 | 2477 | h2.md-focus::before, 2478 | h3.md-focus::before, 2479 | h4.md-focus::before, 2480 | h5.md-focus::before, 2481 | h6.md-focus::before, 2482 | h2::before, 2483 | h3::before, 2484 | h4::before, 2485 | h5::before, 2486 | h6::before { 2487 | left: -30px; 2488 | } 2489 | } 2490 | 2491 | @media screen and (width >= 769px) and (width <= 1280px) { 2492 | #write { 2493 | padding: 32px 64px; 2494 | } 2495 | } 2496 | 2497 | /* 2498 | * ------------------------ 2499 | * @media print 2500 | * ------------------------ 2501 | */ 2502 | @media print { 2503 | html, 2504 | body, 2505 | body #write, 2506 | body content, 2507 | body .typora-export-content { 2508 | width: 100% !important; 2509 | max-width: 100% !important; 2510 | height: auto !important; /* 解决浏览器打印只有一页问题 */ 2511 | padding: 0 !important; 2512 | margin: 0 !important; 2513 | border: none !important; 2514 | } 2515 | 2516 | body .typora-export-sidebar { 2517 | display: none !important; 2518 | width: 0 !important; 2519 | } 2520 | 2521 | ::-webkit-scrollbar { 2522 | width: 0 !important; 2523 | height: 0 !important; 2524 | } 2525 | 2526 | body, 2527 | body div, 2528 | body pre, 2529 | body ul, 2530 | body section, 2531 | body #write { 2532 | scrollbar-width: none !important; 2533 | } 2534 | 2535 | table, 2536 | pre, 2537 | blockquote, 2538 | .md-alert { 2539 | page-break-inside: avoid; 2540 | } 2541 | 2542 | pre { 2543 | word-wrap: break-word; 2544 | } 2545 | 2546 | h1, 2547 | h2, 2548 | h3, 2549 | h4, 2550 | h5, 2551 | h6 { 2552 | margin: 24pt 0 12pt; 2553 | } 2554 | 2555 | h1 { 2556 | font-size: 24pt; 2557 | } 2558 | 2559 | h2 { 2560 | font-size: 21pt; 2561 | } 2562 | 2563 | h3 { 2564 | font-size: 18pt; 2565 | } 2566 | 2567 | h4 { 2568 | font-size: 15pt; 2569 | } 2570 | 2571 | h5 { 2572 | font-size: 13.5pt; 2573 | } 2574 | 2575 | h6 { 2576 | font-size: 12pt; 2577 | } 2578 | 2579 | p { 2580 | margin: 12pt 0; 2581 | font-size: 12pt; 2582 | } 2583 | 2584 | @page { 2585 | size: auto; 2586 | margin: 15mm !important; 2587 | background: white !important; 2588 | 2589 | /* 谷歌浏览器打印设置 */ 2590 | @top-center { 2591 | font-size: 9pt; 2592 | content: ''; 2593 | } 2594 | 2595 | @bottom-center { 2596 | font-size: 9pt; 2597 | content: counter(page) '/' counter(pages); 2598 | } 2599 | } 2600 | } 2601 | } 2602 | -------------------------------------------------------------------------------- /src/styles/markdown/plugins.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * ------------------------ 3 | * pre 代码增强样式,添加头部 4 | * ------------------------ 5 | */ 6 | .code-enhance { 7 | display: flex; 8 | flex-direction: column; 9 | margin-top: 10px; 10 | overflow: hidden; 11 | border-radius: 8px; 12 | 13 | &-header { 14 | box-sizing: border-box; 15 | display: flex; 16 | gap: 8px; 17 | align-items: center; 18 | justify-content: space-between; 19 | height: 32px; 20 | padding: 0 16px; 21 | font-size: 12px; 22 | color: #fff; 23 | background-color: #50505a; 24 | } 25 | 26 | &-content { 27 | pre { 28 | margin: 0; 29 | } 30 | 31 | code { 32 | border: 0; 33 | border-radius: 0; 34 | } 35 | } 36 | 37 | &-copy { 38 | display: inline-flex; 39 | gap: 6px; 40 | align-items: center; 41 | font-size: 12px; 42 | line-height: 1.5; 43 | cursor: pointer; 44 | transition: all 0.3s ease; 45 | 46 | &:hover { 47 | color: #978c46; 48 | } 49 | 50 | &:active { 51 | color: #80752c; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/styles/reset.scss: -------------------------------------------------------------------------------- 1 | /* Reset style sheet */ 2 | * { 3 | box-sizing: border-box; 4 | padding: 0; 5 | margin: 0; 6 | outline: none; 7 | } 8 | 9 | html, 10 | body, 11 | #app, 12 | #watermark { 13 | width: 100%; 14 | height: 100%; 15 | } 16 | 17 | /* 解决 h1 标签在 webkit 内核浏览器中文字大小失效问题 */ 18 | :-webkit-any(article, aside, nav, section) h1 { 19 | font-size: 2em; 20 | } 21 | -------------------------------------------------------------------------------- /src/styles/var.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --app-content-padding: 20px; 3 | --app-content-bg-color: #f5f7f9; 4 | --el-header-height: 60px; 5 | --el-header-padding: 10px; 6 | --el-aside-width: 200px; 7 | --el-footer-height: 30px; 8 | --left-menu-max-width: 200px; 9 | --left-menu-bg-color: #001529; 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 生成 uuid 3 | * @returns 4 | */ 5 | export function generateUUID() { 6 | let timestamp = new Date().getTime() 7 | const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 8 | const r = (timestamp + Math.random() * 16) % 16 | 0 9 | timestamp = Math.floor(timestamp / 16) 10 | return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16) 11 | }) 12 | return uuid 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/markdownit/codeCopyPlugins.ts: -------------------------------------------------------------------------------- 1 | import type MarkdownIt from 'markdown-it' 2 | import type { Renderer } from 'markdown-it/dist/markdown-it.min.js' 3 | import ClipboardJS from 'clipboard' 4 | import { escape } from 'lodash-es' 5 | 6 | const clipboard = new ClipboardJS('.markdown-it-code-copy') 7 | 8 | // 未 copy 时按钮的 innerHTML 9 | const copyInnerHTML = ` 10 | 11 | Copy 12 | ` 13 | // copy 后按钮的 innerHTML 14 | const copiedInnerHTML = ` 15 | 16 | Copied! 17 | ` 18 | 19 | clipboard.on('success', e => { 20 | const trigger = e.trigger 21 | e.clearSelection() 22 | 23 | trigger.innerHTML = copiedInnerHTML 24 | setTimeout(() => { 25 | trigger.innerHTML = copyInnerHTML 26 | }, 3000) 27 | }) 28 | 29 | // 用正则提取出 code 的语言 30 | const getCodeLangFragment = (htmlString: string) => { 31 | const regex = // 32 | const match = htmlString.match(regex) 33 | return match?.[2] || '' 34 | } 35 | 36 | const renderCode = (renderer: Renderer.RenderRule): Renderer.RenderRule => { 37 | return (...args) => { 38 | const [tokens, idx] = args 39 | const content = escape(tokens[idx].content) 40 | const origRendered = renderer.apply(this, args) 41 | 42 | if (content.length === 0) return origRendered 43 | 44 | const lang = getCodeLangFragment(origRendered) 45 | 46 | return ` 47 |
48 |
49 | ${lang} 50 | 51 | ${copyInnerHTML} 52 | 53 |
54 |
55 | ${origRendered} 56 |
57 |
58 | ` 59 | } 60 | } 61 | 62 | /** 63 | * markdown-it 的插件,添加代码语言显示和 copy 代码按钮 64 | */ 65 | export default (md: MarkdownIt) => { 66 | if (md.renderer.rules.code_block != null) { 67 | md.renderer.rules.code_block = renderCode(md.renderer.rules.code_block) 68 | } 69 | 70 | if (md.renderer.rules.fence != null) { 71 | md.renderer.rules.fence = renderCode(md.renderer.rules.fence) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/utils/markdownit/hljsConfig.ts: -------------------------------------------------------------------------------- 1 | import hljs from 'highlight.js/lib/core' 2 | import 'highlight.js/styles/github-dark.min.css' 3 | 4 | import bash from 'highlight.js/lib/languages/bash' 5 | import javascript from 'highlight.js/lib/languages/javascript' 6 | import typescript from 'highlight.js/lib/languages/typescript' 7 | import python from 'highlight.js/lib/languages/python' 8 | import java from 'highlight.js/lib/languages/java' 9 | import sql from 'highlight.js/lib/languages/sql' 10 | import nginx from 'highlight.js/lib/languages/nginx' 11 | import json from 'highlight.js/lib/languages/json' 12 | import yaml from 'highlight.js/lib/languages/yaml' 13 | import xml from 'highlight.js/lib/languages/xml' 14 | import shell from 'highlight.js/lib/languages/shell' 15 | import kotlin from 'highlight.js/lib/languages/kotlin' 16 | 17 | hljs.registerLanguage('bash', bash) 18 | hljs.registerLanguage('javascript', javascript) 19 | hljs.registerLanguage('typescript', typescript) 20 | hljs.registerLanguage('vue', typescript) 21 | hljs.registerLanguage('python', python) 22 | hljs.registerLanguage('java', java) 23 | hljs.registerLanguage('sql', sql) 24 | hljs.registerLanguage('nginx', nginx) 25 | hljs.registerLanguage('json', json) 26 | hljs.registerLanguage('yaml', yaml) 27 | hljs.registerLanguage('xml', xml) 28 | hljs.registerLanguage('shell', shell) 29 | hljs.registerLanguage('kotlin', kotlin) 30 | 31 | export default hljs 32 | -------------------------------------------------------------------------------- /src/utils/markdownit/index.ts: -------------------------------------------------------------------------------- 1 | import MarkdownIt, { type Options } from 'markdown-it' 2 | import hljs from './hljsConfig' 3 | import codeCopyPlugins from './codeCopyPlugins' 4 | 5 | /** 6 | * 初始化 MarkdownIt 7 | * @param options MarkdownIt option 参数 8 | * @returns 9 | */ 10 | function MarkdownItRender(options: Options = {}) { 11 | // Options 默认值 12 | const defaultOptions: Options = { 13 | html: true, 14 | linkify: true, 15 | breaks: true, 16 | xhtmlOut: true, 17 | typographer: true, 18 | highlight: (str, lang): any => { 19 | if (lang && hljs.getLanguage(lang)) { 20 | try { 21 | return `
${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}
` 22 | } catch (e: any) { 23 | throw new Error(e) 24 | } 25 | } 26 | return `
${md.utils.escapeHtml(str)}
` 27 | } 28 | } 29 | 30 | const MegertOptions = { 31 | ...defaultOptions, 32 | ...options 33 | } 34 | const md = new MarkdownIt(MegertOptions).use(codeCopyPlugins).disable('image') 35 | return md 36 | } 37 | 38 | export default MarkdownItRender 39 | -------------------------------------------------------------------------------- /src/utils/proxy.ts: -------------------------------------------------------------------------------- 1 | import type { ProxyOptions } from 'vite' 2 | 3 | type ProxyItem = [string, string] 4 | 5 | type ProxyList = ProxyItem[] 6 | 7 | type ProxyTargetList = Record 8 | 9 | /** 10 | * 创建代理,用于解析 .env.development 代理配置 11 | * @param list 12 | */ 13 | function createProxy(list: ProxyList = []) { 14 | const ret: ProxyTargetList = {} 15 | for (const [prefix, target] of list) { 16 | const httpsRE = /^https:\/\// 17 | const isHttps = httpsRE.test(target) 18 | 19 | // https://github.com/http-party/node-http-proxy#options 20 | ret[prefix] = { 21 | target: target, 22 | changeOrigin: true, 23 | ws: true, 24 | rewrite: path => path.replace(new RegExp(`^${prefix}`), ''), 25 | // https is require secure=false 26 | ...(isHttps ? { secure: false } : {}) 27 | } 28 | } 29 | return ret 30 | } 31 | 32 | export { createProxy } 33 | -------------------------------------------------------------------------------- /src/views/chat/components/AssistantChat.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 116 | 117 | 181 | -------------------------------------------------------------------------------- /src/views/chat/components/ChatHistory.vue: -------------------------------------------------------------------------------- 1 | 133 | 134 | 185 | 186 | 278 | -------------------------------------------------------------------------------- /src/views/chat/components/HumanChat.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 41 | 42 | 76 | -------------------------------------------------------------------------------- /src/views/chat/index.vue: -------------------------------------------------------------------------------- 1 | 286 | 287 | 376 | 377 | 513 | -------------------------------------------------------------------------------- /src/views/documents/index.vue: -------------------------------------------------------------------------------- 1 | 151 | 152 | 233 | 234 | 263 | -------------------------------------------------------------------------------- /src/views/documents/writeForm.vue: -------------------------------------------------------------------------------- 1 | 194 | 195 | 239 | 240 | 266 | -------------------------------------------------------------------------------- /src/views/test/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 273 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["**/*.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 7 | "paths": { 8 | "@/*": ["./src/*"] 9 | }, 10 | "types": ["element-plus/global"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | }, 10 | { 11 | "path": "./tsconfig.vitest.json" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "nightwatch.conf.*", 8 | "playwright.config.*", 9 | "eslint.config.*" 10 | ], 11 | "compilerOptions": { 12 | "noEmit": true, 13 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 14 | 15 | "module": "ESNext", 16 | "moduleResolution": "Bundler", 17 | "types": ["node"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.vitest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "include": ["src/**/__tests__/*", "env.d.ts"], 4 | "exclude": [], 5 | "compilerOptions": { 6 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo", 7 | 8 | "lib": [], 9 | "types": ["node", "jsdom"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig, loadEnv } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import vueDevTools from 'vite-plugin-vue-devtools' 6 | // 按需导入 7 | import AutoImport from 'unplugin-auto-import/vite' 8 | import Components from 'unplugin-vue-components/vite' 9 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' 10 | import { createProxy } from './src/utils/proxy' 11 | 12 | // https://vite.dev/config/ 13 | export default defineConfig(({ mode }) => { 14 | // 获取.env文件的VITE_前缀变量 15 | const env = loadEnv(mode, process.cwd(), '') 16 | 17 | return { 18 | plugins: [ 19 | vue(), 20 | vueDevTools(), 21 | AutoImport({ 22 | resolvers: [ 23 | ElementPlusResolver({ 24 | importStyle: 'sass', 25 | directives: true 26 | }) 27 | ] 28 | }), 29 | Components({ 30 | resolvers: [ 31 | ElementPlusResolver({ 32 | importStyle: 'sass', 33 | directives: true 34 | }) 35 | ] 36 | }) 37 | ], 38 | resolve: { 39 | alias: { 40 | '@': fileURLToPath(new URL('./src', import.meta.url)) 41 | } 42 | }, 43 | css: { 44 | preprocessorOptions: { 45 | scss: { 46 | additionalData: `@use "@/styles/element/index.scss" as *;` 47 | } 48 | } 49 | }, 50 | server: { 51 | port: Number(env.VITE_PORT), 52 | ...(env.VITE_PROXY ? { proxy: createProxy(JSON.parse(env.VITE_PROXY)) } : {}) 53 | } 54 | } 55 | }) 56 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { mergeConfig, defineConfig, configDefaults } from 'vitest/config' 3 | import viteConfig from './vite.config' 4 | 5 | export default defineConfig(configEnv => 6 | mergeConfig( 7 | viteConfig(configEnv), 8 | defineConfig({ 9 | test: { 10 | environment: 'jsdom', 11 | exclude: [...configDefaults.exclude, 'e2e/**'], 12 | root: fileURLToPath(new URL('./', import.meta.url)) 13 | } 14 | }) 15 | ) 16 | ) 17 | --------------------------------------------------------------------------------