0911-m
This commit is contained in:
parent
21a21c1dfd
commit
29e674a81b
81
src/App.vue
81
src/App.vue
@ -11,6 +11,9 @@ const palette: PaletteItem[] = [...elements]
|
|||||||
const viewportRef = ref<HTMLDivElement | null>(null)
|
const viewportRef = ref<HTMLDivElement | null>(null)
|
||||||
const worldRef = ref<HTMLDivElement | null>(null)
|
const worldRef = ref<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
// 右侧 AI 面板显示开关
|
||||||
|
const showAI = ref(true)
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
scale: 1,
|
scale: 1,
|
||||||
minScale: 0.2,
|
minScale: 0.2,
|
||||||
@ -277,6 +280,39 @@ function isNearest(instId: string, cpIndex: number) {
|
|||||||
return !!n && n.instId === instId && n.cpIndex === cpIndex
|
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)
|
// 7.5) 直流电路求解(MNA)
|
||||||
// - 支持:电压源(battery)、电阻(resistor/light_bulb/meter)、开关(ON=0V源,OFF=断开)、电感(DC=0V源)、电容(DC=断开)
|
// - 支持:电压源(battery)、电阻(resistor/light_bulb/meter)、开关(ON=0V源,OFF=断开)、电感(DC=0V源)、电容(DC=断开)
|
||||||
// - 结果:每个实例的端口电压差与电流(方向:从连接点0 -> 连接点1 为正)
|
// - 结果:每个实例的端口电压差与电流(方向:从连接点0 -> 连接点1 为正)
|
||||||
@ -861,7 +897,7 @@ function deleteSelected() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="app">
|
<div class="app" :style="{ '--ai-width': showAI ? '360px' : '0px' }">
|
||||||
<!-- 左侧元件面板 -->
|
<!-- 左侧元件面板 -->
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<h2>元件</h2>
|
<h2>元件</h2>
|
||||||
@ -972,23 +1008,28 @@ function deleteSelected() {
|
|||||||
:key="inst.id + '-' + idx">
|
:key="inst.id + '-' + idx">
|
||||||
<div class="endpoint" :class="{
|
<div class="endpoint" :class="{
|
||||||
active: pendingEndpoint && pendingEndpoint.instId === inst.id && pendingEndpoint.cpIndex === idx,
|
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="{
|
}" :style="{
|
||||||
left: getConnectionPointWorldPos(inst, idx).x + 'px',
|
left: getConnectionPointWorldPos(inst, idx).x + 'px',
|
||||||
top: getConnectionPointWorldPos(inst, idx).y + '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>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- 右侧 AI 助手面板 -->
|
<!-- 右侧 AI 助手面板 -->
|
||||||
<aside class="ai-aside">
|
<aside v-if="showAI" class="ai-aside">
|
||||||
<AIAssistant :state="aiState" @error="onAiError" />
|
<AIAssistant :state="aiState" @error="onAiError" @close="showAI = false" />
|
||||||
</aside>
|
</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="panel-inner">
|
||||||
<div class="prop-title">{{ selectedInst.key }} 属性</div>
|
<div class="prop-title">{{ selectedInst.key }} 属性</div>
|
||||||
<!-- 预设按钮区域:显示当前元件的预设,点击一键应用 -->
|
<!-- 预设按钮区域:显示当前元件的预设,点击一键应用 -->
|
||||||
@ -1044,7 +1085,7 @@ function deleteSelected() {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.app {
|
.app {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 140px 1fr 360px; /* 左栏 / 画布 / AI 辅助 */
|
grid-template-columns: 140px 1fr var(--ai-width, 360px); /* 左栏 / 画布 / AI 辅助 */
|
||||||
grid-template-rows: 100vh;
|
grid-template-rows: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@ -1159,6 +1200,23 @@ function deleteSelected() {
|
|||||||
overflow: hidden;
|
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 {
|
.world {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -1284,11 +1342,18 @@ function deleteSelected() {
|
|||||||
transform: translate(-50%, -50%) scale(1.25);
|
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 {
|
.bottom-panel {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 140px;
|
left: 140px;
|
||||||
/* 与左侧栏对齐 */
|
/* 与左侧栏对齐 */
|
||||||
right: 360px; /* 让出 AI 侧栏宽度 */
|
right: 360px; /* 默认让出 AI 侧栏宽度,实际由内联样式覆盖 */
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: #f9fafb;
|
background: #f9fafb;
|
||||||
border-top: 1px solid #e5e7eb;
|
border-top: 1px solid #e5e7eb;
|
||||||
|
|||||||
@ -11,6 +11,7 @@ const props = defineProps<{
|
|||||||
// 向父组件发出事件:可选,用于保存/载入 AI 偏好设置
|
// 向父组件发出事件:可选,用于保存/载入 AI 偏好设置
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'error', err: string): void
|
(e: 'error', err: string): void
|
||||||
|
(e: 'close'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
let client: OpenAI = new OpenAI({
|
let client: OpenAI = new OpenAI({
|
||||||
@ -109,6 +110,7 @@ onMounted(() => nextTick(scrollToBottom))
|
|||||||
<div class="ai-panel">
|
<div class="ai-panel">
|
||||||
<header class="ai-header">
|
<header class="ai-header">
|
||||||
<div class="title">AI 助手</div>
|
<div class="title">AI 助手</div>
|
||||||
|
<button class="close-btn" title="关闭" @click="emit('close')">×</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="chat-list" ref="listRef">
|
<div class="chat-list" ref="listRef">
|
||||||
@ -152,6 +154,18 @@ onMounted(() => nextTick(scrollToBottom))
|
|||||||
color: #111827;
|
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 {
|
.key-box {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user