Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/pages/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import xSnowflake "github.com/bamboo-services/bamboo-base-go/common/snowflake"
type PageListRequest struct {
ProjectID xSnowflake.SnowflakeID `form:"project_id"` // 项目ID筛选
WorkspaceID xSnowflake.SnowflakeID `form:"workspace_id"` // 所属空间ID筛选,零值不过滤
Status string `form:"status"` // 状态筛选,空值不过滤
Page int `form:"page"` // 页码
Size int `form:"size"` // 每页数量
}
Expand Down
1 change: 1 addition & 0 deletions api/project/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ type ProjectListRequest struct {
Page int `form:"page"` // 页码
Size int `form:"size"` // 每页数量
WorkspaceID xSnowflake.SnowflakeID `form:"workspace_id"` // 所属空间ID筛选,零值不过滤
Search string `form:"search"` // 项目名称/别名模糊搜索,空值不过滤
}
38 changes: 37 additions & 1 deletion components/src/lpw/annotation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,11 @@ describe('annotation', () => {
expect(container.querySelector('#frame-md-annotated')).toBeTruthy()
})

it('Q-04 重叠批注压缩,点击后展开', async () => {
it('Q-04 MD 尺寸下重叠批注压缩,点击后展开;LG 尺寸下默认展开', async () => {
// 模拟 MD 大小 (例如 800px)
window.innerWidth = 800
window.dispatchEvent(new Event('resize'))

const ann: LpwAnnotation = { kind: 'note', message: '第一条' }
const ann2: LpwAnnotation = { kind: 'issue', message: '第二条' }
render(
Expand All @@ -220,6 +224,38 @@ describe('annotation', () => {
expect(screen.getAllByTestId('annotation-bubble').length).toBeGreaterThanOrEqual(
2,
)

// 切回 LG 尺寸 (1200px)
window.innerWidth = 1200
window.dispatchEvent(new Event('resize'))
})

it('移动端/MD 浮动批注按钮与精致侧边抽屉设计', async () => {
window.innerWidth = 640
window.dispatchEvent(new Event('resize'))

const ann: LpwAnnotation = { kind: 'note', message: '移动端批注' }
render(
<div data-testid="lpw-paper">
<AnnotationProvider>
<AnnotationFrame nodeId="n-m" annotation={ann}>
<div>移动正文</div>
</AnnotationFrame>
<AnnotationGutter />
</AnnotationProvider>
</div>,
)

const triggerBtn = screen.getByRole('button', { name: /查看 1 条批注/ })
expect(triggerBtn).toBeTruthy()
fireEvent.click(triggerBtn)

// 打开抽屉
expect(await screen.findByText('文档批注与审查建议')).toBeTruthy()
expect(screen.getAllByText('移动端批注').length).toBeGreaterThanOrEqual(1)

window.innerWidth = 1200
window.dispatchEvent(new Event('resize'))
})

it('supports unicode and case-insensitive flags combined (iu)', () => {
Expand Down
123 changes: 101 additions & 22 deletions components/src/lpw/annotation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "lucide-react";
import type React from "react";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from "../ui/sheet";
import { useAnnotationContext } from "./annotation-context";
import type { RegisteredAnnotation } from "./annotation-context";
import { isSafeAnnotationPattern } from "./safe-pattern";
Expand Down Expand Up @@ -314,8 +315,20 @@ export const AnnotationGutter: React.FC = () => {
const annotations = ctx?.annotations ?? [];
const asideRef = useRef<HTMLElement>(null);
const [tops, setTops] = useState<Record<string, number>>({});
const [expandedKey, setExpandedKey] = useState<string | null>(null);
const [mobileOpen, setMobileOpen] = useState(false);
const [isLg, setIsLg] = useState(false);
const [expandedKeys, setExpandedKeys] = useState<Set<string>>(new Set());
const [collapsedKeys, setCollapsedKeys] = useState<Set<string>>(new Set());
const [sheetOpen, setSheetOpen] = useState(false);

useEffect(() => {
if (typeof window === "undefined") return;
const checkLg = () => {
setIsLg(window.innerWidth >= 1024);
};
checkLg();
window.addEventListener("resize", checkLg);
return () => window.removeEventListener("resize", checkLg);
}, []);

useLayoutEffect(() => {
if (annotations.length === 0) return;
Expand Down Expand Up @@ -362,6 +375,38 @@ export const AnnotationGutter: React.FC = () => {
[annotations, tops],
);

const isGroupExpanded = (key: string, groupLen: number) => {
if (groupLen <= 1) return true;
if (isLg) {
return !collapsedKeys.has(key);
}
return expandedKeys.has(key);
};

const handleToggleGroup = (key: string, currentlyExpanded: boolean) => {
if (currentlyExpanded) {
if (isLg) {
setCollapsedKeys((prev) => new Set([...prev, key]));
} else {
setExpandedKeys((prev) => {
const next = new Set(prev);
next.delete(key);
return next;
});
}
} else {
if (isLg) {
setCollapsedKeys((prev) => {
const next = new Set(prev);
next.delete(key);
return next;
});
} else {
setExpandedKeys((prev) => new Set([...prev, key]));
}
}
};

const clusterTops = useMemo(() => {
const result: Record<string, number> = {};
let minTop = 0;
Expand All @@ -386,12 +431,12 @@ export const AnnotationGutter: React.FC = () => {
ref={asideRef}
data-testid="annotation-gutter"
aria-label="文档批注"
className="relative hidden w-64 shrink-0 self-stretch py-2 pr-2 md:block"
className="relative hidden w-60 lg:w-64 shrink-0 self-stretch py-2 pr-2 md:block"
>
{clusters.map((group) => {
const key = group.map((g) => g.nodeId).join("-");
const top = clusterTops[key] ?? (tops[group[0].nodeId] ?? 0);
const expanded = expandedKey === key || group.length === 1;
const expanded = isGroupExpanded(key, group.length);

return (
<div
Expand All @@ -411,7 +456,7 @@ export const AnnotationGutter: React.FC = () => {
{group.length > 1 ? (
<button
type="button"
onClick={() => setExpandedKey(null)}
onClick={() => handleToggleGroup(key, true)}
className="font-mono text-[10px] text-sea-ink-soft hover:text-sea-ink cursor-pointer"
>
收起
Expand All @@ -423,7 +468,7 @@ export const AnnotationGutter: React.FC = () => {
type="button"
data-testid="annotation-cluster"
aria-label={`展开 ${group.length} 条批注`}
onClick={() => setExpandedKey(key)}
onClick={() => handleToggleGroup(key, false)}
className="relative w-full cursor-pointer text-left"
>
<MessageBubble
Expand All @@ -440,35 +485,69 @@ export const AnnotationGutter: React.FC = () => {
})}
</aside>

{/* 移动端与 MD 屏幕下的优雅隐藏批注触发栏与侧边抽屉 */}
<div
data-testid="annotation-mobile-strip"
className="w-full border-t border-line/60 bg-surface/40 md:hidden"
className="fixed bottom-5 right-5 z-40 lg:hidden print:hidden"
>
<button
type="button"
aria-expanded={mobileOpen}
aria-expanded={sheetOpen}
aria-label={`查看 ${annotations.length} 条批注`}
onClick={() => setMobileOpen((v) => !v)}
className="flex w-full cursor-pointer items-center justify-between px-4 py-3 font-mono text-xs text-sea-ink"
onClick={() => {
setSheetOpen(true);
}}
className="flex cursor-pointer items-center gap-2 border border-line bg-surface-strong/95 px-3.5 py-2 font-mono text-xs text-sea-ink shadow-lg backdrop-blur-sm transition-all hover:border-lagoon hover:shadow-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-lagoon/40"
>
<span className="flex items-center gap-2">
<MessageSquare className="h-4 w-4 text-lagoon" />
批注 ({annotations.length})
<MessageSquare className="h-4 w-4 text-lagoon" />
<span className="font-sans font-medium">批注</span>
<span className="flex h-4 min-w-4 items-center justify-center border border-lagoon/50 bg-lagoon/15 px-1 font-mono text-[10px] font-semibold text-lagoon-deep">
{annotations.length}
</span>
<span className="text-sea-ink-soft">{mobileOpen ? "收起" : "展开"}</span>
</button>
{mobileOpen ? (
<div className="space-y-3 px-4 pb-4">
</div>

<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
<SheetContent
side="right"
className="w-full sm:max-w-md bg-surface-strong border-l border-line p-0 flex flex-col font-sans"
>
<SheetHeader className="p-5 border-b border-line bg-surface/50">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="flex h-7 w-7 items-center justify-center border border-lagoon/40 bg-lagoon/10">
<MessageSquare className="h-4 w-4 text-lagoon" />
</div>
<div>
<SheetTitle className="font-serif text-base font-semibold text-sea-ink">
文档批注与审查建议
</SheetTitle>
<SheetDescription className="font-mono text-[11px] text-sea-ink-soft">
共 {annotations.length} 条批注 · 点击条目直达正文
</SheetDescription>
</div>
</div>
</div>
</SheetHeader>

<div className="flex-1 overflow-y-auto p-5 space-y-3.5">
{annotations.map((item) => (
<MessageBubble
<div
key={item.nodeId}
nodeId={item.nodeId}
annotation={item.annotation}
/>
onClick={() => {
setSheetOpen(false);
ctx?.scrollToNode(item.nodeId);
}}
>
<MessageBubble
nodeId={item.nodeId}
annotation={item.annotation}
/>
</div>
))}
</div>
) : null}
</div>
</SheetContent>
</Sheet>
</>
);
};
27 changes: 27 additions & 0 deletions components/src/lpw/block/image.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,33 @@ describe('ImageBlock', () => {
expect(screen.getByText('图 1. 架构流向')).toBeTruthy()
})

it('默认页面级宽度应为 80%,嵌套在 layout/container 内时应为 100%', () => {
const { rerender } = render(
<ImageBlock
blockId="img-default"
props={{
src: '/preview.png',
alt: '顶层图片',
}}
location={{ jsonPath: '/content/0', idPath: [] }}
/>,
)
const figTop = screen.getByTestId('image-block')
expect(figTop.style.width).toBe('80%')

rerender(
<ImageBlock
blockId="img-nested"
props={{
src: '/preview.png',
alt: '嵌套图片',
}}
location={{ jsonPath: '/content/0/children/0', idPath: ['container-1'] }}
/>,
)
expect(figTop.style.width).toBe('100%')
})

it('图片加载失败触发 onError 时应切换为占位卡', () => {
render(
<ImageBlock
Expand Down
14 changes: 11 additions & 3 deletions components/src/lpw/block/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { LpwBlockSlotProps, LpwImageProps } from '../types'

export const ImageBlock: React.FC<LpwBlockSlotProps<LpwImageProps>> = ({
props,
location,
}) => {
const [hasError, setHasError] = useState(false)
const runtime = useLpwRuntime()
Expand All @@ -14,6 +15,9 @@ export const ImageBlock: React.FC<LpwBlockSlotProps<LpwImageProps>> = ({
setHasError(false)
}, [props.src])

const isNested = Boolean(location?.idPath && location.idPath.length > 0)
const targetWidth = props.width ? props.width : isNested ? '100%' : '80%'

if (hasError) {
return (
<figure
Expand All @@ -31,14 +35,18 @@ export const ImageBlock: React.FC<LpwBlockSlotProps<LpwImageProps>> = ({
}

return (
<figure className="flex max-w-full min-w-0 flex-col items-center font-sans">
<div className="max-w-full min-w-0 border border-line bg-surface p-1.5">
<figure
data-testid="image-block"
className="flex max-w-full min-w-0 flex-col items-center font-sans"
style={{ width: targetWidth, maxWidth: '100%', margin: '0 auto' }}
>
<div className="w-full max-w-full min-w-0 border border-line bg-surface p-1.5 flex justify-center">
<img
src={resolvedSrc}
alt={props.alt}
style={props.width ? { width: props.width } : undefined}
onError={() => setHasError(true)}
className="max-w-full object-contain bg-surface-muted/20"
className="w-full max-w-full object-contain bg-surface-muted/20"
/>
</div>
{props.caption && (
Expand Down
37 changes: 37 additions & 0 deletions components/src/lpw/block/mermaid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,41 @@ describe('MermaidBlock', () => {
expect(screen.getByTestId('mermaid-block')).toBeTruthy()
expect(screen.getByText('图 2. 流程拓扑')).toBeTruthy()
})

it('默认页面级宽度应为 80%,嵌套在 layout/container 内时应为 100%,且支持自定义 width', () => {
const { rerender } = render(
<MermaidBlock
blockId="mm-top"
props={{
content: 'flowchart LR\n A-->B',
}}
location={{ jsonPath: '/content/0', idPath: [] }}
/>,
)
const fig = screen.getByTestId('mermaid-block')
expect(fig.style.width).toBe('80%')

rerender(
<MermaidBlock
blockId="mm-nested"
props={{
content: 'flowchart LR\n A-->B',
}}
location={{ jsonPath: '/content/0/children/0', idPath: ['container-1'] }}
/>,
)
expect(fig.style.width).toBe('100%')

rerender(
<MermaidBlock
blockId="mm-custom"
props={{
content: 'flowchart LR\n A-->B',
width: '60%',
}}
location={{ jsonPath: '/content/0', idPath: [] }}
/>,
)
expect(fig.style.width).toBe('60%')
})
})
9 changes: 8 additions & 1 deletion components/src/lpw/block/mermaid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@ import { MermaidViewport } from './mermaid-viewport'

export const MermaidBlock: React.FC<LpwBlockSlotProps<LpwMermaidProps>> = ({
props,
location,
}) => {
const fenced = '```mermaid\n' + props.content.trim() + '\n```'
const isNested = Boolean(location?.idPath && location.idPath.length > 0)
const targetWidth = props.width ? props.width : isNested ? '100%' : '80%'

return (
<figure data-testid="mermaid-block" className="font-sans">
<figure
data-testid="mermaid-block"
className="font-sans"
style={{ width: targetWidth, maxWidth: '100%', margin: '0 auto' }}
>
<MermaidViewport>
<Markdown>{fenced}</Markdown>
</MermaidViewport>
Expand Down
Loading
Loading