From ec9d373ab8770cb53a5f3c7b051cb3f6bc05acf8 Mon Sep 17 00:00:00 2001 From: inliver233 Date: Wed, 25 Feb 2026 02:13:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0wtf=E9=A1=B6=E9=83=A8?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E5=B8=83=E5=B1=80|=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=A0=87=E7=AD=BE=E6=8E=92=E9=99=A4=E7=AD=9B=E9=80=89|?= =?UTF-8?q?=E6=94=AF=E6=8C=81proxy=E5=8F=82=E6=95=B0=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E5=8F=8D=E4=BB=A3|=E5=A2=9E=E5=8A=A0=E7=AE=80?= =?UTF-8?q?=E5=8D=95=E7=9A=84=E6=A6=82=E8=A6=81=E6=A0=87=E7=AD=BE|?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=80=91=E5=B8=83=E6=B5=81=E9=80=89=E4=B8=AD?= =?UTF-8?q?=E5=8F=8D=E9=A6=88=E4=B8=8E=E5=8D=95=E5=87=BB=E6=94=BE=E5=A4=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/admin/settings.py | 29 +- backend/app/api/public/docs_page.py | 22 +- backend/app/api/public/images.py | 19 +- backend/app/api/public/legacy.py | 39 +- backend/app/api/public/random.py | 31 +- backend/app/api/public/wtf_page.py | 918 ++++++++++++++++++++++- backend/app/core/pximg_reverse_proxy.py | 90 ++- backend/app/core/runtime_settings.py | 19 +- frontend/src/pages/SettingsPage.test.tsx | 7 +- frontend/src/pages/SettingsPage.tsx | 10 + 10 files changed, 1130 insertions(+), 54 deletions(-) diff --git a/backend/app/api/admin/settings.py b/backend/app/api/admin/settings.py index 29e15f6..b9c3265 100644 --- a/backend/app/api/admin/settings.py +++ b/backend/app/api/admin/settings.py @@ -12,7 +12,11 @@ from app.core.runtime_settings import ( runtime_config_from_values, set_runtime_setting, ) -from app.core.pximg_reverse_proxy import DEFAULT_PXIMG_MIRROR_HOST, normalize_pximg_mirror_host +from app.core.pximg_reverse_proxy import ( + DEFAULT_PXIMG_MIRROR_HOST, + normalize_pximg_custom_mirror_host, + normalize_pximg_mirror_host, +) router = APIRouter() @@ -255,6 +259,7 @@ async def get_settings( "image_proxy": { "use_pixiv_cat": bool(runtime.image_proxy_use_pixiv_cat), "pximg_mirror_host": str(runtime.image_proxy_pximg_mirror_host or DEFAULT_PXIMG_MIRROR_HOST), + "extra_pximg_mirror_hosts": list(getattr(runtime, "image_proxy_extra_pximg_mirror_hosts", []) or []), }, "random": random_defaults, "security": {"hide_origin_url_in_public_json": bool(runtime.hide_origin_url_in_public_json)}, @@ -371,6 +376,28 @@ async def update_settings( raise ApiError(code=ErrorCode.BAD_REQUEST, message="Invalid image_proxy.pximg_mirror_host", status_code=400) updates.append(("image_proxy.pximg_mirror_host", str(host))) + if "extra_pximg_mirror_hosts" in image_proxy: + raw = image_proxy.get("extra_pximg_mirror_hosts") + if raw is None: + updates.append(("image_proxy.extra_pximg_mirror_hosts", [])) + else: + if not isinstance(raw, list): + raise ApiError(code=ErrorCode.BAD_REQUEST, message="Invalid image_proxy.extra_pximg_mirror_hosts", status_code=400) + candidates = _as_str_list(raw) + if len(candidates) > 200: + raise ApiError(code=ErrorCode.BAD_REQUEST, message="Invalid image_proxy.extra_pximg_mirror_hosts", status_code=400) + normalized: list[str] = [] + seen: set[str] = set() + for item in candidates: + host = normalize_pximg_custom_mirror_host(item) + if host is None: + raise ApiError(code=ErrorCode.BAD_REQUEST, message="Invalid image_proxy.extra_pximg_mirror_hosts", status_code=400) + if host in seen: + continue + seen.add(host) + normalized.append(host) + updates.append(("image_proxy.extra_pximg_mirror_hosts", normalized)) + random = body.get("random") if random is not None: if not isinstance(random, dict): diff --git a/backend/app/api/public/docs_page.py b/backend/app/api/public/docs_page.py index 36ee5a3..e20927c 100644 --- a/backend/app/api/public/docs_page.py +++ b/backend/app/api/public/docs_page.py @@ -19,6 +19,7 @@ def _build_docs_html(*, base_url: str) -> str: examples = { "img_default": u("/random"), + "img_proxy_cat": u("/random?proxy=i-pixiv-cat"), "img_pixiv_cat": u("/random?pixiv_cat=1"), "img_pixiv_re": u("/random?pixiv_cat=1&pximg_mirror_host=re"), "img_redirect": u("/random?redirect=1"), @@ -303,7 +304,8 @@ def _build_docs_html(*, base_url: str) -> str:

/status 为公开仪表盘:展示 API 状态、图库概览、/random 请求统计;/status.json 为机器可读 JSON。

{examples["status"]}
 {examples["status_json"]}
-

/wtf 为瀑布流:参数与 /random 完全一致(例如 r18/标签/分辨率/热度等),页面内部会自动加上 adaptive=1 与 redirect=1 来做屏幕适配与稳定链接。

+

/wtf 为瀑布流:支持 /random 的全部过滤参数(例如 r18/标签/分辨率/热度/排除标签等),默认会补上 adaptive=1 以更适合不同屏幕。

+

/wtf 额外支持布局参数:view=single|masonry|tiles,以及 wtf_mcols/wtf_mgap/wtf_tcols/wtf_tgap/wtf_tratio(仅影响页面布局)。另有 wtf_gender=girls|boys 与概要标签 @male/@female(仅 /wtf 端展开,用于快速“只看女生/只看男生”)。

{examples["wtf"]}
 {examples["wtf_r18"]}
@@ -332,6 +334,15 @@ def _build_docs_html(*, base_url: str) -> str: quality 会先抽样 N 张候选再按热度/分辨率打分选图。 + + proxy + + 指定本次请求的图片上游镜像(优先级最高)。会隐式开启第三方镜像(等价于 pixiv_cat=1),并覆盖地区自动选择。
+ 内置值:cat/re/nl,也支持 pixiv-cat/i-pixiv-cat 等写法。
+ 自定义镜像:可填写你自建镜像域名(需在管理端“自定义镜像白名单”允许)。
+ 示例:{examples["img_proxy_cat"]} + + pixiv_cat @@ -417,6 +428,15 @@ def _build_docs_html(*, base_url: str) -> str:

示例:R18 + 插画 + 竖图 + 250万像素以上 + 收藏≥2 + 评论≥5 + 标签包含 loli(直接出图)。

{examples["complex"]}
+ +
+

6) 管理端导入(可选)

+

管理端支持从文件批量导入图片链接:.txt(每行一个 URL)或 PixivBatchDownloader 导出的 .json。

+ +
diff --git a/backend/app/api/public/images.py b/backend/app/api/public/images.py index 0b483c0..48f4700 100644 --- a/backend/app/api/public/images.py +++ b/backend/app/api/public/images.py @@ -14,6 +14,7 @@ import sqlalchemy as sa from app.core.pixiv_urls import ALLOWED_IMAGE_EXTS from app.core.pximg_reverse_proxy import ( normalize_pximg_mirror_host, + normalize_pximg_proxy, pick_pximg_mirror_host_for_request, rewrite_pximg_to_mirror, ) @@ -277,6 +278,7 @@ async def proxy_image( background_tasks: BackgroundTasks, pixiv_cat: int = 0, pximg_mirror_host: str | None = None, + proxy: str | None = None, ): ext = (ext or "").lower() if ext not in ALLOWED_IMAGE_EXTS: @@ -284,12 +286,12 @@ async def proxy_image( if pixiv_cat not in {0, 1}: raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported pixiv_cat", status_code=400) - mirror_host_override: str | None = None + pximg_mirror_host_override: str | None = None if pximg_mirror_host is not None: raw = str(pximg_mirror_host or "").strip() if raw: - mirror_host_override = normalize_pximg_mirror_host(raw) - if mirror_host_override is None: + pximg_mirror_host_override = normalize_pximg_mirror_host(raw) + if pximg_mirror_host_override is None: raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported pximg_mirror_host", status_code=400) engine = request.app.state.engine @@ -327,7 +329,16 @@ async def proxy_image( needs_hydrate = tag_row is None runtime = await load_runtime_config(engine) - use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1 + proxy_override: str | None = None + if proxy is not None: + raw = str(proxy or "").strip() + if raw: + proxy_override = normalize_pximg_proxy(raw, extra_hosts=runtime.image_proxy_extra_pximg_mirror_hosts) + if proxy_override is None: + raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported proxy", status_code=400) + + mirror_host_override = proxy_override or pximg_mirror_host_override + use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1 or proxy_override is not None runtime_mirror_host = str(getattr(runtime, "image_proxy_pximg_mirror_host", "") or "").strip() or "i.pixiv.cat" mirror_host = mirror_host_override or ( pick_pximg_mirror_host_for_request(headers=request.headers, fallback_host=runtime_mirror_host) diff --git a/backend/app/api/public/legacy.py b/backend/app/api/public/legacy.py index c2862cb..9f82bf9 100644 --- a/backend/app/api/public/legacy.py +++ b/backend/app/api/public/legacy.py @@ -7,6 +7,7 @@ from app.core.http_stream import stream_url from app.core.pixiv_urls import ALLOWED_IMAGE_EXTS from app.core.pximg_reverse_proxy import ( normalize_pximg_mirror_host, + normalize_pximg_proxy, pick_pximg_mirror_host_for_request, rewrite_pximg_to_mirror, ) @@ -26,6 +27,7 @@ async def legacy_multi( ext: str, pixiv_cat: int = 0, pximg_mirror_host: str | None = None, + proxy: str | None = None, ): if int(illust_id) <= 0: raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported illust_id", status_code=400) @@ -38,12 +40,12 @@ async def legacy_multi( if pixiv_cat not in {0, 1}: raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported pixiv_cat", status_code=400) - mirror_host_override: str | None = None + pximg_mirror_host_override: str | None = None if pximg_mirror_host is not None: raw = str(pximg_mirror_host or "").strip() if raw: - mirror_host_override = normalize_pximg_mirror_host(raw) - if mirror_host_override is None: + pximg_mirror_host_override = normalize_pximg_mirror_host(raw) + if pximg_mirror_host_override is None: raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported pximg_mirror_host", status_code=400) engine = request.app.state.engine @@ -55,7 +57,17 @@ async def legacy_multi( raise ApiError(code=ErrorCode.NOT_FOUND, message="Image not found", status_code=404) runtime = await load_runtime_config(engine) - use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1 + + proxy_override: str | None = None + if proxy is not None: + raw = str(proxy or "").strip() + if raw: + proxy_override = normalize_pximg_proxy(raw, extra_hosts=runtime.image_proxy_extra_pximg_mirror_hosts) + if proxy_override is None: + raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported proxy", status_code=400) + + mirror_host_override = proxy_override or pximg_mirror_host_override + use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1 or proxy_override is not None runtime_mirror_host = str(getattr(runtime, "image_proxy_pximg_mirror_host", "") or "").strip() or "i.pixiv.cat" mirror_host = mirror_host_override or ( pick_pximg_mirror_host_for_request(headers=request.headers, fallback_host=runtime_mirror_host) @@ -91,6 +103,7 @@ async def legacy_single( ext: str, pixiv_cat: int = 0, pximg_mirror_host: str | None = None, + proxy: str | None = None, ): if int(illust_id) <= 0: raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported illust_id", status_code=400) @@ -101,12 +114,12 @@ async def legacy_single( if pixiv_cat not in {0, 1}: raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported pixiv_cat", status_code=400) - mirror_host_override: str | None = None + pximg_mirror_host_override: str | None = None if pximg_mirror_host is not None: raw = str(pximg_mirror_host or "").strip() if raw: - mirror_host_override = normalize_pximg_mirror_host(raw) - if mirror_host_override is None: + pximg_mirror_host_override = normalize_pximg_mirror_host(raw) + if pximg_mirror_host_override is None: raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported pximg_mirror_host", status_code=400) engine = request.app.state.engine @@ -118,7 +131,17 @@ async def legacy_single( raise ApiError(code=ErrorCode.NOT_FOUND, message="Image not found", status_code=404) runtime = await load_runtime_config(engine) - use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1 + + proxy_override: str | None = None + if proxy is not None: + raw = str(proxy or "").strip() + if raw: + proxy_override = normalize_pximg_proxy(raw, extra_hosts=runtime.image_proxy_extra_pximg_mirror_hosts) + if proxy_override is None: + raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported proxy", status_code=400) + + mirror_host_override = proxy_override or pximg_mirror_host_override + use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1 or proxy_override is not None runtime_mirror_host = str(getattr(runtime, "image_proxy_pximg_mirror_host", "") or "").strip() or "i.pixiv.cat" mirror_host = mirror_host_override or ( pick_pximg_mirror_host_for_request(headers=request.headers, fallback_host=runtime_mirror_host) diff --git a/backend/app/api/public/random.py b/backend/app/api/public/random.py index 26efafe..36ef04f 100644 --- a/backend/app/api/public/random.py +++ b/backend/app/api/public/random.py @@ -15,6 +15,7 @@ from app.core.http_stream import stream_url from app.core.imgproxy import build_signed_processing_url, load_imgproxy_config_from_settings from app.core.pximg_reverse_proxy import ( normalize_pximg_mirror_host, + normalize_pximg_proxy, pick_pximg_mirror_host_for_request, rewrite_pximg_to_mirror, ) @@ -131,6 +132,7 @@ async def random_image( adaptive: int = 0, pixiv_cat: int = 0, pximg_mirror_host: str | None = None, + proxy: str | None = None, min_width: int = 0, min_height: int = 0, min_pixels: int = 0, @@ -185,12 +187,12 @@ async def random_image( if pixiv_cat not in {0, 1}: raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported pixiv_cat", status_code=400) - mirror_host_override: str | None = None + pximg_mirror_host_override: str | None = None if pximg_mirror_host is not None: raw = str(pximg_mirror_host or "").strip() if raw: - mirror_host_override = normalize_pximg_mirror_host(raw) - if mirror_host_override is None: + pximg_mirror_host_override = normalize_pximg_mirror_host(raw) + if pximg_mirror_host_override is None: raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported pximg_mirror_host", status_code=400) layout_source = "orientation" @@ -381,7 +383,17 @@ async def random_image( engine = request.app.state.engine Session = create_sessionmaker(engine) runtime = await load_runtime_config(engine) - use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1 + + proxy_override: str | None = None + if proxy is not None: + raw = str(proxy or "").strip() + if raw: + proxy_override = normalize_pximg_proxy(raw, extra_hosts=runtime.image_proxy_extra_pximg_mirror_hosts) + if proxy_override is None: + raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported proxy", status_code=400) + + mirror_host_override = proxy_override or pximg_mirror_host_override + use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1 or proxy_override is not None runtime_mirror_host = str(getattr(runtime, "image_proxy_pximg_mirror_host", "") or "").strip() or "i.pixiv.cat" mirror_host = mirror_host_override or ( pick_pximg_mirror_host_for_request(headers=request.headers, fallback_host=runtime_mirror_host) @@ -757,10 +769,13 @@ async def random_image( if format == "image" and redirect == 1: qp: list[tuple[str, str]] = [] - if int(pixiv_cat) == 1: - qp.append(("pixiv_cat", "1")) - if mirror_host_override is not None: - qp.append(("pximg_mirror_host", str(mirror_host_override))) + if proxy_override is not None: + qp.append(("proxy", str(proxy_override))) + else: + if int(pixiv_cat) == 1: + qp.append(("pixiv_cat", "1")) + if pximg_mirror_host_override is not None: + qp.append(("pximg_mirror_host", str(pximg_mirror_host_override))) qs = ("?" + "&".join([f"{k}={v}" for k, v in qp])) if qp else "" resp = RedirectResponse( url=f"/i/{image.id}.{image.ext}{qs}", diff --git a/backend/app/api/public/wtf_page.py b/backend/app/api/public/wtf_page.py index a3230cf..62a9dc9 100644 --- a/backend/app/api/public/wtf_page.py +++ b/backend/app/api/public/wtf_page.py @@ -43,6 +43,9 @@ def _build_wtf_html(*, base_url: str) -> str: --accent: #c07046; --ok: #1f7a56; --bad: #b42318; + --tile-cols: 4; + --tile-gap: 16px; + --masonry-gap: 0px; --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji"; }} @@ -129,6 +132,10 @@ def _build_wtf_html(*, base_url: str) -> str: color: rgba(43, 29, 22, 0.90); }} .btn:hover {{ filter: brightness(0.98); }} + .btn.primary {{ + background: rgba(192, 112, 70, 0.16); + border-color: rgba(162, 82, 44, 0.34); + }} .seg {{ display: inline-flex; @@ -162,6 +169,7 @@ def _build_wtf_html(*, base_url: str) -> str: }} .muted {{ color: var(--muted2); }} + .muted.tight {{ font-size: 12px; }} code {{ font-family: var(--mono); font-size: 12px; @@ -185,6 +193,7 @@ def _build_wtf_html(*, base_url: str) -> str: box-shadow: 0 10px 30px rgba(40, 24, 16, 0.06); content-visibility: auto; contain-intrinsic-size: 800px; + position: relative; }} @keyframes shimmer {{ 0% {{ background-position: 0% 0; }} @@ -230,11 +239,20 @@ def _build_wtf_html(*, base_url: str) -> str: opacity: 0; transform: translateY(3px); transition: opacity 180ms ease, transform 220ms ease; + cursor: zoom-in; }} .item.loaded img {{ opacity: 1; transform: translateY(0px); }} + .item:hover img {{ + outline: 2px solid rgba(162, 82, 44, 0.42); + outline-offset: -2px; + }} + .item.selected img {{ + outline: 3px solid rgba(162, 82, 44, 0.62); + outline-offset: -3px; + }} .sentinel {{ text-align: center; padding: 12px 0 0; @@ -257,7 +275,7 @@ def _build_wtf_html(*, base_url: str) -> str: margin-top: 14px; display: flex; flex-direction: row; - gap: 0; + gap: var(--masonry-gap); align-items: flex-start; width: 100%; }} @@ -265,7 +283,7 @@ def _build_wtf_html(*, base_url: str) -> str: flex: 1 1 0; display: flex; flex-direction: column; - gap: 0; + gap: var(--masonry-gap); line-height: 0; min-width: 0; }} @@ -295,26 +313,17 @@ def _build_wtf_html(*, base_url: str) -> str: body.view-tiles .feed {{ margin-top: 14px; display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 16px; + grid-template-columns: repeat(var(--tile-cols), minmax(0, 1fr)); + gap: var(--tile-gap); }} @media (max-width: 1100px) {{ - body.view-tiles .feed {{ - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 14px; - }} + :root {{ --tile-cols: 3; --tile-gap: 14px; }} }} @media (max-width: 760px) {{ - body.view-tiles .feed {{ - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px; - }} + :root {{ --tile-cols: 2; --tile-gap: 12px; }} }} @media (max-width: 520px) {{ - body.view-tiles .feed {{ - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 10px; - }} + :root {{ --tile-cols: 2; --tile-gap: 10px; }} }} body.view-tiles .item {{ border-radius: 14px; @@ -354,6 +363,165 @@ def _build_wtf_html(*, base_url: str) -> str: outline: 2px solid rgba(162, 82, 44, 0.55); outline-offset: 2px; }} + + details.settings {{ + margin-top: 10px; + border: 1px solid var(--border); + background: rgba(255, 250, 243, 0.90); + border-radius: 12px; + padding: 10px 10px 10px; + }} + details.settings summary {{ + cursor: pointer; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + user-select: none; + color: rgba(43, 29, 22, 0.92); + font-size: 13px; + list-style: none; + }} + details.settings summary::-webkit-details-marker {{ display: none; }} + details.settings[open] summary {{ margin-bottom: 10px; }} + .settings-grid {{ + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; + }} + @media (max-width: 980px) {{ + .settings-grid {{ grid-template-columns: repeat(2, minmax(0, 1fr)); }} + }} + @media (max-width: 520px) {{ + .settings-grid {{ grid-template-columns: 1fr; }} + }} + .field label {{ + display: block; + margin: 0 0 5px; + font-size: 11px; + color: var(--muted2); + }} + .ctrl {{ + width: 100%; + border: 1px solid var(--border); + background: rgba(43, 29, 22, 0.04); + border-radius: 10px; + padding: 8px 10px; + font-size: 12px; + color: rgba(43, 29, 22, 0.92); + }} + textarea.ctrl {{ + min-height: 48px; + resize: vertical; + font-family: var(--mono); + line-height: 1.4; + }} + .quick {{ + margin-top: 10px; + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + }} + .qchip {{ + cursor: pointer; + border: 1px solid var(--border); + background: rgba(255, 250, 243, 0.92); + border-radius: 999px; + padding: 6px 10px; + font-size: 12px; + color: rgba(43, 29, 22, 0.90); + white-space: nowrap; + }} + .qchip:hover {{ filter: brightness(0.98); }} + .qchip.active {{ + border-color: rgba(162, 82, 44, 0.36); + background: rgba(192, 112, 70, 0.12); + }} + .actions {{ + margin-top: 10px; + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + }} + + .modal {{ + position: fixed; + inset: 0; + display: none; + align-items: center; + justify-content: center; + padding: 18px; + z-index: 9999; + }} + @media (max-width: 520px) {{ + .modal {{ padding: 10px; }} + }} + .modal.open {{ display: flex; }} + .modal-backdrop {{ + position: absolute; + inset: 0; + background: rgba(24, 16, 12, 0.68); + backdrop-filter: blur(4px); + }} + .modal-card {{ + position: relative; + z-index: 1; + width: min(1120px, 96vw); + max-height: 92vh; + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(255, 250, 243, 0.96); + border-radius: 16px; + box-shadow: 0 22px 60px rgba(0,0,0,0.28); + overflow: hidden; + display: flex; + flex-direction: column; + }} + .modal-card img {{ + width: 100%; + height: auto; + max-height: 82vh; + object-fit: contain; + background: rgba(43, 29, 22, 0.06); + }} + .modal-close {{ + position: absolute; + top: 8px; + right: 8px; + width: 34px; + height: 34px; + border-radius: 10px; + border: 1px solid rgba(58, 38, 26, 0.18); + background: rgba(255, 250, 243, 0.92); + cursor: pointer; + font-size: 18px; + line-height: 32px; + color: rgba(43, 29, 22, 0.88); + }} + .modal-close:hover {{ filter: brightness(0.98); }} + .modal-meta {{ + padding: 10px 12px 12px; + border-top: 1px solid rgba(58, 38, 26, 0.10); + font-size: 12px; + line-height: 1.4; + color: rgba(67, 51, 44, 0.78); + display: flex; + flex-wrap: wrap; + gap: 8px; + }} + .modal-meta a {{ + color: var(--link); + text-decoration: none; + font-family: var(--mono); + font-size: 11px; + padding: 2px 6px; + border-radius: 8px; + }} + .modal-meta a:hover {{ + background: rgba(192, 112, 70, 0.14); + text-decoration: none; + }} @@ -377,12 +545,128 @@ def _build_wtf_html(*, base_url: str) -> str: + +
+ + 筛选 / 布局 + 展开可快速调参(支持 URL 分享) + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ 快捷: + + + + + + + + + +
+ +
+ + + + 提示:@male/@female 为 /wtf 端展开的“概要标签”。 +
+
加载中…
+ +