✨ feat(vscode): FastAPI 接口卡片的调用树带上前端调用它的地方

This commit is contained in:
cruldra committed 2026-10-12 12:22:51 +11:00
1 parent d4459e93b3
commit 29cedcb751
30 files changed
+1316 -130

No files matched your search

@@ -1,21 +1,28 @@
/**
* 调用树弹框:盖在编辑区上,预览的卡片留在后面。点遮罩、点叉、按 Esc 关掉,焦点回到打开它的按钮。
* 点树上的一行跳到它在的文件和行。接口在契约文件里没对上时写明原因,不当成没人调用。
*/
import type { ReactElement } from 'react'
import type { CallTree, CallTreeNode } from './types'
import { Box, Braces, X } from 'lucide-react'
import type { CallJump, CallTree, CallTreeNode, ContractMiss, KindBadge } from './types'
import { Box, Braces, Send, X } from 'lucide-react'
import { useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import { kindProps } from './BlockTree'
export function CallTreeDialog({ kind, name, where, status, trees, onClose }: {
/** 当前块的种类,整条路径用这个种类色 */
export function CallTreeDialog({ kind, badge, name, where, status, trees, contractMiss, onClose, onJump }: {
/** 当前块的种类,整条路径用这个种类色;库规则的卡片按 badge 的颜色 */
kind: string
badge?: KindBadge
name: string
where: string
status: 'loading' | 'empty' | 'ready'
trees: readonly CallTree[]
/** 接口在契约文件里没对上时有 */
contractMiss?: ContractMiss
onClose: () => void
/** 点了树上的一行 */
onJump: (jump: CallJump) => void
}): ReactElement {
const [selected, setSelected] = useState(0)
const tree = trees[Math.min(selected, Math.max(trees.length - 1, 0))]
@@ -49,7 +56,7 @@ export function CallTreeDialog({ kind, name, where, status, trees, onClose }: {
aria-modal="true"
aria-label={name ? `调用树 ${name}` : '调用树'}
className="cr-call-dialog"
data-kind={kind}
{...kindProps({ kind, badge })}
>
<div className="cr-call-title">
<span className="cr-call-heading">调用树</span>
@@ -79,8 +86,9 @@ export function CallTreeDialog({ kind, name, where, status, trees, onClose }: {
)}
<div className="cr-call-body">
{status === 'loading' && <p className="cr-call-note">正在查调用关系</p>}
{status === 'empty' && <p className="cr-call-note">没有调用它的地方</p>}
{status === 'ready' && tree && <CallNodes node={tree.root} depth={0} path={tree.id} />}
{status !== 'loading' && contractMiss && <ContractNote miss={contractMiss} />}
{status === 'empty' && !contractMiss && <p className="cr-call-note">没有调用它的地方</p>}
{status === 'ready' && tree && <CallNodes node={tree.root} depth={0} path={tree.id} onJump={onJump} />}
</div>
</div>
</div>,
@@ -88,26 +96,48 @@ export function CallTreeDialog({ kind, name, where, status, trees, onClose }: {
)
}
function CallNodes({ node, depth, path }: { node: CallTreeNode, depth: number, path: string }): ReactElement {
const Icon = node.kind === 'class' ? Box : Braces
/**
* 接口在契约文件里没对上:分不出是哪一条时列出那几条;没有这一条时让人重新生成、列出找到的契约文件;
* 一份契约文件都没有时说没找到。
*/
function ContractNote({ miss }: { miss: ContractMiss }): ReactElement {
if (miss.files.length === 0)
return <p className="cr-call-note">工作区里没找到接口契约文件(openapi-typescript 生成的 .d.ts 或 openapi.json)</p>
const [text, items] = miss.ambiguous
? ['接口契约文件里有几条都对得上这个接口,按 operationId 也分不出是哪一条', miss.ambiguous]
: ['接口契约文件里没有这个接口,重新生成后再看', miss.files]
return (
<div className="cr-call-note">
<p>{text}</p>
<ul className="cr-call-files">
{items.map(item => <li key={item} className="cr-call-loc">{item}</li>)}
</ul>
</div>
)
}
const NODE_ICONS: Record<string, typeof Braces> = { class: Box, request: Send }
function CallNodes({ node, depth, path, onJump }: { node: CallTreeNode, depth: number, path: string, onJump: (jump: CallJump) => void }): ReactElement {
const Icon = NODE_ICONS[node.kind] ?? Braces
return (
<>
<div className="cr-call-row" style={{ paddingLeft: depth * 22 }}>
<span className="cr-call-guide" />
<div className="cr-call-card" data-current={node.current ? '' : undefined}>
<div className="cr-call-line">
<button type="button" className="cr-call-card" data-current={node.current ? '' : undefined} onClick={() => onJump(node.jump)}>
<span className="cr-call-line">
<Icon size={14} strokeWidth={1.8} aria-hidden />
<span className="cr-call-name">{node.name}</span>
{node.current && <span className="cr-call-pill">当前</span>}
</div>
<div className="cr-call-sub">
</span>
<span className="cr-call-sub">
{node.detail && <span>{node.detail}</span>}
<span className="cr-call-loc">{node.where}</span>
</div>
</div>
</span>
</button>
</div>
{node.children.map((child, index) => (
<CallNodes key={`${path}/${index}`} node={child} depth={depth + 1} path={`${path}/${index}`} />
<CallNodes key={`${path}/${index}`} node={child} depth={depth + 1} path={`${path}/${index}`} onJump={onJump} />
))}
{node.otherCalls !== undefined && node.otherCalls > 0 && (
<div className="cr-call-row" style={{ paddingLeft: (depth + 1) * 22 }}>