Skip to content

Commit 84f2ae0

Browse files
author
touale
committed
feat: customize Swagger UI and move plugin description
1 parent c74ac4a commit 84f2ae0

5 files changed

Lines changed: 293 additions & 21 deletions

File tree

src/framex/driver/application.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import pytz
1212
from fastapi import Depends, FastAPI
13-
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
13+
from fastapi.openapi.docs import get_redoc_html
1414
from fastapi.openapi.utils import get_openapi
1515
from fastapi.responses import HTMLResponse
1616
from starlette import status
@@ -24,7 +24,7 @@
2424
from framex.config import settings
2525
from framex.consts import API_PRE_STR, DOCS_URL, OPENAPI_URL, PROJECT_NAME, REDOC_URL, VERSION
2626
from framex.driver.auth import authenticate, oauth_callback
27-
from framex.utils import format_uptime, safe_error_message
27+
from framex.utils import build_swagger_ui_html, format_uptime, safe_error_message
2828

2929
FRAME_START_TIME = datetime.now(tz=UTC)
3030
SHANGHAI_TZ = ZoneInfo("Asia/Shanghai")
@@ -107,7 +107,7 @@ async def _on_start(deployment: Any) -> None:
107107

108108
@application.get(DOCS_URL, include_in_schema=False)
109109
async def get_documentation(_: Annotated[str, Depends(authenticate)]) -> HTMLResponse:
110-
return get_swagger_ui_html(openapi_url=OPENAPI_URL, title="FrameX Docs")
110+
return build_swagger_ui_html(openapi_url=OPENAPI_URL, title="FrameX Docs")
111111

112112
@application.get(REDOC_URL, include_in_schema=False)
113113
async def get_redoc_documentation(_: Annotated[str, Depends(authenticate)]) -> HTMLResponse:

src/framex/plugin/base.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,3 @@ async def _call_remote_api(self, api_name: str, **kwargs: Any) -> Any:
7373

7474
def _post_call_remote_api_hook(self, data: Any) -> Any:
7575
return data
76-
77-
78-
def build_plugin_description(
79-
author: str,
80-
version: str,
81-
description: str,
82-
repo: str,
83-
) -> str:
84-
return (
85-
f"**Author**: {author}\n\n"
86-
f"**Version**: {version}\n\n"
87-
f"**Description**: {description}\n\n"
88-
f"**Repo**: [{repo}]({repo})"
89-
)

src/framex/plugin/on.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@
99

1010
from framex.adapter import get_adapter
1111
from framex.consts import API_STR, PROXY_PLUGIN_NAME
12-
from framex.plugin.base import build_plugin_description
1312
from framex.plugin.model import ApiType, PluginApi, PluginDeployment
14-
from framex.utils import cache_decode, cache_encode, extract_method_params, plugin_to_deployment_name
13+
from framex.utils import (
14+
build_plugin_description,
15+
cache_decode,
16+
cache_encode,
17+
extract_method_params,
18+
plugin_to_deployment_name,
19+
)
1520

1621
from . import _current_plugin, call_plugin_api
1722

src/framex/plugins/proxy/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from framex.consts import BACKEND_NAME, PROXY_FUNC_HTTP_PATH, PROXY_PLUGIN_NAME
1414
from framex.log import logger
1515
from framex.plugin import BasePlugin, PluginApi, PluginMetadata, on_register
16-
from framex.plugin.base import build_plugin_description
1716
from framex.plugin.model import ApiType
1817
from framex.plugin.on import on_request
1918
from framex.plugins.proxy.builder import (
@@ -26,7 +25,7 @@
2625
)
2726
from framex.plugins.proxy.config import VERSION, ProxyPluginConfig, settings
2827
from framex.plugins.proxy.model import ProxyFunc, ProxyFuncHttpBody
29-
from framex.utils import cache_decode, cache_encode, shorten_str
28+
from framex.utils import build_plugin_description, cache_decode, cache_encode, shorten_str
3029

3130
__plugin_meta__ = PluginMetadata(
3231
name="proxy",

src/framex/utils.py

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from pathlib import Path
1212
from typing import Any
1313

14+
from fastapi.responses import HTMLResponse
1415
from pydantic import BaseModel
1516

1617

@@ -160,3 +161,284 @@ def safe_error_message(e: Exception) -> str:
160161

161162
def shorten_str(data: str, max_len: int = 45) -> str:
162163
return data if len(data) <= max_len else data[: max_len - 3] + "..."
164+
165+
166+
def build_swagger_ui_html(openapi_url: str, title: str) -> HTMLResponse:
167+
return HTMLResponse(
168+
f"""
169+
<!DOCTYPE html>
170+
<html lang="zh-CN">
171+
<head>
172+
<meta charset="UTF-8" />
173+
<meta name="viewport" content="width=device-width, initial-scale=1" />
174+
<title>{title}</title>
175+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist/swagger-ui.css" />
176+
<style>
177+
:root {{
178+
--fx-bg: #f6f8fb;
179+
--fx-card: #ffffff;
180+
--fx-border: #e6eaf0;
181+
--fx-border-soft: #eef2f6;
182+
--fx-text: #1f2a37;
183+
--fx-text-soft: #475467;
184+
--fx-text-muted: #98a2b3;
185+
--fx-link: #175cd3;
186+
--fx-link-hover: #1849a9;
187+
--fx-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
188+
}}
189+
190+
html, body {{
191+
margin: 0;
192+
padding: 0;
193+
background: var(--fx-bg);
194+
}}
195+
196+
body {{
197+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
198+
"Segoe UI", sans-serif;
199+
color: var(--fx-text);
200+
}}
201+
202+
.swagger-ui {{
203+
max-width: 1600px;
204+
margin: 0 auto;
205+
padding: 16px 12px 28px;
206+
}}
207+
208+
.swagger-ui .topbar {{
209+
display: none;
210+
}}
211+
212+
.swagger-ui .information-container {{
213+
padding-bottom: 4px;
214+
}}
215+
216+
.swagger-ui .info {{
217+
margin: 0 0 14px 0;
218+
}}
219+
220+
.swagger-ui .info .title {{
221+
color: var(--fx-text);
222+
font-size: 26px;
223+
font-weight: 700;
224+
letter-spacing: 0.2px;
225+
}}
226+
227+
.swagger-ui .scheme-container {{
228+
background: transparent;
229+
box-shadow: none;
230+
padding: 0;
231+
margin: 0 0 8px 0;
232+
}}
233+
234+
.swagger-ui .opblock-tag-section {{
235+
margin-bottom: 8px;
236+
border: 1px solid var(--fx-border);
237+
border-radius: 12px;
238+
overflow: hidden;
239+
background: var(--fx-card);
240+
box-shadow: var(--fx-shadow);
241+
}}
242+
243+
/* 外层三列: tag | description | arrow */
244+
.swagger-ui .opblock-tag {{
245+
display: grid !important;
246+
grid-template-columns: 420px minmax(0, 1fr) 28px;
247+
column-gap: 18px;
248+
align-items: center;
249+
padding: 9px 14px !important;
250+
margin: 0 !important;
251+
background: #fff;
252+
border-bottom: 1px solid var(--fx-border-soft);
253+
}}
254+
255+
.swagger-ui .opblock-tag:hover {{
256+
background: #fafbfc;
257+
}}
258+
259+
/* tag 标题 */
260+
.swagger-ui .opblock-tag .nostyle {{
261+
grid-column: 1;
262+
min-width: 0;
263+
margin: 0;
264+
white-space: normal !important;
265+
word-break: break-word;
266+
overflow-wrap: anywhere;
267+
line-height: 1.3;
268+
font-size: 15px !important;
269+
font-weight: 700 !important;
270+
color: var(--fx-text) !important;
271+
}}
272+
273+
/* description 容器 */
274+
.swagger-ui .opblock-tag small {{
275+
grid-column: 2;
276+
display: block !important;
277+
min-width: 0;
278+
margin: 0 !important;
279+
padding: 0 !important;
280+
white-space: normal !important;
281+
color: var(--fx-text-soft) !important;
282+
font-size: 12.5px !important;
283+
line-height: 1.45 !important;
284+
}}
285+
286+
.swagger-ui .opblock-tag small .markdown {{
287+
margin: 0 !important;
288+
padding: 0 !important;
289+
}}
290+
291+
.swagger-ui .opblock-tag small .markdown p {{
292+
margin: 0 !important;
293+
padding: 0 !important;
294+
}}
295+
296+
/* 第一行 description */
297+
.swagger-ui .opblock-tag small .markdown p:first-child {{
298+
margin-bottom: 3px !important;
299+
color: var(--fx-text) !important;
300+
font-size: 13.5px !important;
301+
line-height: 1.5 !important;
302+
letter-spacing: 0.05px;
303+
max-width: 760px;
304+
}}
305+
306+
.swagger-ui .opblock-tag small .markdown p:first-child strong {{
307+
font-weight: 600 !important;
308+
color: var(--fx-text) !important;
309+
}}
310+
311+
/* 第二行: 作者、版本、Repo */
312+
.swagger-ui .opblock-tag small .markdown p:last-child {{
313+
color: var(--fx-text-soft) !important;
314+
font-size: 12px !important;
315+
line-height: 1.4 !important;
316+
}}
317+
318+
/* Repo 链接 */
319+
.swagger-ui .opblock-tag small a {{
320+
color: var(--fx-link);
321+
text-decoration: none;
322+
font-weight: 500;
323+
}}
324+
325+
.swagger-ui .opblock-tag small a:hover {{
326+
color: var(--fx-link-hover);
327+
text-decoration: underline;
328+
}}
329+
330+
/* 右侧展开箭头 */
331+
.swagger-ui .opblock-tag > button {{
332+
grid-column: 3 !important;
333+
justify-self: end !important;
334+
align-self: center !important;
335+
margin: 0 !important;
336+
padding: 0 !important;
337+
background: transparent !important;
338+
border: none !important;
339+
box-shadow: none !important;
340+
width: 24px;
341+
height: 24px;
342+
}}
343+
344+
.swagger-ui .opblock-tag > button svg {{
345+
display: block !important;
346+
width: 20px !important;
347+
height: 20px !important;
348+
opacity: 0.75;
349+
transition: all 0.15s ease;
350+
}}
351+
352+
.swagger-ui .opblock-tag:hover > button svg {{
353+
opacity: 1;
354+
transform: translateX(2px);
355+
}}
356+
357+
.swagger-ui .opblock {{
358+
margin: 0 !important;
359+
border-radius: 0 !important;
360+
box-shadow: none !important;
361+
}}
362+
363+
.swagger-ui .opblock:last-child {{
364+
border-bottom: none !important;
365+
}}
366+
367+
.swagger-ui .opblock-summary-path,
368+
.swagger-ui .opblock-summary-description {{
369+
white-space: normal !important;
370+
word-break: break-word;
371+
}}
372+
373+
@media (max-width: 1400px) {{
374+
.swagger-ui .opblock-tag {{
375+
grid-template-columns: 360px minmax(0, 1fr) 28px;
376+
}}
377+
378+
.swagger-ui .opblock-tag small .markdown p:first-child {{
379+
max-width: 680px;
380+
}}
381+
}}
382+
383+
@media (max-width: 1100px) {{
384+
.swagger-ui .opblock-tag {{
385+
grid-template-columns: 300px minmax(0, 1fr) 28px;
386+
column-gap: 14px;
387+
}}
388+
389+
.swagger-ui .opblock-tag small .markdown p:first-child {{
390+
max-width: none;
391+
}}
392+
}}
393+
394+
@media (max-width: 900px) {{
395+
.swagger-ui .opblock-tag {{
396+
grid-template-columns: 1fr;
397+
row-gap: 6px;
398+
}}
399+
400+
.swagger-ui .opblock-tag .nostyle,
401+
.swagger-ui .opblock-tag small,
402+
.swagger-ui .opblock-tag > button {{
403+
grid-column: auto !important;
404+
}}
405+
406+
.swagger-ui .opblock-tag > button {{
407+
justify-self: end !important;
408+
}}
409+
}}
410+
</style>
411+
</head>
412+
<body>
413+
<div id="swagger-ui"></div>
414+
415+
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist/swagger-ui-bundle.js"></script>
416+
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist/swagger-ui-standalone-preset.js"></script>
417+
<script>
418+
window.ui = SwaggerUIBundle({{
419+
url: "{openapi_url}",
420+
dom_id: "#swagger-ui",
421+
deepLinking: true,
422+
docExpansion: "none",
423+
defaultModelsExpandDepth: -1,
424+
displayRequestDuration: true,
425+
presets: [
426+
SwaggerUIBundle.presets.apis,
427+
SwaggerUIStandalonePreset
428+
],
429+
layout: "BaseLayout"
430+
}});
431+
</script>
432+
</body>
433+
</html>
434+
"""
435+
) # roqa
436+
437+
438+
def build_plugin_description(
439+
author: str,
440+
version: str,
441+
description: str,
442+
repo: str,
443+
) -> str:
444+
return f"**{description}**\n\n\n👤 {author} · 🧩 {version} · [🔗 Repo]({repo})"

0 commit comments

Comments
 (0)