修复 AGC 资源搜索框与栏目标题栏挤在同一水平带的重叠
- styles.css:新增 `.game-resource-book-tools` 工具条带,搜索框、筛选条与状态提示在里面按流式排布;`.game-resource-search` 去掉 `margin: 10px 12px 0`,尺寸与间距交给工具条带。 - styles.css:`.game-resource-manager` 加 `padding-top: var(--game-resource-book-tools-height, 0px)`,`.game-resource-book-scene` 的 `inset: 0` 改成同一变量让出顶边;管理区与画本场景共用一条实测高度,两者不再落在同一条排布带上(总览态标题栏与分页态栏目标题栏都整体下移)。 - styles.css:`@media (max-width: 560px)` 下搜索框独占整行,窄屏筛选条换行时工具条带变高、场景按实测高度继续下移,不靠 z-index 硬压。 - styles.css:更新工具条带注释,说明 `z-index: 40` 仍保留(高于场景的 20),作用只剩"工具条内容溢出时不被画本盖住"。 - index.tsx:把画本场景以外的流式工具条包进 `game-resource-book-tools`,搜索框挂 `resourceBookToolsRef`,用 ResizeObserver 实测 `offsetTop + offsetHeight` 写入 `--game-resource-book-tools-height`;搜索、筛选、「清除搜索并定位」与 `resourceSearchRef` 全部零改动。 - index.tsx:新增 `resourceCanvasSceneSize()`,视口居中、「适应内容」、总览入场中心、滚轮与缩放锚点都按"管理区盒子减掉工具条带"计算,不再按"场景一直顶到管理区顶端"排布;`handleResourceBookWheel` 与 `zoomResourceBookBy` 补上新增依赖。 - index.tsx:`measureResourceBookOverview()` 改用画本场景自己的 rect 作为总览栏目矩形原点,场景盒子上移后栏目矩形与世界坐标仍严格同源。 - project-development.suite.ts:在既有 CSS 声明级守卫里补断言——管理区 `padding-top` 与场景 `inset` 必须指向同一个 `--game-resource-book-tools-height` 变量,工具条带是绝对定位的独立排布行,栏目标题栏 z-index 低于工具条带;选择器加行首断言,避免误取 `.game-workbench-stage > .game-resource-manager`。 - project-development.suite.ts:在栏目大纲用例里补真实渲染的 DOM 归属断言——搜索框的父链不得命中画本场景或资源总览层,工具条带是管理区直接子节点;该断言在改动前失败、改动后通过,已用"把工具条带类名改掉"的变异验证它不是恒真假守卫。
This commit is contained in:
@@ -5640,6 +5640,13 @@ iframe.preview-frame {
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
/*
|
||||
* 顶部工具条带(搜索框、筛选条、状态提示)在流内占一条独立排布带,
|
||||
* `--game-resource-book-tools-height` 由实测高度写入。铺满管理区的画本场景按同一
|
||||
* 变量把自己缩到这条带下面,栏目标题栏与栏目大纲就不会和搜索框落在同一条排布带上
|
||||
* (分页态与总览态都适用)。
|
||||
*/
|
||||
padding-top: var(--game-resource-book-tools-height, 0px);
|
||||
}
|
||||
|
||||
.game-resource-book-manager {
|
||||
@@ -5651,7 +5658,8 @@ iframe.preview-frame {
|
||||
.game-resource-book-scene {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
inset: 0;
|
||||
/* 顶边让出工具条带:栏目标题栏与栏目大纲整体下移,不与搜索框争同一条排布带。 */
|
||||
inset: var(--game-resource-book-tools-height, 0px) 0 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
transition: none;
|
||||
@@ -6158,20 +6166,35 @@ iframe.preview-frame {
|
||||
}
|
||||
|
||||
/*
|
||||
* 资源画本的卡片与栏目标题栏都画在铺满资源区的 `.game-resource-book-scene`
|
||||
* (z-index 20)里,流式工具条默认被它整个盖住。搜索框和它下面的提示条必须抬到
|
||||
* 场景之上(与右下角缩放 Dock 同为 40),否则放开 display 也只是"画了但看不见"。
|
||||
* 工具条带:搜索框、筛选条与状态提示在这里按独立排布行排列,画本场景从这条带下面
|
||||
* 开始(见 `.game-resource-manager` 的 padding-top)。工具条里的浮层元素仍高于场景
|
||||
* (z-index 40 > 场景的 20),工具条内容溢出时不会被画本盖住。
|
||||
*/
|
||||
.game-resource-book-tools {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px 12px 4px;
|
||||
}
|
||||
|
||||
.game-resource-book-tools > * {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.game-resource-search {
|
||||
position: relative;
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
/* 常态居中,避开总览态标题和分页态栏目标题栏左右两端的内容。 */
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-self: center;
|
||||
width: min(300px, 100%);
|
||||
margin: 10px 12px 0;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #ecdcd4;
|
||||
border-radius: 10px;
|
||||
@@ -8859,6 +8882,16 @@ iframe.preview-frame {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/*
|
||||
* 窄屏(移动端优先):筛选条与标签开始换行时,搜索框改为整行独占,
|
||||
* 工具条带变高后画本场景按实测高度继续下移,任何宽度下都不与标题栏重叠。
|
||||
*/
|
||||
@media (max-width: 560px) {
|
||||
.game-resource-search {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.game-resource-recovery-item,
|
||||
.game-resource-recovery-error {
|
||||
|
||||
@@ -422,6 +422,21 @@ function resourceCanvasElementSize(element: HTMLElement | null) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源工具条(搜索框、筛选条、状态提示)在管理区顶部占一条独立排布带,画本场景的盒子
|
||||
* 从这条带下面开始。按视口排布资源时要减掉这条带,否则"适应内容"仍按旧的
|
||||
* "场景一直顶到管理区顶端"居中,把卡片排到工具条底下。
|
||||
*/
|
||||
function resourceCanvasSceneSize(
|
||||
size: { width: number; height: number },
|
||||
chromeHeight: number,
|
||||
) {
|
||||
return {
|
||||
width: size.width,
|
||||
height: Math.max(0, size.height - Math.max(0, chromeHeight)),
|
||||
};
|
||||
}
|
||||
|
||||
function resourceCanvasViewportsEqual(
|
||||
left: CanvasViewport,
|
||||
right: CanvasViewport,
|
||||
@@ -1223,6 +1238,11 @@ export default function ProjectDevelopmentView({
|
||||
createResourceBookTransitionController(),
|
||||
);
|
||||
const resourceBookManagerRef = useRef<HTMLDivElement>(null);
|
||||
/**
|
||||
* 工具条带的第一条流式元素(搜索框)。工具条带本身没有独立包裹节点,命中它就能把
|
||||
* "第一条流式元素顶边到管理区顶边"的高度算出来,正好是画本场景要让出的那条带。
|
||||
*/
|
||||
const resourceBookToolsRef = useRef<HTMLLabelElement>(null);
|
||||
const resourceBookPlanKeysRef = useRef(
|
||||
new Map<string, ResourceBookSceneCardPlan>(),
|
||||
);
|
||||
@@ -1236,6 +1256,15 @@ export default function ProjectDevelopmentView({
|
||||
width: 900,
|
||||
height: 640,
|
||||
});
|
||||
const [resourceBookToolsHeight, setResourceBookToolsHeight] = useState(0);
|
||||
/**
|
||||
* 场景局部的可排布尺寸:管理区盒子减掉顶部工具条带。视口居中与"适应内容"都按它算。
|
||||
*/
|
||||
const sceneFitCanvasSize = useMemo(
|
||||
() =>
|
||||
resourceCanvasSceneSize(resourceBookSceneSize, resourceBookToolsHeight),
|
||||
[resourceBookSceneSize, resourceBookToolsHeight],
|
||||
);
|
||||
const [sortMode, setSortMode] = useState<ResourceSortMode>('dependency');
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [resourceCanvasCategoryFilters, setResourceCanvasCategoryFilters] =
|
||||
@@ -2061,11 +2090,16 @@ export default function ProjectDevelopmentView({
|
||||
return;
|
||||
}
|
||||
const managerRect = manager.getBoundingClientRect();
|
||||
// 画本场景是管理区内缩掉顶部工具条带之后的盒子;总览栏目矩形与场景世界都按它定位。
|
||||
const sceneRect =
|
||||
manager
|
||||
.querySelector<HTMLElement>('.game-resource-book-scene')
|
||||
?.getBoundingClientRect() ?? managerRect;
|
||||
setResourceBookSceneSize((current) =>
|
||||
Math.abs(current.width - managerRect.width) < 0.5 &&
|
||||
Math.abs(current.height - managerRect.height) < 0.5
|
||||
Math.abs(current.width - sceneRect.width) < 0.5 &&
|
||||
Math.abs(current.height - sceneRect.height) < 0.5
|
||||
? current
|
||||
: { width: managerRect.width, height: managerRect.height },
|
||||
: { width: sceneRect.width, height: sceneRect.height },
|
||||
);
|
||||
const next = new Map<ResourceCategory, ResourceBookOverviewRect>();
|
||||
resourceBookThumbnailElementsRef.current.forEach((element, category) => {
|
||||
@@ -2126,6 +2160,29 @@ export default function ProjectDevelopmentView({
|
||||
window.removeEventListener('resize', measureResourceBookOverview);
|
||||
};
|
||||
}, [measureResourceBookOverview, resourceBookView, canvasResources.length]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const tools = resourceBookToolsRef.current;
|
||||
if (!tools) {
|
||||
return undefined;
|
||||
}
|
||||
const measureTools = () => {
|
||||
const height = tools.offsetTop + tools.offsetHeight;
|
||||
setResourceBookToolsHeight((current) =>
|
||||
Math.abs(current - height) < 0.5 ? current : height,
|
||||
);
|
||||
};
|
||||
measureTools();
|
||||
if (typeof ResizeObserver === 'undefined') {
|
||||
window.addEventListener('resize', measureTools);
|
||||
return () => window.removeEventListener('resize', measureTools);
|
||||
}
|
||||
// 工具条会随窗口宽度换行(窄屏下搜索框与筛选条各占一行),高度必须实测:
|
||||
// 场景盒子按它下移,两者才不会落在同一条排布带上。
|
||||
const observer = new ResizeObserver(measureTools);
|
||||
observer.observe(tools);
|
||||
return () => observer.disconnect();
|
||||
}, [mode, resourceBookView, searchText]);
|
||||
const viewedResourceCategory =
|
||||
mode === 'resources' ? activePageCategory : null;
|
||||
useEffect(() => {
|
||||
@@ -2302,8 +2359,8 @@ export default function ProjectDevelopmentView({
|
||||
// 返回总览时也不会跳成另一张图。
|
||||
const resourceBookCategoryViewCenters = useMemo(() => {
|
||||
const canvasSize =
|
||||
resourceBookSceneSize.width > 0 && resourceBookSceneSize.height > 0
|
||||
? resourceBookSceneSize
|
||||
sceneFitCanvasSize.width > 0 && sceneFitCanvasSize.height > 0
|
||||
? sceneFitCanvasSize
|
||||
: {
|
||||
width: RESOURCE_CANVAS_FALLBACK_WIDTH,
|
||||
height: RESOURCE_CANVAS_FALLBACK_HEIGHT,
|
||||
@@ -2322,7 +2379,7 @@ export default function ProjectDevelopmentView({
|
||||
] as const;
|
||||
}),
|
||||
);
|
||||
}, [resourceBookSceneSize, resourceCanvasViewports, sortMode]);
|
||||
}, [sceneFitCanvasSize, resourceCanvasViewports, sortMode]);
|
||||
const setResourceCanvasViewport = useCallback(
|
||||
(
|
||||
category: ResourceCategory,
|
||||
@@ -2815,14 +2872,14 @@ export default function ProjectDevelopmentView({
|
||||
|
||||
const categoryFitKey = `${projectPath}\n${manifest.projectId}\n${sortMode}\n${category}`;
|
||||
const fitCanvasSize =
|
||||
resourceBookSceneSize.width > 0 && resourceBookSceneSize.height > 0
|
||||
? resourceBookSceneSize
|
||||
sceneFitCanvasSize.width > 0 && sceneFitCanvasSize.height > 0
|
||||
? sceneFitCanvasSize
|
||||
: {
|
||||
width: RESOURCE_CANVAS_FALLBACK_WIDTH,
|
||||
height: RESOURCE_CANVAS_FALLBACK_HEIGHT,
|
||||
};
|
||||
const fitMaxScale =
|
||||
resourceBookSceneSize.width > 0 && resourceBookSceneSize.height > 0
|
||||
sceneFitCanvasSize.width > 0 && sceneFitCanvasSize.height > 0
|
||||
? RESOURCE_CANVAS_BOOK_FIT_MAX_SCALE
|
||||
: 1;
|
||||
const categoryBounds = resourceCanvasContentBounds(
|
||||
@@ -2867,7 +2924,7 @@ export default function ProjectDevelopmentView({
|
||||
// sort/category pair. Mark that fit as settled so the paged-canvas
|
||||
// layout effect does not immediately recalculate it from a shared DOM
|
||||
// measurement and overwrite the stored per-category viewport.
|
||||
if (resourceBookSceneSize.width > 0 && resourceBookSceneSize.height > 0) {
|
||||
if (sceneFitCanvasSize.width > 0 && sceneFitCanvasSize.height > 0) {
|
||||
resourceCanvasFitKeysRef.current.add(categoryFitKey);
|
||||
}
|
||||
|
||||
@@ -2880,7 +2937,7 @@ export default function ProjectDevelopmentView({
|
||||
manifest.projectId,
|
||||
projectPath,
|
||||
resourceBookState,
|
||||
resourceBookSceneSize,
|
||||
sceneFitCanvasSize,
|
||||
resourceCardSizeByResourceId,
|
||||
resourcePositionsByCategory,
|
||||
setResourceCanvasViewport,
|
||||
@@ -3825,14 +3882,14 @@ export default function ProjectDevelopmentView({
|
||||
activePageCategory,
|
||||
fitResourceCanvasViewportToContent({
|
||||
bounds: resourceCanvasFitBounds,
|
||||
canvasSize: resourceBookSceneSize,
|
||||
canvasSize: sceneFitCanvasSize,
|
||||
padding: RESOURCE_CANVAS_FIT_PADDING,
|
||||
maxScale: RESOURCE_CANVAS_BOOK_FIT_MAX_SCALE,
|
||||
}),
|
||||
);
|
||||
}, [
|
||||
activePageCategory,
|
||||
resourceBookSceneSize,
|
||||
sceneFitCanvasSize,
|
||||
resourceBookView,
|
||||
resourceCanvasFitBounds,
|
||||
setResourceCanvasViewport,
|
||||
@@ -3844,7 +3901,8 @@ export default function ProjectDevelopmentView({
|
||||
const sceneElement = resourceBookManagerRef.current;
|
||||
const rect = sceneElement?.getBoundingClientRect();
|
||||
const anchorX = rect ? rect.width / 2 : 0;
|
||||
const anchorY = rect ? rect.height / 2 : 0;
|
||||
// 锚点取真正看得见的画本区域中心:顶部工具条带不算画布。
|
||||
const anchorY = rect ? (rect.height - resourceBookToolsHeight) / 2 : 0;
|
||||
if (resourceBookView === 'main') {
|
||||
const viewport = resourceBookMainViewportRef.current;
|
||||
const next = zoomResourceBookViewport(viewport, factor, {
|
||||
@@ -3868,7 +3926,12 @@ export default function ProjectDevelopmentView({
|
||||
if (next.scale === viewport.scale) return;
|
||||
setResourceCanvasViewport(category, next);
|
||||
},
|
||||
[activePageCategory, resourceBookView, setResourceCanvasViewport],
|
||||
[
|
||||
activePageCategory,
|
||||
resourceBookToolsHeight,
|
||||
resourceBookView,
|
||||
setResourceCanvasViewport,
|
||||
],
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -3904,9 +3967,10 @@ export default function ProjectDevelopmentView({
|
||||
}
|
||||
event.preventDefault();
|
||||
const rect = sceneElement.getBoundingClientRect();
|
||||
// 工具条带里没有画布,滚轮锚点先减掉它,缩放才不会以被盖住的区域为中心。
|
||||
const screenPoint = {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top,
|
||||
y: event.clientY - rect.top - resourceBookToolsHeight,
|
||||
};
|
||||
if (resourceBookView === 'main') {
|
||||
const current = resourceBookMainViewportRef.current;
|
||||
@@ -3938,7 +4002,12 @@ export default function ProjectDevelopmentView({
|
||||
});
|
||||
setResourceCanvasViewport(category, result.viewport, true);
|
||||
},
|
||||
[activePageCategory, resourceBookView, setResourceCanvasViewport],
|
||||
[
|
||||
activePageCategory,
|
||||
resourceBookToolsHeight,
|
||||
resourceBookView,
|
||||
setResourceCanvasViewport,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -5735,85 +5804,90 @@ export default function ProjectDevelopmentView({
|
||||
<Maximize2 size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<label className="game-resource-search">
|
||||
<Search size={16} aria-hidden="true" />
|
||||
<input
|
||||
ref={resourceSearchRef}
|
||||
type="search"
|
||||
aria-label="搜索项目资源"
|
||||
value={searchText}
|
||||
placeholder="搜索资源名称、路径或类型"
|
||||
onChange={(event) => {
|
||||
advanceFocusGeneration();
|
||||
setSearchText(event.currentTarget.value);
|
||||
setHiddenCommittedResourceId(null);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<PlatformResourceFilterBar
|
||||
ariaLabel="资源筛选"
|
||||
categoryItems={RESOURCE_REFERENCE_FILTERS}
|
||||
activeCategoryId={resourceCategoryFilter}
|
||||
onCategoryChange={showResourceCategoryFilter}
|
||||
tagItems={resourceTagLibrary}
|
||||
activeTags={resourceCanvasActiveTags}
|
||||
onToggleTag={(tag) => {
|
||||
setResourceCanvasActiveTags((current) =>
|
||||
current.includes(tag)
|
||||
? current.filter((item) => item !== tag)
|
||||
: [...current, tag],
|
||||
);
|
||||
}}
|
||||
className="game-resource-category-filter"
|
||||
/>
|
||||
{resourceWorkbenchNotice ? (
|
||||
<div className="game-resource-live-notice" role="status">
|
||||
<span>{resourceWorkbenchNotice}</span>
|
||||
{hiddenCommittedResourceId ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const intent = pendingResourceFocusRef.current;
|
||||
if (
|
||||
!intent ||
|
||||
intent.resourceId !== hiddenCommittedResourceId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const focusGeneration = advanceFocusGeneration();
|
||||
pendingResourceFocusRef.current = {
|
||||
...intent,
|
||||
focusGeneration,
|
||||
completed: false,
|
||||
};
|
||||
setSearchText('');
|
||||
setHiddenCommittedResourceId(null);
|
||||
setResourceWorkbenchNotice('正在定位新资源…');
|
||||
}}
|
||||
>
|
||||
清除搜索并定位
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{attachments.some(
|
||||
(attachment) => attachment.status === 'failed',
|
||||
) ? (
|
||||
<section
|
||||
className="game-attachment-errors"
|
||||
aria-label="附件导入失败"
|
||||
role="status"
|
||||
<div className="game-resource-book-tools">
|
||||
<label
|
||||
ref={resourceBookToolsRef}
|
||||
className="game-resource-search"
|
||||
>
|
||||
{attachments
|
||||
.filter((attachment) => attachment.status === 'failed')
|
||||
.map((attachment) => (
|
||||
<p key={`${attachment.fileName}:${attachment.error}`}>
|
||||
<strong>{attachment.fileName}</strong>
|
||||
<span>{attachment.error || '附件导入失败'}</span>
|
||||
</p>
|
||||
))}
|
||||
</section>
|
||||
) : null}
|
||||
<Search size={16} aria-hidden="true" />
|
||||
<input
|
||||
ref={resourceSearchRef}
|
||||
type="search"
|
||||
aria-label="搜索项目资源"
|
||||
value={searchText}
|
||||
placeholder="搜索资源名称、路径或类型"
|
||||
onChange={(event) => {
|
||||
advanceFocusGeneration();
|
||||
setSearchText(event.currentTarget.value);
|
||||
setHiddenCommittedResourceId(null);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<PlatformResourceFilterBar
|
||||
ariaLabel="资源筛选"
|
||||
categoryItems={RESOURCE_REFERENCE_FILTERS}
|
||||
activeCategoryId={resourceCategoryFilter}
|
||||
onCategoryChange={showResourceCategoryFilter}
|
||||
tagItems={resourceTagLibrary}
|
||||
activeTags={resourceCanvasActiveTags}
|
||||
onToggleTag={(tag) => {
|
||||
setResourceCanvasActiveTags((current) =>
|
||||
current.includes(tag)
|
||||
? current.filter((item) => item !== tag)
|
||||
: [...current, tag],
|
||||
);
|
||||
}}
|
||||
className="game-resource-category-filter"
|
||||
/>
|
||||
{resourceWorkbenchNotice ? (
|
||||
<div className="game-resource-live-notice" role="status">
|
||||
<span>{resourceWorkbenchNotice}</span>
|
||||
{hiddenCommittedResourceId ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const intent = pendingResourceFocusRef.current;
|
||||
if (
|
||||
!intent ||
|
||||
intent.resourceId !== hiddenCommittedResourceId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const focusGeneration = advanceFocusGeneration();
|
||||
pendingResourceFocusRef.current = {
|
||||
...intent,
|
||||
focusGeneration,
|
||||
completed: false,
|
||||
};
|
||||
setSearchText('');
|
||||
setHiddenCommittedResourceId(null);
|
||||
setResourceWorkbenchNotice('正在定位新资源…');
|
||||
}}
|
||||
>
|
||||
清除搜索并定位
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{attachments.some(
|
||||
(attachment) => attachment.status === 'failed',
|
||||
) ? (
|
||||
<section
|
||||
className="game-attachment-errors"
|
||||
aria-label="附件导入失败"
|
||||
role="status"
|
||||
>
|
||||
{attachments
|
||||
.filter((attachment) => attachment.status === 'failed')
|
||||
.map((attachment) => (
|
||||
<p key={`${attachment.fileName}:${attachment.error}`}>
|
||||
<strong>{attachment.fileName}</strong>
|
||||
<span>{attachment.error || '附件导入失败'}</span>
|
||||
</p>
|
||||
))}
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
<section
|
||||
className={`game-resource-book-main${resourceBookState.view === 'child' ? ' is-background' : ''}`}
|
||||
aria-label="资源总览"
|
||||
|
||||
@@ -1191,22 +1191,32 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
|
||||
// 但"在场景之上"只是不被盖住,不等于两者不重叠:上一版把搜索框常态居中浮在画本顶上,
|
||||
// 分页态栏目标题栏正好被压在同一水平带上。所以还要钉住"工具条带独占顶部那条带"。
|
||||
const scene = styleRuleBody(styles, '\\.game-resource-book-scene');
|
||||
expect(scene).toMatch(/inset:\s*var\(--game-resource-book-tools-height\)/u);
|
||||
|
||||
// 管理区给工具条带留出的高度与场景让出的顶边必须是同一个变量,否则两者会再次互相压住。
|
||||
const manager = styleRuleBody(styles, '\\.game-resource-manager');
|
||||
//
|
||||
// 不重叠由两条声明级约定保证,缺一条就会退回重叠:
|
||||
// 1. 管理区按实测的工具条带高度留出 padding-top;
|
||||
// 2. 画本场景把自己的顶边按同一个变量下移。
|
||||
// 两条都指向 `--game-resource-book-tools-height`,所以断言两边用的是同一个变量,
|
||||
// 而不是各写各的数字(写死数字时任何一处改动都会重新压住搜索框)。
|
||||
const toolsHeightVariable = '--game-resource-book-tools-height';
|
||||
// 选择器前缀加行首断言:`.game-workbench-stage > .game-resource-manager` 这种
|
||||
// 后代选择器里也含同名类,不加断言会取到那条只有 grid-row 的规则。
|
||||
const manager = styleRuleBody(styles, '(?:^|\\n)\\.game-resource-manager');
|
||||
expect(manager).toMatch(
|
||||
/padding-top:\s*var\(--game-resource-book-tools-height\)/u,
|
||||
new RegExp(`padding-top:\\s*var\\(${toolsHeightVariable}`, 'u'),
|
||||
);
|
||||
|
||||
const scene = styleRuleBody(styles, '\\.game-resource-book-scene');
|
||||
expect(scene).toMatch(
|
||||
new RegExp(`inset:\\s*var\\(${toolsHeightVariable}`, 'u'),
|
||||
);
|
||||
// 场景只让出顶边:宽度与底边必须仍是 0,否则画本区域会缩成一条缝。
|
||||
expect(scene).toMatch(/inset:\s*var\([^)]*\)\s+0\s+0;/u);
|
||||
|
||||
// 工具条带是绝对定位的独立排布行,不是流内浮层;搜索框按整行宽度算,
|
||||
// 不与任何栏目标题栏共享同一水平带。
|
||||
// 不与任何栏目标题栏共享同一水平带(分页态标题栏 z-index 30 < 工具条带 40)。
|
||||
const tools = styleRuleBody(styles, '\\.game-resource-book-tools');
|
||||
expect(tools).toMatch(/position:\s*absolute/u);
|
||||
expect(styleNumber(tools, 'top')).toBe(0);
|
||||
|
||||
// 搜到的是场景里真正的栏目标题栏(分页态被激活时铺满整行),它必须整体排在工具条带下面。
|
||||
const titlebar = styleRuleBody(
|
||||
styles,
|
||||
'\\.game-resource-book-scene-titlebar',
|
||||
@@ -1215,8 +1225,11 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
styleNumber(tools, 'z-index'),
|
||||
);
|
||||
|
||||
expect(tsxSource).toContain('game-resource-book-tools');
|
||||
expect(tsxSource).not.toContain('game-resource-book-scene-titlebar');
|
||||
// 结构上搜索框必须待在工具条带里:离开这条带就又会浮回画本顶部。
|
||||
const toolsTag = tsxSource.indexOf('game-resource-book-tools');
|
||||
const searchTag = tsxSource.indexOf('game-resource-search');
|
||||
expect(toolsTag).toBeGreaterThan(-1);
|
||||
expect(searchTag).toBeGreaterThan(toolsTag);
|
||||
});
|
||||
|
||||
it('keeps the outline dock readable without hover', () => {
|
||||
@@ -1325,6 +1338,34 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
.getByRole('button', { name: '角色与对象' })
|
||||
.getAttribute('aria-current'),
|
||||
).toBe('page');
|
||||
|
||||
// 搜索框与栏目标题栏分属两条排布带,这不是 CSS 声明能表达的:画本场景是管理区里
|
||||
// 铺满整块的绝对定位层,搜索框只要还在场景内部,就一定会被栏目标题栏压在同一个
|
||||
// 水平带上。所以这里钉住渲染出来的 DOM 归属关系。
|
||||
const searchLabel = screen.getByLabelText('搜索项目资源').closest('label');
|
||||
expect(searchLabel?.className).toContain('game-resource-search');
|
||||
|
||||
const toolsBand = searchLabel?.parentElement ?? null;
|
||||
expect(toolsBand?.className).toContain('game-resource-book-tools');
|
||||
|
||||
const manager = toolsBand?.parentElement ?? null;
|
||||
expect(manager?.className).toContain('game-resource-manager');
|
||||
|
||||
// 画本场景是管理区里铺满整块的绝对定位层:搜索框只要落在它内部,就会被栏目标题栏
|
||||
// 压在同一个水平带上。入场动画会替换场景节点,所以这里用祖先链判断归属关系,
|
||||
// 不依赖某一个具体节点的身份,也不受节点替换影响。
|
||||
// 从父节点起算:label 自己就带 `game-resource-search`,closest 要排除自身。
|
||||
const searchAncestors = searchLabel?.parentElement ?? null;
|
||||
expect(searchAncestors?.closest('.game-resource-book-scene')).toBeNull();
|
||||
expect(searchAncestors?.closest('.game-resource-book-main')).toBeNull();
|
||||
expect(searchAncestors?.closest('.game-resource-book-tools')).toBe(
|
||||
toolsBand,
|
||||
);
|
||||
// 两条排布带在管理区里是并列的:搜索框所在的工具条带与画本场景各占一条。
|
||||
expect(toolsBand?.parentElement).toBe(manager);
|
||||
expect(
|
||||
manager?.querySelector(':scope > .game-resource-book-scene'),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('keeps the shared selection overlay colours resolvable from the scene root', () => {
|
||||
|
||||
Reference in New Issue
Block a user