Compare commits
6 Commits
9d2bb71ea7
...
ff2074b400
| Author | SHA1 | Date | |
|---|---|---|---|
| ff2074b400 | |||
| 8efb55827c | |||
| dcaa1fddf2 | |||
| e29f27e2eb | |||
| 3117af670b | |||
| ab396b01f2 |
@@ -0,0 +1,10 @@
|
||||
[*]
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{js,ts,mjs,mts}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
+41
-16
@@ -1,4 +1,12 @@
|
||||
import {defineConfig} from 'vitepress'
|
||||
import { defineConfig } from 'vitepress'
|
||||
import { useSidebar } from 'vitepress-openapi'
|
||||
import spec from './theme/openapi.json' with { type: 'json' }
|
||||
|
||||
|
||||
const sidebar = useSidebar({
|
||||
spec,
|
||||
linkPrefix: '/dev/api-docs/'
|
||||
})
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
@@ -7,29 +15,46 @@ export default defineConfig({
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{text: '主页', link: '/'},
|
||||
{text: '使用', link: '/use/start'}
|
||||
{ text: '主页', link: '/' },
|
||||
{ text: '使用', link: '/use/start' },
|
||||
{ text: '开发', link: '/dev/start' },
|
||||
],
|
||||
|
||||
sidebar: [
|
||||
{
|
||||
sidebar: {
|
||||
'use': [{
|
||||
text: '使用',
|
||||
items: [
|
||||
{text: '开始', link: '/use/start'},
|
||||
{text: '默认行为', link: '/use/default-action'},
|
||||
{text: '用户系统', link: '/use/user-system'},
|
||||
{ text: '开始', link: '/use/start' },
|
||||
{ text: '默认行为', link: '/use/default_action' },
|
||||
{ text: '用户系统', link: '/use/user_system' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '开发',
|
||||
items: [
|
||||
{text: '后端响应', link: '/dev/backend-response'},
|
||||
]
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
'dev': [
|
||||
{
|
||||
text: '开发',
|
||||
items: [
|
||||
{ text: '开始', link: '/dev/start' },
|
||||
{ text: '后端响应', link: '/dev/backend_response' },
|
||||
{ text: '邮件模板', link: '/dev/email_templates' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'API 文档',
|
||||
items: [
|
||||
{ text: 'API', link: '/dev/api-docs/api' },
|
||||
...sidebar.generateSidebarGroups({}),
|
||||
]
|
||||
},]
|
||||
},
|
||||
|
||||
socialLinks: [
|
||||
{icon: 'github', link: 'https://github.com/vuejs/vitepress'}
|
||||
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' }
|
||||
]
|
||||
},
|
||||
|
||||
sitemap: {
|
||||
hostname: 'https://docs.nyahome.cn'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,51 @@
|
||||
:root {
|
||||
--vp-c-brand-1: #64ffc4;
|
||||
--vp-c-brand-2: #9354ff;
|
||||
}
|
||||
|
||||
/* ===== 简约滚动条美化 ===== */
|
||||
|
||||
/* 适用于 Webkit 内核(Chrome/Edge/Safari) */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
/* 垂直滚动条宽度 */
|
||||
height: 6px;
|
||||
/* 水平滚动条高度 */
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
/* 轨道透明,极简 */
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
/* 滑块半透明灰 */
|
||||
border-radius: 3px;
|
||||
/* 小圆角 */
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
/* 悬停稍微深一点 */
|
||||
}
|
||||
|
||||
/* Firefox 兼容 */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
|
||||
}
|
||||
|
||||
/* vitepress-openapi */
|
||||
div.vitepress-openapi {
|
||||
border: 1px solid #64ffc4;
|
||||
border-radius: 6px;
|
||||
margin: 48px auto 0;
|
||||
padding: 6px 20px;
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.vitepress-openapi p {
|
||||
line-height: 8px;
|
||||
}
|
||||
@@ -1,5 +1,26 @@
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import { theme, useOpenapi } from 'vitepress-openapi/client'
|
||||
import 'vitepress-openapi/dist/style.css'
|
||||
// @ts-ignore
|
||||
import './custom.css'
|
||||
import { Theme } from 'vitepress'
|
||||
|
||||
export default DefaultTheme
|
||||
import spec from './openapi.json' with { type: 'json' }
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
async enhanceApp({ app }) {
|
||||
useOpenapi({
|
||||
spec,
|
||||
config: {
|
||||
codeSamples: {
|
||||
defaultLang: 'python',
|
||||
},
|
||||
server: {
|
||||
allowCustomServer: true,
|
||||
},
|
||||
}
|
||||
})
|
||||
theme.enhanceApp({ app })
|
||||
}
|
||||
} satisfies Theme
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
aside: false
|
||||
outline: false
|
||||
title: NyaHome API Docs
|
||||
---
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTheme, locals } from 'vitepress-openapi/client'
|
||||
import { useRoute } from 'vitepress'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
useTheme({
|
||||
i18n: {
|
||||
locale: 'zh',
|
||||
},
|
||||
})
|
||||
|
||||
const operationId = route.data.params.operationId
|
||||
</script>
|
||||
|
||||
<OAOperation :operationId="operationId">
|
||||
<template #branding>
|
||||
<div class="vitepress-openapi">
|
||||
<p>API 文档是基于最新代码自动生成的</p>
|
||||
<p>由 VitePress OpenAPI 提供文档支持</p>
|
||||
</div>
|
||||
</template>
|
||||
</OAOperation>
|
||||
@@ -0,0 +1,17 @@
|
||||
import { usePaths } from 'vitepress-openapi'
|
||||
import spec from '../../.vitepress/theme/openapi.json' with {type: 'json'}
|
||||
|
||||
export default {
|
||||
paths() {
|
||||
return usePaths({ spec })
|
||||
.getPathsByVerbs()
|
||||
.map(({ operationId, summary }) => {
|
||||
return {
|
||||
params: {
|
||||
operationId,
|
||||
pageTitle: `${summary} - NyaHome API`,
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# API
|
||||
|
||||
以下是自动生成的 API 文档,包含 NyaHome 的 FastAPI 后端拥有的所有路由端点的有关信息。
|
||||
|
||||
API 文档会跟随代码仓库主分支的最新状况进行同步。也就是说,此处的 API 文档永远显示最新版本。
|
||||
|
||||
## 查看 NyaHome Swagger UI 或 ReDoc
|
||||
|
||||
FastAPI 提供内置的文档功能,位于开发服务器的 `/docs` 和 `/redoc` 两处路径。
|
||||
NyaHome 提供关闭它们的选项(并且默认关闭),但如果你有开发需求,打开它们自然是比较方便的。
|
||||
|
||||
由 FastAPI 打开的文档是由当前代码生成的,可以反应开发中的最新更改,或者查看当前版本的路由信息(如果你没有使用最新版本)。
|
||||
|
||||
## 导出 openapi.json
|
||||
|
||||
你可以从一个部署完成的 NyaHome 实例中导出 openapi.json。
|
||||
|
||||
```bash
|
||||
uv run nyahome openapi [path]
|
||||
```
|
||||
|
||||
以上命令会生成 openapi.json 文件。`path` 参数的默认值是 `openapi.json`,意味着 openapi.json 会被保存在执行位置(仓库根目录)。
|
||||
|
||||
你也可以从一个正在运行的 NyaHome 实例访问 `/openapi.json` 来在线查看 openapi.json,此功能也默认关闭。
|
||||
|
||||
## 构建文档自动集成
|
||||
|
||||
`pyproject.toml` 中配置了一个项目任务,来方便将最新的 openapi.json 导出到 `/docs` 路径下的指定位置,从而与 VitePress 进行集成。
|
||||
|
||||
```bash
|
||||
uv run task openapi-docs
|
||||
```
|
||||
|
||||
这会将 openapi.json 保存到 `/docs/.vitepress/theme/openapi.json`。事实上这是对上面的 `nyahome openapi` 命令的封装。
|
||||
|
||||
此命令一般适合在 CI/CD 流水线中运行比较合适,或者在开发阶段需要预览 API 文档的渲染效果。
|
||||
@@ -0,0 +1 @@
|
||||
# 后端响应
|
||||
@@ -13,6 +13,8 @@ HTML。
|
||||
pnpm mjml mjml/filename.mjml -o public/templates/filename.j2
|
||||
```
|
||||
|
||||
NyaHome 的开发状况稳定之后,可能会再优化邮件模板的编译流程。
|
||||
|
||||
Jinja2 在 NyaHome 进程中读取模板,渲染变量,然后由 aiosmptplib 发送渲染好的 HTML 邮件。
|
||||
|
||||
## 在 PyCharm 中预览 mjml 源文件
|
||||
@@ -22,4 +24,21 @@ Jinja2 在 NyaHome 进程中读取模板,渲染变量,然后由 aiosmptplib
|
||||
:::warning
|
||||
MJML Support 插件不支持 JetBrains 远程开发。在远程开发(包括使用 Gateway 进行 WSL 开发)中,IDE client 无法对 mjml
|
||||
源文件进行实时预览。
|
||||
|
||||
MJML Support 的开发者表示该插件不会支持远程开发。
|
||||
:::
|
||||
|
||||
## 渲染变量
|
||||
|
||||
Jinja2 负责将需要动态渲染的变量渲染到经过变异的 Jinja2 模板中。如果你碰过 Django 或者 Flask,应该会对这种格式很熟悉。
|
||||
|
||||
```html
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Maple Mono CN;font-size:13px;line-height:1;text-align:left;color:#000000;">{{ site_name }} 的管理员选择向此邮箱发送了一封测试邮件。此邮件不含有任何有效内容。</div>
|
||||
</td>
|
||||
</tr>
|
||||
```
|
||||
|
||||
本文档不过多设计 Jinja2 的模板变量写法。只需要知道,这些变量会在发送前被填入具体的内容。例如,<code v-pre>{{ site_name }}</code>
|
||||
会被渲染成 `config_manager.get("site_name", "Nya Home")`。
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# 开始
|
||||
|
||||
## 从代码仓库克隆项目
|
||||
@@ -0,0 +1,11 @@
|
||||
# 开始
|
||||
|
||||
## 从 uv 安装
|
||||
|
||||
NyaHome 作为一个 Python 模块发布在了 pypi 上,因此可以使用 uv 将 NyaHome 作为工具安装,然后随处运行。
|
||||
|
||||
你需要选定一个合适的目录,并在每次都从此处运行 NyaHome。NyaHome 需要在一个指定目录存放数据文件。
|
||||
|
||||
```bash
|
||||
uv tool install nyahome
|
||||
```
|
||||
|
||||
+4
-2
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"mjml": "^5.2.2",
|
||||
"vitepress": "2.0.0-alpha.17"
|
||||
"vitepress": "2.0.0-alpha.17",
|
||||
"vitepress-openapi": "^0.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vitepress dev docs",
|
||||
"dev": "vitepress dev docs --port 6173",
|
||||
"build": "vitepress build docs",
|
||||
"preview": "vitepress preview docs"
|
||||
}
|
||||
|
||||
Generated
+178
@@ -14,6 +14,9 @@ importers:
|
||||
vitepress:
|
||||
specifier: 2.0.0-alpha.17
|
||||
version: 2.0.0-alpha.17(postcss@8.5.15)
|
||||
vitepress-openapi:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0(vitepress@2.0.0-alpha.17(postcss@8.5.15))(vue@3.5.34)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -210,12 +213,30 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@floating-ui/core@1.7.5':
|
||||
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
|
||||
|
||||
'@floating-ui/dom@1.7.6':
|
||||
resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
|
||||
|
||||
'@floating-ui/utils@0.2.11':
|
||||
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
|
||||
|
||||
'@floating-ui/vue@1.1.11':
|
||||
resolution: {integrity: sha512-HzHKCNVxnGS35r9fCHBc3+uCnjw9IWIlCPL683cGgM9Kgj2BiAl8x1mS7vtvP6F9S/e/q4O6MApwSHj8hNLGfw==}
|
||||
|
||||
'@iconify-json/simple-icons@1.2.83':
|
||||
resolution: {integrity: sha512-6Pp9V++XisT9RKH7FB4RLPqUDzcmLtSma0ovOEIoEWGrXtHwBFsH7oN1z8vvCVCb95fb87QgR46/zRLyN9Y3kg==}
|
||||
|
||||
'@iconify/types@2.0.0':
|
||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||
|
||||
'@internationalized/date@3.12.1':
|
||||
resolution: {integrity: sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==}
|
||||
|
||||
'@internationalized/number@3.6.6':
|
||||
resolution: {integrity: sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ==}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -395,6 +416,17 @@ packages:
|
||||
'@shikijs/vscode-textmate@10.0.2':
|
||||
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
|
||||
|
||||
'@swc/helpers@0.5.21':
|
||||
resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==}
|
||||
|
||||
'@tanstack/virtual-core@3.15.0':
|
||||
resolution: {integrity: sha512-0AwPGx0I8QxPYjAxShT/+z+ZOe9u8mW5rsXvivCTjRfRmz9a43+3mRyi4wwlyoUqOC56q/jatKa0Bh9M99BEHQ==}
|
||||
|
||||
'@tanstack/vue-virtual@3.13.25':
|
||||
resolution: {integrity: sha512-/ez+t68a5O4CgVysvk7Bav0XbSYSYufOVHZveXF+DYO9hvtg2UheYzR0YkniCeUtXmMjDne1dDqwBMkOmEUOow==}
|
||||
peerDependencies:
|
||||
vue: ^2.7.0 || ^3.0.0
|
||||
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
@@ -556,6 +588,10 @@ packages:
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
aria-hidden@1.2.6:
|
||||
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
@@ -616,10 +652,17 @@ packages:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||
|
||||
cliui@8.0.1:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
@@ -719,6 +762,9 @@ packages:
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
defu@6.1.7:
|
||||
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
|
||||
|
||||
dequal@2.0.3:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -986,12 +1032,21 @@ packages:
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
lucide-vue-next@1.0.0:
|
||||
resolution: {integrity: sha512-V6SPvx1IHTj/UY+FrIYWV5faISsPSb8BnWSFDxAtezWKvWc9ZZ40PDrdu1/Qb5vg4lHWr1hs1BAMGVGm6V1Xdg==}
|
||||
deprecated: Package deprecated. Please use @lucide/vue instead.
|
||||
peerDependencies:
|
||||
vue: '>=3.0.1'
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
mark.js@8.11.1:
|
||||
resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==}
|
||||
|
||||
markdown-it-link-attributes@4.0.1:
|
||||
resolution: {integrity: sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ==}
|
||||
|
||||
mdast-util-to-hast@13.2.1:
|
||||
resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
|
||||
|
||||
@@ -1151,6 +1206,9 @@ packages:
|
||||
nth-check@2.1.1:
|
||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||
|
||||
ohash@2.0.11:
|
||||
resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
|
||||
|
||||
oniguruma-parser@0.12.2:
|
||||
resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==}
|
||||
|
||||
@@ -1403,6 +1461,11 @@ packages:
|
||||
regex@6.1.0:
|
||||
resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
|
||||
|
||||
reka-ui@2.9.8:
|
||||
resolution: {integrity: sha512-7dxaBJ6nQ0zOQZXPV45219tTEgZPstmihBLS9ABPhSiPiJ8SiF0sacfZHFaBptS0v9N4tzsevq+8MNBpE4p5JQ==}
|
||||
peerDependencies:
|
||||
vue: '>= 3.4.0'
|
||||
|
||||
require-directory@2.1.1:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1497,6 +1560,9 @@ packages:
|
||||
trim-lines@3.0.1:
|
||||
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
undici@6.25.0:
|
||||
resolution: {integrity: sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==}
|
||||
engines: {node: '>=18.17'}
|
||||
@@ -1575,6 +1641,13 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
vitepress-openapi@0.2.0:
|
||||
resolution: {integrity: sha512-suCYD59HG0P+XKauEm2GayqeXiVqBnDfaltmwcqNS3zOUDgI/nDCN75z4zZqLGHLLw/YhryNko8LOykTaUGmtQ==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
|
||||
peerDependencies:
|
||||
vitepress: '>=1.0.0'
|
||||
vue: ^3.0.0
|
||||
|
||||
vitepress@2.0.0-alpha.17:
|
||||
resolution: {integrity: sha512-Z3VPUpwk/bHYqt1uMVOOK1/4xFiWQov1GNc2FvMdz6kvje4JRXEOngVI9C+bi5jeedMSHiA4dwKkff1NCvbZ9Q==}
|
||||
hasBin: true
|
||||
@@ -1590,6 +1663,17 @@ packages:
|
||||
postcss:
|
||||
optional: true
|
||||
|
||||
vue-demi@0.14.10:
|
||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@vue/composition-api': ^1.0.0-rc.1
|
||||
vue: ^3.0.0-0 || ^2.6.0
|
||||
peerDependenciesMeta:
|
||||
'@vue/composition-api':
|
||||
optional: true
|
||||
|
||||
vue@3.5.34:
|
||||
resolution: {integrity: sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==}
|
||||
peerDependencies:
|
||||
@@ -1748,12 +1832,40 @@ snapshots:
|
||||
'@esbuild/win32-x64@0.27.7':
|
||||
optional: true
|
||||
|
||||
'@floating-ui/core@1.7.5':
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.11
|
||||
|
||||
'@floating-ui/dom@1.7.6':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.7.5
|
||||
'@floating-ui/utils': 0.2.11
|
||||
|
||||
'@floating-ui/utils@0.2.11': {}
|
||||
|
||||
'@floating-ui/vue@1.1.11(vue@3.5.34)':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.7.6
|
||||
'@floating-ui/utils': 0.2.11
|
||||
vue-demi: 0.14.10(vue@3.5.34)
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
||||
'@iconify-json/simple-icons@1.2.83':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify/types@2.0.0': {}
|
||||
|
||||
'@internationalized/date@3.12.1':
|
||||
dependencies:
|
||||
'@swc/helpers': 0.5.21
|
||||
|
||||
'@internationalized/number@3.6.6':
|
||||
dependencies:
|
||||
'@swc/helpers': 0.5.21
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
@@ -1885,6 +1997,17 @@ snapshots:
|
||||
|
||||
'@shikijs/vscode-textmate@10.0.2': {}
|
||||
|
||||
'@swc/helpers@0.5.21':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@tanstack/virtual-core@3.15.0': {}
|
||||
|
||||
'@tanstack/vue-virtual@3.13.25(vue@3.5.34)':
|
||||
dependencies:
|
||||
'@tanstack/virtual-core': 3.15.0
|
||||
vue: 3.5.34
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
@@ -2027,6 +2150,10 @@ snapshots:
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
aria-hidden@1.2.6:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
baseline-browser-mapping@2.10.31: {}
|
||||
@@ -2105,12 +2232,18 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
|
||||
cliui@8.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 7.0.0
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
@@ -2229,6 +2362,8 @@ snapshots:
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
defu@6.1.7: {}
|
||||
|
||||
dequal@2.0.3: {}
|
||||
|
||||
detect-node@2.1.0: {}
|
||||
@@ -2502,12 +2637,18 @@ snapshots:
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lucide-vue-next@1.0.0(vue@3.5.34):
|
||||
dependencies:
|
||||
vue: 3.5.34
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
mark.js@8.11.1: {}
|
||||
|
||||
markdown-it-link-attributes@4.0.1: {}
|
||||
|
||||
mdast-util-to-hast@13.2.1:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
@@ -3026,6 +3167,8 @@ snapshots:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
||||
ohash@2.0.11: {}
|
||||
|
||||
oniguruma-parser@0.12.2: {}
|
||||
|
||||
oniguruma-to-es@4.3.6:
|
||||
@@ -3270,6 +3413,22 @@ snapshots:
|
||||
dependencies:
|
||||
regex-utilities: 2.3.0
|
||||
|
||||
reka-ui@2.9.8(vue@3.5.34):
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.7.6
|
||||
'@floating-ui/vue': 1.1.11(vue@3.5.34)
|
||||
'@internationalized/date': 3.12.1
|
||||
'@internationalized/number': 3.6.6
|
||||
'@tanstack/vue-virtual': 3.13.25(vue@3.5.34)
|
||||
'@vueuse/core': 14.3.0(vue@3.5.34)
|
||||
'@vueuse/shared': 14.3.0(vue@3.5.34)
|
||||
aria-hidden: 1.2.6
|
||||
defu: 6.1.7
|
||||
ohash: 2.0.11
|
||||
vue: 3.5.34
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
@@ -3390,6 +3549,8 @@ snapshots:
|
||||
|
||||
trim-lines@3.0.1: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
undici@6.25.0: {}
|
||||
|
||||
unist-util-is@6.0.1:
|
||||
@@ -3446,6 +3607,19 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
vitepress-openapi@0.2.0(vitepress@2.0.0-alpha.17(postcss@8.5.15))(vue@3.5.34):
|
||||
dependencies:
|
||||
'@vueuse/core': 14.3.0(vue@3.5.34)
|
||||
class-variance-authority: 0.7.1
|
||||
clsx: 2.1.1
|
||||
lucide-vue-next: 1.0.0(vue@3.5.34)
|
||||
markdown-it-link-attributes: 4.0.1
|
||||
reka-ui: 2.9.8(vue@3.5.34)
|
||||
vitepress: 2.0.0-alpha.17(postcss@8.5.15)
|
||||
vue: 3.5.34
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
|
||||
vitepress@2.0.0-alpha.17(postcss@8.5.15):
|
||||
dependencies:
|
||||
'@docsearch/css': 4.6.3
|
||||
@@ -3494,6 +3668,10 @@ snapshots:
|
||||
- universal-cookie
|
||||
- yaml
|
||||
|
||||
vue-demi@0.14.10(vue@3.5.34):
|
||||
dependencies:
|
||||
vue: 3.5.34
|
||||
|
||||
vue@3.5.34:
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.34
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
@@ -27,6 +27,7 @@ dependencies = [
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"docstring-parser>=0.18.0",
|
||||
"mypy>=2.1.0",
|
||||
"ruff>=0.15.14",
|
||||
"taskipy>=1.14.1",
|
||||
@@ -110,3 +111,5 @@ migrate = "alembic upgrade head"
|
||||
rollback = "alembic downgrade -1"
|
||||
# 查看数据库版本
|
||||
current = "alembic current"
|
||||
# 为 docs 文档生成最新 openapi.json
|
||||
openapi-docs = "nyahome openapi docs/.vitepress/theme/openapi.json"
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
"""
|
||||
FastAPI Docstring-to-OpenAPI Enricher
|
||||
=====================================
|
||||
|
||||
自动解析 Google Style docstring,将 Args / Returns / Raises 注入到 OpenAPI Schema 的对应位置,
|
||||
让 vitepress-openapi 等工具能够正确渲染参数表格和返回值说明。
|
||||
|
||||
功能特性
|
||||
--------
|
||||
1. 参数描述注入:路径参数、查询参数、Header、Cookie、请求体字段
|
||||
2. 返回值描述注入:自动写入 2xx 响应的 description 及 schema.description
|
||||
3. 异常描述注入:以 Markdown 格式附加到 operation description
|
||||
4. 嵌套 Schema 递归处理:支持 Pydantic v1/v2 的嵌套模型、List、Optional 等
|
||||
5. \f 截断兼容:与 FastAPI 原生行为保持一致
|
||||
6. 全局 Schema 补全:为 components/schemas 中缺少描述的字段补充 docstring 说明
|
||||
7. 零侵入:只需替换 ``app.openapi``,业务代码无需任何修改
|
||||
|
||||
依赖安装
|
||||
--------
|
||||
pip install docstring-parser
|
||||
|
||||
使用方式
|
||||
--------
|
||||
from fastapi import FastAPI
|
||||
from fastapi_docstring_openapi import enrich_openapi_from_docstrings
|
||||
|
||||
app = FastAPI()
|
||||
enrich_openapi_from_docstrings(app)
|
||||
|
||||
# 你的路由注册...
|
||||
|
||||
完整示例见文件底部 ``if __name__ == "__main__":`` 块。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import logging
|
||||
from typing import Any, Dict, Optional, Set
|
||||
|
||||
from docstring_parser import Docstring, DocstringStyle, ParseError, parse
|
||||
from fastapi import FastAPI
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _parse_docstring(func: Any, style: DocstringStyle = DocstringStyle.AUTO) -> Optional[Docstring]: # noqa ANN401 有意为之
|
||||
"""
|
||||
解析函数的 Google Style docstring,失败时返回 None。
|
||||
|
||||
Returns:
|
||||
成功解析时返回 docstring_parser.Docstring。
|
||||
失败时返回 None。
|
||||
"""
|
||||
doc = inspect.getdoc(func)
|
||||
if not doc:
|
||||
return None
|
||||
if "\f" in doc:
|
||||
doc = doc.split("\f")[0].strip()
|
||||
try:
|
||||
return parse(doc, style=style)
|
||||
except ParseError:
|
||||
return None
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Unexpected error parsing docstring for %s",
|
||||
getattr(func, "__name__", func),
|
||||
exc_info=True,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _build_param_lookup(parsed_doc: Docstring) -> Dict[str, str]:
|
||||
"""
|
||||
从解析后的 docstring 构建 {参数名: 描述} 映射。
|
||||
|
||||
Returns:
|
||||
{参数名: 描述}
|
||||
"""
|
||||
lookup: Dict[str, str] = {}
|
||||
for param in getattr(parsed_doc, "params", []) or []:
|
||||
name = getattr(param, "arg_name", None)
|
||||
desc = getattr(param, "description", None) or ""
|
||||
if name:
|
||||
# 合并多行描述并清理缩进
|
||||
lookup[name] = _dedent_description(desc)
|
||||
return lookup
|
||||
|
||||
|
||||
def _build_returns_description(parsed_doc: Docstring) -> Optional[str]:
|
||||
"""
|
||||
从解析后的 docstring 构建返回值描述。
|
||||
|
||||
Returns:
|
||||
可能的字符串为返回值描述。
|
||||
"""
|
||||
ret = getattr(parsed_doc, "returns", None)
|
||||
if not ret:
|
||||
return None
|
||||
type_name = getattr(ret, "type_name", None) or ""
|
||||
desc = getattr(ret, "description", None) or ""
|
||||
if type_name and desc:
|
||||
return f"**{type_name}**: {desc}"
|
||||
return desc or None
|
||||
|
||||
|
||||
def _build_raises_description(parsed_doc: Docstring) -> Optional[str]:
|
||||
"""
|
||||
从解析后的 docstring 构建异常描述(Markdown 列表)。
|
||||
|
||||
Returns:
|
||||
可能的字符串为异常描述。
|
||||
"""
|
||||
raises = getattr(parsed_doc, "raises", None)
|
||||
if not raises:
|
||||
return None
|
||||
parts: list[str] = []
|
||||
for exc in raises:
|
||||
type_name = getattr(exc, "type_name", None) or ""
|
||||
desc = getattr(exc, "description", None) or ""
|
||||
if type_name and desc:
|
||||
parts.append(f"- **{type_name}**: {desc}")
|
||||
elif type_name:
|
||||
parts.append(f"- **{type_name}**")
|
||||
elif desc:
|
||||
parts.append(f"- {desc}")
|
||||
return "\n".join(parts) if parts else None
|
||||
|
||||
|
||||
def _dedent_description(text: str) -> str:
|
||||
"""
|
||||
清理 docstring 描述中的多余缩进,保留段落结构。
|
||||
|
||||
Returns:
|
||||
docstring 字符串
|
||||
"""
|
||||
if not text:
|
||||
return ""
|
||||
lines = text.splitlines()
|
||||
# 找到最小公共缩进(排除空行)
|
||||
min_indent = min(
|
||||
(len(line) - len(line.lstrip()) for line in lines if line.strip()),
|
||||
default=0,
|
||||
)
|
||||
cleaned = [line[min_indent:] if line.strip() else "" for line in lines]
|
||||
return "\n".join(cleaned).strip()
|
||||
|
||||
|
||||
def _get_clean_operation_description(parsed_doc: Docstring) -> str:
|
||||
"""
|
||||
生成干净的 operation description。
|
||||
|
||||
Returns:
|
||||
策略:保留 summary + long_description,去掉 Args/Returns/Raises 等机器块。
|
||||
"""
|
||||
parts: list[str] = []
|
||||
short = getattr(parsed_doc, "short_description", None)
|
||||
long_ = getattr(parsed_doc, "long_description", None)
|
||||
|
||||
if short:
|
||||
parts.append(short)
|
||||
if long_:
|
||||
parts.append(long_)
|
||||
|
||||
return "\n\n".join(parts).strip()
|
||||
|
||||
|
||||
def _inject_schema_properties(
|
||||
schema: Dict[str, Any],
|
||||
param_lookup: Dict[str, str],
|
||||
visited_refs: Optional[Set[str]] = None,
|
||||
openapi_schema: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
递归注入 schema properties 的描述。
|
||||
|
||||
支持对象、数组、allOf/anyOf/oneOf、$ref 引用(全局 components/schemas 补全)。
|
||||
"""
|
||||
if visited_refs is None:
|
||||
visited_refs = set()
|
||||
if not isinstance(schema, dict):
|
||||
return
|
||||
|
||||
# 处理 $ref:在 components/schemas 中查找并补全
|
||||
ref = schema.get("$ref")
|
||||
if ref and openapi_schema:
|
||||
if ref not in visited_refs:
|
||||
visited_refs.add(ref)
|
||||
# 提取 schema 名,例如 "#/components/schemas/User" -> "User"
|
||||
schema_name = ref.split("/")[-1]
|
||||
components = openapi_schema.get("components", {}).get("schemas", {})
|
||||
target = components.get(schema_name)
|
||||
if target:
|
||||
_inject_schema_properties(target, param_lookup, visited_refs, openapi_schema)
|
||||
return
|
||||
|
||||
# 处理 properties(对象字段)
|
||||
properties = schema.get("properties")
|
||||
if isinstance(properties, dict):
|
||||
for prop_name, prop_schema in properties.items():
|
||||
if prop_name in param_lookup: # noqa SIM102
|
||||
# 只有当 docstring 描述非空时才写入
|
||||
if param_lookup[prop_name]:
|
||||
prop_schema["description"] = param_lookup[prop_name]
|
||||
# 递归处理子 schema
|
||||
_inject_schema_properties(prop_schema, param_lookup, visited_refs, openapi_schema)
|
||||
|
||||
# 处理 items(数组类型)
|
||||
items = schema.get("items")
|
||||
if items:
|
||||
_inject_schema_properties(items, param_lookup, visited_refs, openapi_schema)
|
||||
|
||||
# 处理 allOf / anyOf / oneOf
|
||||
for key in ("allOf", "anyOf", "oneOf"):
|
||||
for sub in schema.get(key, []):
|
||||
_inject_schema_properties(sub, param_lookup, visited_refs, openapi_schema)
|
||||
|
||||
# 处理 additionalProperties
|
||||
additional = schema.get("additionalProperties")
|
||||
if isinstance(additional, dict):
|
||||
_inject_schema_properties(additional, param_lookup, visited_refs, openapi_schema)
|
||||
|
||||
|
||||
def enrich_openapi_from_docstrings(
|
||||
app: FastAPI,
|
||||
*,
|
||||
prefer_docstring_over_field: bool = True,
|
||||
append_raises_to_description: bool = True,
|
||||
docstring_style: DocstringStyle = DocstringStyle.AUTO,
|
||||
) -> None:
|
||||
"""
|
||||
为 FastAPI 应用启用 docstring 驱动的 OpenAPI 增强。
|
||||
|
||||
参数
|
||||
----
|
||||
app : FastAPI
|
||||
目标应用实例。
|
||||
prefer_docstring_over_field : bool, 默认 True
|
||||
当 docstring 中的参数描述与 Pydantic Field(description=...) 冲突时,
|
||||
是否优先使用 docstring 的描述。
|
||||
append_raises_to_description : bool, 默认 True
|
||||
是否将 Raises 段落以 Markdown 格式追加到 operation description。
|
||||
docstring_style : DocstringStyle, 默认 AUTO
|
||||
解析风格。团队统一用 Google 时可显式传入 ``DocstringStyle.GOOGLE``。
|
||||
"""
|
||||
|
||||
# 可选择保存原始的 openapi 函数(如果有)
|
||||
# original_openapi = app.openapi
|
||||
|
||||
def custom_openapi() -> Dict[str, Any]:
|
||||
# 如果已经有缓存,直接返回
|
||||
if app.openapi_schema:
|
||||
return app.openapi_schema
|
||||
|
||||
# 生成基础 schema
|
||||
openapi_schema = get_openapi(
|
||||
title=app.title,
|
||||
version=app.version,
|
||||
openapi_version=app.openapi_version,
|
||||
description=app.description,
|
||||
routes=app.routes,
|
||||
)
|
||||
|
||||
for route in app.routes:
|
||||
if not hasattr(route, "endpoint") or not route.endpoint:
|
||||
continue
|
||||
|
||||
path = getattr(route, "path", None)
|
||||
methods = [m.lower() for m in route.methods] if hasattr(route, "methods") else []
|
||||
if not path or not methods:
|
||||
continue
|
||||
|
||||
# 解析 docstring
|
||||
parsed = _parse_docstring(route.endpoint, style=docstring_style)
|
||||
if not parsed:
|
||||
continue
|
||||
|
||||
param_lookup = _build_param_lookup(parsed)
|
||||
returns_desc = _build_returns_description(parsed)
|
||||
raises_desc = _build_raises_description(parsed)
|
||||
clean_desc = _get_clean_operation_description(parsed)
|
||||
|
||||
for method in methods:
|
||||
if method == "head":
|
||||
# FastAPI 通常不把 HEAD 单独写入 OpenAPI,或者与 GET 共享
|
||||
continue
|
||||
if method not in openapi_schema.get("paths", {}).get(path, {}):
|
||||
continue
|
||||
|
||||
op = openapi_schema["paths"][path][method]
|
||||
|
||||
# ---------- 1. 更新 operation description ----------
|
||||
if clean_desc:
|
||||
op["description"] = clean_desc
|
||||
|
||||
# ---------- 2. 注入参数描述(路径/查询/Header/Cookie)----------
|
||||
for param in op.get("parameters", []):
|
||||
param_name = param.get("name")
|
||||
if param_name and param_name in param_lookup:
|
||||
existing = param.get("description", "")
|
||||
new_desc = param_lookup[param_name]
|
||||
if new_desc and (prefer_docstring_over_field or not existing):
|
||||
param["description"] = new_desc
|
||||
|
||||
# ---------- 3. 注入请求体 schema 字段描述 ----------
|
||||
if "requestBody" in op:
|
||||
content = op["requestBody"].get("content", {})
|
||||
for media_obj in content.values():
|
||||
schema = media_obj.get("schema", {})
|
||||
if schema:
|
||||
_inject_schema_properties(schema, param_lookup, openapi_schema=openapi_schema)
|
||||
|
||||
# ---------- 4. 注入返回值描述 ----------
|
||||
if returns_desc:
|
||||
for code, resp in op.get("responses", {}).items():
|
||||
if code.startswith("2"): # 2xx 成功响应
|
||||
# 更新响应级 description
|
||||
resp["description"] = returns_desc
|
||||
# 同时注入到响应 schema 的 description(如果存在)
|
||||
for media_obj in resp.get("content", {}).values():
|
||||
schema = media_obj.get("schema", {})
|
||||
if schema and not schema.get("description"):
|
||||
schema["description"] = returns_desc
|
||||
# 递归注入 schema 内部字段
|
||||
_inject_schema_properties(schema, param_lookup, openapi_schema=openapi_schema)
|
||||
|
||||
# ---------- 5. 异常描述追加 ----------
|
||||
if append_raises_to_description and raises_desc:
|
||||
current_desc = op.get("description", "")
|
||||
raises_md = f"## 异常\n\n{raises_desc}"
|
||||
# 避免重复追加
|
||||
if raises_md not in current_desc:
|
||||
op["description"] = f"{current_desc}\n\n{raises_md}".strip()
|
||||
|
||||
app.openapi_schema = openapi_schema
|
||||
return app.openapi_schema
|
||||
|
||||
app.openapi = custom_openapi # type: ignore[method-assign]
|
||||
@@ -42,15 +42,17 @@ class OtpMemoryStore(ABC):
|
||||
async def _cleanup(self) -> None:
|
||||
while True:
|
||||
await asyncio.sleep(60)
|
||||
logger.info(f"[{self.type_name}] 开始定时清理过期验证码。")
|
||||
logger.debug(f"[{self.type_name}] 开始定时清理过期验证码。")
|
||||
expires = []
|
||||
count = 0
|
||||
for address, item in self._store.items():
|
||||
if item.expire_time < time.time():
|
||||
logger.debug(f"[{self.type_name}] 移除过期的 {address}")
|
||||
expires.append(address)
|
||||
count += 1
|
||||
for address in expires:
|
||||
self._store.pop(address)
|
||||
logger.info(f"[{self.type_name}] 清理完成。")
|
||||
logger.debug(f"[{self.type_name}] 清理完成,清理了 {count} 个过期验证码。")
|
||||
|
||||
def verify(self, address: str, user_id: int, verify_code: str) -> bool:
|
||||
item = self._store.get(address)
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
避免在此文件中引用 router 和 service 模块内的代码。
|
||||
"""
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
|
||||
@@ -54,5 +56,18 @@ def run() -> None:
|
||||
)
|
||||
|
||||
|
||||
@app.command()
|
||||
def openapi(
|
||||
path: Annotated[str, typer.Argument(help="导出的 json 格式 openapi.json 应该保存为……")] = "openapi.json",
|
||||
) -> None:
|
||||
"""
|
||||
根据代码导出 NyaHome 的 openapi.json 。
|
||||
"""
|
||||
from nyahome.server import save_openapi_json
|
||||
|
||||
save_openapi_json(path)
|
||||
console.print(f"[cyan]已经保存 openapi.json 到 {path} 。[/cyan]")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
|
||||
@@ -19,7 +19,7 @@ from .response_model import ReturnDto
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
admin_router = APIRouter(tags=["admin"], prefix="/admin")
|
||||
admin_router = APIRouter(tags=["Admin"], prefix="/admin")
|
||||
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
@@ -53,3 +54,15 @@ app.add_middleware(
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
def save_openapi_json(save_path: str | Path) -> None:
|
||||
from docstring_parser import DocstringStyle
|
||||
|
||||
from nyahome.cli.openapi_docstring import enrich_openapi_from_docstrings
|
||||
|
||||
# 增强 openapi docstring 参数文档提取
|
||||
enrich_openapi_from_docstrings(app, docstring_style=DocstringStyle.GOOGLE)
|
||||
|
||||
with open(save_path, "w") as f:
|
||||
json.dump(app.openapi(), f, indent=2)
|
||||
|
||||
@@ -355,6 +355,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docstring-parser"
|
||||
version = "0.18.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.19.2"
|
||||
@@ -740,6 +749,7 @@ dependencies = [
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "docstring-parser" },
|
||||
{ name = "mypy" },
|
||||
{ name = "ruff" },
|
||||
{ name = "taskipy" },
|
||||
@@ -772,6 +782,7 @@ requires-dist = [
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "docstring-parser", specifier = ">=0.18.0" },
|
||||
{ name = "mypy", specifier = ">=2.1.0" },
|
||||
{ name = "ruff", specifier = ">=0.15.14" },
|
||||
{ name = "taskipy", specifier = ">=1.14.1" },
|
||||
|
||||
Vendored
+2
@@ -15,6 +15,7 @@ declare module 'vue' {
|
||||
AiiModelAddModal: typeof import('./src/components/chatroom/AiiModelAddModal.vue')['default']
|
||||
AiiProviderAddModal: typeof import('./src/components/chatroom/AiiProviderAddModal.vue')['default']
|
||||
ChangeEmailModal: typeof import('./src/components/admin/ChangeEmailModal.vue')['default']
|
||||
ChangePhoneModal: typeof import('./src/components/admin/ChangePhoneModal.vue')['default']
|
||||
ChatControlPanel: typeof import('./src/components/chatroom/ChatControlPanel.vue')['default']
|
||||
ChatMessage: typeof import('./src/components/chatroom/ChatMessage.vue')['default']
|
||||
ChatPromptQuicker: typeof import('./src/components/chatroom/ChatPromptQuicker.vue')['default']
|
||||
@@ -84,6 +85,7 @@ declare global {
|
||||
const AiiModelAddModal: typeof import('./src/components/chatroom/AiiModelAddModal.vue')['default']
|
||||
const AiiProviderAddModal: typeof import('./src/components/chatroom/AiiProviderAddModal.vue')['default']
|
||||
const ChangeEmailModal: typeof import('./src/components/admin/ChangeEmailModal.vue')['default']
|
||||
const ChangePhoneModal: typeof import('./src/components/admin/ChangePhoneModal.vue')['default']
|
||||
const ChatControlPanel: typeof import('./src/components/chatroom/ChatControlPanel.vue')['default']
|
||||
const ChatMessage: typeof import('./src/components/chatroom/ChatMessage.vue')['default']
|
||||
const ChatPromptQuicker: typeof import('./src/components/chatroom/ChatPromptQuicker.vue')['default']
|
||||
|
||||
+9
-9
@@ -1,14 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import {dateZhCN, zhCN} from 'naive-ui'
|
||||
import {useNowUser} from '@/stores/now-user.ts'
|
||||
import {onMounted} from 'vue'
|
||||
import {useHead} from "@unhead/vue";
|
||||
import { dateZhCN, zhCN } from 'naive-ui'
|
||||
import { useNowUser } from '@/stores/now-user.ts'
|
||||
import { onMounted } from 'vue'
|
||||
import { useHead } from '@unhead/vue'
|
||||
|
||||
const NOWUSER = useNowUser()
|
||||
|
||||
useHead({
|
||||
titleTemplate: "%s | NayHome"
|
||||
titleTemplate: '%s | NayHome',
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -18,9 +18,9 @@ onMounted(async () => {
|
||||
try {
|
||||
await NOWUSER.loadUserInfo(Number(user_id), access_token)
|
||||
} catch {
|
||||
localStorage.removeItem("user-id")
|
||||
localStorage.removeItem('user-id')
|
||||
localStorage.removeItem('access-token')
|
||||
console.log("已移除 localstorage 中存储的验证信息。")
|
||||
console.log('已移除 localstorage 中存储的验证信息。')
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -29,7 +29,7 @@ onMounted(async () => {
|
||||
<template>
|
||||
<n-config-provider id="aapp" :date-locale="dateZhCN" :locale="zhCN">
|
||||
<div class="header-container">
|
||||
<page-header/>
|
||||
<page-header />
|
||||
</div>
|
||||
<div class="content-container">
|
||||
<n-message-provider>
|
||||
@@ -37,7 +37,7 @@ onMounted(async () => {
|
||||
</n-message-provider>
|
||||
</div>
|
||||
<div class="footer-container">🌸 Nya Home ~</div>
|
||||
<n-global-style/>
|
||||
<n-global-style />
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
div#app {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="in-dev">
|
||||
<n-text class="in-dev-title">功能开发中</n-text>
|
||||
<n-text class="in-dev-content">
|
||||
已经被画在饼上辽,请耐心等待喵!
|
||||
</n-text>
|
||||
<n-text class="in-dev-content"> 已经被画在饼上辽,请耐心等待喵! </n-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue'
|
||||
import {api} from '@/tools/web.ts'
|
||||
import type {ReturnDto} from '@/types/response.ts'
|
||||
import {useMessage} from 'naive-ui'
|
||||
import { ref } from 'vue'
|
||||
import { api } from '@/tools/web.ts'
|
||||
import type { ReturnDto } from '@/types/response.ts'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import VerifyCodeModal from '@/components/admin/VerifyCodeModal.vue'
|
||||
import {useNowUser} from "@/stores/now-user.ts";
|
||||
import { useNowUser } from '@/stores/now-user.ts'
|
||||
|
||||
const MESSAGE = useMessage()
|
||||
const NOWUSER = useNowUser()
|
||||
|
||||
const showModal = defineModel('showModal', {required: true})
|
||||
const showModal = defineModel('showModal', { required: true })
|
||||
|
||||
const showVerifyCodeModal = ref(false)
|
||||
const newEmail = ref("")
|
||||
const verifyCode = ref("")
|
||||
const newEmail = ref('')
|
||||
const verifyCode = ref('')
|
||||
|
||||
function sendEmail() {
|
||||
api
|
||||
.post('/admin/me/email-verify/send/', JSON.stringify({to: newEmail.value}))
|
||||
.post('/admin/me/email-verify/send/', JSON.stringify({ to: newEmail.value }))
|
||||
.then((res) => res.data as ReturnDto)
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
@@ -33,10 +33,14 @@ function sendEmail() {
|
||||
}
|
||||
|
||||
function verifyEmail() {
|
||||
api.post('/admin/me/email-verify/', JSON.stringify({
|
||||
to: newEmail.value,
|
||||
verify_code: String(verifyCode.value).split(",").join(""),
|
||||
}))
|
||||
api
|
||||
.post(
|
||||
'/admin/me/email-verify/',
|
||||
JSON.stringify({
|
||||
to: newEmail.value,
|
||||
verify_code: String(verifyCode.value).split(',').join(''),
|
||||
}),
|
||||
)
|
||||
.then((res) => res.data as ReturnDto)
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
@@ -59,7 +63,7 @@ function verifyEmail() {
|
||||
<n-form label-placement="left">
|
||||
<n-p>你需要使用新的邮件地址接收一个验证码来完成修改。</n-p>
|
||||
<n-form-item path="to" label="新的邮件地址">
|
||||
<n-input v-model:value="newEmail"/>
|
||||
<n-input v-model:value="newEmail" />
|
||||
</n-form-item>
|
||||
<n-flex>
|
||||
<n-button type="warning" @click="sendEmail()">获取验证码</n-button>
|
||||
@@ -69,8 +73,11 @@ function verifyEmail() {
|
||||
</n-form>
|
||||
</n-modal>
|
||||
|
||||
<verify-code-modal v-model:show-modal="showVerifyCodeModal" v-model:verify-code="verifyCode"
|
||||
:verify="verifyEmail"/>
|
||||
<verify-code-modal
|
||||
v-model:show-modal="showVerifyCodeModal"
|
||||
v-model:verify-code="verifyCode"
|
||||
:verify="verifyEmail"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const showModal = defineModel('showModal', { required: true })
|
||||
|
||||
const newPhone = ref('')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal v-model:show="showModal" preset="card" style="width: 600px" title="修改手机号">
|
||||
<n-form label-placement="left">
|
||||
<n-p>你需要使用新的手机号接收一个验证码来完成修改。</n-p>
|
||||
<n-form-item path="to" label="新的手机号">
|
||||
<n-input v-model:value="newPhone" />
|
||||
</n-form-item>
|
||||
<n-flex>
|
||||
<n-button type="warning" disabled>获取验证码</n-button>
|
||||
<n-button type="tertiary">暂不支持喵!</n-button>
|
||||
<n-tag type="info">验证码有效期为 5 分钟,且不允许多个同时有效。</n-tag>
|
||||
</n-flex>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,21 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
title: string;
|
||||
title: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card :title>
|
||||
<template #header-extra>
|
||||
<slot name="extra"/>
|
||||
<slot name="extra" />
|
||||
</template>
|
||||
<slot name="default"/>
|
||||
<slot name="default" />
|
||||
<template #action>
|
||||
<slot name="action"/>
|
||||
<slot name="action" />
|
||||
</template>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue'
|
||||
import {api} from '@/tools/web.js'
|
||||
import type {ReturnDto} from '@/types/response.js'
|
||||
import {useMessage} from 'naive-ui'
|
||||
import {AxiosError} from 'axios'
|
||||
import {useNowUser} from '@/stores/now-user.js'
|
||||
import { ref } from 'vue'
|
||||
import { api } from '@/tools/web.js'
|
||||
import type { ReturnDto } from '@/types/response.js'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { AxiosError } from 'axios'
|
||||
import { useNowUser } from '@/stores/now-user.js'
|
||||
|
||||
const MESSAGE = useMessage()
|
||||
const NOWUSER = useNowUser()
|
||||
@@ -55,9 +55,9 @@ function login() {
|
||||
<template>
|
||||
<div>
|
||||
<div class="user-action nyahome-card" v-if="NOWUSER.isLogin" style="position: relative">
|
||||
<img :src="NOWUSER.background_url" alt="User Background" class="user-action-background">
|
||||
<div class="card-content" style="margin-top: auto; margin-bottom: 20px;">
|
||||
<n-avatar :size="96" circle :src="NOWUSER.avatar_url"/>
|
||||
<img :src="NOWUSER.background_url" alt="User Background" class="user-action-background" />
|
||||
<div class="card-content" style="margin-top: auto; margin-bottom: 20px">
|
||||
<n-avatar :size="96" circle :src="NOWUSER.avatar_url" />
|
||||
<n-h2 style="margin: 0">
|
||||
{{ NOWUSER.display_name ? NOWUSER.display_name : NOWUSER.name }}
|
||||
</n-h2>
|
||||
@@ -82,13 +82,13 @@ function login() {
|
||||
<n-radio-button value="register">注册</n-radio-button>
|
||||
</n-radio-group>
|
||||
<div class="card-content" v-if="page === 'login'">
|
||||
<n-avatar :size="96" circle/>
|
||||
<n-avatar :size="96" circle />
|
||||
<n-radio-group v-model:value="loginMethod">
|
||||
<n-radio-button value="name">用户名</n-radio-button>
|
||||
<n-radio-button value="email">邮箱</n-radio-button>
|
||||
<n-radio-button value="phone">手机</n-radio-button>
|
||||
</n-radio-group>
|
||||
<n-input v-model:value="loginForm.username" class="card-input" placeholder=""/>
|
||||
<n-input v-model:value="loginForm.username" class="card-input" placeholder="" />
|
||||
<n-input
|
||||
v-model:value="loginForm.password"
|
||||
class="card-input"
|
||||
@@ -103,9 +103,9 @@ function login() {
|
||||
</div>
|
||||
|
||||
<div class="card-content" v-else>
|
||||
<n-avatar :size="96" circle/>
|
||||
<n-input class="card-input" placeholder="用户名"/>
|
||||
<n-input class="card-input" placeholder="密码" type="password" show-password-toggle/>
|
||||
<n-avatar :size="96" circle />
|
||||
<n-input class="card-input" placeholder="用户名" />
|
||||
<n-input class="card-input" placeholder="密码" type="password" show-password-toggle />
|
||||
<n-flex class="card-input">
|
||||
<n-button type="primary" class="card-button">注册</n-button>
|
||||
</n-flex>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
import {api} from "@/tools/web.ts";
|
||||
import {useNowUser} from "@/stores/now-user.ts";
|
||||
import {useMessage} from "naive-ui";
|
||||
import {useRouter} from "vue-router";
|
||||
import { ref } from 'vue'
|
||||
import { api } from '@/tools/web.ts'
|
||||
import { useNowUser } from '@/stores/now-user.ts'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const ROUTER = useRouter();
|
||||
const ROUTER = useRouter()
|
||||
const MESSAGE = useMessage()
|
||||
const NOWUSER = useNowUser()
|
||||
|
||||
const showModal = defineModel("showModal", {required: true})
|
||||
const showModal = defineModel('showModal', { required: true })
|
||||
|
||||
const changeForm = ref({
|
||||
old_password: '',
|
||||
@@ -17,40 +17,41 @@ const changeForm = ref({
|
||||
})
|
||||
|
||||
function change() {
|
||||
api.post("/admin/me/password/", JSON.stringify(changeForm.value))
|
||||
api
|
||||
.post('/admin/me/password/', JSON.stringify(changeForm.value))
|
||||
.then(() => {
|
||||
MESSAGE.success("密码修改成功,请重新登录。")
|
||||
MESSAGE.success('密码修改成功,请重新登录。')
|
||||
NOWUSER.isLogin = false
|
||||
localStorage.removeItem("user-id")
|
||||
localStorage.removeItem("access-token")
|
||||
ROUTER.push("/")
|
||||
localStorage.removeItem('user-id')
|
||||
localStorage.removeItem('access-token')
|
||||
ROUTER.push('/')
|
||||
})
|
||||
.catch((err) => {
|
||||
MESSAGE.error(`密码修改失败:${err}`)
|
||||
MESSAGE.warning("如果您忘记了原密码,请选择「忘记密码」。")
|
||||
MESSAGE.warning('如果您忘记了原密码,请选择「忘记密码」。')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal style="width: 600px;" v-model:show="showModal" title="修改密码" preset="card">
|
||||
<n-modal style="width: 600px" v-model:show="showModal" title="修改密码" preset="card">
|
||||
<n-form label-align="right" label-placement="left" label-width="auto" :model="changeForm">
|
||||
<n-form-item label="原密码" path="old_password">
|
||||
<n-input v-model:value="changeForm.old_password"/>
|
||||
<n-input v-model:value="changeForm.old_password" />
|
||||
</n-form-item>
|
||||
<n-form-item label="新密码" path="new_password">
|
||||
<n-input v-model:value="changeForm.new_password" type="password" show-password-toggle/>
|
||||
<n-input v-model:value="changeForm.new_password" type="password" show-password-toggle />
|
||||
</n-form-item>
|
||||
<n-form-item label="确认修改">
|
||||
<n-flex>
|
||||
<n-button type="error" @click="change()">确认修改</n-button>
|
||||
<n-tag type="warning" size="large">修改密码会注销所有已登录状态,您将需要重新登录。</n-tag>
|
||||
<n-tag type="warning" size="large"
|
||||
>修改密码会注销所有已登录状态,您将需要重新登录。</n-tag
|
||||
>
|
||||
</n-flex>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted, ref, watch} from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import AiiProviderAddModal from '@/components/chatroom/AiiProviderAddModal.vue'
|
||||
import {api} from '@/tools/web.js'
|
||||
import type {ReturnDto} from '@/types/response.js'
|
||||
import {type SelectOption, useMessage} from 'naive-ui'
|
||||
import type {AiiProviderPublicWithoutKey} from '@/types/aii.js'
|
||||
import { api } from '@/tools/web.js'
|
||||
import type { ReturnDto } from '@/types/response.js'
|
||||
import { type SelectOption, useMessage } from 'naive-ui'
|
||||
import type { AiiProviderPublicWithoutKey } from '@/types/aii.js'
|
||||
|
||||
const MESSAGE = useMessage()
|
||||
|
||||
const showModal = defineModel<boolean>('showModal', {required: true})
|
||||
const showModal = defineModel<boolean>('showModal', { required: true })
|
||||
|
||||
const showAddProviderModal = ref(false)
|
||||
const selectProvider = ref<number | null>(null)
|
||||
@@ -120,21 +120,19 @@ function onConfirm() {
|
||||
<n-form :model="addModelForm" label-placement="left" label-width="auto" label-align="right">
|
||||
<n-form-item label="模型提供商" path="aii_provider_id">
|
||||
<n-flex style="width: 100%" justify="right" align="center">
|
||||
<n-select v-model:value="selectProvider" :options="providerOptions"/>
|
||||
<n-select v-model:value="selectProvider" :options="providerOptions" />
|
||||
<n-tag round type="info">修改已添加的提供商?请前往管理中心</n-tag>
|
||||
<n-button secondary type="success" size="small" round @click="loadProviders()"
|
||||
>刷新
|
||||
</n-button
|
||||
>
|
||||
>刷新
|
||||
</n-button>
|
||||
<n-button secondary type="warning" size="small" round @click="showAddProviderModal = true"
|
||||
>添加
|
||||
</n-button
|
||||
>
|
||||
>添加
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-form-item>
|
||||
<n-form-item label="模型名称" path="model_name">
|
||||
<n-flex style="width: 100%" justify="right" align="center">
|
||||
<n-input v-model:value="addModelForm.model_name"/>
|
||||
<n-input v-model:value="addModelForm.model_name" />
|
||||
<n-flex style="overflow: auto">
|
||||
<n-button
|
||||
secondary
|
||||
@@ -144,14 +142,12 @@ function onConfirm() {
|
||||
v-for="m in remoteModels"
|
||||
v-bind:key="m"
|
||||
@click="addModelForm.model_name = m"
|
||||
>{{ m }}
|
||||
</n-button
|
||||
>
|
||||
>{{ m }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-button secondary type="success" size="small" round @click="onGetRemoteModels()"
|
||||
>获取模型列表
|
||||
</n-button
|
||||
>
|
||||
>获取模型列表
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-form-item>
|
||||
<n-form-item label="最大上下文" path="max_context_length">
|
||||
@@ -166,7 +162,7 @@ function onConfirm() {
|
||||
</n-flex>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<aii-provider-add-modal v-model:show-modal="showAddProviderModal"/>
|
||||
<aii-provider-add-modal v-model:show-modal="showAddProviderModal" />
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue'
|
||||
import {api} from '@/tools/web.js'
|
||||
import type {ReturnDto} from '@/types/response.js'
|
||||
import {useMessage} from 'naive-ui'
|
||||
import { ref } from 'vue'
|
||||
import { api } from '@/tools/web.js'
|
||||
import type { ReturnDto } from '@/types/response.js'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
const MESSAGE = useMessage()
|
||||
|
||||
const showModal = defineModel('showModal', {required: true})
|
||||
const showModal = defineModel('showModal', { required: true })
|
||||
|
||||
const addProviderForm = ref({
|
||||
id: 0,
|
||||
@@ -53,13 +53,13 @@ function onConfirm() {
|
||||
<n-modal v-model:show="showModal" preset="card" title="添加模型提供商">
|
||||
<n-form :model="addProviderForm" label-placement="left" label-width="auto" label-align="right">
|
||||
<n-form-item label="名称" path="name">
|
||||
<n-input v-model:value="addProviderForm.name"/>
|
||||
<n-input v-model:value="addProviderForm.name" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Base URL" path="base_url">
|
||||
<n-input v-model:value="addProviderForm.base_url"/>
|
||||
<n-input v-model:value="addProviderForm.base_url" />
|
||||
</n-form-item>
|
||||
<n-form-item label="API Key" path="api_key">
|
||||
<n-input v-model:value="addProviderForm.api_key"/>
|
||||
<n-input v-model:value="addProviderForm.api_key" />
|
||||
</n-form-item>
|
||||
<n-form-item label="添加完成">
|
||||
<n-flex>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted, ref} from 'vue'
|
||||
import {api} from '@/tools/web.js'
|
||||
import type {ReturnDto} from '@/types/response.js'
|
||||
import {type SelectOption, useMessage} from 'naive-ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { api } from '@/tools/web.js'
|
||||
import type { ReturnDto } from '@/types/response.js'
|
||||
import { type SelectOption, useMessage } from 'naive-ui'
|
||||
import AiiModelAddModal from '@/components/chatroom/AiiModelAddModal.vue'
|
||||
import type {AiiModelPublic} from '@/types/aii.js'
|
||||
import type { AiiModelPublic } from '@/types/aii.js'
|
||||
import ChatPromptQuicker from '@/components/chatroom/ChatPromptQuicker.vue'
|
||||
import ScriptDrawer from '@/components/chatroom/ScriptDrawer.vue'
|
||||
|
||||
const MESSAGE = useMessage()
|
||||
|
||||
const selectedModel = defineModel<number | null>('selectModel', {required: true})
|
||||
const quickerPrompt = defineModel<string>('quickerPrompt', {required: true})
|
||||
const selectedModel = defineModel<number | null>('selectModel', { required: true })
|
||||
const quickerPrompt = defineModel<string>('quickerPrompt', { required: true })
|
||||
|
||||
const {script} = defineProps<{
|
||||
const { script } = defineProps<{
|
||||
script: string
|
||||
}>()
|
||||
|
||||
@@ -68,11 +68,11 @@ onMounted(() => {
|
||||
</n-button-group>
|
||||
</n-flex>
|
||||
</template>
|
||||
<n-select v-model:value="selectedModel" :options="modelOptions"/>
|
||||
<aii-model-add-modal v-model:show-modal="showModal"/>
|
||||
<n-select v-model:value="selectedModel" :options="modelOptions" />
|
||||
<aii-model-add-modal v-model:show-modal="showModal" />
|
||||
</n-card>
|
||||
|
||||
<chat-prompt-quicker v-model:prompt-prefix="quickerPrompt"/>
|
||||
<chat-prompt-quicker v-model:prompt-prefix="quickerPrompt" />
|
||||
|
||||
<n-card title="剧本">
|
||||
<template #header-extra>故事设定 · 世界书</template>
|
||||
@@ -82,7 +82,7 @@ onMounted(() => {
|
||||
故事设定 · 世界书
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<script-drawer :script v-model:show-drawer="showScriptDrawer"/>
|
||||
<script-drawer :script v-model:show-drawer="showScriptDrawer" />
|
||||
</n-card>
|
||||
|
||||
<n-card title="设置">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import {md} from '@/tools/md.js'
|
||||
import {onMounted, ref, useTemplateRef} from 'vue'
|
||||
import { md } from '@/tools/md.js'
|
||||
import { onMounted, ref, useTemplateRef } from 'vue'
|
||||
|
||||
const {role, msg} = defineProps<{
|
||||
const { role, msg } = defineProps<{
|
||||
role: 'aii' | 'user'
|
||||
msg: string
|
||||
onMessageEdit: (oldMessage: string, newMessage: string, change: 'aii' | 'user') => void
|
||||
@@ -37,7 +37,7 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div :class="[`${role}-message`, 'message']" ref="self">
|
||||
<p v-html="md.render(msg)"/>
|
||||
<p v-html="md.render(msg)" />
|
||||
<n-button class="modify-button" secondary type="info" circle @click="showModal = true">
|
||||
⚙️
|
||||
</n-button>
|
||||
@@ -70,7 +70,7 @@ onMounted(() => {
|
||||
>
|
||||
<n-h3 prefix="bar" v-if="showEditor">编辑中</n-h3>
|
||||
<n-input v-if="showEditor" type="textarea" :rows="10" v-model:value="editorMessage"></n-input>
|
||||
<n-code v-else :code="msg" word-wrap/>
|
||||
<n-code v-else :code="msg" word-wrap />
|
||||
<!--suppress VueUnrecognizedSlot -->
|
||||
<template #footer>
|
||||
<n-flex align="center" style="padding-top: 10px">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import {createChatTableMessages} from '@/components/chatroom/chat-table-messages.js'
|
||||
import {md} from '@/tools/md.js'
|
||||
import { createChatTableMessages } from '@/components/chatroom/chat-table-messages.js'
|
||||
import { md } from '@/tools/md.js'
|
||||
|
||||
defineProps<{
|
||||
content: string | null
|
||||
@@ -15,8 +15,8 @@ defineProps<{
|
||||
onMessageDelete: (message: string, change: 'aii' | 'user') => void
|
||||
}>()
|
||||
|
||||
const message = defineModel<string>('message', {required: true})
|
||||
const mode = defineModel<'continue' | 'expand'>('mode', {required: true})
|
||||
const message = defineModel<string>('message', { required: true })
|
||||
const mode = defineModel<'continue' | 'expand'>('mode', { required: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -26,15 +26,15 @@ const mode = defineModel<'continue' | 'expand'>('mode', {required: true})
|
||||
v-if="content !== null"
|
||||
:is="createChatTableMessages(content, onMessageEdit, onMessageDelete)"
|
||||
/>
|
||||
<div v-if="aiiMessage !== null" class="user-message message" v-html="md.render(message)"/>
|
||||
<div v-if="aiiMessage !== null" class="user-message message" v-html="md.render(message)" />
|
||||
<div v-if="aiiMessage !== null" class="aii-message-streaming message">
|
||||
<div class="thinking">{{ aiiThinking }}</div>
|
||||
<div v-html="md.render(aiiMessage)"/>
|
||||
<div v-html="md.render(aiiMessage)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="aiiMessage === null" class="editor">
|
||||
<n-input v-model:value="message" type="textarea" :resizable="false"/>
|
||||
<n-input v-model:value="message" type="textarea" :resizable="false" />
|
||||
<n-flex justify="right" align="center">
|
||||
<n-button type="tertiary" size="small" circle>!</n-button>
|
||||
<n-switch
|
||||
|
||||
@@ -10,11 +10,24 @@ defineProps<{
|
||||
|
||||
<template>
|
||||
<div class="chatroom-card">
|
||||
<n-image v-if="infoMode" class="image" object-fit="cover" preview-disabled :src="feature_image"
|
||||
width="140"
|
||||
height="100"/>
|
||||
<n-image v-else class="image" object-fit="cover" preview-disabled :src="feature_image"
|
||||
width="84" height="60"/>
|
||||
<n-image
|
||||
v-if="infoMode"
|
||||
class="image"
|
||||
object-fit="cover"
|
||||
preview-disabled
|
||||
:src="feature_image"
|
||||
width="140"
|
||||
height="100"
|
||||
/>
|
||||
<n-image
|
||||
v-else
|
||||
class="image"
|
||||
object-fit="cover"
|
||||
preview-disabled
|
||||
:src="feature_image"
|
||||
width="84"
|
||||
height="60"
|
||||
/>
|
||||
<div class="card-body">
|
||||
<n-text class="name">{{ name }}</n-text>
|
||||
<n-ellipsis :line-clamp="2" style="max-width: 100%" class="description">
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, watch} from 'vue'
|
||||
import type {ChatroomPublic} from '@/types/chatroom.js'
|
||||
import {api} from '@/tools/web.js'
|
||||
import type {ReturnDto} from '@/types/response.js'
|
||||
import {useMessage} from 'naive-ui'
|
||||
import UploadFileModal from "@/components/file/UploadFileModal.vue";
|
||||
import SelectFileModal from "@/components/file/SelectFileModal.vue";
|
||||
import type {UploadFileDto} from "@/types/user.js";
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { ChatroomPublic } from '@/types/chatroom.js'
|
||||
import { api } from '@/tools/web.js'
|
||||
import type { ReturnDto } from '@/types/response.js'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import UploadFileModal from '@/components/file/UploadFileModal.vue'
|
||||
import SelectFileModal from '@/components/file/SelectFileModal.vue'
|
||||
import type { UploadFileDto } from '@/types/user.js'
|
||||
|
||||
const MESSAGE = useMessage()
|
||||
|
||||
const showModal = defineModel<boolean>('showModal', {required: true})
|
||||
const showModal = defineModel<boolean>('showModal', { required: true })
|
||||
|
||||
const showSelectModal = ref(false)
|
||||
const showUploadModal = ref(false)
|
||||
@@ -34,7 +34,7 @@ watch(image_url, () => {
|
||||
})
|
||||
|
||||
async function loadFiles() {
|
||||
return await api.get("/file/").then(res => files.value = res.data as UploadFileDto[])
|
||||
return await api.get('/file/').then((res) => (files.value = res.data as UploadFileDto[]))
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
@@ -56,8 +56,13 @@ function onSubmit() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal v-model:show="showModal" preset="card" title="创建聊天室" content-scrollable
|
||||
style="width: 800px;">
|
||||
<n-modal
|
||||
v-model:show="showModal"
|
||||
preset="card"
|
||||
title="创建聊天室"
|
||||
content-scrollable
|
||||
style="width: 800px"
|
||||
>
|
||||
<n-form
|
||||
:model="createChatroomForm"
|
||||
label-placement="left"
|
||||
@@ -65,17 +70,19 @@ function onSubmit() {
|
||||
label-width="auto"
|
||||
>
|
||||
<n-form-item path="name" label="名称">
|
||||
<n-input v-model:value="createChatroomForm.name"/>
|
||||
<n-input v-model:value="createChatroomForm.name" />
|
||||
</n-form-item>
|
||||
<n-form-item path="description" label="简介">
|
||||
<n-input type="textarea" v-model:value="createChatroomForm.description"/>
|
||||
<n-input type="textarea" v-model:value="createChatroomForm.description" />
|
||||
</n-form-item>
|
||||
<n-form-item path="feature_image" label="特色图像">
|
||||
<n-flex style="width: 100%;" :wrap="false">
|
||||
<n-input v-model:value="createChatroomForm.feature_image"
|
||||
placeholder="留空以使用默认图像"/>
|
||||
<n-button secondary type="info" @click="showSelectModal = true;">选择</n-button>
|
||||
<n-button secondary type="warning" @click="showUploadModal = true;">上传</n-button>
|
||||
<n-flex style="width: 100%" :wrap="false">
|
||||
<n-input
|
||||
v-model:value="createChatroomForm.feature_image"
|
||||
placeholder="留空以使用默认图像"
|
||||
/>
|
||||
<n-button secondary type="info" @click="showSelectModal = true">选择</n-button>
|
||||
<n-button secondary type="warning" @click="showUploadModal = true">上传</n-button>
|
||||
</n-flex>
|
||||
</n-form-item>
|
||||
<n-form-item label="确认?">
|
||||
@@ -83,9 +90,14 @@ function onSubmit() {
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
|
||||
<select-file-modal :max="1" :extensions="['png', 'jpeg', 'jpg']" :load-files="loadFiles"
|
||||
v-model:show-modal="showSelectModal" v-model:select-files="selectFiles"/>
|
||||
<upload-file-modal v-model:show-modal="showUploadModal" :after-leave="loadFiles"/>
|
||||
<select-file-modal
|
||||
:max="1"
|
||||
:extensions="['png', 'jpeg', 'jpg']"
|
||||
:load-files="loadFiles"
|
||||
v-model:show-modal="showSelectModal"
|
||||
v-model:select-files="selectFiles"
|
||||
/>
|
||||
<upload-file-modal v-model:show-modal="showUploadModal" :after-leave="loadFiles" />
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import {api} from '@/tools/web.js'
|
||||
import {ref, watch} from 'vue'
|
||||
import {useRoute} from 'vue-router'
|
||||
import type {ChatScript} from '@/types/chatroom.js'
|
||||
import type {ReturnDto} from '@/types/response.js'
|
||||
import {useMessage} from 'naive-ui'
|
||||
import { api } from '@/tools/web.js'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import type { ChatScript } from '@/types/chatroom.js'
|
||||
import type { ReturnDto } from '@/types/response.js'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import XamlModal from '@/components/XamlModal.vue'
|
||||
|
||||
const ROUTE = useRoute()
|
||||
const MESSAGE = useMessage()
|
||||
|
||||
const showDrawer = defineModel('showDrawer', {required: true})
|
||||
const showDrawer = defineModel('showDrawer', { required: true })
|
||||
|
||||
const showXamlModal = ref(false)
|
||||
|
||||
const {script} = defineProps<{
|
||||
const { script } = defineProps<{
|
||||
script: string
|
||||
}>()
|
||||
|
||||
@@ -48,10 +48,9 @@ watch(
|
||||
() => {
|
||||
try {
|
||||
scriptForm.value = JSON.parse(script) as ChatScript
|
||||
} catch {
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
{immediate: true},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -127,7 +126,7 @@ watch(
|
||||
</template>
|
||||
</n-drawer-content>
|
||||
|
||||
<xaml-modal v-model:show-modal="showXamlModal"/>
|
||||
<xaml-modal v-model:show-modal="showXamlModal" />
|
||||
</n-drawer>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import type {UploadFileDto} from "@/types/user.js";
|
||||
import {useNowUser} from "@/stores/now-user.js";
|
||||
import {computed} from "vue";
|
||||
import type { UploadFileDto } from '@/types/user.js'
|
||||
import { useNowUser } from '@/stores/now-user.js'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const NOWUSER = useNowUser()
|
||||
|
||||
const {file} = defineProps<{
|
||||
const { file } = defineProps<{
|
||||
file: UploadFileDto
|
||||
}>()
|
||||
|
||||
const is_you = computed(() => NOWUSER.id === file.uploader_id)
|
||||
|
||||
const showModal = defineModel("showModal", {required: true})
|
||||
const showModal = defineModel('showModal', { required: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal v-model:show="showModal" preset="card" style="width: 1000px;" title="文件信息">
|
||||
<n-modal v-model:show="showModal" preset="card" style="width: 1000px" title="文件信息">
|
||||
<div class="card-content">
|
||||
<n-image :width="500" :height="500" object-fit="contain" :src="file.download_url"/>
|
||||
<n-image :width="500" :height="500" object-fit="contain" :src="file.download_url" />
|
||||
<div class="side">
|
||||
<n-h3>{{ file.original_name }}</n-h3>
|
||||
<n-p>保存文件名:{{ file.safe_name }}</n-p>
|
||||
<n-p>上传用户ID:{{ file.uploader_id }}
|
||||
<n-p
|
||||
>上传用户ID:{{ file.uploader_id }}
|
||||
<n-tag v-if="is_you" type="primary">你</n-tag>
|
||||
</n-p>
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type {UploadFileDto} from "@/types/user.js";
|
||||
import {computed, onMounted, ref, useTemplateRef} from "vue";
|
||||
import FileModal from "@/components/file/FileModal.vue";
|
||||
import type { UploadFileDto } from '@/types/user.js'
|
||||
import { computed, onMounted, ref, useTemplateRef } from 'vue'
|
||||
import FileModal from '@/components/file/FileModal.vue'
|
||||
|
||||
const {file, size, enableSelect, onSelect, onRemove} = defineProps<{
|
||||
const { file, size, enableSelect, onSelect, onRemove } = defineProps<{
|
||||
file: UploadFileDto
|
||||
size: number
|
||||
enableSelect?: boolean
|
||||
@@ -11,12 +11,12 @@ const {file, size, enableSelect, onSelect, onRemove} = defineProps<{
|
||||
onRemove?: (file: UploadFileDto) => boolean
|
||||
}>()
|
||||
|
||||
const th = useTemplateRef("th")
|
||||
const th = useTemplateRef('th')
|
||||
|
||||
const showModal = ref(false)
|
||||
const selected = ref(false)
|
||||
|
||||
const ALLOWED_EXTENSIONS = ["jpg", "jpeg", "png"]
|
||||
const ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png']
|
||||
|
||||
onMounted(() => {
|
||||
if (ALLOWED_EXTENSIONS.includes(file.safe_name.split('.').at(-1)!.toLowerCase())) {
|
||||
@@ -32,13 +32,13 @@ function onClick() {
|
||||
if (selected.value && onRemove) {
|
||||
if (onRemove(file)) {
|
||||
selected.value = false
|
||||
th.value?.classList.remove("selected")
|
||||
th.value?.classList.remove('selected')
|
||||
console.log(`选中文件:${file.original_name}`)
|
||||
}
|
||||
} else if (!selected.value && onSelect) {
|
||||
if (onSelect(file)) {
|
||||
selected.value = true
|
||||
th.value?.classList.add("selected")
|
||||
th.value?.classList.add('selected')
|
||||
console.log(`取消文件:${file.original_name}`)
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ const size_px = computed(() => `${size}px`)
|
||||
<template>
|
||||
<div class="file-thumbnail" ref="th" @click="onClick"></div>
|
||||
|
||||
<file-modal :file v-model:show-modal="showModal"/>
|
||||
<file-modal :file v-model:show-modal="showModal" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -64,7 +64,9 @@ div.file-thumbnail {
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
|
||||
transition: border-color 0.3s, box-shadow 0.3s;
|
||||
transition:
|
||||
border-color 0.3s,
|
||||
box-shadow 0.3s;
|
||||
}
|
||||
|
||||
div.selected {
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import {selectFilesCom} from "@/components/file/upload-files.js";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import type {UploadFileDto} from "@/types/user.js";
|
||||
import {useMessage} from "naive-ui";
|
||||
import { selectFilesCom } from '@/components/file/upload-files.js'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { UploadFileDto } from '@/types/user.js'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
const MESSAGE = useMessage()
|
||||
|
||||
const {max, extensions, loadFiles} = defineProps<{
|
||||
const { max, extensions, loadFiles } = defineProps<{
|
||||
max: number
|
||||
extensions: string[]
|
||||
loadFiles: () => Promise<UploadFileDto[]>
|
||||
}>()
|
||||
|
||||
const showModal = defineModel("showModal", {required: true})
|
||||
const showModal = defineModel('showModal', { required: true })
|
||||
|
||||
const files = ref<UploadFileDto[]>([])
|
||||
const tempFiles = ref<UploadFileDto[]>([])
|
||||
const selectFiles = defineModel<UploadFileDto[]>("selectFiles", {required: true})
|
||||
const selectFiles = defineModel<UploadFileDto[]>('selectFiles', { required: true })
|
||||
|
||||
function selectFile(file: UploadFileDto) {
|
||||
if (tempFiles.value.length < max) {
|
||||
tempFiles.value.push(file)
|
||||
return true
|
||||
} else {
|
||||
MESSAGE.warning("可选择文件数量达到上限……")
|
||||
MESSAGE.warning('可选择文件数量达到上限……')
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -37,31 +37,42 @@ function removeFile(file: UploadFileDto) {
|
||||
}
|
||||
|
||||
watch(showModal, async () => {
|
||||
tempFiles.value = [] // 每次打开模态框时都重置已选文件
|
||||
tempFiles.value = [] // 每次打开模态框时都重置已选文件
|
||||
files.value = await loadFiles()
|
||||
})
|
||||
|
||||
const tip_1 = computed(() => max > 1 ? `请选择至少 ${max} 个文件。` : "请选择一个文件。")
|
||||
const tip_1 = computed(() => (max > 1 ? `请选择至少 ${max} 个文件。` : '请选择一个文件。'))
|
||||
const tip_2 = computed(() => `允许的文件类型:${extensions.join('、')}。`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal preset="card" style="max-width: 600px; max-height: 600px;" title="选择文件"
|
||||
content-scrollable
|
||||
v-model:show="showModal">
|
||||
<n-modal
|
||||
preset="card"
|
||||
style="max-width: 600px; max-height: 600px"
|
||||
title="选择文件"
|
||||
content-scrollable
|
||||
v-model:show="showModal"
|
||||
>
|
||||
<n-flex vertical>
|
||||
<n-alert type="info">
|
||||
{{ tip_1 }}
|
||||
{{ tip_2 }}
|
||||
</n-alert>
|
||||
<component :is="selectFilesCom(files, selectFile, removeFile)"/>
|
||||
<n-button type="primary" secondary @click="selectFiles = tempFiles; showModal = false;">
|
||||
<component :is="selectFilesCom(files, selectFile, removeFile)" />
|
||||
<n-button
|
||||
type="primary"
|
||||
secondary
|
||||
@click="
|
||||
() => {
|
||||
selectFiles = tempFiles
|
||||
showModal = false
|
||||
}
|
||||
"
|
||||
>
|
||||
确认选择
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import {type UploadCustomRequestOptions, type UploadFileInfo} from "naive-ui";
|
||||
import {api} from "@/tools/web.js";
|
||||
import type {UploadFileDto} from "@/types/user.js";
|
||||
import {shallowRef, useTemplateRef} from "vue";
|
||||
import { type UploadCustomRequestOptions, type UploadFileInfo } from 'naive-ui'
|
||||
import { api } from '@/tools/web.js'
|
||||
import type { UploadFileDto } from '@/types/user.js'
|
||||
import { shallowRef, useTemplateRef } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
afterLeave?: () => void;
|
||||
afterLeave?: () => void
|
||||
}>()
|
||||
|
||||
const showModal = defineModel("showModal", {required: true})
|
||||
const showModal = defineModel('showModal', { required: true })
|
||||
|
||||
const upload = useTemplateRef("upload")
|
||||
const upload = useTemplateRef('upload')
|
||||
|
||||
const fileList = shallowRef<UploadFileInfo[]>([])
|
||||
|
||||
async function handle_upload({file, onFinish, onError, onProgress}: UploadCustomRequestOptions) {
|
||||
const formData = new FormData();
|
||||
async function handle_upload({ file, onFinish, onError, onProgress }: UploadCustomRequestOptions) {
|
||||
const formData = new FormData()
|
||||
console.log(file.file)
|
||||
formData.append("file", file.file!)
|
||||
formData.append('file', file.file!)
|
||||
console.log(formData)
|
||||
|
||||
try {
|
||||
const data = await api.post("/file/upload/", formData, {
|
||||
headers: {
|
||||
'Content-Type': undefined // 取消全局默认的 application/json 很重要!!!!!!!!
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const percent = Math.ceil(
|
||||
(progressEvent.loaded / progressEvent.total!) * 100
|
||||
)
|
||||
onProgress({percent}) // 更新进度条
|
||||
}
|
||||
}).then((res) => res.data as UploadFileDto)
|
||||
const data = await api
|
||||
.post('/file/upload/', formData, {
|
||||
headers: {
|
||||
'Content-Type': undefined, // 取消全局默认的 application/json 很重要!!!!!!!!
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const percent = Math.ceil((progressEvent.loaded / progressEvent.total!) * 100)
|
||||
onProgress({ percent }) // 更新进度条
|
||||
},
|
||||
})
|
||||
.then((res) => res.data as UploadFileDto)
|
||||
|
||||
file.url = data.download_url
|
||||
onFinish()
|
||||
@@ -47,12 +47,23 @@ function onUpload() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal style="width: 600px;" preset="card" v-model:show="showModal"
|
||||
title="上传文件" content-scrollable
|
||||
@after-leave="afterLeave">
|
||||
<n-modal
|
||||
style="width: 600px"
|
||||
preset="card"
|
||||
v-model:show="showModal"
|
||||
title="上传文件"
|
||||
content-scrollable
|
||||
@after-leave="afterLeave"
|
||||
>
|
||||
<n-flex vertical>
|
||||
<n-upload multiple ref="upload" :default-upload="false" list-type="image"
|
||||
:custom-request="handle_upload" v-model:file-list="fileList">
|
||||
<n-upload
|
||||
multiple
|
||||
ref="upload"
|
||||
:default-upload="false"
|
||||
list-type="image"
|
||||
:custom-request="handle_upload"
|
||||
v-model:file-list="fileList"
|
||||
>
|
||||
<n-upload-dragger>
|
||||
<n-p>拖拽文件到此区域可以快速上传。</n-p>
|
||||
</n-upload-dragger>
|
||||
|
||||
@@ -1,30 +1,41 @@
|
||||
import type {UploadFileDto} from "@/types/user.ts";
|
||||
import FileThumbnail from "@/components/file/FileThumbnail.vue";
|
||||
import {NEmpty, NFlex} from "naive-ui";
|
||||
import type { UploadFileDto } from '@/types/user.ts'
|
||||
import FileThumbnail from '@/components/file/FileThumbnail.vue'
|
||||
import { NEmpty, NFlex } from 'naive-ui'
|
||||
|
||||
export function uploadFilesCom(files: UploadFileDto[]) {
|
||||
if (files.length === 0) {
|
||||
return <NEmpty description="你还没有上传任何文件。" size="large"/>
|
||||
return <NEmpty description="你还没有上传任何文件。" size="large" />
|
||||
}
|
||||
return <NFlex>
|
||||
{files.map((file: UploadFileDto) => {
|
||||
return <FileThumbnail size={120} file={file}></FileThumbnail>;
|
||||
})}
|
||||
</NFlex>
|
||||
return (
|
||||
<NFlex>
|
||||
{files.map((file: UploadFileDto) => {
|
||||
return <FileThumbnail size={120} file={file}></FileThumbnail>
|
||||
})}
|
||||
</NFlex>
|
||||
)
|
||||
}
|
||||
|
||||
export function selectFilesCom(
|
||||
files: UploadFileDto[],
|
||||
onSelect: (file: UploadFileDto) => boolean,
|
||||
onRemove: (file: UploadFileDto) => boolean
|
||||
onRemove: (file: UploadFileDto) => boolean,
|
||||
) {
|
||||
if (files.length === 0) {
|
||||
return <NEmpty description="你还没有上传任何文件。" size="large"/>
|
||||
return <NEmpty description="你还没有上传任何文件。" size="large" />
|
||||
}
|
||||
return <NFlex>
|
||||
{files.map((file: UploadFileDto) => {
|
||||
return <FileThumbnail size={82} file={file} enableSelect onSelect={onSelect}
|
||||
onRemove={onRemove}></FileThumbnail>;
|
||||
})}
|
||||
</NFlex>
|
||||
return (
|
||||
<NFlex>
|
||||
{files.map((file: UploadFileDto) => {
|
||||
return (
|
||||
<FileThumbnail
|
||||
size={82}
|
||||
file={file}
|
||||
enableSelect
|
||||
onSelect={onSelect}
|
||||
onRemove={onRemove}
|
||||
></FileThumbnail>
|
||||
)
|
||||
})}
|
||||
</NFlex>
|
||||
)
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import {createApp} from 'vue'
|
||||
import {createPinia} from 'pinia'
|
||||
import {createHead} from "@unhead/vue/client";
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createHead } from '@unhead/vue/client'
|
||||
|
||||
import '@/assets/main.scss'
|
||||
import '@/assets/beautiful.scss'
|
||||
|
||||
@@ -1,59 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import UserAction from "@/components/admin/UserAction.vue";
|
||||
import type {MenuOption} from "naive-ui";
|
||||
import {computed, onMounted, ref, useTemplateRef} from "vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import {useNowUser} from "@/stores/now-user.js";
|
||||
import {useHead} from "@unhead/vue";
|
||||
import UserAction from '@/components/admin/UserAction.vue'
|
||||
import type {MenuOption} from 'naive-ui'
|
||||
import {computed, onMounted, ref, useTemplateRef} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {useNowUser} from '@/stores/now-user.js'
|
||||
import {useHead} from '@unhead/vue'
|
||||
|
||||
useHead({
|
||||
titleTemplate: "%s | 管理面板 | NayHome"
|
||||
titleTemplate: '%s | 管理面板 | NayHome',
|
||||
})
|
||||
|
||||
const ROUTER = useRouter()
|
||||
const NOWUSER = useNowUser()
|
||||
|
||||
const menu = useTemplateRef("menu")
|
||||
const menu = useTemplateRef('menu')
|
||||
|
||||
const selectOption = ref("")
|
||||
const selectOption = ref('')
|
||||
const options = computed<MenuOption[]>(() => [
|
||||
{
|
||||
label: "总览",
|
||||
key: "",
|
||||
label: '总览',
|
||||
key: '',
|
||||
},
|
||||
{
|
||||
label: "用户",
|
||||
key: "user-basic",
|
||||
label: '用户',
|
||||
key: 'user-basic',
|
||||
children: [
|
||||
{
|
||||
label: "资料",
|
||||
key: "user-info"
|
||||
label: '资料',
|
||||
key: 'user-info',
|
||||
},
|
||||
{
|
||||
label: "安全",
|
||||
key: "user-security"
|
||||
}
|
||||
]
|
||||
label: '安全',
|
||||
key: 'user-security',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "内容",
|
||||
key: "user-creation",
|
||||
label: '内容',
|
||||
key: 'user-creation',
|
||||
children: [
|
||||
{
|
||||
label: "上传",
|
||||
key: "user-upload"
|
||||
label: '上传',
|
||||
key: 'user-upload',
|
||||
},
|
||||
{
|
||||
label: "剧本",
|
||||
key: "user-script"
|
||||
}
|
||||
]
|
||||
label: '剧本',
|
||||
key: 'user-script',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "NyaHome 管理后台",
|
||||
key: "nyahome",
|
||||
label: 'NyaHome 管理后台',
|
||||
key: 'nyahome',
|
||||
show: NOWUSER.is_admin,
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
function handleMenuClick(key: string) {
|
||||
@@ -61,12 +61,14 @@ function handleMenuClick(key: string) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const key = ROUTER.currentRoute.value.fullPath.replace("/admin/", "")
|
||||
if (key) {
|
||||
const key = ROUTER.currentRoute.value.fullPath.replace('/admin/', '')
|
||||
if (key.endsWith('/admin')) {
|
||||
selectOption.value = ''
|
||||
} else if (key) {
|
||||
selectOption.value = key
|
||||
menu.value?.showOption(key)
|
||||
} else {
|
||||
selectOption.value = ""
|
||||
selectOption.value = ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -80,7 +82,7 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<router-view v-slot="{Component}">
|
||||
<router-view v-slot="{ Component }">
|
||||
<div id="user-page-content">
|
||||
<keep-alive>
|
||||
<component :is="Component"/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import {useRoute} from 'vue-router'
|
||||
import {onMounted, ref, useTemplateRef, watch} from 'vue'
|
||||
import {onMounted, reactive, ref, useTemplateRef, watch} from 'vue'
|
||||
import {api} from '@/tools/web.ts'
|
||||
import type {ReturnDto} from '@/types/response.ts'
|
||||
import type {Chatroom} from '@/types/chatroom.ts'
|
||||
@@ -11,6 +11,14 @@ import ChatControlPanel from '@/components/chatroom/ChatControlPanel.vue'
|
||||
import {fetchEventSource} from '@microsoft/fetch-event-source'
|
||||
import type {AiiTokenInfo} from '@/types/aii.ts'
|
||||
import {SEE_YOU_TOMORROW} from '@/types/syt.ts'
|
||||
import {useHead} from '@unhead/vue'
|
||||
|
||||
const pageHead = reactive({
|
||||
title: '正在加载聊天室...',
|
||||
titleTemplate: '%s | 聊天室 | NyaHome'
|
||||
})
|
||||
|
||||
useHead(pageHead)
|
||||
|
||||
const ROUTE = useRoute()
|
||||
const MESSAGE = useMessage()
|
||||
@@ -45,6 +53,7 @@ function load() {
|
||||
}
|
||||
})
|
||||
.then((cr) => {
|
||||
pageHead.title = cr.name
|
||||
crName.value = cr.name
|
||||
crDescription.value = cr.description
|
||||
crFeatureImage.value = cr.feature_image
|
||||
@@ -235,8 +244,12 @@ function enableSidebar() {
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="main-column">
|
||||
<chatroom-card :id="Number(ROUTE.params.id)" :name="crName" :description="crDescription"
|
||||
:feature_image="crFeatureImage"/>
|
||||
<chatroom-card
|
||||
:id="Number(ROUTE.params.id)"
|
||||
:name="crName"
|
||||
:description="crDescription"
|
||||
:feature_image="crFeatureImage"
|
||||
/>
|
||||
<chat-table
|
||||
:content="crContent"
|
||||
:aii-thinking
|
||||
|
||||
@@ -5,7 +5,12 @@ import {api} from '@/tools/web.ts'
|
||||
import type {ChatroomPublic} from '@/types/chatroom.ts'
|
||||
import type {ReturnDto} from '@/types/response.ts'
|
||||
import ChatroomCreatorModal from '@/components/chatroom/ChatroomCreatorModal.vue'
|
||||
import {useNowUser} from "@/stores/now-user.ts";
|
||||
import {useNowUser} from '@/stores/now-user.ts'
|
||||
import {useHead} from "@unhead/vue";
|
||||
|
||||
useHead({
|
||||
title: "聊天室列表",
|
||||
})
|
||||
|
||||
const NOWUSER = useNowUser()
|
||||
|
||||
@@ -23,9 +28,13 @@ function load() {
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => NOWUSER.isLogin, () => {
|
||||
load()
|
||||
}, {immediate: true})
|
||||
watch(
|
||||
() => NOWUSER.isLogin,
|
||||
() => {
|
||||
load()
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import InDev from '@/components/InDev.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="marketplace-page">
|
||||
<div class="marketplace-header-card"></div>
|
||||
|
||||
<in-dev />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
div.marketplace-page {
|
||||
min-width: min(1200px, 90%);
|
||||
width: min(1200px, 90%);
|
||||
min-height: 0;
|
||||
padding: 6px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
div.marketplace-header-card {
|
||||
background-image: url('/nyahome/normal-header.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,19 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import UserAction from '@/components/admin/UserAction.vue'
|
||||
import { useHead } from '@unhead/vue'
|
||||
|
||||
useHead({
|
||||
title: '首页',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex vertical style="padding: 6px 20px">
|
||||
<div class="welcome-page">
|
||||
<n-flex>
|
||||
<n-card class="welcome-card" title="Welcome to Welcome!"></n-card>
|
||||
<div class="user-action-card">
|
||||
<user-action/>
|
||||
<user-action />
|
||||
</div>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
div.welcome-page {
|
||||
min-width: min(1200px, 90%);
|
||||
width: min(1200px, 90%);
|
||||
min-height: 0;
|
||||
padding: 6px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.welcome-card {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -1,73 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import ConfigCard from "@/components/admin/ConfigCard.vue";
|
||||
import {useHead} from "@unhead/vue";
|
||||
import {ref} from "vue";
|
||||
import {api} from "@/tools/web.ts";
|
||||
import InDev from "@/components/InDev.vue";
|
||||
import {useMessage} from "naive-ui";
|
||||
import type {ReturnDto} from "@/types/response.ts";
|
||||
import ConfigCard from '@/components/admin/ConfigCard.vue'
|
||||
import { useHead } from '@unhead/vue'
|
||||
import { ref } from 'vue'
|
||||
import { api } from '@/tools/web.ts'
|
||||
import InDev from '@/components/InDev.vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import type { ReturnDto } from '@/types/response.ts'
|
||||
|
||||
interface SiteConfig {
|
||||
site_name: string;
|
||||
site_url: string;
|
||||
backend_url: string;
|
||||
site_name: string
|
||||
site_url: string
|
||||
backend_url: string
|
||||
|
||||
jwt_secret_key: string;
|
||||
jwt_secret_key: string
|
||||
|
||||
smtp_enable: boolean;
|
||||
smtp_sender: string;
|
||||
smtp_hostname: string;
|
||||
smtp_port: number;
|
||||
smtp_username: string;
|
||||
smtp_password: string;
|
||||
smtp_use_tls: boolean;
|
||||
smtp_enable: boolean
|
||||
smtp_sender: string
|
||||
smtp_hostname: string
|
||||
smtp_port: number
|
||||
smtp_username: string
|
||||
smtp_password: string
|
||||
smtp_use_tls: boolean
|
||||
}
|
||||
|
||||
const MESSAGE = useMessage()
|
||||
|
||||
useHead({
|
||||
title: "NyaHome 管理后台"
|
||||
title: 'NyaHome 管理后台',
|
||||
})
|
||||
|
||||
const siteConfig = ref<SiteConfig | null>(null);
|
||||
const siteConfig = ref<SiteConfig | null>(null)
|
||||
|
||||
function getConfig() {
|
||||
api.get("/admin/site_config/")
|
||||
.then((res) => {
|
||||
siteConfig.value = res.data as SiteConfig
|
||||
MESSAGE.success("成功获取设置~")
|
||||
})
|
||||
api.get('/admin/site_config/').then((res) => {
|
||||
siteConfig.value = res.data as SiteConfig
|
||||
MESSAGE.success('成功获取设置~')
|
||||
})
|
||||
}
|
||||
|
||||
function saveConfig() {
|
||||
api.post("/admin/site_config/", JSON.stringify(siteConfig.value))
|
||||
.then((res) => {
|
||||
siteConfig.value = res.data as SiteConfig
|
||||
MESSAGE.success("保存并刷新设置成功~")
|
||||
})
|
||||
api.post('/admin/site_config/', JSON.stringify(siteConfig.value)).then((res) => {
|
||||
siteConfig.value = res.data as SiteConfig
|
||||
MESSAGE.success('保存并刷新设置成功~')
|
||||
})
|
||||
}
|
||||
|
||||
const testMailTo = ref("25565@qq.com")
|
||||
const testMailTo = ref('25565@qq.com')
|
||||
|
||||
function sendTestMail() {
|
||||
api.post("/admin/email-test/", JSON.stringify({to: testMailTo.value}))
|
||||
.then(res => res.data as ReturnDto)
|
||||
.then(data => data.success)
|
||||
.then(success => {
|
||||
if (success) {
|
||||
MESSAGE.success("邮件发送成功,请稍等片刻,然后检查收件箱~")
|
||||
} else {
|
||||
MESSAGE.error("后端表示邮件发送失败,请检查日志输出。")
|
||||
}
|
||||
})
|
||||
api
|
||||
.post('/admin/email-test/', JSON.stringify({ to: testMailTo.value }))
|
||||
.then((res) => res.data as ReturnDto)
|
||||
.then((data) => data.success)
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
MESSAGE.success('邮件发送成功,请稍等片刻,然后检查收件箱~')
|
||||
} else {
|
||||
MESSAGE.error('后端表示邮件发送失败,请检查日志输出。')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card>
|
||||
<template #header>
|
||||
<n-h3 prefix="bar" style="margin: 0;">NyaHome 管理后台</n-h3>
|
||||
<n-h3 prefix="bar" style="margin: 0">NyaHome 管理后台</n-h3>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<n-flex>
|
||||
@@ -80,19 +78,19 @@ function sendTestMail() {
|
||||
<n-tabs type="card" v-if="siteConfig !== null">
|
||||
<n-tab-pane name="user" tab="用户" display-directive="show">
|
||||
<config-card title="全部用户">
|
||||
<in-dev/>
|
||||
<in-dev />
|
||||
</config-card>
|
||||
</n-tab-pane>
|
||||
|
||||
<n-tab-pane name="chatroom" tab="聊天室" display-directive="show">
|
||||
<config-card title="全部聊天室">
|
||||
<in-dev/>
|
||||
<in-dev />
|
||||
</config-card>
|
||||
</n-tab-pane>
|
||||
|
||||
<n-tab-pane name="script" tab="剧本" display-directive="show">
|
||||
<config-card title="全部剧本">
|
||||
<in-dev/>
|
||||
<in-dev />
|
||||
</config-card>
|
||||
</n-tab-pane>
|
||||
|
||||
@@ -101,29 +99,30 @@ function sendTestMail() {
|
||||
<config-card title="基本信息">
|
||||
<n-form>
|
||||
<n-form-item label="站点名称">
|
||||
<n-input v-model:value="siteConfig.site_name"/>
|
||||
<n-input v-model:value="siteConfig.site_name" />
|
||||
</n-form-item>
|
||||
<n-form-item label="站点地址">
|
||||
<n-input v-model:value="siteConfig.site_url"/>
|
||||
<n-input v-model:value="siteConfig.site_url" />
|
||||
</n-form-item>
|
||||
<n-alert type="info" class="in-form-alert">
|
||||
如果您需要将 NyaHome 的前后端分开部署,则需要在此设置后端地址。您需要自行处理跨域问题。
|
||||
如果您需要将 NyaHome
|
||||
的前后端分开部署,则需要在此设置后端地址。您需要自行处理跨域问题。
|
||||
</n-alert>
|
||||
<n-form-item label="FastAPI 后端地址">
|
||||
<n-input v-model:value="siteConfig.backend_url"/>
|
||||
<n-input v-model:value="siteConfig.backend_url" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</config-card>
|
||||
|
||||
<config-card title="搜索引擎设置与 SEO">
|
||||
<in-dev/>
|
||||
<in-dev />
|
||||
</config-card>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
|
||||
<n-tab-pane name="permission" tab="权限设置" display-directive="show">
|
||||
<config-card title="用户权限">
|
||||
<in-dev/>
|
||||
<in-dev />
|
||||
</config-card>
|
||||
</n-tab-pane>
|
||||
|
||||
@@ -132,11 +131,11 @@ function sendTestMail() {
|
||||
<config-card title="JWT">
|
||||
<n-form>
|
||||
<n-alert type="info" class="in-form-alert">
|
||||
JWT(Json Web Token)签名需要一个密钥,你可以手动提供一个,或者自行生成一个。<br/>
|
||||
JWT(Json Web Token)签名需要一个密钥,你可以手动提供一个,或者自行生成一个。<br />
|
||||
修改此密钥会导致所有用户的登录状态丢失(你也会),请一次性设置一个足够安全的。
|
||||
</n-alert>
|
||||
<n-form-item label="JWT 密钥">
|
||||
<n-input v-model:value="siteConfig.jwt_secret_key"/>
|
||||
<n-input v-model:value="siteConfig.jwt_secret_key" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</config-card>
|
||||
@@ -148,37 +147,38 @@ function sendTestMail() {
|
||||
<config-card title="邮件 SMTP">
|
||||
<n-form>
|
||||
<n-alert type="info" class="in-form-alert">
|
||||
NayHome 无法自己发送邮件,需要配置 SMTP 服务。<br/>
|
||||
NayHome 无法自己发送邮件,需要配置 SMTP 服务。<br />
|
||||
或者你也可以关闭邮件功能,当然芒果还是建议你配置一下的。
|
||||
</n-alert>
|
||||
<n-form-item label="启用邮件功能(SMTP)">
|
||||
<n-switch v-model:value="siteConfig.smtp_enable"/>
|
||||
<n-switch v-model:value="siteConfig.smtp_enable" />
|
||||
</n-form-item>
|
||||
<n-form-item label="发件人邮件地址">
|
||||
<n-input v-model:value="siteConfig.smtp_sender"/>
|
||||
<n-input v-model:value="siteConfig.smtp_sender" />
|
||||
</n-form-item>
|
||||
<n-form-item label="SMTP 主机名">
|
||||
<n-input v-model:value="siteConfig.smtp_hostname"/>
|
||||
<n-input v-model:value="siteConfig.smtp_hostname" />
|
||||
</n-form-item>
|
||||
<n-form-item label="SMTP 端口">
|
||||
<n-input-number v-model:value="siteConfig.smtp_port"/>
|
||||
<n-input-number v-model:value="siteConfig.smtp_port" />
|
||||
</n-form-item>
|
||||
<n-form-item label="SMTP 用户名">
|
||||
<n-input v-model:value="siteConfig.smtp_username"/>
|
||||
<n-input v-model:value="siteConfig.smtp_username" />
|
||||
</n-form-item>
|
||||
<n-form-item label="SMTP 密码(一般应当是一个独立的应用程序密码)">
|
||||
<n-input v-model:value="siteConfig.smtp_password"/>
|
||||
<n-input v-model:value="siteConfig.smtp_password" />
|
||||
</n-form-item>
|
||||
<n-form-item label="使用 TLS/SSL 加密">
|
||||
<n-switch v-model:value="siteConfig.smtp_use_tls"/>
|
||||
<n-switch v-model:value="siteConfig.smtp_use_tls" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #action>
|
||||
<n-flex vertical>
|
||||
<n-text>你可以在这里测试 NayHome 的邮件系统能否使用上述 SMTP
|
||||
<n-text
|
||||
>你可以在这里测试 NayHome 的邮件系统能否使用上述 SMTP
|
||||
设置工作,这会发送一封测试邮件。
|
||||
</n-text>
|
||||
<n-input v-model:value="testMailTo"/>
|
||||
<n-input v-model:value="testMailTo" />
|
||||
<n-button secondary type="warning" @click="sendTestMail()">发送测试邮件</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
@@ -187,7 +187,7 @@ function sendTestMail() {
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
|
||||
<n-empty size="large" v-else description="请尝试手动获取设置..."/>
|
||||
<n-empty size="large" v-else description="请尝试手动获取设置..." />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import {useNowUser} from "@/stores/now-user.js";
|
||||
import {computed} from "vue";
|
||||
import {useHead} from "@unhead/vue";
|
||||
import { useNowUser } from '@/stores/now-user.js'
|
||||
import { computed } from 'vue'
|
||||
import { useHead } from '@unhead/vue'
|
||||
|
||||
useHead({
|
||||
title: "总览"
|
||||
title: '总览',
|
||||
})
|
||||
|
||||
const NOWUSER = useNowUser()
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { useNowUser } from '@/stores/now-user.js'
|
||||
import { ref, watch } from 'vue'
|
||||
import {useNowUser} from '@/stores/now-user.js'
|
||||
import {ref, watch} from 'vue'
|
||||
import SelectFileModal from '@/components/file/SelectFileModal.vue'
|
||||
import { api } from '@/tools/web.js'
|
||||
import type { UploadFileDto, UserDto } from '@/types/user.js'
|
||||
import { useHead } from '@unhead/vue'
|
||||
import {api} from '@/tools/web.js'
|
||||
import type {UploadFileDto, UserDto} from '@/types/user.js'
|
||||
import {useHead} from '@unhead/vue'
|
||||
import ChangeEmailModal from '@/components/admin/ChangeEmailModal.vue'
|
||||
import ChangePhoneModal from '@/components/admin/ChangePhoneModal.vue'
|
||||
|
||||
useHead({
|
||||
title: '用户资料',
|
||||
@@ -68,7 +69,7 @@ watch(
|
||||
() => {
|
||||
reInitForm()
|
||||
},
|
||||
{ immediate: true },
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
async function save() {
|
||||
@@ -90,14 +91,14 @@ async function save() {
|
||||
<div class="ui-content">
|
||||
<n-form style="width: 450px" label-width="auto" label-placement="left" label-align="right">
|
||||
<n-form-item label="用户名">
|
||||
<n-input v-model:value="infoForm.name" />
|
||||
<n-input v-model:value="infoForm.name"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="展示名称">
|
||||
<n-input v-model:value="infoForm.display_name" />
|
||||
<n-input v-model:value="infoForm.display_name"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="头像">
|
||||
<n-flex>
|
||||
<n-avatar v-model:src="infoForm.avatar_url" :size="96" circle />
|
||||
<n-avatar v-model:src="infoForm.avatar_url" :size="96" circle/>
|
||||
<n-flex vertical>
|
||||
<n-tag type="info">需在「内容-上传」中提前上传图像。</n-tag>
|
||||
<n-tag type="warning">使用方形图像以获得最佳效果。</n-tag>
|
||||
@@ -107,7 +108,7 @@ async function save() {
|
||||
secondary
|
||||
type="tertiary"
|
||||
@click="infoForm.avatar_url = NOWUSER.avatar_url"
|
||||
>重置
|
||||
>重置
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
@@ -115,7 +116,7 @@ async function save() {
|
||||
</n-form-item>
|
||||
<n-form-item label="个人背景">
|
||||
<n-flex>
|
||||
<n-avatar v-model:src="infoForm.background_url" :size="96" object-fit="cover" />
|
||||
<n-avatar v-model:src="infoForm.background_url" :size="96" object-fit="cover"/>
|
||||
<n-flex vertical>
|
||||
<n-tag type="info">需在「内容-上传」中提前上传图像。</n-tag>
|
||||
<n-flex>
|
||||
@@ -124,7 +125,7 @@ async function save() {
|
||||
secondary
|
||||
type="tertiary"
|
||||
@click="infoForm.background_url = NOWUSER.background_url"
|
||||
>重置
|
||||
>重置
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
@@ -138,18 +139,20 @@ async function save() {
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="邮箱">
|
||||
<n-input v-model:value="NOWUSER.email" disabled />
|
||||
<n-input v-model:value="NOWUSER.email" disabled/>
|
||||
</n-form-item>
|
||||
<n-form-item label="手机号">
|
||||
<n-input v-model:value="NOWUSER.phone" disabled />
|
||||
<n-input v-model:value="NOWUSER.phone" disabled/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-flex>
|
||||
<n-button class="ui-button" type="primary" @click="save">保存</n-button>
|
||||
<n-button class="ui-button" type="warning" @click="showChangeEmailModal = true"
|
||||
>更改邮箱</n-button
|
||||
>
|
||||
<n-button class="ui-button" type="warning">更改手机号</n-button>
|
||||
<n-button class="ui-button" type="warning" secondary @click="showChangeEmailModal = true">
|
||||
更改邮箱
|
||||
</n-button>
|
||||
<n-button class="ui-button" type="warning" secondary @click="showChangePhoneModal = true">
|
||||
更改手机号
|
||||
</n-button>
|
||||
<n-button class="ui-button" type="tertiary">重置全部</n-button>
|
||||
</n-flex>
|
||||
</div>
|
||||
@@ -168,7 +171,8 @@ async function save() {
|
||||
v-model:show-modal="showBackgroundModal"
|
||||
v-model:select-files="background_selectFiles"
|
||||
/>
|
||||
<change-email-modal v-model:show-modal="showChangeEmailModal" />
|
||||
<change-email-modal v-model:show-modal="showChangeEmailModal"/>
|
||||
<change-phone-modal v-model:show-modal="showChangePhoneModal"/>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import InDev from "@/components/InDev.vue";
|
||||
import {useHead} from "@unhead/vue";
|
||||
import InDev from '@/components/InDev.vue'
|
||||
import { useHead } from '@unhead/vue'
|
||||
|
||||
useHead({
|
||||
title: "剧本"
|
||||
title: '剧本',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card>
|
||||
<template #header>
|
||||
<n-h3 prefix="bar" style="margin: 0;">个人剧本库</n-h3>
|
||||
<n-h3 prefix="bar" style="margin: 0">个人剧本库</n-h3>
|
||||
</template>
|
||||
|
||||
<in-dev/>
|
||||
<in-dev />
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import {useNowUser} from "@/stores/now-user.js";
|
||||
import UserPasswordModal from "@/components/admin/UserPasswordModal.vue";
|
||||
import {h, ref} from "vue";
|
||||
import {api} from "@/tools/web.ts";
|
||||
import {type DataTableColumn, NTag, NText} from "naive-ui";
|
||||
import InDev from "@/components/InDev.vue";
|
||||
import {useHead} from "@unhead/vue";
|
||||
import { useNowUser } from '@/stores/now-user.js'
|
||||
import UserPasswordModal from '@/components/admin/UserPasswordModal.vue'
|
||||
import { h, ref } from 'vue'
|
||||
import { api } from '@/tools/web.ts'
|
||||
import { type DataTableColumn, NTag, NText } from 'naive-ui'
|
||||
import InDev from '@/components/InDev.vue'
|
||||
import { useHead } from '@unhead/vue'
|
||||
|
||||
useHead({
|
||||
title: "用户安全"
|
||||
title: '用户安全',
|
||||
})
|
||||
|
||||
const NOWUSER = useNowUser()
|
||||
@@ -16,8 +16,8 @@ const NOWUSER = useNowUser()
|
||||
const showPasswordModal = ref(false)
|
||||
|
||||
interface SecureChange {
|
||||
created_at: number;
|
||||
type: "login" | "change_password" | "change_email" | "change_phone"
|
||||
created_at: number
|
||||
type: 'login' | 'change_password' | 'change_email' | 'change_phone'
|
||||
old: string | null
|
||||
new: string | null
|
||||
}
|
||||
@@ -26,56 +26,53 @@ const secureChanges = ref<SecureChange[]>([])
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "记录时间",
|
||||
key: "created_at",
|
||||
title: '记录时间',
|
||||
key: 'created_at',
|
||||
render(row) {
|
||||
const date = new Date(row.created_at * 1000)
|
||||
return h(
|
||||
NText,
|
||||
{},
|
||||
{default: () => date.toLocaleString()}
|
||||
)
|
||||
}
|
||||
return h(NText, {}, { default: () => date.toLocaleString() })
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
key: "type",
|
||||
title: '类型',
|
||||
key: 'type',
|
||||
render: (row) => {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
type: "info"
|
||||
type: 'info',
|
||||
},
|
||||
{default: () => row.type}
|
||||
{ default: () => row.type },
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "事件之前",
|
||||
key: "old",
|
||||
title: '事件之前',
|
||||
key: 'old',
|
||||
},
|
||||
{
|
||||
title: "事件之后",
|
||||
key: "new",
|
||||
}
|
||||
title: '事件之后',
|
||||
key: 'new',
|
||||
},
|
||||
] as DataTableColumn<SecureChange>[]
|
||||
|
||||
function loadSecureChanges() {
|
||||
api.get("/admin/me/secure_changes/")
|
||||
.then(res => secureChanges.value = res.data as SecureChange[])
|
||||
api
|
||||
.get('/admin/me/secure_changes/')
|
||||
.then((res) => (secureChanges.value = res.data as SecureChange[]))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card>
|
||||
<template #header>
|
||||
<n-h3 prefix="bar" style="margin: 0;">密码</n-h3>
|
||||
<n-h3 prefix="bar" style="margin: 0">密码</n-h3>
|
||||
</template>
|
||||
|
||||
<n-flex vertical>
|
||||
<n-alert type="warning" v-if="NOWUSER.id === 1">
|
||||
您正在使用 NyaHome 初始化时创建的管理员账号,此账号的默认密码为 admin。
|
||||
<strong>您应该及时修改默认密码。</strong><br/>
|
||||
<strong>您应该及时修改默认密码。</strong><br />
|
||||
如果您已修改密码,请忽略。
|
||||
</n-alert>
|
||||
<n-flex>
|
||||
@@ -84,27 +81,27 @@ function loadSecureChanges() {
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<user-password-modal v-model:show-modal="showPasswordModal"/>
|
||||
<user-password-modal v-model:show-modal="showPasswordModal" />
|
||||
</n-card>
|
||||
|
||||
<n-card>
|
||||
<template #header>
|
||||
<n-h3 prefix="bar" style="margin: 0;">其他登录方式</n-h3>
|
||||
<n-h3 prefix="bar" style="margin: 0">其他登录方式</n-h3>
|
||||
</template>
|
||||
|
||||
<n-flex vertical>
|
||||
<n-alert type="info">
|
||||
在这里连接第三方账户之后,可以使用它们进行登录。<br/>
|
||||
在这里连接第三方账户之后,可以使用它们进行登录。<br />
|
||||
<strong>必须先在这里连接后才能使用第三方账户进行登录。</strong>
|
||||
</n-alert>
|
||||
|
||||
<in-dev/>
|
||||
<in-dev />
|
||||
</n-flex>
|
||||
</n-card>
|
||||
|
||||
<n-card>
|
||||
<template #header>
|
||||
<n-h3 prefix="bar" style="margin: 0;">两步验证</n-h3>
|
||||
<n-h3 prefix="bar" style="margin: 0">两步验证</n-h3>
|
||||
</template>
|
||||
|
||||
<n-flex vertical>
|
||||
@@ -112,21 +109,19 @@ function loadSecureChanges() {
|
||||
启用两步验证可以更好地保护您的账户,这会强制此账号在登录时进行额外验证。
|
||||
</n-alert>
|
||||
|
||||
<in-dev/>
|
||||
<in-dev />
|
||||
</n-flex>
|
||||
</n-card>
|
||||
|
||||
<n-card>
|
||||
<template #header>
|
||||
<n-h3 prefix="bar" style="margin: 0;">安全事件记录</n-h3>
|
||||
<n-h3 prefix="bar" style="margin: 0">安全事件记录</n-h3>
|
||||
</template>
|
||||
<n-flex vertical>
|
||||
<n-data-table :columns :data="secureChanges"/>
|
||||
<n-data-table :columns :data="secureChanges" />
|
||||
<n-button secondary type="warning" @click="loadSecureChanges()">查询(更新)</n-button>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,62 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, watch} from "vue";
|
||||
import UploadFileModal from "@/components/file/UploadFileModal.vue";
|
||||
import {api} from "@/tools/web.js";
|
||||
import type {UploadFileDto} from "@/types/user.js";
|
||||
import {useNowUser} from "@/stores/now-user.js";
|
||||
import {uploadFilesCom} from "@/components/file/upload-files.js";
|
||||
import {useHead} from "@unhead/vue";
|
||||
import { ref, watch } from 'vue'
|
||||
import UploadFileModal from '@/components/file/UploadFileModal.vue'
|
||||
import { api } from '@/tools/web.js'
|
||||
import type { UploadFileDto } from '@/types/user.js'
|
||||
import { useNowUser } from '@/stores/now-user.js'
|
||||
import { uploadFilesCom } from '@/components/file/upload-files.js'
|
||||
import { useHead } from '@unhead/vue'
|
||||
|
||||
useHead({
|
||||
title: "上传"
|
||||
title: '上传',
|
||||
})
|
||||
|
||||
const NOWUSER = useNowUser();
|
||||
const NOWUSER = useNowUser()
|
||||
|
||||
const showUploadModal = ref(false)
|
||||
|
||||
const files = ref<UploadFileDto[]>([])
|
||||
|
||||
function load() {
|
||||
api.get("/file/")
|
||||
.then(res => {
|
||||
files.value = res.data as UploadFileDto[]
|
||||
})
|
||||
api.get('/file/').then((res) => {
|
||||
files.value = res.data as UploadFileDto[]
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => NOWUSER.isLogin, () => {
|
||||
load()
|
||||
}, {immediate: true})
|
||||
watch(
|
||||
() => NOWUSER.isLogin,
|
||||
() => {
|
||||
load()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card>
|
||||
<template #header>
|
||||
<n-h3 prefix="bar" style="margin: 0;">上传文件</n-h3>
|
||||
<n-h3 prefix="bar" style="margin: 0">上传文件</n-h3>
|
||||
</template>
|
||||
|
||||
<n-flex vertical>
|
||||
<n-alert type="info">
|
||||
接受的文件类型:
|
||||
</n-alert>
|
||||
<n-alert type="info"> 接受的文件类型: </n-alert>
|
||||
<n-button @click="showUploadModal = true">打开上传向导</n-button>
|
||||
|
||||
<upload-file-modal v-model:show-modal="showUploadModal" :after-leave="() => {load()}"/>
|
||||
<upload-file-modal
|
||||
v-model:show-modal="showUploadModal"
|
||||
:after-leave="
|
||||
() => {
|
||||
load()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
|
||||
<n-card>
|
||||
<template #header>
|
||||
<n-h3 prefix="bar" style="margin: 0;">个人文件库</n-h3>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
您已经上传的文件都在这里,可以选择性地删除以及重新下载。
|
||||
<n-h3 prefix="bar" style="margin: 0">个人文件库</n-h3>
|
||||
</template>
|
||||
<template #header-extra> 您已经上传的文件都在这里,可以选择性地删除以及重新下载。 </template>
|
||||
|
||||
<component :is="uploadFilesCom(files)"/>
|
||||
<component :is="uploadFilesCom(files)" />
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
+29
-23
@@ -1,14 +1,15 @@
|
||||
import {createRouter, createWebHashHistory} from 'vue-router'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import ChatroomPage from '@/pages/ChatroomPage.vue'
|
||||
import WelcomePage from '@/pages/WelcomePage.vue'
|
||||
import Chatroom1Page from "@/pages/Chatroom1Page.vue";
|
||||
import AdminPage from "@/pages/AdminPage.vue";
|
||||
import AdminOverview from "@/pages/admin/AdminOverview.vue";
|
||||
import AdminUserInfo from "@/pages/admin/AdminUserInfo.vue";
|
||||
import AdminUserSecurity from "@/pages/admin/AdminUserSecurity.vue";
|
||||
import AdminUserUpload from "@/pages/admin/AdminUserUpload.vue";
|
||||
import AdminNyahome from "@/pages/admin/AdminNyahome.vue";
|
||||
import AdminUserScript from "@/pages/admin/AdminUserScript.vue";
|
||||
import Chatroom1Page from '@/pages/Chatroom1Page.vue'
|
||||
import AdminPage from '@/pages/AdminPage.vue'
|
||||
import AdminOverview from '@/pages/admin/AdminOverview.vue'
|
||||
import AdminUserInfo from '@/pages/admin/AdminUserInfo.vue'
|
||||
import AdminUserSecurity from '@/pages/admin/AdminUserSecurity.vue'
|
||||
import AdminUserUpload from '@/pages/admin/AdminUserUpload.vue'
|
||||
import AdminNyahome from '@/pages/admin/AdminNyahome.vue'
|
||||
import AdminUserScript from '@/pages/admin/AdminUserScript.vue'
|
||||
import Marketplace from '@/pages/Marketplace.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
@@ -28,42 +29,47 @@ const router = createRouter({
|
||||
path: '/chatroom',
|
||||
component: ChatroomPage,
|
||||
},
|
||||
{
|
||||
name: 'marketplace',
|
||||
path: '/marketplace',
|
||||
component: Marketplace,
|
||||
},
|
||||
{
|
||||
name: 'admin',
|
||||
path: '/admin/',
|
||||
component: AdminPage,
|
||||
children: [
|
||||
{
|
||||
name: "admin-overview",
|
||||
path: "",
|
||||
name: 'admin-overview',
|
||||
path: '',
|
||||
component: AdminOverview,
|
||||
},
|
||||
{
|
||||
name: "admin-user-info",
|
||||
path: "user-info",
|
||||
name: 'admin-user-info',
|
||||
path: 'user-info',
|
||||
component: AdminUserInfo,
|
||||
},
|
||||
{
|
||||
name: "admin-user-security",
|
||||
path: "user-security",
|
||||
name: 'admin-user-security',
|
||||
path: 'user-security',
|
||||
component: AdminUserSecurity,
|
||||
},
|
||||
{
|
||||
name: "admin-user-upload",
|
||||
path: "user-upload",
|
||||
name: 'admin-user-upload',
|
||||
path: 'user-upload',
|
||||
component: AdminUserUpload,
|
||||
},
|
||||
{
|
||||
name: "admin-user-script",
|
||||
path: "user-script",
|
||||
name: 'admin-user-script',
|
||||
path: 'user-script',
|
||||
component: AdminUserScript,
|
||||
},
|
||||
{
|
||||
name: "admin-nyahome",
|
||||
path: "nyahome",
|
||||
name: 'admin-nyahome',
|
||||
path: 'nyahome',
|
||||
component: AdminNyahome,
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {ref} from 'vue'
|
||||
import {api, setApiToken} from '@/tools/web.ts'
|
||||
import type {UserDto} from '@/types/user.ts'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { api, setApiToken } from '@/tools/web.ts'
|
||||
import type { UserDto } from '@/types/user.ts'
|
||||
|
||||
export const useNowUser = defineStore('now-user', () => {
|
||||
const isLogin = ref(false)
|
||||
@@ -22,9 +22,7 @@ export const useNowUser = defineStore('now-user', () => {
|
||||
|
||||
let user: UserDto
|
||||
try {
|
||||
user = await api
|
||||
.get('/admin/me/')
|
||||
.then((res) => res.data as UserDto)
|
||||
user = await api.get('/admin/me/').then((res) => res.data as UserDto)
|
||||
} catch (err) {
|
||||
console.error(`请求用户信息时失败:${err}`)
|
||||
throw err
|
||||
|
||||
@@ -13,13 +13,10 @@
|
||||
// Bundler mode provides a smoother developer experience.
|
||||
"module": "preserve",
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
// Include Node.js types and avoid accidentally including other `@types/*` packages.
|
||||
"types": ["node"],
|
||||
|
||||
// Disable emitting output during `vue-tsc --build`, which is used for type-checking only.
|
||||
"noEmit": true,
|
||||
|
||||
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
|
||||
// Specified here to keep it out of the root directory.
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
|
||||
|
||||
+11
-16
@@ -4,15 +4,15 @@ import {defineConfig} from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import {readFileSync} from "node:fs";
|
||||
import {resolve} from "path";
|
||||
import {readFileSync} from 'node:fs'
|
||||
import {resolve} from 'path'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import {NaiveUiResolver} from 'unplugin-vue-components/resolvers'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import {unheadVueComposablesImports} from "@unhead/vue";
|
||||
import {unheadVueComposablesImports} from '@unhead/vue'
|
||||
|
||||
// 从 package.json 里搞到 WebUI 版本号
|
||||
const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8'));
|
||||
const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8'))
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
@@ -24,22 +24,17 @@ export default defineConfig({
|
||||
imports: [
|
||||
'vue',
|
||||
{
|
||||
'naive-ui': [
|
||||
'useDialog',
|
||||
'useMessage',
|
||||
'useNotification',
|
||||
'useLoadingBar'
|
||||
]
|
||||
'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar'],
|
||||
},
|
||||
unheadVueComposablesImports,
|
||||
]
|
||||
],
|
||||
}),
|
||||
Components({
|
||||
resolvers: [NaiveUiResolver()]
|
||||
})
|
||||
resolvers: [NaiveUiResolver()],
|
||||
}),
|
||||
],
|
||||
define: {
|
||||
__VERSION__: JSON.stringify(pkg.version)
|
||||
__VERSION__: JSON.stringify(pkg.version),
|
||||
},
|
||||
build: {
|
||||
rolldownOptions: {
|
||||
@@ -47,11 +42,11 @@ export default defineConfig({
|
||||
index: resolve(__dirname, 'index.html'),
|
||||
},
|
||||
},
|
||||
outDir: "../src/nyahome/static"
|
||||
outDir: '../src/nyahome/static',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
|
||||
Reference in New Issue
Block a user