Present architecture as a plain click-to-enlarge image
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 5m3s
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 5m3s
This commit is contained in:
@@ -43,7 +43,7 @@ main 推送通过 CI 验证后自动部署;Pull Request 只验证,不部署
|
||||
|
||||
`content/docs/00-endor-concepts-draft.md` 解释全球入口、区域网关与 Runtime、分片、CDN、身份及跨区恢复的概念设计,页面为 `/docs/00-endor-concepts-draft`。
|
||||
|
||||
图形源码位于 `tools/figures/src/endor_figures/architecture.py`,由 `pnpm figures` 生成 `apps/docs/public/figures/endor-architecture.svg`。Python 负责可复现的节点、全连接和复制路径,React 查看器提供放大、滚动与 SVG 下载。无需外部图标、字体或绘图服务。图中的数量是示意,身份服务/CDN 接入属于建议,所有未实现的关系均以草案说明。
|
||||
图形源码位于 `tools/figures/src/endor_figures/architecture.py`,由 `pnpm figures` 生成 `apps/docs/public/figures/endor-architecture.svg`。Python 负责可复现的节点、全连接和复制路径,正文直接显示图片,点击可放大;大图支持原尺寸查看、滚动和 Escape/空白处关闭。无需外部图标、字体或绘图服务。图中的数量是示意,身份服务/CDN 接入属于建议,所有未实现的关系均以草案说明。
|
||||
|
||||
拓扑检查:`uv run --locked --project tools/figures python -m unittest discover -s tools/figures/tests -v`。容器检查流程会运行同一检查。
|
||||
|
||||
|
||||
@@ -1,104 +1,66 @@
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
const source = "/figures/endor-architecture.svg";
|
||||
const figureWidth = 2000;
|
||||
|
||||
export function ArchitectureFigure({ alt }: { alt: string }) {
|
||||
const dialog = useRef<HTMLDialogElement>(null);
|
||||
const trigger = useRef<HTMLButtonElement>(null);
|
||||
const pan = useRef<HTMLDivElement>(null);
|
||||
const [fit, setFit] = useState(true);
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const stage = useRef<HTMLDivElement>(null);
|
||||
const [originalSize, setOriginalSize] = useState(false);
|
||||
|
||||
function open() {
|
||||
setFit(true);
|
||||
setZoom(1);
|
||||
setOriginalSize(false);
|
||||
dialog.current?.showModal();
|
||||
}
|
||||
function changeZoom(delta: number) {
|
||||
const current = fit
|
||||
? ((pan.current?.clientWidth ?? figureWidth + 16) - 16) / figureWidth
|
||||
: zoom;
|
||||
setFit(false);
|
||||
setZoom(
|
||||
Math.max(0.1, Math.min(2, Math.round((current + delta) * 100) / 100)),
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<figure className="architecture-figure">
|
||||
<span className="figure-toolbar">
|
||||
<span>ENDOR / 架构拓扑</span>
|
||||
<button ref={trigger} onClick={open}>
|
||||
放大查看 ↗
|
||||
</button>
|
||||
</span>
|
||||
<>
|
||||
<button
|
||||
className="figure-preview"
|
||||
ref={trigger}
|
||||
className="architecture-preview"
|
||||
onClick={open}
|
||||
aria-label="放大 Endor 架构图"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={source} alt={alt} />
|
||||
</button>
|
||||
<span className="figure-caption">
|
||||
多区域拓扑 · 独立分片群与统一备份 · 点击放大查看
|
||||
</span>
|
||||
<dialog
|
||||
ref={dialog}
|
||||
className="figure-dialog"
|
||||
aria-label="Endor 架构图查看器"
|
||||
className="image-lightbox"
|
||||
aria-label="Endor 架构图大图"
|
||||
onClick={(event) => {
|
||||
if (
|
||||
event.target === dialog.current ||
|
||||
event.target === stage.current
|
||||
) {
|
||||
dialog.current?.close();
|
||||
}
|
||||
}}
|
||||
onClose={() => trigger.current?.focus()}
|
||||
>
|
||||
<div className="figure-viewer-toolbar">
|
||||
<strong>Endor 架构拓扑</strong>
|
||||
<div className="figure-controls">
|
||||
<button
|
||||
onClick={() => changeZoom(-0.25)}
|
||||
aria-label="缩小架构图"
|
||||
disabled={!fit && zoom <= 0.1}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<span aria-live="polite">
|
||||
{fit ? "适应宽度" : `${Math.round(zoom * 100)}%`}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => changeZoom(0.25)}
|
||||
aria-label="放大架构图"
|
||||
disabled={!fit && zoom >= 2}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button onClick={() => setFit(true)}>适应</button>
|
||||
<a href={source} download="endor-architecture.svg">
|
||||
下载 SVG
|
||||
</a>
|
||||
<button
|
||||
onClick={() => dialog.current?.close()}
|
||||
aria-label="关闭架构图"
|
||||
>
|
||||
关闭 ×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={pan}
|
||||
className="figure-pan"
|
||||
tabIndex={0}
|
||||
aria-label="架构图滚动区域,可使用方向键滚动"
|
||||
<button
|
||||
className="image-lightbox-close"
|
||||
aria-label="关闭图片"
|
||||
onClick={() => dialog.current?.close()}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={source}
|
||||
alt={alt}
|
||||
style={{ width: fit ? "100%" : figureWidth * zoom }}
|
||||
/>
|
||||
×
|
||||
</button>
|
||||
<div
|
||||
ref={stage}
|
||||
className="image-lightbox-stage"
|
||||
tabIndex={0}
|
||||
aria-label="图片滚动区域"
|
||||
>
|
||||
<button
|
||||
className={`image-lightbox-picture${originalSize ? " original-size" : ""}`}
|
||||
onClick={() => setOriginalSize((value) => !value)}
|
||||
aria-label={originalSize ? "图片适应屏幕" : "查看图片原尺寸"}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={source} alt={alt} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="figure-viewer-hint">
|
||||
实线:请求 /
|
||||
依赖 · 紫色:身份 · 蓝色点线:回源 · 金色:统一备份。放大后可横向、纵向滚动,Esc
|
||||
关闭。
|
||||
</p>
|
||||
</dialog>
|
||||
</figure>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1341,166 +1341,109 @@ button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* The topology stays a reproducible SVG; React only supplies the viewer. */
|
||||
.architecture-figure {
|
||||
display: block;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin: 28px 0;
|
||||
}
|
||||
.figure-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: #f5f8f3;
|
||||
font:
|
||||
11px ui-monospace,
|
||||
monospace;
|
||||
color: #6c806f;
|
||||
}
|
||||
.figure-toolbar button {
|
||||
color: var(--green);
|
||||
border: 1px solid #d7e3d4;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
background: white;
|
||||
}
|
||||
.figure-preview {
|
||||
/* Inline diagrams behave like ordinary article images with click-to-enlarge. */
|
||||
.architecture-preview {
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: #fcfdfb;
|
||||
margin: 28px 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
.prose .figure-preview img {
|
||||
.prose .architecture-preview img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
.figure-caption {
|
||||
display: block;
|
||||
padding: 10px 16px;
|
||||
font-size: 11px;
|
||||
color: #758576;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.figure-dialog {
|
||||
padding: 0;
|
||||
width: 96vw;
|
||||
max-width: 1900px;
|
||||
height: 94dvh;
|
||||
max-height: 94dvh;
|
||||
border: 1px solid #dce5d9;
|
||||
border-radius: 12px;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
background: #fcfdfb;
|
||||
box-shadow: 0 20px 100px #18312530;
|
||||
}
|
||||
.figure-dialog::backdrop {
|
||||
background: #1e322e66;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.figure-dialog[open] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.figure-viewer-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
padding: 12px 18px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--line);
|
||||
align-items: center;
|
||||
}
|
||||
.figure-viewer-toolbar strong {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
}
|
||||
.figure-viewer-toolbar small {
|
||||
color: #a68b52;
|
||||
font-size: 11px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.figure-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
color: #647c67;
|
||||
}
|
||||
.figure-controls button,
|
||||
.figure-controls a {
|
||||
min-height: 32px;
|
||||
min-width: 32px;
|
||||
padding: 4px 9px;
|
||||
border: 1px solid #dae4d6;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
font-weight: 400;
|
||||
text-decoration: none;
|
||||
}
|
||||
.figure-controls button:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: default;
|
||||
}
|
||||
.figure-controls > span {
|
||||
min-width: 62px;
|
||||
text-align: center;
|
||||
}
|
||||
.figure-pan {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overscroll-behavior: contain;
|
||||
padding: 8px;
|
||||
}
|
||||
.prose .figure-pan img {
|
||||
display: block;
|
||||
max-width: none;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: #fcfdfb;
|
||||
background: transparent;
|
||||
}
|
||||
.figure-viewer-hint {
|
||||
margin: 0 !important;
|
||||
padding: 10px 18px;
|
||||
border-top: 1px solid var(--line);
|
||||
background: #fff;
|
||||
color: #798d79;
|
||||
font-size: 10px !important;
|
||||
line-height: 1.7 !important;
|
||||
.image-lightbox {
|
||||
width: 100vw;
|
||||
height: 100dvh;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
.image-lightbox::backdrop {
|
||||
background: rgba(245, 247, 244, 0.97);
|
||||
}
|
||||
.image-lightbox-stage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding: 32px;
|
||||
}
|
||||
.image-lightbox-picture {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
margin: auto;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
.prose .image-lightbox-picture img {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: calc(100vw - 64px);
|
||||
max-height: calc(100dvh - 64px);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.image-lightbox-picture.original-size {
|
||||
cursor: zoom-out;
|
||||
}
|
||||
.prose .image-lightbox-picture.original-size img {
|
||||
width: 2000px;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
}
|
||||
.image-lightbox-close {
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #52655a;
|
||||
font:
|
||||
28px/1 ui-sans-serif,
|
||||
system-ui,
|
||||
sans-serif;
|
||||
}
|
||||
.image-lightbox-close:hover {
|
||||
background: #edf2ec;
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.figure-viewer-toolbar {
|
||||
padding: 10px;
|
||||
gap: 8px;
|
||||
.image-lightbox-stage {
|
||||
padding: 16px;
|
||||
}
|
||||
.figure-controls {
|
||||
gap: 5px;
|
||||
.prose .image-lightbox-picture img {
|
||||
max-width: calc(100vw - 32px);
|
||||
max-height: calc(100dvh - 32px);
|
||||
}
|
||||
.figure-controls button,
|
||||
.figure-controls a {
|
||||
min-height: 36px;
|
||||
}
|
||||
.figure-toolbar {
|
||||
padding: 10px;
|
||||
font-size: 10px;
|
||||
}
|
||||
.figure-caption {
|
||||
font-size: 10px;
|
||||
padding: 10px;
|
||||
.prose .image-lightbox-picture.original-size img {
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# 普通图片交互验收 · 2026-10-27
|
||||
|
||||
按用户的两处页面批注,删除架构图上下工具栏、说明条、边框和卡片包装。正文只显示可点击的图片。放大后只显示图片和关闭按钮,支持再次点击图片切换原尺寸、点击空白关闭、Escape关闭并恢复焦点。
|
||||
|
||||
- `pnpm lint`、`pnpm typecheck`、`pnpm --filter @endor/docs run build --webpack` 通过。
|
||||
- 浏览器检查旧 `.figure-toolbar`、`.figure-caption`、`.figure-controls` 数量为0;正文图片及点击区域计算边框均为0px。
|
||||
- 实际验证点击图片打开、点击空白关闭、焦点回到正文图片、再次打开及Escape关闭。
|
||||
- 390px手机宽度下正文无横向溢出;原尺寸图片宽2000px,仅在大图滚动区域内滚动,页面仍宽390px。
|
||||
- 浏览器error/warn日志为空。
|
||||
- `inline.png` 为无框正文图片;`enlarged.png` 为简洁放大效果。
|
||||
|
||||
图片数据与Python制图源码未变;README同步移除旧工具栏/下载控件的功能描述。
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
Reference in New Issue
Block a user