├── .env.example ├── .gitignore ├── .npmrc ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── app.vue ├── assets ├── css │ ├── github-languages-colors.css │ └── main.css └── img │ └── loading.gif ├── components ├── AppBar.client.vue ├── AppFooter.client.vue ├── ArticleCard.vue ├── ArticleEditor.client.vue ├── ArticleTimeline.vue ├── Backdrop.client.vue ├── Btn.vue ├── ColorModeSwitch.client.vue ├── Dialog.client.vue ├── Drawer.client.vue ├── GiscusCard.client.vue ├── LandingAnimation.vue ├── LoadingIndicator.client.vue ├── LoginForm.client.vue ├── RefreshButton.vue ├── TextBlock.vue ├── TextField.vue └── TextFieldContainer.vue ├── error.vue ├── eslint.config.js ├── nuxt.config.ts ├── package.json ├── pages ├── [path].vue ├── activity.vue ├── archive.vue ├── index.vue ├── new.vue ├── setting.vue └── tag │ └── [tag].vue ├── plugins └── toastification.ts ├── pnpm-lock.yaml ├── public ├── avatar.webp ├── banner.webp ├── favicon.ico └── robots.txt ├── server ├── api │ ├── access │ │ └── index.get.ts │ ├── ai │ │ └── summary.ts │ ├── article │ │ ├── [_id].delete.ts │ │ ├── [path].get.ts │ │ ├── delete │ │ │ └── [_id].get.ts │ │ ├── index.get.ts │ │ ├── index.post.ts │ │ ├── index.put.ts │ │ ├── pinned │ │ │ └── index.get.ts │ │ ├── recent │ │ │ └── index.get.ts │ │ └── tag │ │ │ └── [tag].get.ts │ ├── attribute │ │ ├── [key].get.ts │ │ └── [key].post.ts │ ├── auth │ │ ├── login.post.ts │ │ ├── logout.post.ts │ │ └── user.get.ts │ ├── picture │ │ ├── [_id].get.ts │ │ └── index.post.ts │ ├── ping.get.ts │ └── tag │ │ └── index.get.ts ├── data │ ├── access.ts │ ├── article.ts │ ├── attribute.ts │ └── picture.ts ├── middleware │ └── auth.ts ├── models │ ├── access.schema.ts │ ├── article.schema.ts │ ├── attribute.schema.ts │ └── picture.schema.ts ├── tsconfig.json └── utils │ ├── ai.ts │ ├── assertion.ts │ ├── auth.ts │ └── request.ts ├── store ├── auth.ts ├── index.ts └── setting.ts ├── tailwind.config.ts ├── tsconfig.json ├── types ├── api.d.ts └── models.d.ts └── utils └── api.ts /.env.example: -------------------------------------------------------------------------------- 1 | # MongoDB 数据库配置 2 | MONGODB_URI='mongodb://username:password@127.0.0.1:27017/blog' 3 | 4 | # 管理员账号密码 5 | ADMIN_USERNAME='admin' 6 | ADMIN_PASSWORD='admin' 7 | # (可选) JWT 加密密钥 8 | AUTH_SECRET='secret' 9 | 10 | # (可选) 讯飞星火认知大模型 API, 目前仅支持 V3.5 模型 11 | # 讯飞开放平台: https://console.xfyun.cn/services/bm35 12 | SPARK_APP_ID='' 13 | SPARK_API_SECRET='' 14 | SPARK_API_KEY='' 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | .vscode/**/* 21 | !.vscode/launch.json 22 | 23 | # Local env files 24 | .env 25 | .env.* 26 | !.env.example 27 | 28 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "chrome", 6 | "request": "launch", 7 | "name": "client: chrome", 8 | "url": "http://localhost:3000", 9 | "webRoot": "${workspaceFolder}" 10 | }, 11 | { 12 | "type": "firefox", 13 | "request": "launch", 14 | "name": "client: firefox", 15 | "url": "http://localhost:3000", 16 | "webRoot": "${workspaceFolder}" 17 | }, 18 | { 19 | "type": "node", 20 | "request": "launch", 21 | "name": "server: nuxt", 22 | "outputCapture": "std", 23 | "program": "${workspaceFolder}/node_modules/nuxi/bin/nuxi.mjs", 24 | "args": [ 25 | "dev", 26 | "--host" 27 | ] 28 | } 29 | ], 30 | "compounds": [ 31 | { 32 | "name": "fullstack: nuxt", 33 | "configurations": [ 34 | "server: nuxt", 35 | "client: firefox" 36 | ] 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FlapyPan Blog 2 | 3 | [![Netlify Status](https://api.netlify.com/api/v1/badges/873d3af7-747a-4c77-b2f5-cd2ef2a454e9/deploy-status)](https://app.netlify.com/sites/flapypan/deploys) 4 | 5 | 一款简单高效的在线个人博客系统 6 | 7 | ## 部署运行 8 | 9 | - `node.js` (version >= 18) 10 | - `pnpm` (version >= 6) 11 | - `MongoDB` (version >= 6) 12 | 13 | ### Serverless 部署 14 | 15 | 请注意配置必要的环境变量,可参考:[.env.example](.env.example) 16 | 17 | Serverless 部署推荐使用 [MongoDB Atlas](https://www.mongodb.com/atlas) 免费数据库 18 | 19 | #### Vercel 20 | 21 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FFlapyPan%2Fflapypan-blog&env=MONGODB_URI,ADMIN_USERNAME,ADMIN_PASSWORD,AUTH_SECRET) 22 | 23 | #### Netlify 24 | 25 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https%3A%2F%2Fgithub.com%2FFlapyPan%2Fflapypan-blog#MONGODB_URI=&ADMIN_USERNAME=&ADMIN_PASSWORD=&AUTH_SECRET=) 26 | 27 | #### AWS Amplify 28 | 29 | 30 | 31 | ### 自主部署 32 | 33 | `.env` 配置文件参考:[.env.example](.env.example) 34 | 35 | #### Node.js 36 | 37 | ```shell 38 | git clone https://github.com/FlapyPan/flapypan-blog 39 | cd flapypan-blog 40 | pnpm insall 41 | # 开发模式 42 | pnpm run dev 43 | # 生产模式 44 | pnpm run build && pnpm run preview 45 | ``` 46 | 47 | #### Deno.js 48 | 49 | 适配中... 50 | 51 | #### Bun.js 52 | 53 | 适配中... 54 | 55 | ## 评论系统配置(可选) 56 | 57 | 参考 Giscus [官方教程](https://giscus.app/zh-CN) 58 | 59 | 获取到对应的 `repo` `repoid` `category` `categoryid` 后,登录博客后台填入相应的位置保存即可 60 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 42 | -------------------------------------------------------------------------------- /assets/css/github-languages-colors.css: -------------------------------------------------------------------------------- 1 | .lang-1C-Enterprise { 2 | color: #814ccc; 3 | } 4 | 5 | .lang-2-Dimensional-Array { 6 | color: #38761d; 7 | } 8 | 9 | .lang-4D { 10 | color: #004289; 11 | } 12 | 13 | .lang-ABAP { 14 | color: #e8274b; 15 | } 16 | 17 | .lang-ABAP-CDS { 18 | color: #555e25; 19 | } 20 | 21 | .lang-AGS-Script { 22 | color: #b9d9ff; 23 | } 24 | 25 | .lang-AIDL { 26 | color: #34eb6b; 27 | } 28 | 29 | .lang-AL { 30 | color: #3aa2b5; 31 | } 32 | 33 | .lang-AMPL { 34 | color: #e6efbb; 35 | } 36 | 37 | .lang-ANTLR { 38 | color: #9dc3ff; 39 | } 40 | 41 | .lang-API-Blueprint { 42 | color: #2acca8; 43 | } 44 | 45 | .lang-APL { 46 | color: #5a8164; 47 | } 48 | 49 | .lang-ASP.NET { 50 | color: #9400ff; 51 | } 52 | 53 | .lang-ATS { 54 | color: #1ac620; 55 | } 56 | 57 | .lang-ActionScript { 58 | color: #882b0f; 59 | } 60 | 61 | .lang-Ada { 62 | color: #02f88c; 63 | } 64 | 65 | .lang-Adblock-Filter-List { 66 | color: #800000; 67 | } 68 | 69 | .lang-Adobe-Font-Metrics { 70 | color: #fa0f00; 71 | } 72 | 73 | .lang-Agda { 74 | color: #315665; 75 | } 76 | 77 | .lang-Alloy { 78 | color: #64c800; 79 | } 80 | 81 | .lang-Alpine-Abuild { 82 | color: #0d597f; 83 | } 84 | 85 | .lang-Altium-Designer { 86 | color: #a89663; 87 | } 88 | 89 | .lang-AngelScript { 90 | color: #c7d7dc; 91 | } 92 | 93 | .lang-Ant-Build-System { 94 | color: #a9157e; 95 | } 96 | 97 | .lang-Antlers { 98 | color: #ff269e; 99 | } 100 | 101 | .lang-ApacheConf { 102 | color: #d12127; 103 | } 104 | 105 | .lang-Apex { 106 | color: #1797c0; 107 | } 108 | 109 | .lang-Apollo-Guidance-Computer { 110 | color: #0b3d91; 111 | } 112 | 113 | .lang-AppleScript { 114 | color: #101f1f; 115 | } 116 | 117 | .lang-Arc { 118 | color: #aa2afe; 119 | } 120 | 121 | .lang-AsciiDoc { 122 | color: #73a0c5; 123 | } 124 | 125 | .lang-AspectJ { 126 | color: #a957b0; 127 | } 128 | 129 | .lang-Assembly { 130 | color: #6e4c13; 131 | } 132 | 133 | .lang-Astro { 134 | color: #ff5a03; 135 | } 136 | 137 | .lang-Asymptote { 138 | color: #ff0000; 139 | } 140 | 141 | .lang-Augeas { 142 | color: #9cc134; 143 | } 144 | 145 | .lang-AutoHotkey { 146 | color: #6594b9; 147 | } 148 | 149 | .lang-AutoIt { 150 | color: #1c3552; 151 | } 152 | 153 | .lang-Avro-IDL { 154 | color: #0040ff; 155 | } 156 | 157 | .lang-Awk { 158 | color: #c30e9b; 159 | } 160 | 161 | .lang-BASIC { 162 | color: #ff0000; 163 | } 164 | 165 | .lang-Ballerina { 166 | color: #ff5000; 167 | } 168 | 169 | .lang-Batchfile { 170 | color: #c1f12e; 171 | } 172 | 173 | .lang-Beef { 174 | color: #a52f4e; 175 | } 176 | 177 | .lang-Berry { 178 | color: #15a13c; 179 | } 180 | 181 | .lang-BibTeX { 182 | color: #778899; 183 | } 184 | 185 | .lang-Bicep { 186 | color: #519aba; 187 | } 188 | 189 | .lang-Bikeshed { 190 | color: #5562ac; 191 | } 192 | 193 | .lang-Bison { 194 | color: #6a463f; 195 | } 196 | 197 | .lang-BitBake { 198 | color: #00bce4; 199 | } 200 | 201 | .lang-Blade { 202 | color: #f7523f; 203 | } 204 | 205 | .lang-BlitzBasic { 206 | color: #00ffae; 207 | } 208 | 209 | .lang-BlitzMax { 210 | color: #cd6400; 211 | } 212 | 213 | .lang-Bluespec { 214 | color: #12223c; 215 | } 216 | 217 | .lang-Bluespec-BH { 218 | color: #12223c; 219 | } 220 | 221 | .lang-Boo { 222 | color: #d4bec1; 223 | } 224 | 225 | .lang-Boogie { 226 | color: #c80fa0; 227 | } 228 | 229 | .lang-Brainfuck { 230 | color: #2f2530; 231 | } 232 | 233 | .lang-BrighterScript { 234 | color: #66aabb; 235 | } 236 | 237 | .lang-Brightscript { 238 | color: #662d91; 239 | } 240 | 241 | .lang-Browserslist { 242 | color: #ffd539; 243 | } 244 | 245 | .lang-C { 246 | color: #555555; 247 | } 248 | 249 | .lang-C\# { 250 | color: #178600; 251 | } 252 | 253 | .lang-C\+\+ { 254 | color: #f34b7d; 255 | } 256 | 257 | .lang-CAP-CDS { 258 | color: #0092d1; 259 | } 260 | 261 | .lang-CLIPS { 262 | color: #00a300; 263 | } 264 | 265 | .lang-CMake { 266 | color: #da3434; 267 | } 268 | 269 | .lang-COLLADA { 270 | color: #f1a42b; 271 | } 272 | 273 | .lang-CSON { 274 | color: #244776; 275 | } 276 | 277 | .lang-CSS { 278 | color: #563d7c; 279 | } 280 | 281 | .lang-CSV { 282 | color: #237346; 283 | } 284 | 285 | .lang-CUE { 286 | color: #5886e1; 287 | } 288 | 289 | .lang-CWeb { 290 | color: #00007a; 291 | } 292 | 293 | .lang-Cabal-Config { 294 | color: #483465; 295 | } 296 | 297 | .lang-Cadence { 298 | color: #00ef8b; 299 | } 300 | 301 | .lang-Cairo { 302 | color: #ff4a48; 303 | } 304 | 305 | .lang-CameLIGO { 306 | color: #3be133; 307 | } 308 | 309 | .lang-Ceylon { 310 | color: #dfa535; 311 | } 312 | 313 | .lang-Chapel { 314 | color: #8dc63f; 315 | } 316 | 317 | .lang-ChucK { 318 | color: #3f8000; 319 | } 320 | 321 | .lang-Circom { 322 | color: #707575; 323 | } 324 | 325 | .lang-Cirru { 326 | color: #ccccff; 327 | } 328 | 329 | .lang-Clarion { 330 | color: #db901e; 331 | } 332 | 333 | .lang-Clarity { 334 | color: #5546ff; 335 | } 336 | 337 | .lang-Classic-ASP { 338 | color: #6a40fd; 339 | } 340 | 341 | .lang-Clean { 342 | color: #3f85af; 343 | } 344 | 345 | .lang-Click { 346 | color: #e4e6f3; 347 | } 348 | 349 | .lang-Clojure { 350 | color: #db5855; 351 | } 352 | 353 | .lang-Closure-Templates { 354 | color: #0d948f; 355 | } 356 | 357 | .lang-Cloud-Firestore-Security-Rules { 358 | color: #ffa000; 359 | } 360 | 361 | .lang-CodeQL { 362 | color: #140f46; 363 | } 364 | 365 | .lang-CoffeeScript { 366 | color: #244776; 367 | } 368 | 369 | .lang-ColdFusion { 370 | color: #ed2cd6; 371 | } 372 | 373 | .lang-ColdFusion-CFC { 374 | color: #ed2cd6; 375 | } 376 | 377 | .lang-Common-Lisp { 378 | color: #3fb68b; 379 | } 380 | 381 | .lang-Common-Workflow-Language { 382 | color: #b5314c; 383 | } 384 | 385 | .lang-Component-Pascal { 386 | color: #b0ce4e; 387 | } 388 | 389 | .lang-Coq { 390 | color: #d0b68c; 391 | } 392 | 393 | .lang-Crystal { 394 | color: #000100; 395 | } 396 | 397 | .lang-Csound { 398 | color: #1a1a1a; 399 | } 400 | 401 | .lang-Csound-Document { 402 | color: #1a1a1a; 403 | } 404 | 405 | .lang-Csound-Score { 406 | color: #1a1a1a; 407 | } 408 | 409 | .lang-Cuda { 410 | color: #3a4e3a; 411 | } 412 | 413 | .lang-Curry { 414 | color: #531242; 415 | } 416 | 417 | .lang-Cypher { 418 | color: #34c0eb; 419 | } 420 | 421 | .lang-Cython { 422 | color: #fedf5b; 423 | } 424 | 425 | .lang-D { 426 | color: #ba595e; 427 | } 428 | 429 | .lang-D2 { 430 | color: #526ee8; 431 | } 432 | 433 | .lang-DM { 434 | color: #447265; 435 | } 436 | 437 | .lang-Dafny { 438 | color: #ffec25; 439 | } 440 | 441 | .lang-Darcs-Patch { 442 | color: #8eff23; 443 | } 444 | 445 | .lang-Dart { 446 | color: #00b4ab; 447 | } 448 | 449 | .lang-DataWeave { 450 | color: #003a52; 451 | } 452 | 453 | .lang-Debian-Package-Control-File { 454 | color: #d70751; 455 | } 456 | 457 | .lang-DenizenScript { 458 | color: #fbee96; 459 | } 460 | 461 | .lang-Dhall { 462 | color: #dfafff; 463 | } 464 | 465 | .lang-DirectX-3D-File { 466 | color: #aace60; 467 | } 468 | 469 | .lang-Dockerfile { 470 | color: #384d54; 471 | } 472 | 473 | .lang-Dogescript { 474 | color: #cca760; 475 | } 476 | 477 | .lang-Dotenv { 478 | color: #e5d559; 479 | } 480 | 481 | .lang-Dylan { 482 | color: #6c616e; 483 | } 484 | 485 | .lang-E { 486 | color: #ccce35; 487 | } 488 | 489 | .lang-ECL { 490 | color: #8a1267; 491 | } 492 | 493 | .lang-ECLiPSe { 494 | color: #001d9d; 495 | } 496 | 497 | .lang-EJS { 498 | color: #a91e50; 499 | } 500 | 501 | .lang-EQ { 502 | color: #a78649; 503 | } 504 | 505 | .lang-Earthly { 506 | color: #2af0ff; 507 | } 508 | 509 | .lang-Easybuild { 510 | color: #069406; 511 | } 512 | 513 | .lang-Ecere-Projects { 514 | color: #913960; 515 | } 516 | 517 | .lang-Ecmarkup { 518 | color: #eb8131; 519 | } 520 | 521 | .lang-EditorConfig { 522 | color: #fff1f2; 523 | } 524 | 525 | .lang-Eiffel { 526 | color: #4d6977; 527 | } 528 | 529 | .lang-Elixir { 530 | color: #6e4a7e; 531 | } 532 | 533 | .lang-Elm { 534 | color: #60b5cc; 535 | } 536 | 537 | .lang-Elvish { 538 | color: #55bb55; 539 | } 540 | 541 | .lang-Elvish-Transcript { 542 | color: #55bb55; 543 | } 544 | 545 | .lang-Emacs-Lisp { 546 | color: #c065db; 547 | } 548 | 549 | .lang-EmberScript { 550 | color: #fff4f3; 551 | } 552 | 553 | .lang-Erlang { 554 | color: #b83998; 555 | } 556 | 557 | .lang-Euphoria { 558 | color: #ff790b; 559 | } 560 | 561 | .lang-F\# { 562 | color: #b845fc; 563 | } 564 | 565 | .lang-F\* { 566 | color: #572e30; 567 | } 568 | 569 | .lang-FIGlet-Font { 570 | color: #ffddbb; 571 | } 572 | 573 | .lang-FLUX { 574 | color: #88ccff; 575 | } 576 | 577 | .lang-Factor { 578 | color: #636746; 579 | } 580 | 581 | .lang-Fancy { 582 | color: #7b9db4; 583 | } 584 | 585 | .lang-Fantom { 586 | color: #14253c; 587 | } 588 | 589 | .lang-Faust { 590 | color: #c37240; 591 | } 592 | 593 | .lang-Fennel { 594 | color: #fff3d7; 595 | } 596 | 597 | .lang-Filebench-WML { 598 | color: #f6b900; 599 | } 600 | 601 | .lang-Fluent { 602 | color: #ffcc33; 603 | } 604 | 605 | .lang-Forth { 606 | color: #341708; 607 | } 608 | 609 | .lang-Fortran { 610 | color: #4d41b1; 611 | } 612 | 613 | .lang-Fortran-Free-Form { 614 | color: #4d41b1; 615 | } 616 | 617 | .lang-FreeBasic { 618 | color: #141ac9; 619 | } 620 | 621 | .lang-FreeMarker { 622 | color: #0050b2; 623 | } 624 | 625 | .lang-Frege { 626 | color: #00cafe; 627 | } 628 | 629 | .lang-Futhark { 630 | color: #5f021f; 631 | } 632 | 633 | .lang-G-code { 634 | color: #d08cf2; 635 | } 636 | 637 | .lang-GAML { 638 | color: #ffc766; 639 | } 640 | 641 | .lang-GAMS { 642 | color: #f49a22; 643 | } 644 | 645 | .lang-GAP { 646 | color: #0000cc; 647 | } 648 | 649 | .lang-GCC-Machine-Description { 650 | color: #ffcfab; 651 | } 652 | 653 | .lang-GDScript { 654 | color: #355570; 655 | } 656 | 657 | .lang-GEDCOM { 658 | color: #003058; 659 | } 660 | 661 | .lang-GLSL { 662 | color: #5686a5; 663 | } 664 | 665 | .lang-GSC { 666 | color: #ff6800; 667 | } 668 | 669 | .lang-Game-Maker-Language { 670 | color: #71b417; 671 | } 672 | 673 | .lang-Gemfile.lock { 674 | color: #701516; 675 | } 676 | 677 | .lang-Gemini { 678 | color: #ff6900; 679 | } 680 | 681 | .lang-Genero { 682 | color: #63408e; 683 | } 684 | 685 | .lang-Genero-Forms { 686 | color: #d8df39; 687 | } 688 | 689 | .lang-Genie { 690 | color: #fb855d; 691 | } 692 | 693 | .lang-Genshi { 694 | color: #951531; 695 | } 696 | 697 | .lang-Gentoo-Ebuild { 698 | color: #9400ff; 699 | } 700 | 701 | .lang-Gentoo-Eclass { 702 | color: #9400ff; 703 | } 704 | 705 | .lang-Gerber-Image { 706 | color: #d20b00; 707 | } 708 | 709 | .lang-Gherkin { 710 | color: #5b2063; 711 | } 712 | 713 | .lang-Git-Attributes { 714 | color: #f44d27; 715 | } 716 | 717 | .lang-Git-Config { 718 | color: #f44d27; 719 | } 720 | 721 | .lang-Git-Revision-List { 722 | color: #f44d27; 723 | } 724 | 725 | .lang-Gleam { 726 | color: #ffaff3; 727 | } 728 | 729 | .lang-Glyph { 730 | color: #c1ac7f; 731 | } 732 | 733 | .lang-Gnuplot { 734 | color: #f0a9f0; 735 | } 736 | 737 | .lang-Go { 738 | color: #00add8; 739 | } 740 | 741 | .lang-Go-Checksums { 742 | color: #00add8; 743 | } 744 | 745 | .lang-Go-Module { 746 | color: #00add8; 747 | } 748 | 749 | .lang-Go-Workspace { 750 | color: #00add8; 751 | } 752 | 753 | .lang-Godot-Resource { 754 | color: #355570; 755 | } 756 | 757 | .lang-Golo { 758 | color: #88562a; 759 | } 760 | 761 | .lang-Gosu { 762 | color: #82937f; 763 | } 764 | 765 | .lang-Grace { 766 | color: #615f8b; 767 | } 768 | 769 | .lang-Gradle { 770 | color: #02303a; 771 | } 772 | 773 | .lang-Gradle-Kotlin-DSL { 774 | color: #02303a; 775 | } 776 | 777 | .lang-Grammatical-Framework { 778 | color: #ff0000; 779 | } 780 | 781 | .lang-GraphQL { 782 | color: #e10098; 783 | } 784 | 785 | .lang-Groovy { 786 | color: #4298b8; 787 | } 788 | 789 | .lang-Groovy-Server-Pages { 790 | color: #4298b8; 791 | } 792 | 793 | .lang-HAProxy { 794 | color: #106da9; 795 | } 796 | 797 | .lang-HCL { 798 | color: #844fba; 799 | } 800 | 801 | .lang-HLSL { 802 | color: #aace60; 803 | } 804 | 805 | .lang-HOCON { 806 | color: #9ff8ee; 807 | } 808 | 809 | .lang-HTML { 810 | color: #e34c26; 811 | } 812 | 813 | .lang-HTTP { 814 | color: #005c9c; 815 | } 816 | 817 | .lang-HXML { 818 | color: #f68712; 819 | } 820 | 821 | .lang-Hack { 822 | color: #878787; 823 | } 824 | 825 | .lang-Haml { 826 | color: #ece2a9; 827 | } 828 | 829 | .lang-Handlebars { 830 | color: #f7931e; 831 | } 832 | 833 | .lang-Harbour { 834 | color: #0e60e3; 835 | } 836 | 837 | .lang-Haskell { 838 | color: #5e5086; 839 | } 840 | 841 | .lang-Haxe { 842 | color: #df7900; 843 | } 844 | 845 | .lang-HiveQL { 846 | color: #dce200; 847 | } 848 | 849 | .lang-HolyC { 850 | color: #ffefaf; 851 | } 852 | 853 | .lang-Hosts-File { 854 | color: #308888; 855 | } 856 | 857 | .lang-Hy { 858 | color: #7790b2; 859 | } 860 | 861 | .lang-IDL { 862 | color: #a3522f; 863 | } 864 | 865 | .lang-IGOR-Pro { 866 | color: #0000cc; 867 | } 868 | 869 | .lang-INI { 870 | color: #d1dbe0; 871 | } 872 | 873 | .lang-Idris { 874 | color: #b30000; 875 | } 876 | 877 | .lang-Ignore-List { 878 | color: #000000; 879 | } 880 | 881 | .lang-ImageJ-Macro { 882 | color: #99aaff; 883 | } 884 | 885 | .lang-Imba { 886 | color: #16cec6; 887 | } 888 | 889 | .lang-Inno-Setup { 890 | color: #264b99; 891 | } 892 | 893 | .lang-Io { 894 | color: #a9188d; 895 | } 896 | 897 | .lang-Ioke { 898 | color: #078193; 899 | } 900 | 901 | .lang-Isabelle { 902 | color: #fefe00; 903 | } 904 | 905 | .lang-Isabelle-ROOT { 906 | color: #fefe00; 907 | } 908 | 909 | .lang-J { 910 | color: #9eedff; 911 | } 912 | 913 | .lang-JAR-Manifest { 914 | color: #b07219; 915 | } 916 | 917 | .lang-JCL { 918 | color: #d90e09; 919 | } 920 | 921 | .lang-JFlex { 922 | color: #dbca00; 923 | } 924 | 925 | .lang-JSON { 926 | color: #292929; 927 | } 928 | 929 | .lang-JSON-with-Comments { 930 | color: #292929; 931 | } 932 | 933 | .lang-JSON5 { 934 | color: #267cb9; 935 | } 936 | 937 | .lang-JSONLD { 938 | color: #0c479c; 939 | } 940 | 941 | .lang-JSONiq { 942 | color: #40d47e; 943 | } 944 | 945 | .lang-Janet { 946 | color: #0886a5; 947 | } 948 | 949 | .lang-Jasmin { 950 | color: #d03600; 951 | } 952 | 953 | .lang-Java { 954 | color: #b07219; 955 | } 956 | 957 | .lang-Java-Properties { 958 | color: #2a6277; 959 | } 960 | 961 | .lang-Java-Server-Pages { 962 | color: #2a6277; 963 | } 964 | 965 | .lang-JavaScript { 966 | color: #f1e05a; 967 | } 968 | 969 | .lang-Jest-Snapshot { 970 | color: #15c213; 971 | } 972 | 973 | .lang-JetBrains-MPS { 974 | color: #21d789; 975 | } 976 | 977 | .lang-Jinja { 978 | color: #a52a22; 979 | } 980 | 981 | .lang-Jison { 982 | color: #56b3cb; 983 | } 984 | 985 | .lang-Jison-Lex { 986 | color: #56b3cb; 987 | } 988 | 989 | .lang-Jolie { 990 | color: #843179; 991 | } 992 | 993 | .lang-Jsonnet { 994 | color: #0064bd; 995 | } 996 | 997 | .lang-Julia { 998 | color: #a270ba; 999 | } 1000 | 1001 | .lang-Jupyter-Notebook { 1002 | color: #da5b0b; 1003 | } 1004 | 1005 | .lang-Just { 1006 | color: #384d54; 1007 | } 1008 | 1009 | .lang-KRL { 1010 | color: #28430a; 1011 | } 1012 | 1013 | .lang-Kaitai-Struct { 1014 | color: #773b37; 1015 | } 1016 | 1017 | .lang-KakouneScript { 1018 | color: #6f8042; 1019 | } 1020 | 1021 | .lang-KerboScript { 1022 | color: #41adf0; 1023 | } 1024 | 1025 | .lang-KiCad-Layout { 1026 | color: #2f4aab; 1027 | } 1028 | 1029 | .lang-KiCad-Legacy-Layout { 1030 | color: #2f4aab; 1031 | } 1032 | 1033 | .lang-KiCad-Schematic { 1034 | color: #2f4aab; 1035 | } 1036 | 1037 | .lang-Kotlin { 1038 | color: #a97bff; 1039 | } 1040 | 1041 | .lang-LFE { 1042 | color: #4c3023; 1043 | } 1044 | 1045 | .lang-LLVM { 1046 | color: #185619; 1047 | } 1048 | 1049 | .lang-LOLCODE { 1050 | color: #cc9900; 1051 | } 1052 | 1053 | .lang-LSL { 1054 | color: #3d9970; 1055 | } 1056 | 1057 | .lang-LabVIEW { 1058 | color: #fede06; 1059 | } 1060 | 1061 | .lang-Lark { 1062 | color: #2980b9; 1063 | } 1064 | 1065 | .lang-Lasso { 1066 | color: #999999; 1067 | } 1068 | 1069 | .lang-Latte { 1070 | color: #f2a542; 1071 | } 1072 | 1073 | .lang-Less { 1074 | color: #1d365d; 1075 | } 1076 | 1077 | .lang-Lex { 1078 | color: #dbca00; 1079 | } 1080 | 1081 | .lang-LigoLANG { 1082 | color: #0e74ff; 1083 | } 1084 | 1085 | .lang-LilyPond { 1086 | color: #9ccc7c; 1087 | } 1088 | 1089 | .lang-Liquid { 1090 | color: #67b8de; 1091 | } 1092 | 1093 | .lang-Literate-Agda { 1094 | color: #315665; 1095 | } 1096 | 1097 | .lang-Literate-CoffeeScript { 1098 | color: #244776; 1099 | } 1100 | 1101 | .lang-Literate-Haskell { 1102 | color: #5e5086; 1103 | } 1104 | 1105 | .lang-LiveScript { 1106 | color: #499886; 1107 | } 1108 | 1109 | .lang-Logtalk { 1110 | color: #295b9a; 1111 | } 1112 | 1113 | .lang-LookML { 1114 | color: #652b81; 1115 | } 1116 | 1117 | .lang-Lua { 1118 | color: #000080; 1119 | } 1120 | 1121 | .lang-MATLAB { 1122 | color: #e16737; 1123 | } 1124 | 1125 | .lang-MAXScript { 1126 | color: #00a6a6; 1127 | } 1128 | 1129 | .lang-MDX { 1130 | color: #fcb32c; 1131 | } 1132 | 1133 | .lang-MLIR { 1134 | color: #5ec8db; 1135 | } 1136 | 1137 | .lang-MQL4 { 1138 | color: #62a8d6; 1139 | } 1140 | 1141 | .lang-MQL5 { 1142 | color: #4a76b8; 1143 | } 1144 | 1145 | .lang-MTML { 1146 | color: #b7e1f4; 1147 | } 1148 | 1149 | .lang-Macaulay2 { 1150 | color: #d8ffff; 1151 | } 1152 | 1153 | .lang-Makefile { 1154 | color: #427819; 1155 | } 1156 | 1157 | .lang-Mako { 1158 | color: #7e858d; 1159 | } 1160 | 1161 | .lang-Markdown { 1162 | color: #083fa1; 1163 | } 1164 | 1165 | .lang-Marko { 1166 | color: #42bff2; 1167 | } 1168 | 1169 | .lang-Mask { 1170 | color: #f97732; 1171 | } 1172 | 1173 | .lang-Mathematica { 1174 | color: #dd1100; 1175 | } 1176 | 1177 | .lang-Max { 1178 | color: #c4a79c; 1179 | } 1180 | 1181 | .lang-Mercury { 1182 | color: #ff2b2b; 1183 | } 1184 | 1185 | .lang-Mermaid { 1186 | color: #ff3670; 1187 | } 1188 | 1189 | .lang-Meson { 1190 | color: #007800; 1191 | } 1192 | 1193 | .lang-Metal { 1194 | color: #8f14e9; 1195 | } 1196 | 1197 | .lang-MiniYAML { 1198 | color: #ff1111; 1199 | } 1200 | 1201 | .lang-Mint { 1202 | color: #02b046; 1203 | } 1204 | 1205 | .lang-Mirah { 1206 | color: #c7a938; 1207 | } 1208 | 1209 | .lang-Modelica { 1210 | color: #de1d31; 1211 | } 1212 | 1213 | .lang-Modula-2 { 1214 | color: #10253f; 1215 | } 1216 | 1217 | .lang-Modula-3 { 1218 | color: #223388; 1219 | } 1220 | 1221 | .lang-Monkey-C { 1222 | color: #8d6747; 1223 | } 1224 | 1225 | .lang-MoonScript { 1226 | color: #ff4585; 1227 | } 1228 | 1229 | .lang-Motoko { 1230 | color: #fbb03b; 1231 | } 1232 | 1233 | .lang-Motorola-68K-Assembly { 1234 | color: #005daa; 1235 | } 1236 | 1237 | .lang-Move { 1238 | color: #4a137a; 1239 | } 1240 | 1241 | .lang-Mustache { 1242 | color: #724b3b; 1243 | } 1244 | 1245 | .lang-NCL { 1246 | color: #28431f; 1247 | } 1248 | 1249 | .lang-NPM-Config { 1250 | color: #cb3837; 1251 | } 1252 | 1253 | .lang-NWScript { 1254 | color: #111522; 1255 | } 1256 | 1257 | .lang-Nasal { 1258 | color: #1d2c4e; 1259 | } 1260 | 1261 | .lang-Nearley { 1262 | color: #990000; 1263 | } 1264 | 1265 | .lang-Nemerle { 1266 | color: #3d3c6e; 1267 | } 1268 | 1269 | .lang-NetLinx { 1270 | color: #0aa0ff; 1271 | } 1272 | 1273 | .lang-NetLogo { 1274 | color: #ff6375; 1275 | } 1276 | 1277 | .lang-NewLisp { 1278 | color: #87aed7; 1279 | } 1280 | 1281 | .lang-Nextflow { 1282 | color: #3ac486; 1283 | } 1284 | 1285 | .lang-Nginx { 1286 | color: #009639; 1287 | } 1288 | 1289 | .lang-Nim { 1290 | color: #ffc200; 1291 | } 1292 | 1293 | .lang-Nit { 1294 | color: #009917; 1295 | } 1296 | 1297 | .lang-Nix { 1298 | color: #7e7eff; 1299 | } 1300 | 1301 | .lang-Nu { 1302 | color: #c9df40; 1303 | } 1304 | 1305 | .lang-NumPy { 1306 | color: #9c8af9; 1307 | } 1308 | 1309 | .lang-Nunjucks { 1310 | color: #3d8137; 1311 | } 1312 | 1313 | .lang-Nushell { 1314 | color: #4e9906; 1315 | } 1316 | 1317 | .lang-OASv2-json { 1318 | color: #85ea2d; 1319 | } 1320 | 1321 | .lang-OASv2-yaml { 1322 | color: #85ea2d; 1323 | } 1324 | 1325 | .lang-OASv3-json { 1326 | color: #85ea2d; 1327 | } 1328 | 1329 | .lang-OASv3-yaml { 1330 | color: #85ea2d; 1331 | } 1332 | 1333 | .lang-OCaml { 1334 | color: #ef7a08; 1335 | } 1336 | 1337 | .lang-ObjectScript { 1338 | color: #424893; 1339 | } 1340 | 1341 | .lang-Objective-C { 1342 | color: #438eff; 1343 | } 1344 | 1345 | .lang-Objective-C\+ { 1346 | color: #6866fb; 1347 | } 1348 | 1349 | .lang-Objective-J { 1350 | color: #ff0c5a; 1351 | } 1352 | 1353 | .lang-Odin { 1354 | color: #60affe; 1355 | } 1356 | 1357 | .lang-Omgrofl { 1358 | color: #cabbff; 1359 | } 1360 | 1361 | .lang-Opal { 1362 | color: #f7ede0; 1363 | } 1364 | 1365 | .lang-Open-Policy-Agent { 1366 | color: #7d9199; 1367 | } 1368 | 1369 | .lang-OpenAPI-Specification-v2 { 1370 | color: #85ea2d; 1371 | } 1372 | 1373 | .lang-OpenAPI-Specification-v3 { 1374 | color: #85ea2d; 1375 | } 1376 | 1377 | .lang-OpenCL { 1378 | color: #ed2e2d; 1379 | } 1380 | 1381 | .lang-OpenEdge-ABL { 1382 | color: #5ce600; 1383 | } 1384 | 1385 | .lang-OpenQASM { 1386 | color: #aa70ff; 1387 | } 1388 | 1389 | .lang-OpenSCAD { 1390 | color: #e5cd45; 1391 | } 1392 | 1393 | .lang-Option-List { 1394 | color: #476732; 1395 | } 1396 | 1397 | .lang-Org { 1398 | color: #77aa99; 1399 | } 1400 | 1401 | .lang-Oxygene { 1402 | color: #cdd0e3; 1403 | } 1404 | 1405 | .lang-Oz { 1406 | color: #fab738; 1407 | } 1408 | 1409 | .lang-P4 { 1410 | color: #7055b5; 1411 | } 1412 | 1413 | .lang-PDDL { 1414 | color: #0d00ff; 1415 | } 1416 | 1417 | .lang-PEG.js { 1418 | color: #234d6b; 1419 | } 1420 | 1421 | .lang-PHP { 1422 | color: #4f5d95; 1423 | } 1424 | 1425 | .lang-PLSQL { 1426 | color: #dad8d8; 1427 | } 1428 | 1429 | .lang-PLpgSQL { 1430 | color: #336790; 1431 | } 1432 | 1433 | .lang-POV-Ray-SDL { 1434 | color: #6bac65; 1435 | } 1436 | 1437 | .lang-Pact { 1438 | color: #f7a8b8; 1439 | } 1440 | 1441 | .lang-Pan { 1442 | color: #cc0000; 1443 | } 1444 | 1445 | .lang-Papyrus { 1446 | color: #6600cc; 1447 | } 1448 | 1449 | .lang-Parrot { 1450 | color: #f3ca0a; 1451 | } 1452 | 1453 | .lang-Pascal { 1454 | color: #e3f171; 1455 | } 1456 | 1457 | .lang-Pawn { 1458 | color: #dbb284; 1459 | } 1460 | 1461 | .lang-Pep8 { 1462 | color: #c76f5b; 1463 | } 1464 | 1465 | .lang-Perl { 1466 | color: #0298c3; 1467 | } 1468 | 1469 | .lang-PicoLisp { 1470 | color: #6067af; 1471 | } 1472 | 1473 | .lang-PigLatin { 1474 | color: #fcd7de; 1475 | } 1476 | 1477 | .lang-Pike { 1478 | color: #005390; 1479 | } 1480 | 1481 | .lang-PlantUML { 1482 | color: #fbbd16; 1483 | } 1484 | 1485 | .lang-PogoScript { 1486 | color: #d80074; 1487 | } 1488 | 1489 | .lang-Polar { 1490 | color: #ae81ff; 1491 | } 1492 | 1493 | .lang-Portugol { 1494 | color: #f8bd00; 1495 | } 1496 | 1497 | .lang-PostCSS { 1498 | color: #dc3a0c; 1499 | } 1500 | 1501 | .lang-PostScript { 1502 | color: #da291c; 1503 | } 1504 | 1505 | .lang-PowerBuilder { 1506 | color: #8f0f8d; 1507 | } 1508 | 1509 | .lang-PowerShell { 1510 | color: #012456; 1511 | } 1512 | 1513 | .lang-Prisma { 1514 | color: #0c344b; 1515 | } 1516 | 1517 | .lang-Processing { 1518 | color: #0096d8; 1519 | } 1520 | 1521 | .lang-Procfile { 1522 | color: #3b2f63; 1523 | } 1524 | 1525 | .lang-Prolog { 1526 | color: #74283c; 1527 | } 1528 | 1529 | .lang-Promela { 1530 | color: #de0000; 1531 | } 1532 | 1533 | .lang-Propeller-Spin { 1534 | color: #7fa2a7; 1535 | } 1536 | 1537 | .lang-Pug { 1538 | color: #a86454; 1539 | } 1540 | 1541 | .lang-Puppet { 1542 | color: #302b6d; 1543 | } 1544 | 1545 | .lang-PureBasic { 1546 | color: #5a6986; 1547 | } 1548 | 1549 | .lang-PureScript { 1550 | color: #1d222d; 1551 | } 1552 | 1553 | .lang-Pyret { 1554 | color: #ee1e10; 1555 | } 1556 | 1557 | .lang-Python { 1558 | color: #3572a5; 1559 | } 1560 | 1561 | .lang-Python-console { 1562 | color: #3572a5; 1563 | } 1564 | 1565 | .lang-Python-traceback { 1566 | color: #3572a5; 1567 | } 1568 | 1569 | .lang-Q\# { 1570 | color: #fed659; 1571 | } 1572 | 1573 | .lang-QML { 1574 | color: #44a51c; 1575 | } 1576 | 1577 | .lang-Qt-Script { 1578 | color: #00b841; 1579 | } 1580 | 1581 | .lang-Quake { 1582 | color: #882233; 1583 | } 1584 | 1585 | .lang-R { 1586 | color: #198ce7; 1587 | } 1588 | 1589 | .lang-RAML { 1590 | color: #77d9fb; 1591 | } 1592 | 1593 | .lang-RBS { 1594 | color: #701516; 1595 | } 1596 | 1597 | .lang-RDoc { 1598 | color: #701516; 1599 | } 1600 | 1601 | .lang-REXX { 1602 | color: #d90e09; 1603 | } 1604 | 1605 | .lang-RMarkdown { 1606 | color: #198ce7; 1607 | } 1608 | 1609 | .lang-RPGLE { 1610 | color: #2bde21; 1611 | } 1612 | 1613 | .lang-RUNOFF { 1614 | color: #665a4e; 1615 | } 1616 | 1617 | .lang-Racket { 1618 | color: #3c5caa; 1619 | } 1620 | 1621 | .lang-Ragel { 1622 | color: #9d5200; 1623 | } 1624 | 1625 | .lang-Raku { 1626 | color: #0000fb; 1627 | } 1628 | 1629 | .lang-Rascal { 1630 | color: #fffaa0; 1631 | } 1632 | 1633 | .lang-ReScript { 1634 | color: #ed5051; 1635 | } 1636 | 1637 | .lang-Reason { 1638 | color: #ff5847; 1639 | } 1640 | 1641 | .lang-ReasonLIGO { 1642 | color: #ff5847; 1643 | } 1644 | 1645 | .lang-Rebol { 1646 | color: #358a5b; 1647 | } 1648 | 1649 | .lang-Record-Jar { 1650 | color: #0673ba; 1651 | } 1652 | 1653 | .lang-Red { 1654 | color: #f50000; 1655 | } 1656 | 1657 | .lang-Regular-Expression { 1658 | color: #009a00; 1659 | } 1660 | 1661 | .lang-Rez { 1662 | color: #ffdab3; 1663 | } 1664 | 1665 | .lang-Ring { 1666 | color: #2d54cb; 1667 | } 1668 | 1669 | .lang-Riot { 1670 | color: #a71e49; 1671 | } 1672 | 1673 | .lang-RobotFramework { 1674 | color: #00c0b5; 1675 | } 1676 | 1677 | .lang-Roff { 1678 | color: #ecdebe; 1679 | } 1680 | 1681 | .lang-Roff-Manpage { 1682 | color: #ecdebe; 1683 | } 1684 | 1685 | .lang-Rouge { 1686 | color: #cc0088; 1687 | } 1688 | 1689 | .lang-RouterOS-Script { 1690 | color: #de3941; 1691 | } 1692 | 1693 | .lang-Ruby { 1694 | color: #701516; 1695 | } 1696 | 1697 | .lang-Rust { 1698 | color: #dea584; 1699 | } 1700 | 1701 | .lang-SAS { 1702 | color: #b34936; 1703 | } 1704 | 1705 | .lang-SCSS { 1706 | color: #c6538c; 1707 | } 1708 | 1709 | .lang-SPARQL { 1710 | color: #0c4597; 1711 | } 1712 | 1713 | .lang-SQF { 1714 | color: #3f3f3f; 1715 | } 1716 | 1717 | .lang-SQL { 1718 | color: #e38c00; 1719 | } 1720 | 1721 | .lang-SQLPL { 1722 | color: #e38c00; 1723 | } 1724 | 1725 | .lang-SRecode-Template { 1726 | color: #348a34; 1727 | } 1728 | 1729 | .lang-STL { 1730 | color: #373b5e; 1731 | } 1732 | 1733 | .lang-SVG { 1734 | color: #ff9900; 1735 | } 1736 | 1737 | .lang-SaltStack { 1738 | color: #646464; 1739 | } 1740 | 1741 | .lang-Sass { 1742 | color: #a53b70; 1743 | } 1744 | 1745 | .lang-Scala { 1746 | color: #c22d40; 1747 | } 1748 | 1749 | .lang-Scaml { 1750 | color: #bd181a; 1751 | } 1752 | 1753 | .lang-Scenic { 1754 | color: #fdc700; 1755 | } 1756 | 1757 | .lang-Scheme { 1758 | color: #1e4aec; 1759 | } 1760 | 1761 | .lang-Scilab { 1762 | color: #ca0f21; 1763 | } 1764 | 1765 | .lang-Self { 1766 | color: #0579aa; 1767 | } 1768 | 1769 | .lang-ShaderLab { 1770 | color: #222c37; 1771 | } 1772 | 1773 | .lang-Shell { 1774 | color: #89e051; 1775 | } 1776 | 1777 | .lang-ShellCheck-Config { 1778 | color: #cecfcb; 1779 | } 1780 | 1781 | .lang-Shen { 1782 | color: #120f14; 1783 | } 1784 | 1785 | .lang-Simple-File-Verification { 1786 | color: #c9bfed; 1787 | } 1788 | 1789 | .lang-Singularity { 1790 | color: #64e6ad; 1791 | } 1792 | 1793 | .lang-Slash { 1794 | color: #007eff; 1795 | } 1796 | 1797 | .lang-Slice { 1798 | color: #003fa2; 1799 | } 1800 | 1801 | .lang-Slim { 1802 | color: #2b2b2b; 1803 | } 1804 | 1805 | .lang-SmPL { 1806 | color: #c94949; 1807 | } 1808 | 1809 | .lang-Smalltalk { 1810 | color: #596706; 1811 | } 1812 | 1813 | .lang-Smarty { 1814 | color: #f0c040; 1815 | } 1816 | 1817 | .lang-Smithy { 1818 | color: #c44536; 1819 | } 1820 | 1821 | .lang-Snakemake { 1822 | color: #419179; 1823 | } 1824 | 1825 | .lang-Solidity { 1826 | color: #aa6746; 1827 | } 1828 | 1829 | .lang-SourcePawn { 1830 | color: #f69e1d; 1831 | } 1832 | 1833 | .lang-Squirrel { 1834 | color: #800000; 1835 | } 1836 | 1837 | .lang-Stan { 1838 | color: #b2011d; 1839 | } 1840 | 1841 | .lang-Standard-ML { 1842 | color: #dc566d; 1843 | } 1844 | 1845 | .lang-Starlark { 1846 | color: #76d275; 1847 | } 1848 | 1849 | .lang-Stata { 1850 | color: #1a5f91; 1851 | } 1852 | 1853 | .lang-StringTemplate { 1854 | color: #3fb34f; 1855 | } 1856 | 1857 | .lang-Stylus { 1858 | color: #ff6347; 1859 | } 1860 | 1861 | .lang-SubRip-Text { 1862 | color: #9e0101; 1863 | } 1864 | 1865 | .lang-SugarSS { 1866 | color: #2fcc9f; 1867 | } 1868 | 1869 | .lang-SuperCollider { 1870 | color: #46390b; 1871 | } 1872 | 1873 | .lang-Svelte { 1874 | color: #ff3e00; 1875 | } 1876 | 1877 | .lang-Sway { 1878 | color: #dea584; 1879 | } 1880 | 1881 | .lang-Sweave { 1882 | color: #198ce7; 1883 | } 1884 | 1885 | .lang-Swift { 1886 | color: #f05138; 1887 | } 1888 | 1889 | .lang-SystemVerilog { 1890 | color: #dae1c2; 1891 | } 1892 | 1893 | .lang-TI-Program { 1894 | color: #a0aa87; 1895 | } 1896 | 1897 | .lang-TL-Verilog { 1898 | color: #c40023; 1899 | } 1900 | 1901 | .lang-TLA { 1902 | color: #4b0079; 1903 | } 1904 | 1905 | .lang-TOML { 1906 | color: #9c4221; 1907 | } 1908 | 1909 | .lang-TSQL { 1910 | color: #e38c00; 1911 | } 1912 | 1913 | .lang-TSV { 1914 | color: #237346; 1915 | } 1916 | 1917 | .lang-TSX { 1918 | color: #3178c6; 1919 | } 1920 | 1921 | .lang-TXL { 1922 | color: #0178b8; 1923 | } 1924 | 1925 | .lang-Talon { 1926 | color: #333333; 1927 | } 1928 | 1929 | .lang-Tcl { 1930 | color: #e4cc98; 1931 | } 1932 | 1933 | .lang-TeX { 1934 | color: #3d6117; 1935 | } 1936 | 1937 | .lang-Terra { 1938 | color: #00004c; 1939 | } 1940 | 1941 | .lang-TextMate-Properties { 1942 | color: #df66e4; 1943 | } 1944 | 1945 | .lang-Textile { 1946 | color: #ffe7ac; 1947 | } 1948 | 1949 | .lang-Thrift { 1950 | color: #d12127; 1951 | } 1952 | 1953 | .lang-Turing { 1954 | color: #cf142b; 1955 | } 1956 | 1957 | .lang-Twig { 1958 | color: #c1d026; 1959 | } 1960 | 1961 | .lang-TypeScript { 1962 | color: #3178c6; 1963 | } 1964 | 1965 | .lang-Typst { 1966 | color: #239dad; 1967 | } 1968 | 1969 | .lang-Unified-Parallel-C { 1970 | color: #4e3617; 1971 | } 1972 | 1973 | .lang-Unity3D-Asset { 1974 | color: #222c37; 1975 | } 1976 | 1977 | .lang-Uno { 1978 | color: #9933cc; 1979 | } 1980 | 1981 | .lang-UnrealScript { 1982 | color: #a54c4d; 1983 | } 1984 | 1985 | .lang-UrWeb { 1986 | color: #ccccee; 1987 | } 1988 | 1989 | .lang-V { 1990 | color: #4f87c4; 1991 | } 1992 | 1993 | .lang-VBA { 1994 | color: #867db1; 1995 | } 1996 | 1997 | .lang-VBScript { 1998 | color: #15dcdc; 1999 | } 2000 | 2001 | .lang-VCL { 2002 | color: #148aa8; 2003 | } 2004 | 2005 | .lang-VHDL { 2006 | color: #adb2cb; 2007 | } 2008 | 2009 | .lang-Vala { 2010 | color: #a56de2; 2011 | } 2012 | 2013 | .lang-Valve-Data-Format { 2014 | color: #f26025; 2015 | } 2016 | 2017 | .lang-Velocity-Template-Language { 2018 | color: #507cff; 2019 | } 2020 | 2021 | .lang-Verilog { 2022 | color: #b2b7f8; 2023 | } 2024 | 2025 | .lang-Vim-Help-File { 2026 | color: #199f4b; 2027 | } 2028 | 2029 | .lang-Vim-Script { 2030 | color: #199f4b; 2031 | } 2032 | 2033 | .lang-Vim-Snippet { 2034 | color: #199f4b; 2035 | } 2036 | 2037 | .lang-Visual-Basic-.NET { 2038 | color: #945db7; 2039 | } 2040 | 2041 | .lang-Visual-Basic-6\.0 { 2042 | color: #2c6353; 2043 | } 2044 | 2045 | .lang-Volt { 2046 | color: #1f1f1f; 2047 | } 2048 | 2049 | .lang-Vue { 2050 | color: #41b883; 2051 | } 2052 | 2053 | .lang-Vyper { 2054 | color: #2980b9; 2055 | } 2056 | 2057 | .lang-WDL { 2058 | color: #42f1f4; 2059 | } 2060 | 2061 | .lang-WGSL { 2062 | color: #1a5e9a; 2063 | } 2064 | 2065 | .lang-Web-Ontology-Language { 2066 | color: #5b70bd; 2067 | } 2068 | 2069 | .lang-WebAssembly { 2070 | color: #04133b; 2071 | } 2072 | 2073 | .lang-WebAssembly-Interface-Type { 2074 | color: #6250e7; 2075 | } 2076 | 2077 | .lang-Whiley { 2078 | color: #d5c397; 2079 | } 2080 | 2081 | .lang-Wikitext { 2082 | color: #fc5757; 2083 | } 2084 | 2085 | .lang-Windows-Registry-Entries { 2086 | color: #52d5ff; 2087 | } 2088 | 2089 | .lang-Witcher-Script { 2090 | color: #ff0000; 2091 | } 2092 | 2093 | .lang-Wollok { 2094 | color: #a23738; 2095 | } 2096 | 2097 | .lang-World-of-Warcraft-Addon-Data { 2098 | color: #f7e43f; 2099 | } 2100 | 2101 | .lang-Wren { 2102 | color: #383838; 2103 | } 2104 | 2105 | .lang-X10 { 2106 | color: #4b6bef; 2107 | } 2108 | 2109 | .lang-XC { 2110 | color: #99da07; 2111 | } 2112 | 2113 | .lang-XML { 2114 | color: #0060ac; 2115 | } 2116 | 2117 | .lang-XML-Property-List { 2118 | color: #0060ac; 2119 | } 2120 | 2121 | .lang-XQuery { 2122 | color: #5232e7; 2123 | } 2124 | 2125 | .lang-XSLT { 2126 | color: #eb8ceb; 2127 | } 2128 | 2129 | .lang-Xojo { 2130 | color: #81bd41; 2131 | } 2132 | 2133 | .lang-Xonsh { 2134 | color: #285eef; 2135 | } 2136 | 2137 | .lang-Xtend { 2138 | color: #24255d; 2139 | } 2140 | 2141 | .lang-YAML { 2142 | color: #cb171e; 2143 | } 2144 | 2145 | .lang-YARA { 2146 | color: #220000; 2147 | } 2148 | 2149 | .lang-YASnippet { 2150 | color: #32ab90; 2151 | } 2152 | 2153 | .lang-Yacc { 2154 | color: #4b6c4b; 2155 | } 2156 | 2157 | .lang-Yul { 2158 | color: #794932; 2159 | } 2160 | 2161 | .lang-ZAP { 2162 | color: #0d665e; 2163 | } 2164 | 2165 | .lang-ZIL { 2166 | color: #dc75e5; 2167 | } 2168 | 2169 | .lang-ZenScript { 2170 | color: #00bcd1; 2171 | } 2172 | 2173 | .lang-Zephir { 2174 | color: #118f9e; 2175 | } 2176 | 2177 | .lang-Zig { 2178 | color: #ec915c; 2179 | } 2180 | 2181 | .lang-Zimpl { 2182 | color: #d67711; 2183 | } 2184 | 2185 | .lang-eC { 2186 | color: #913960; 2187 | } 2188 | 2189 | .lang-fish { 2190 | color: #4aae47; 2191 | } 2192 | 2193 | .lang-hoon { 2194 | color: #00b171; 2195 | } 2196 | 2197 | .lang-jq { 2198 | color: #c7254e; 2199 | } 2200 | 2201 | .lang-kvlang { 2202 | color: #1da6e0; 2203 | } 2204 | 2205 | .lang-mIRC-Script { 2206 | color: #3d57c3; 2207 | } 2208 | 2209 | .lang-mcfunction { 2210 | color: #e22837; 2211 | } 2212 | 2213 | .lang-mupad { 2214 | color: #244963; 2215 | } 2216 | 2217 | .lang-nanorc { 2218 | color: #2d004d; 2219 | } 2220 | 2221 | .lang-nesC { 2222 | color: #94b0c7; 2223 | } 2224 | 2225 | .lang-ooc { 2226 | color: #b0b77e; 2227 | } 2228 | 2229 | .lang-q { 2230 | color: #0040cd; 2231 | } 2232 | 2233 | .lang-reStructuredText { 2234 | color: #141414; 2235 | } 2236 | 2237 | .lang-sed { 2238 | color: #64b970; 2239 | } 2240 | 2241 | .lang-wisp { 2242 | color: #7582d1; 2243 | } 2244 | 2245 | .lang-xBase { 2246 | color: #403a40; 2247 | } 2248 | -------------------------------------------------------------------------------- /assets/css/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --safe-top: env(safe-area-inset-top, 0); 7 | --safe-right: env(safe-area-inset-right, 0); 8 | --safe-bottom: env(safe-area-inset-bottom, 0); 9 | --safe-left: env(safe-area-inset-left, 0); 10 | --primary-500: #0ea5e9; 11 | 12 | color-scheme: light dark; 13 | 14 | &.light { 15 | color-scheme: light; 16 | } 17 | 18 | &.dark { 19 | color-scheme: dark; 20 | } 21 | } 22 | 23 | ::selection { 24 | @apply bg-primary-500 bg-opacity-70 text-white; 25 | } 26 | 27 | html:has(.backdrop) { 28 | overflow: clip !important; 29 | } 30 | 31 | html, 32 | body { 33 | @apply w-full overflow-x-clip bg-white dark:bg-black text-stone-900 dark:text-stone-100; 34 | } 35 | 36 | img { 37 | @apply object-cover; 38 | } 39 | 40 | input, 41 | textarea { 42 | @apply rounded-md px-3 py-2 transition; 43 | @apply accent-primary-500 bg-white dark:bg-black; 44 | @apply border border-stone-200 dark:border-stone-800; 45 | 46 | &[disabled] { 47 | @apply cursor-not-allowed opacity-30 bg-stone-300 dark:bg-stone-700; 48 | } 49 | 50 | &:focus { 51 | @apply outline-none ring ring-primary-500; 52 | } 53 | 54 | &[type="checkbox"] { 55 | @apply h-4 w-4 cursor-pointer accent-primary-500 ring-0; 56 | } 57 | } 58 | 59 | label { 60 | @apply cursor-pointer select-none; 61 | } 62 | 63 | hr { 64 | @apply border-primary-100 dark:border-primary-950; 65 | } 66 | 67 | .page-leave-active, 68 | .page-enter-active { 69 | @apply transform-gpu transition-all duration-300; 70 | } 71 | 72 | .page-enter-from, 73 | .page-leave-to { 74 | @apply opacity-0; 75 | } 76 | 77 | :root, 78 | .themed-scrollbar { 79 | @apply scrollbar scrollbar-track-transparent scrollbar-thumb-primary-200 80 | scrollbar-track-rounded-full scrollbar-thumb-rounded-full scrollbar-corner-rounded-full 81 | scrollbar-w-2 dark:scrollbar-thumb-primary-900; 82 | } 83 | 84 | :root.dark { 85 | @apply scrollbar-thumb-primary-900 scrollbar-track-transparent; 86 | } 87 | 88 | :is(#read, #edit).md-editor { 89 | @apply bg-white font-sans dark:bg-black rounded-md; 90 | 91 | .default-theme { 92 | --md-theme-color: rgb(39 39 42 / var(--tw-text-opacity)); 93 | --md-theme-link-color: var(--primary-500); 94 | --md-theme-link-hover-color: var(--primary-500); 95 | 96 | blockquote { 97 | @apply border-l-primary-500; 98 | } 99 | 100 | pre:before { 101 | @apply bg-stone-800; 102 | } 103 | 104 | code { 105 | @apply text-current bg-stone-100 dark:bg-stone-800 rounded py-0 px-1; 106 | } 107 | 108 | pre code { 109 | @apply bg-stone-800; 110 | 111 | padding: 1em; 112 | color: var(--md-theme-code-block-color); 113 | border-radius: 0 0 5px 5px; 114 | } 115 | 116 | ul { 117 | @apply list-disc; 118 | } 119 | } 120 | 121 | pre, 122 | code { 123 | @apply font-mono; 124 | } 125 | } 126 | 127 | .dark :is(#read, #edit).md-editor .default-theme { 128 | --md-theme-color: rgb(228 228 231 / var(--tw-text-opacity)); 129 | --md-theme-color-reverse: #222; 130 | --md-theme-color-hover: #191919; 131 | --md-theme-color-hover-inset: #444; 132 | --md-theme-border-color: #2d2d2d; 133 | --md-theme-border-color-reverse: #e6e6e6; 134 | --md-theme-border-color-inset: #5a5a5a; 135 | --md-theme-bg-color: #000; 136 | --md-theme-bg-color-inset: #111; 137 | --md-theme-bg-color-scrollbar-track: #0f0f0f; 138 | --md-theme-bg-color-scrollbar-thumb: #2d2d2d; 139 | --md-theme-bg-color-scrollbar-thumb-hover: #3a3a3a; 140 | --md-theme-bg-color-scrollbar-thumb-active: #3a3a3a; 141 | --md-theme-code-copy-tips-color: inherit; 142 | --md-theme-code-copy-tips-bg-color: #3a3a3a; 143 | --md-theme-code-active-color: #e6c07b; 144 | } 145 | 146 | .md-editor-catalog { 147 | 148 | .md-editor-catalog-link > span:hover, 149 | .md-editor-catalog-active > span { 150 | @apply text-primary-500; 151 | } 152 | } 153 | 154 | .Vue-Toastification__container { 155 | @apply print:hidden; 156 | } 157 | 158 | @keyframes rotate { 159 | to { 160 | transform: rotate(360deg); 161 | } 162 | } 163 | 164 | @layer components { 165 | .safe-inset-0 { 166 | inset: var(--safe-top) var(--safe-right) var(--safe-bottom) var(--safe-left); 167 | } 168 | 169 | .safe-left-0 { 170 | left: var(--safe-left); 171 | } 172 | 173 | .safe-right-0 { 174 | right: var(--safe-right); 175 | } 176 | 177 | .safe-top-0 { 178 | top: var(--safe-top); 179 | } 180 | 181 | .safe-bottom-0 { 182 | bottom: var(--safe-bottom); 183 | } 184 | 185 | .page { 186 | @apply container mx-auto max-w-7xl px-2 pb-16 pt-16 sm:pt-24 sm:px-6; 187 | } 188 | 189 | .card { 190 | @apply bg-primary-50 dark:bg-stone-800; 191 | } 192 | 193 | .animation-rotate { 194 | animation: rotate 1s infinite; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /assets/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blyrin/flapypan-blog/de249d878eadd5250ffa88497e503957a91b6173/assets/img/loading.gif -------------------------------------------------------------------------------- /components/AppBar.client.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | 208 | -------------------------------------------------------------------------------- /components/AppFooter.client.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /components/ArticleCard.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 52 | -------------------------------------------------------------------------------- /components/ArticleEditor.client.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 146 | -------------------------------------------------------------------------------- /components/ArticleTimeline.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 34 | -------------------------------------------------------------------------------- /components/Backdrop.client.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 20 | -------------------------------------------------------------------------------- /components/Btn.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 61 | -------------------------------------------------------------------------------- /components/ColorModeSwitch.client.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 53 | -------------------------------------------------------------------------------- /components/Dialog.client.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 50 | -------------------------------------------------------------------------------- /components/Drawer.client.vue: -------------------------------------------------------------------------------- 1 | 88 | 89 | 103 | -------------------------------------------------------------------------------- /components/GiscusCard.client.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 40 | -------------------------------------------------------------------------------- /components/LandingAnimation.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 36 | 37 | 40 | -------------------------------------------------------------------------------- /components/LoadingIndicator.client.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | 20 | 22 | -------------------------------------------------------------------------------- /components/LoginForm.client.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 56 | -------------------------------------------------------------------------------- /components/RefreshButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /components/TextBlock.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 33 | -------------------------------------------------------------------------------- /components/TextField.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 33 | -------------------------------------------------------------------------------- /components/TextFieldContainer.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 30 | -------------------------------------------------------------------------------- /error.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 40 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | import { FlatCompat } from '@eslint/eslintrc' 3 | 4 | const compat = new FlatCompat() 5 | 6 | export default antfu( 7 | {}, 8 | ...compat.config({ 9 | extends: ['plugin:tailwindcss/recommended'], 10 | }), 11 | { 12 | rules: { 13 | 'antfu/if-newline': 'off', 14 | 'curly': ['error', 'multi-line'], 15 | 'style/arrow-parens': ['error', 'always'], 16 | 'style/brace-style': ['error', '1tbs', { allowSingleLine: true }], 17 | 'node/prefer-global/buffer': 'off', 18 | 'vue/html-self-closing': ['error', { 19 | html: { 20 | void: 'always', 21 | normal: 'never', 22 | component: 'always', 23 | }, 24 | svg: 'always', 25 | math: 'always', 26 | }], 27 | }, 28 | }, 29 | ) 30 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | devtools: { enabled: true }, 4 | typescript: { 5 | typeCheck: true, 6 | }, 7 | appConfig: { 8 | nuxtIcon: {}, 9 | }, 10 | ssr: true, 11 | routeRules: { 12 | '/tag': { redirect: '/archive' }, 13 | }, 14 | modules: [ 15 | '@nuxtjs/color-mode', 16 | 'nuxt-icon', 17 | '@formkit/auto-animate/nuxt', 18 | '@pinia/nuxt', 19 | 'nuxt-mongoose', 20 | ], 21 | colorMode: { 22 | preference: 'system', 23 | fallback: 'light', 24 | classPrefix: '', 25 | classSuffix: '', 26 | }, 27 | build: { 28 | transpile: ['jsonwebtoken', 'vue-toastification'], 29 | }, 30 | vite: { 31 | build: { 32 | modulePreload: { polyfill: true }, 33 | }, 34 | }, 35 | vue: { 36 | compilerOptions: { 37 | // 排除自定义组件,防止 vue 编译处理 38 | isCustomElement: (tag) => ['giscus-widget'].includes(tag), 39 | }, 40 | }, 41 | app: { 42 | head: { 43 | meta: [ 44 | { charset: 'utf-8' }, 45 | { name: 'viewport', content: 'width=device-width, initial-scale=1.0' }, 46 | ], 47 | link: [ 48 | { 49 | rel: 'preconnect', 50 | href: 'https://gstatic.loli.net', 51 | crossorigin: 'anonymous', 52 | }, 53 | { 54 | rel: 'stylesheet', 55 | href: 'https://fonts.loli.net/css2?family=Inter:wght@100..900&display=swap', 56 | }, 57 | { 58 | rel: 'stylesheet', 59 | href: 'https://fonts.loli.net/css2?family=Fira+Code:wght@300..700&display=swap', 60 | }, 61 | ], 62 | }, 63 | pageTransition: { name: 'page', mode: 'out-in' }, 64 | }, 65 | css: ['vue-toastification/dist/index.css', '~/assets/css/main.css'], 66 | postcss: { 67 | plugins: { 68 | 'postcss-import': {}, 69 | 'tailwindcss/nesting': 'postcss-nesting', 70 | 'tailwindcss': {}, 71 | 'postcss-preset-env': { 72 | features: { 'nesting-rules': false }, 73 | }, 74 | }, 75 | }, 76 | }) 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flapypan-blog", 3 | "type": "module", 4 | "version": "2.3.13", 5 | "description": "A simple and efficient blogging system.", 6 | "author": "FlapyPan", 7 | "license": "MPL-2.0", 8 | "homepage": "https://www.flapypan.top/", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/FlapyPan/flapypan-blog" 12 | }, 13 | "engines": { 14 | "node": ">=18" 15 | }, 16 | "scripts": { 17 | "postinstall": "nuxt prepare", 18 | "dev": "nuxt dev", 19 | "build": "nuxt build", 20 | "preview": "node .output/server/index.mjs", 21 | "lint": "eslint .", 22 | "lint:fix": "eslint . --fix" 23 | }, 24 | "dependencies": { 25 | "@eslint/eslintrc": "^3.0.2", 26 | "@formkit/auto-animate": "^0.8.1", 27 | "@nuxtjs/color-mode": "^3.3.3", 28 | "@pinia/nuxt": "^0.5.1", 29 | "@vueuse/core": "^10.9.0", 30 | "giscus": "^1.5.0", 31 | "jsonwebtoken": "^9.0.2", 32 | "md-editor-v3": "^4.12.2", 33 | "nuxt-icon": "^0.6.10", 34 | "nuxt-mongoose": "^1.0.5", 35 | "sharp": "^0.33.3", 36 | "vue": "^3.4.21", 37 | "vue-router": "^4.3.0", 38 | "vue-toastification": "2.0.0-rc.5", 39 | "ws": "^8.16.0", 40 | "zod": "^3.22.4" 41 | }, 42 | "devDependencies": { 43 | "@antfu/eslint-config": "^2.9.0", 44 | "@nuxt/devtools": "latest", 45 | "@types/jsonwebtoken": "^9.0.6", 46 | "@types/ws": "^8.5.10", 47 | "eslint": "^8.57.0", 48 | "eslint-plugin-tailwindcss": "^3.15.1", 49 | "nuxt": "^3.11.1", 50 | "postcss-preset-env": "^9.5.2", 51 | "tailwind-scrollbar": "^3.1.0", 52 | "tailwindcss": "^3.4.1", 53 | "typescript": "^5.4.3", 54 | "vue-tsc": "^1.8.27" 55 | }, 56 | "browserslist": [ 57 | "Chrome >= 87", 58 | "Firefox >= 78", 59 | "Safari >= 14", 60 | "Edge >= 88" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /pages/[path].vue: -------------------------------------------------------------------------------- 1 | 217 | 218 | 398 | 399 | 401 | -------------------------------------------------------------------------------- /pages/activity.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 88 | -------------------------------------------------------------------------------- /pages/archive.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 77 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 145 | -------------------------------------------------------------------------------- /pages/new.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 33 | -------------------------------------------------------------------------------- /pages/setting.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 149 | -------------------------------------------------------------------------------- /pages/tag/[tag].vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 47 | -------------------------------------------------------------------------------- /plugins/toastification.ts: -------------------------------------------------------------------------------- 1 | import Toast, { POSITION, type PluginOptions } from 'vue-toastification' 2 | 3 | export default defineNuxtPlugin(({ vueApp }) => { 4 | vueApp.use(Toast, { 5 | timeout: 3000, 6 | draggable: true, 7 | position: POSITION.BOTTOM_CENTER, 8 | } as PluginOptions) 9 | }) 10 | -------------------------------------------------------------------------------- /public/avatar.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blyrin/flapypan-blog/de249d878eadd5250ffa88497e503957a91b6173/public/avatar.webp -------------------------------------------------------------------------------- /public/banner.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blyrin/flapypan-blog/de249d878eadd5250ffa88497e503957a91b6173/public/banner.webp -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blyrin/flapypan-blog/de249d878eadd5250ffa88497e503957a91b6173/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /api/ 3 | Disallow: /api/static/ 4 | Disallow: /new/ 5 | Disallow: /setting/ 6 | -------------------------------------------------------------------------------- /server/api/access/index.get.ts: -------------------------------------------------------------------------------- 1 | import { getAllAccessCount, getTodayAccessCount } from '~/server/data/access' 2 | 3 | export default eventHandler(async () => { 4 | const [all, today] = await Promise.all([getAllAccessCount(), getTodayAccessCount()]) 5 | return { all, today } 6 | }) 7 | -------------------------------------------------------------------------------- /server/api/ai/summary.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod' 2 | import { getArticleById, modifyArticle } from '~/server/data/article' 3 | import { sparkAI } from '~/server/utils/ai' 4 | 5 | export default eventHandler(async (event) => { 6 | const { _id } = await readSafeBody(event, { _id: z.string() }) 7 | const { content } = ensure(await getArticleById(_id), '不存在的文章', 404) 8 | const summary = await sparkAI.summary(content) 9 | await modifyArticle({ _id, summary }) 10 | return {} 11 | }) 12 | -------------------------------------------------------------------------------- /server/api/article/[_id].delete.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod' 2 | import { deleteArticle } from '~/server/data/article' 3 | 4 | export default eventHandler(async (event) => { 5 | const { _id } = readParams(event, { _id: z.string() }) 6 | return deleteArticle(_id) 7 | }) 8 | -------------------------------------------------------------------------------- /server/api/article/[path].get.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod' 2 | import { getArticleByPath } from '~/server/data/article' 3 | import { addAccess, getArticleAccessCount } from '~/server/data/access' 4 | 5 | export default eventHandler(async (event) => { 6 | const { path } = readParams(event, { path: z.string() }) 7 | const article = ensure(await getArticleByPath(path), '不存在的文章', 404) 8 | void addAccess({ 9 | ip: getRealIP(event), 10 | referrer: getHeader(event, 'Referer'), 11 | ua: getHeader(event, 'User-Agent'), 12 | articleId: article._id, 13 | }) 14 | const accessCount = await getArticleAccessCount(article._id) 15 | return { ...article.toObject(), accessCount } 16 | }) 17 | -------------------------------------------------------------------------------- /server/api/article/delete/[_id].get.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod' 2 | import { deleteArticle } from '~/server/data/article' 3 | 4 | export default eventHandler(async (event) => { 5 | // DELETE /api/article/[_id] 方法报 404 的临时解决方案 6 | const { _id } = readParams(event, { _id: z.string() }) 7 | return deleteArticle(_id) 8 | }) 9 | -------------------------------------------------------------------------------- /server/api/article/index.get.ts: -------------------------------------------------------------------------------- 1 | import { getArticleList } from '~/server/data/article' 2 | 3 | export default eventHandler(async () => { 4 | return getArticleList() 5 | }) 6 | -------------------------------------------------------------------------------- /server/api/article/index.post.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod' 2 | import { addArticle } from '~/server/data/article' 3 | 4 | export default eventHandler(async (event) => { 5 | const data = await readSafeBody(event, { 6 | title: z.string().min(2, '标题长度2-32').max(32, '标题长度2-32'), 7 | path: z 8 | .string() 9 | .min(2, '路径长度2-32') 10 | .max(64, '路径长度2-32') 11 | .regex(/^[a-z0-9:@._-]+$/, '路径只允许小写字母、数字、冒号、@、英文点、下划线、分隔符'), 12 | cover: z.string().nullish(), 13 | content: z.string().min(1, '内容不能为空'), 14 | tags: z.string().array(), 15 | pinned: z.boolean().nullish(), 16 | }) 17 | return addArticle(data) 18 | }) 19 | -------------------------------------------------------------------------------- /server/api/article/index.put.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod' 2 | import { modifyArticle } from '~/server/data/article' 3 | 4 | export default eventHandler(async (event) => { 5 | const data = await readSafeBody(event, { 6 | _id: z.string().min(1), 7 | title: z.string().min(2, '标题长度2-32').max(32, '标题长度2-32').nullish(), 8 | path: z.string() 9 | .min(2, '路径长度2-32') 10 | .max(64, '路径长度2-32') 11 | .regex(/^[a-z0-9:@._-]+$/, '路径只允许小写字母,数字,冒号,@,英文点,下划线,分隔符') 12 | .nullish(), 13 | cover: z.string().nullish(), 14 | content: z.string().min(1, '内容不能为空').nullish(), 15 | tags: z.string().array().nullish(), 16 | pinned: z.boolean().nullish(), 17 | }) 18 | return modifyArticle(data) 19 | }) 20 | -------------------------------------------------------------------------------- /server/api/article/pinned/index.get.ts: -------------------------------------------------------------------------------- 1 | import { getPinnedArticleList } from '~/server/data/article' 2 | 3 | export default eventHandler(async () => getPinnedArticleList()) 4 | -------------------------------------------------------------------------------- /server/api/article/recent/index.get.ts: -------------------------------------------------------------------------------- 1 | import { getRecentArticleList } from '~/server/data/article' 2 | 3 | export default eventHandler(async () => getRecentArticleList()) 4 | -------------------------------------------------------------------------------- /server/api/article/tag/[tag].get.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod' 2 | import { getArticleListByTag } from '~/server/data/article' 3 | 4 | export default eventHandler(async (event) => { 5 | const { tag } = readParams(event, { tag: z.string() }) 6 | // 浏览器会将路径进行编码,这里进行解码 7 | const decodedTag = decodeURI(tag) 8 | return getArticleListByTag(decodedTag) 9 | }) 10 | -------------------------------------------------------------------------------- /server/api/attribute/[key].get.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod' 2 | import { getAttr } from '~/server/data/attribute' 3 | 4 | export default eventHandler(async (event) => { 5 | const accept = getHeader(event, 'Accept')?.split(',')[0] 6 | if (accept) { 7 | setHeader(event, 'Content-Type', accept) 8 | } 9 | const { key } = readParams(event, { key: z.string() }) 10 | return await getAttr(key) 11 | }) 12 | -------------------------------------------------------------------------------- /server/api/attribute/[key].post.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod' 2 | import { setAttr } from '~/server/data/attribute' 3 | 4 | export default eventHandler(async (event) => { 5 | const { key } = readParams(event, { key: z.string() }) 6 | const body = (await readBody(event)) ?? null 7 | return await setAttr(key, body) 8 | }) 9 | -------------------------------------------------------------------------------- /server/api/auth/login.post.ts: -------------------------------------------------------------------------------- 1 | // 登录 2 | export default eventHandler(login) 3 | -------------------------------------------------------------------------------- /server/api/auth/logout.post.ts: -------------------------------------------------------------------------------- 1 | // 退出登录 2 | export default eventHandler(logout) 3 | -------------------------------------------------------------------------------- /server/api/auth/user.get.ts: -------------------------------------------------------------------------------- 1 | // 检查登录状态 2 | export default eventHandler(auth) 3 | -------------------------------------------------------------------------------- /server/api/picture/[_id].get.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod' 2 | import { getPicture } from '~/server/data/picture' 3 | 4 | export default cachedEventHandler( 5 | async (event) => { 6 | const { _id } = readParams(event, { _id: z.string() }) 7 | return ensure(await getPicture(_id), '不存在的图片', 404) 8 | }, 9 | { maxAge: 31536000, headersOnly: true }, 10 | ) 11 | -------------------------------------------------------------------------------- /server/api/picture/index.post.ts: -------------------------------------------------------------------------------- 1 | import { addPicture } from '~/server/data/picture' 2 | 3 | export default eventHandler(async (event) => { 4 | const multiPartData = ensure(await readMultipartFormData(event), '错误的请求类型') 5 | const file = ensure(multiPartData[0], '请上传文件') 6 | ensure(file.type?.startsWith('image/'), '请上传图片文件') 7 | return (await addPicture(file)).toString() 8 | }) 9 | -------------------------------------------------------------------------------- /server/api/ping.get.ts: -------------------------------------------------------------------------------- 1 | export default eventHandler(() => 'pong') 2 | -------------------------------------------------------------------------------- /server/api/tag/index.get.ts: -------------------------------------------------------------------------------- 1 | import { getAllTags } from '~/server/data/article' 2 | 3 | export default eventHandler(async () => getAllTags()) 4 | -------------------------------------------------------------------------------- /server/data/access.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectId } from 'bson' 2 | 3 | interface AccessAddRequest { 4 | ip?: string 5 | referrer?: string 6 | ua?: string 7 | articleId?: string | ObjectId 8 | } 9 | 10 | export function addAccess(access: AccessAddRequest) { 11 | new AccessSchema(access).save() 12 | } 13 | 14 | export function getArticleAccessCount(articleId: string | ObjectId): Promise { 15 | return AccessSchema.countDocuments({ articleId }) 16 | } 17 | 18 | export function getTodayAccessCount(): Promise { 19 | const begin = new Date() 20 | begin.setHours(0, 0, 0, 0) 21 | const end = new Date() 22 | return AccessSchema.countDocuments({ createdAt: { $gte: begin, $lte: end } }) 23 | } 24 | 25 | export function getAllAccessCount(): Promise { 26 | return AccessSchema.countDocuments() 27 | } 28 | -------------------------------------------------------------------------------- /server/data/article.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectId } from 'bson' 2 | 3 | const listSelect = { 4 | content: 0, 5 | } 6 | 7 | export function getRecentArticleList() { 8 | return ArticleSchema.find({}).limit(8).sort({ updatedAt: -1 }).select(listSelect) 9 | } 10 | 11 | export function getArticleList() { 12 | return ArticleSchema.find({}).sort({ createdAt: -1 }).select(listSelect) 13 | } 14 | 15 | export async function getAllTags() { 16 | const tags = await ArticleSchema.distinct('tags') 17 | return tags.filter((tag) => !!tag) 18 | } 19 | 20 | export function getArticleById(_id: string | ObjectId) { 21 | return ArticleSchema.findById(_id) 22 | } 23 | 24 | export function getArticleByPath(path: string) { 25 | return ArticleSchema.findOne({ path }) 26 | } 27 | 28 | export function getArticleListByTag(tag: string) { 29 | return ArticleSchema.find({ tags: tag }).sort({ createdAt: -1 }) 30 | } 31 | 32 | export function getPinnedArticleList() { 33 | return ArticleSchema.find({ pinned: true }) 34 | .sort({ updatedAt: 1 }) 35 | .select({ _id: 1, title: 1, path: 1 }) 36 | } 37 | 38 | interface ArticleAddRequest { 39 | content: string 40 | tags: string[] 41 | title: string 42 | path: string 43 | cover?: string | null 44 | pinned?: boolean | null 45 | } 46 | 47 | export async function addArticle(article: ArticleAddRequest) { 48 | const saved = await new ArticleSchema({ 49 | ...article, 50 | updatedAt: new Date(), 51 | }).save() 52 | return { path: saved.path } 53 | } 54 | 55 | interface ArticleModifyRequest { 56 | _id: string | ObjectId 57 | title?: string | null 58 | path?: string | null 59 | cover?: string | null 60 | content?: string | null 61 | summary?: string | null 62 | tags?: string[] | null 63 | } 64 | 65 | export async function modifyArticle(article: ArticleModifyRequest) { 66 | const newData: any = { ...article } 67 | if (newData.content) newData.updatedAt = new Date() 68 | await ArticleSchema.findByIdAndUpdate(article._id, newData) 69 | return { path: article.path } 70 | } 71 | 72 | export async function deleteArticle(_id: string | ObjectId) { 73 | await ArticleSchema.deleteOne({ _id }) 74 | } 75 | -------------------------------------------------------------------------------- /server/data/attribute.ts: -------------------------------------------------------------------------------- 1 | export async function getAttr(key: string) { 2 | const pair = await AttributeSchema.findOne({ key }) 3 | return pair?.value as T 4 | } 5 | 6 | export async function setAttr(key: string, value: T) { 7 | if ((await AttributeSchema.countDocuments({ key })) > 0) { 8 | await AttributeSchema.findOneAndUpdate({ key }, { $set: { value } }) 9 | } else { 10 | await new AttributeSchema({ key, value }).save() 11 | } 12 | return getAttr(key) 13 | } 14 | -------------------------------------------------------------------------------- /server/data/picture.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'node:crypto' 2 | import type { MultiPartData } from 'h3' 3 | import type { ObjectId } from 'bson' 4 | import sharp from 'sharp' 5 | 6 | export async function addPicture({ data, filename }: MultiPartData): Promise { 7 | const name = filename ?? randomUUID() 8 | const bytes = await sharp(data, { animated: true }).webp().toBuffer() 9 | const { _id } = await new PictureSchema({ bytes, name }).save() 10 | return _id 11 | } 12 | 13 | export async function getPicture(_id: string) { 14 | const picture = await PictureSchema.findById(_id) 15 | return picture?.bytes 16 | } 17 | -------------------------------------------------------------------------------- /server/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | export default eventHandler((event) => { 2 | const { method, path } = event 3 | do { 4 | // 不认证非 api 请求 5 | if (!path.startsWith('/api')) break 6 | // 不认证 get 请求 7 | if (method === 'GET') break 8 | // 不认证登录请求 9 | if (path.startsWith('/api/auth/login')) break 10 | 11 | event.context.user = auth(event) 12 | } while (false) 13 | }) 14 | -------------------------------------------------------------------------------- /server/models/access.schema.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from 'mongoose' 2 | import type { IAccess } from '~/types/models' 3 | import { defineMongooseModel } from '#nuxt/mongoose' 4 | 5 | export const AccessSchema = defineMongooseModel('Access', { 6 | ip: { 7 | type: Schema.Types.String, 8 | }, 9 | referrer: { 10 | type: Schema.Types.String, 11 | }, 12 | ua: { 13 | type: Schema.Types.String, 14 | }, 15 | articleId: { 16 | type: Schema.Types.ObjectId, 17 | index: true, 18 | }, 19 | createdAt: { 20 | type: Schema.Types.Date, 21 | index: true, 22 | default: () => new Date(), 23 | }, 24 | }) 25 | -------------------------------------------------------------------------------- /server/models/article.schema.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from 'mongoose' 2 | import type { IArticle } from '~/types/models' 3 | import { defineMongooseModel } from '#nuxt/mongoose' 4 | 5 | export const ArticleSchema = defineMongooseModel('Article', { 6 | title: { 7 | type: Schema.Types.String, 8 | required: true, 9 | }, 10 | path: { 11 | type: Schema.Types.String, 12 | required: true, 13 | unique: true, 14 | }, 15 | cover: { 16 | type: Schema.Types.String, 17 | default: '/banner.webp', 18 | }, 19 | content: { 20 | type: Schema.Types.String, 21 | required: true, 22 | }, 23 | summary: { 24 | type: Schema.Types.String, 25 | }, 26 | pinned: { 27 | type: Schema.Types.Boolean, 28 | index: true, 29 | default: false, 30 | }, 31 | tags: { 32 | type: [Schema.Types.String], 33 | index: true, 34 | default: () => [], 35 | }, 36 | createdAt: { 37 | type: Schema.Types.Date, 38 | index: true, 39 | default: () => new Date(), 40 | }, 41 | updatedAt: { 42 | type: Schema.Types.Date, 43 | index: true, 44 | default: () => new Date(), 45 | }, 46 | }) 47 | -------------------------------------------------------------------------------- /server/models/attribute.schema.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from 'mongoose' 2 | import type { IAttribute } from '~/types/models' 3 | import { defineMongooseModel } from '#nuxt/mongoose' 4 | 5 | export const AttributeSchema = defineMongooseModel('Attribute', { 6 | key: { 7 | type: Schema.Types.String, 8 | required: true, 9 | unique: true, 10 | }, 11 | value: { 12 | type: Schema.Types.Mixed, 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /server/models/picture.schema.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from 'mongoose' 2 | import type { IPicture } from '~/types/models' 3 | import { defineMongooseModel } from '#nuxt/mongoose' 4 | 5 | export const PictureSchema = defineMongooseModel('Picture', { 6 | name: { 7 | type: Schema.Types.String, 8 | required: true, 9 | }, 10 | bytes: { 11 | type: Buffer, 12 | required: true, 13 | }, 14 | createdAt: { 15 | type: Schema.Types.Date, 16 | default: () => new Date(), 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json", 3 | "exclude": [ 4 | "../eslint.config.js" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /server/utils/ai.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import type { BinaryLike } from 'node:crypto' 3 | import { createHmac } from 'node:crypto' 4 | import type { ErrorEvent } from 'ws' 5 | import WebSocket from 'ws' 6 | 7 | export interface SparkAIConversation { 8 | role: 'system' | 'user' | 'assistant' 9 | content: string 10 | } 11 | 12 | export interface SparkAIResult { 13 | header: { 14 | code: number 15 | message: string 16 | status: 0 | 1 | 2 17 | } 18 | payload: { 19 | choices: { 20 | seq: number 21 | text: [{ content: string }] 22 | } 23 | } 24 | } 25 | 26 | function sha256Digest(secret: BinaryLike): (signOrigin: BinaryLike) => string { 27 | const hmac = createHmac('sha256', secret) 28 | return (signOrigin: BinaryLike) => { 29 | hmac.update(signOrigin) 30 | return hmac.digest('base64') 31 | } 32 | } 33 | 34 | async function getWebsocketUrl({ apiKey, apiSecret }: { apiKey: string, apiSecret: string }): Promise { 35 | const url = new URL('wss://spark-api.xf-yun.com/v3.5/chat') 36 | const host = url.host 37 | const path = url.pathname 38 | const date = new Date().toUTCString() 39 | const signOrigin = `host: ${host}\ndate: ${date}\nGET ${path} HTTP/1.1` 40 | const sign = sha256Digest(apiSecret)(signOrigin) 41 | const authorizationOrigin = `api_key="${apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${sign}"` 42 | const authorization = btoa(authorizationOrigin) 43 | url.searchParams.set('authorization', authorization) 44 | url.searchParams.set('date', date) 45 | url.searchParams.set('host', host) 46 | return url 47 | } 48 | 49 | function openWebsocket(url: URL): (data: string) => Promise { 50 | return async (data: string): Promise => { 51 | const ws = new WebSocket(url) 52 | ws.onopen = () => ws.send(data) 53 | let timeout: ReturnType 54 | return new Promise((_resolve, _reject) => { 55 | const buffer: string[] = [] 56 | const resolve = () => _resolve(buffer.join('')) 57 | const reject = (e: Error | ErrorEvent) => { 58 | if (buffer.length > 0) { 59 | resolve() 60 | } else { 61 | _reject(e) 62 | } 63 | } 64 | timeout = setTimeout(() => reject(Error('请求超时')), 60000) 65 | let lastSeq = -1 66 | ws.onmessage = ({ data }) => { 67 | const { header, payload: { choices } } = JSON.parse(data.toString()) as SparkAIResult 68 | if (header.code !== 0) { 69 | reject(Error(header.message)) 70 | return 71 | } 72 | if (choices.seq > lastSeq && choices.text.length > 0) { 73 | lastSeq = choices.seq 74 | buffer.push(choices.text[0].content) 75 | } 76 | if (header.status === 2) { 77 | resolve() 78 | } 79 | } 80 | ws.onerror = reject 81 | }).finally(() => { 82 | clearTimeout(timeout) 83 | }) 84 | } 85 | } 86 | 87 | async function sendConversation(text: Array): Promise { 88 | const appId = ensure(process.env.SPARK_APP_ID, '未设置SPARK_APP_ID', 500) 89 | const url = await getWebsocketUrl({ 90 | apiKey: ensure(process.env.SPARK_API_KEY, '未设置SPARK_API_KEY', 500), 91 | apiSecret: ensure(process.env.SPARK_API_SECRET, '未设置SPARK_API_SECRET', 500), 92 | }) 93 | const sender = openWebsocket(url) 94 | const data = JSON.stringify({ 95 | header: { app_id: appId }, 96 | parameter: { chat: { domain: 'generalv3.5', max_tokens: 128 } }, 97 | payload: { message: { text } }, 98 | }) 99 | return await sender(data) 100 | } 101 | 102 | export const sparkAI = { 103 | sendConversation, 104 | summary(text: string): Promise { 105 | return sendConversation([ 106 | { 107 | role: 'system', 108 | content: '简洁地概括文章的主要内容,突出强调作者的核心论点和结论,控制在80个字以内', 109 | }, 110 | { 111 | role: 'user', 112 | content: text, 113 | }, 114 | ]) 115 | }, 116 | } 117 | -------------------------------------------------------------------------------- /server/utils/assertion.ts: -------------------------------------------------------------------------------- 1 | export function nonNullable( 2 | v: T | null | undefined, 3 | message: string, 4 | status: number = 400, 5 | ): asserts v is NonNullable { 6 | if (!v) throw createError({ message, statusCode: status }) 7 | } 8 | 9 | export function ensure( 10 | v: T | null | undefined, 11 | message: string, 12 | status: number = 400, 13 | ): NonNullable { 14 | nonNullable(v, message, status) 15 | return v 16 | } 17 | -------------------------------------------------------------------------------- /server/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { randomUUID } from 'node:crypto' 3 | import type { H3Event } from 'h3' 4 | import z from 'zod' 5 | import { sign, verify } from 'jsonwebtoken' 6 | 7 | interface SessionUser { 8 | username: string 9 | scope: string[] 10 | } 11 | 12 | const ADMIN_USERNAME = process.env.ADMIN_USERNAME 13 | const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD 14 | const AUTH_SECRET = process.env.AUTH_SECRET || randomUUID() 15 | const COOKIE_KEY = 'flapypan-blog' 16 | 17 | /** 18 | * 登录获取token 19 | */ 20 | export async function login(event: H3Event): Promise { 21 | const result = z.object({ 22 | username: z.literal(ensure(ADMIN_USERNAME, '用户名或密码未设置', 500)), 23 | password: z.literal(ensure(ADMIN_PASSWORD, '用户名或密码未设置', 500)), 24 | remember: z.boolean().nullish(), 25 | }).safeParse(await readBody(event)) 26 | if (!result.success) { 27 | throw createError({ statusCode: 401, message: '错误的用户名或密码' }) 28 | } 29 | const user: SessionUser = { 30 | username: result.data.username, 31 | scope: ['user', 'admin'], 32 | } 33 | const expiresIn = result.data.remember ? '30d' : '1d' 34 | const token = sign(user, AUTH_SECRET, { expiresIn }) 35 | setCookie(event, COOKIE_KEY, token, { 36 | httpOnly: true, 37 | sameSite: true, 38 | expires: result.data.remember ? new Date(Date.now() + 2592000000) : undefined, 39 | }) 40 | return user 41 | } 42 | 43 | /** 44 | * 退出登录 45 | * @param {H3Event} event 46 | */ 47 | export function logout(event: H3Event): unknown { 48 | setCookie(event, COOKIE_KEY, '', { maxAge: 0 }) 49 | return {} 50 | } 51 | 52 | /** 53 | * 获取登录信息 54 | */ 55 | export function auth(event: H3Event): SessionUser { 56 | const token = getCookie(event, COOKIE_KEY) 57 | if (!token) { 58 | throw createError({ statusCode: 401, message: '未登录' }) 59 | } 60 | try { 61 | return verify(token, AUTH_SECRET) as SessionUser 62 | } catch (e) { 63 | logout(event) 64 | throw createError({ statusCode: 401, message: '登录失效' }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /server/utils/request.ts: -------------------------------------------------------------------------------- 1 | import type { ZodRawShape } from 'zod' 2 | import z from 'zod' 3 | import type { EventHandlerRequest, H3Event } from 'h3' 4 | 5 | const IP_HEADERS = [ 6 | 'X-Forwarded-For', 7 | 'Proxy-Client-IP', 8 | 'WL-Proxy-Client-IP', 9 | 'HTTP_CLIENT_IP', 10 | 'HTTP_X_FORWARDED_FOR', 11 | ] 12 | 13 | export function getRealIP(event: H3Event) { 14 | for (const header of IP_HEADERS) { 15 | const ip = getHeader(event, header) 16 | if (ip) return ip 17 | } 18 | return getRequestIP(event) 19 | } 20 | 21 | function parse(data: unknown, shape: T) { 22 | const result = z.object(shape).safeParse(data) 23 | if (!result.success) { 24 | console.warn(`表单校验错误`, result.error) 25 | throw createError({ 26 | statusCode: 400, 27 | message: result.error.errors[0].message, 28 | }) 29 | } 30 | return result.data 31 | } 32 | 33 | export async function readSafeBody( 34 | event: H3Event, 35 | shape: T, 36 | ) { 37 | const data = await readBody(event) 38 | return parse(data, shape) 39 | } 40 | 41 | export function readParams(event: H3Event, shape: T) { 42 | const data = event.context.params 43 | return parse(data, shape) 44 | } 45 | -------------------------------------------------------------------------------- /store/auth.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { useToast } from 'vue-toastification' 3 | 4 | export const useAuthStore = defineStore('auth', () => { 5 | const isLogin = shallowRef(false) 6 | const loginDialogVisible = shallowRef(false) 7 | 8 | async function check() { 9 | try { 10 | await api(`/auth/user`) 11 | isLogin.value = true 12 | } catch { 13 | isLogin.value = false 14 | } 15 | } 16 | 17 | const login = async (form: { username: string, password: string, remember: boolean }) => { 18 | try { 19 | await api(`/auth/login`, 'POST', form) 20 | isLogin.value = true 21 | useToast().success('登录成功') 22 | } catch (e) { 23 | isLogin.value = false 24 | throw e 25 | } 26 | } 27 | 28 | const logout = async () => { 29 | try { 30 | await api(`/auth/logout`, 'POST') 31 | } finally { 32 | isLogin.value = false 33 | useToast().info('退出登录') 34 | } 35 | } 36 | 37 | return { 38 | isLogin, 39 | loginDialogVisible, 40 | check, 41 | login, 42 | logout, 43 | } 44 | }) 45 | -------------------------------------------------------------------------------- /store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth' 2 | export * from './setting' 3 | -------------------------------------------------------------------------------- /store/setting.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export const useSettingStore = defineStore('setting', () => { 4 | const setting = ref({ 5 | siteTitle: `FlapyPan's Blog`, 6 | favicon: '/avatar.webp', 7 | avatar: '/avatar.webp', 8 | banner: '/banner.webp', 9 | name: 'FlapyPan', 10 | email: 'flapypan@gmail.com', 11 | info: '个人博客', 12 | hitoko: true, 13 | footer: 'Copyright', 14 | wakatime: '', 15 | giscusRepo: '', 16 | giscusRepoId: '', 17 | giscusCategory: '', 18 | giscusCategoryId: '', 19 | }) 20 | 21 | const load = async () => { 22 | const data = await api>('/attribute/settings') 23 | setting.value = { ...setting.value, ...data } 24 | } 25 | 26 | const save = async () => { 27 | const data = await api>('/attribute/settings', 'POST', setting.value) 28 | setting.value = { ...setting.value, ...data } 29 | } 30 | 31 | return { 32 | setting, 33 | load, 34 | save, 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | import { fontFamily } from 'tailwindcss/defaultTheme' 3 | import colors from 'tailwindcss/colors' 4 | import scrollbar from 'tailwind-scrollbar' 5 | 6 | const sans = ['Inter', ...fontFamily.sans] 7 | const serif = ['Merriweather', ...fontFamily.serif] 8 | const mono = [...new Set(['Fira Code', ...fontFamily.mono, ...sans])] 9 | 10 | export default { 11 | darkMode: 'class', 12 | content: [ 13 | './components/**/*.{js,vue,ts}', 14 | './layouts/**/*.vue', 15 | './pages/**/*.vue', 16 | './plugins/**/*.{js,ts}', 17 | './app.vue', 18 | ], 19 | theme: { 20 | extend: { 21 | colors: { 22 | primary: colors.sky, 23 | secondary: colors.emerald, 24 | }, 25 | }, 26 | fontFamily: { sans, serif, mono }, 27 | }, 28 | plugins: [scrollbar({ nocompatible: true })], 29 | } satisfies Config 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /types/api.d.ts: -------------------------------------------------------------------------------- 1 | import type { IAccess, IArticle, IAttribute, IPicture } from '~/types/models' 2 | 3 | type WithId = T & { _id: string } 4 | 5 | type Access = WithId 6 | interface AccessData { 7 | today?: number 8 | all?: number 9 | } 10 | 11 | type Article = WithId 12 | type ArticleWithoutContent = Omit 13 | type ArticleDraft = Omit<(IArticle & { _id?: string }), 'createdAt' | 'updatedAt'> 14 | 15 | type Attribute = WithId 16 | 17 | type Picture = WithId 18 | -------------------------------------------------------------------------------- /types/models.d.ts: -------------------------------------------------------------------------------- 1 | import type { Types } from 'mongoose' 2 | 3 | interface IAccess { 4 | ip?: string 5 | referrer?: string 6 | ua?: string 7 | articleId?: Types.ObjectId | string 8 | createdAt: Date 9 | } 10 | 11 | interface IArticle { 12 | title: string 13 | path: string 14 | cover?: string 15 | content: string 16 | summary?: string 17 | pinned?: boolean 18 | tags: string[] 19 | createdAt: Date 20 | updatedAt: Date 21 | } 22 | 23 | interface IAttribute { 24 | key: string 25 | value?: any 26 | } 27 | 28 | interface IPicture { 29 | name: string 30 | bytes: Buffer 31 | createdAt: Date 32 | } 33 | -------------------------------------------------------------------------------- /utils/api.ts: -------------------------------------------------------------------------------- 1 | import { useToast } from 'vue-toastification' 2 | import { useAuthStore } from '~/store' 3 | 4 | /** 5 | * 6 | * @param {string} url 请求路径 7 | * @param {'GET' | 'POST' | 'PUT' | 'DELETE'} [method] 请求方法 8 | * @param {any} [payload] 请求数据 9 | * @param {boolean} [jsonPayload] payload 是否为 json 10 | * @return {Promise<*>} 11 | */ 12 | export default async function api( 13 | url: string, 14 | method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', 15 | payload?: any, 16 | jsonPayload: boolean = true, 17 | ): Promise { 18 | const event = useRequestEvent() 19 | const headers: Record = { 20 | ...useRequestHeaders([ 21 | 'cookie', 22 | 'X-Forwarded-For', 23 | 'Proxy-Client-IP', 24 | 'WL-Proxy-Client-IP', 25 | 'HTTP_CLIENT_IP', 26 | 'HTTP_X_FORWARDED_FOR', 27 | 'Referer', 28 | 'User-Agent', 29 | ]), 30 | } 31 | if (jsonPayload) headers['Content-Type'] = 'application/json' 32 | try { 33 | const res = await $fetch.raw(url, { 34 | baseURL: '/api', 35 | method, 36 | headers, 37 | mode: 'same-origin', 38 | credentials: 'same-origin', 39 | redirect: 'follow', 40 | body: jsonPayload ? JSON.stringify(payload) : payload, 41 | }) 42 | if (import.meta.browser) { 43 | if (event) { 44 | const cookies = (res.headers.get('set-cookie') || '').split(',') 45 | for (const cookie of cookies) { 46 | appendResponseHeader(event, 'set-cookie', cookie) 47 | } 48 | } 49 | } 50 | return res._data as T 51 | } catch (e) { 52 | const { data, message, status } = e as any 53 | if (status === 401) { 54 | const auth = useAuthStore() 55 | auth.isLogin = false 56 | } else if (import.meta.browser) { 57 | useToast().error(data?.message || message) 58 | } 59 | throw new Error(`${status} ${data?.message || message}`) 60 | } 61 | } 62 | --------------------------------------------------------------------------------