This commit is contained in:
feie9454 2025-09-11 10:19:03 +08:00
parent 21a21c1dfd
commit 29e674a81b
2 changed files with 87 additions and 8 deletions

View File

@ -11,6 +11,9 @@ const palette: PaletteItem[] = [...elements]
const viewportRef = ref<HTMLDivElement | null>(null)
const worldRef = ref<HTMLDivElement | null>(null)
// AI
const showAI = ref(true)
const state = reactive({
scale: 1,
minScale: 0.2,
@ -277,6 +280,39 @@ function isNearest(instId: string, cpIndex: number) {
return !!n && n.instId === instId && n.cpIndex === cpIndex
}
//
const hoveredEndpoint = ref<EndpointRef | null>(null)
const connectedEndpointSet = computed<Set<string>>(() => {
const set = new Set<string>()
const start = hoveredEndpoint.value
if (!start) return set
// 线
const key = (ep: EndpointRef) => `${ep.instId}:${ep.cpIndex}`
const adj = new Map<string, string[]>()
const add = (a: EndpointRef, b: EndpointRef) => {
const ka = key(a), kb = key(b)
if (!adj.has(ka)) adj.set(ka, [])
if (!adj.has(kb)) adj.set(kb, [])
adj.get(ka)!.push(kb)
adj.get(kb)!.push(ka)
}
for (const w of wires) add(w.a, w.b)
// BFS/DFS
const startKey = key(start)
const stack = [startKey]
set.add(startKey)
while (stack.length) {
const cur = stack.pop()!
for (const nxt of adj.get(cur) || []) {
if (!set.has(nxt)) { set.add(nxt); stack.push(nxt) }
}
}
return set
})
function isConnectedEndpoint(instId: string, cpIndex: number) {
return connectedEndpointSet.value.has(`${instId}:${cpIndex}`)
}
// 7.5) MNA
// - (battery)(resistor/light_bulb/meter)(ON=0VOFF=)(DC=0V)(DC=)
// - 0 -> 1
@ -861,7 +897,7 @@ function deleteSelected() {
</script>
<template>
<div class="app">
<div class="app" :style="{ '--ai-width': showAI ? '360px' : '0px' }">
<!-- 左侧元件面板 -->
<aside class="sidebar">
<h2>元件</h2>
@ -972,23 +1008,28 @@ function deleteSelected() {
:key="inst.id + '-' + idx">
<div class="endpoint" :class="{
active: pendingEndpoint && pendingEndpoint.instId === inst.id && pendingEndpoint.cpIndex === idx,
hover: isNearest(inst.id, idx)
hover: isNearest(inst.id, idx),
connected: hoveredEndpoint && isConnectedEndpoint(inst.id, idx)
}" :style="{
left: getConnectionPointWorldPos(inst, idx).x + 'px',
top: getConnectionPointWorldPos(inst, idx).y + 'px'
}" @click.stop="(e) => onEndpointClick(inst.id, idx, e)" :title="cp.name || ('P' + (idx + 1))"></div>
}" @click.stop="(e) => onEndpointClick(inst.id, idx, e)"
@mouseenter="() => { hoveredEndpoint = { instId: inst.id, cpIndex: idx } }"
@mouseleave="() => { if (hoveredEndpoint && hoveredEndpoint.instId === inst.id && hoveredEndpoint.cpIndex === idx) hoveredEndpoint = null }"
:title="cp.name || ('P' + (idx + 1))"></div>
</template>
</div>
</div>
</main>
<!-- 右侧 AI 助手面板 -->
<aside class="ai-aside">
<AIAssistant :state="aiState" @error="onAiError" />
<aside v-if="showAI" class="ai-aside">
<AIAssistant :state="aiState" @error="onAiError" @close="showAI = false" />
</aside>
<button v-else class="ai-reopen" title="打开 AI 助手" @click="showAI = true"></button>
<!-- 底部属性面板编辑选中元件的业务属性非尺寸 -->
<footer v-if="selectedInst" class="bottom-panel">
<footer v-if="selectedInst" class="bottom-panel" :style="{ right: showAI ? '360px' : '0' }">
<div class="panel-inner">
<div class="prop-title">{{ selectedInst.key }} 属性</div>
<!-- 预设按钮区域显示当前元件的预设点击一键应用 -->
@ -1044,7 +1085,7 @@ function deleteSelected() {
<style scoped>
.app {
display: grid;
grid-template-columns: 140px 1fr 360px; /* 左栏 / 画布 / AI 辅助 */
grid-template-columns: 140px 1fr var(--ai-width, 360px); /* 左栏 / 画布 / AI 辅助 */
grid-template-rows: 100vh;
overflow: hidden;
}
@ -1159,6 +1200,23 @@ function deleteSelected() {
overflow: hidden;
}
/* 右侧重新打开箭头按钮(在隐藏时显示) */
.ai-reopen {
position: fixed;
right: 8px;
top: 50%;
transform: translateY(-50%);
z-index: 10;
padding: 8px 10px;
border: 1px solid #e5e7eb;
border-radius: 999px 0 0 999px;
background: #fff;
color: #111827;
cursor: pointer;
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}
.ai-reopen:hover { background: #f3f4f6; }
.world {
position: absolute;
left: 0;
@ -1284,11 +1342,18 @@ function deleteSelected() {
transform: translate(-50%, -50%) scale(1.25);
}
/* 连通端点高亮:当某端点悬停时,其所有通过导线连通的端点以强调色显示 */
.endpoint.connected {
border-color: #10b981; /* teal-500 */
background: #ecfdf5; /* teal-50 */
transform: translate(-50%, -50%) scale(1.25);
}
.bottom-panel {
position: fixed;
left: 140px;
/* 与左侧栏对齐 */
right: 360px; /* 让出 AI 侧栏宽度 */
right: 360px; /* 默认让出 AI 侧栏宽度,实际由内联样式覆盖 */
bottom: 0;
background: #f9fafb;
border-top: 1px solid #e5e7eb;

View File

@ -11,6 +11,7 @@ const props = defineProps<{
// / AI
const emit = defineEmits<{
(e: 'error', err: string): void
(e: 'close'): void
}>()
let client: OpenAI = new OpenAI({
@ -109,6 +110,7 @@ onMounted(() => nextTick(scrollToBottom))
<div class="ai-panel">
<header class="ai-header">
<div class="title">AI 助手</div>
<button class="close-btn" title="关闭" @click="emit('close')">×</button>
</header>
<div class="chat-list" ref="listRef">
@ -152,6 +154,18 @@ onMounted(() => nextTick(scrollToBottom))
color: #111827;
}
.close-btn {
padding: 4px 8px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: #fff;
color: #111827;
cursor: pointer;
font-weight: 700;
line-height: 1;
}
.close-btn:hover { background: #f3f4f6; }
.key-box {
flex: 1;
display: flex;