Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab into dpp

This commit is contained in:
alivender 2025-05-14 19:45:10 +08:00
commit 48ae3b5975
25 changed files with 4409 additions and 1591 deletions

View File

@ -39,9 +39,11 @@ run-web:
npm run build
npm run preview
dev: dev-server
# 测试服务器
dev-server: _show-dir
cd server && dotnet run
cd server && dotnet run --watch
# 运行网页客户端
dev-web:

14
package-lock.json generated
View File

@ -25,7 +25,6 @@
"@tailwindcss/postcss": "^4.0.12",
"@tsconfig/node22": "^22.0.0",
"@types/node": "^22.13.4",
"@vitejs/plugin-basic-ssl": "^2.0.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"@vue/tsconfig": "^0.7.0",
@ -1647,19 +1646,6 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@vitejs/plugin-basic-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.0.0.tgz",
"integrity": "sha512-gc9Tjg8bUxBVSTzeWT3Njc0Cl3PakHFKdNfABnZWiUgbxqmHDEn7uECv3fHVylxoYgNzAcmU7ZrILz+BwSo3sA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"peerDependencies": {
"vite": "^6.0.0"
}
},
"node_modules/@vitejs/plugin-vue": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz",

View File

@ -31,7 +31,6 @@
"@tailwindcss/postcss": "^4.0.12",
"@tsconfig/node22": "^22.0.0",
"@types/node": "^22.13.4",
"@vitejs/plugin-basic-ssl": "^2.0.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"@vue/tsconfig": "^0.7.0",

View File

@ -192,26 +192,6 @@ public class JtagController : ControllerBase
return "This is Jtag Controller";
}
/// <summary>
/// 执行一个Jtag命令
/// </summary>
/// <param name="address"> 设备地址 </param>
/// <param name="port"> 设备端口 </param>
/// <param name="hexDevAddr"> 16进制设备目的地址(Jtag) </param>
/// <param name="hexCmd"> 16进制命令 </param>
[HttpPost("RunCommand")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> RunCommand(string address, int port, string hexDevAddr, string hexCmd)
{
var jtagCtrl = new JtagClient.Jtag(address, port);
var ret = await jtagCtrl.WriteFIFO(Convert.ToUInt32(hexDevAddr, 16), Convert.ToUInt32(hexCmd, 16));
if (ret.IsSuccessful) { return TypedResults.Ok(ret.Value); }
else { return TypedResults.InternalServerError(ret.Error); }
}
/// <summary>
/// 获取Jtag ID Code
/// </summary>
@ -349,6 +329,11 @@ public class JtagController : ControllerBase
return TypedResults.InternalServerError(retBuffer.Error);
revBuffer = retBuffer.Value;
for (int i = 0; i < revBuffer.Length; i++)
{
revBuffer[i] = Common.Number.ReverseBits(revBuffer[i]);
}
await memoryStream.WriteAsync(revBuffer, 0, bytesRead);
totalBytesRead += bytesRead;
}

View File

@ -432,199 +432,44 @@ public class Jtag
return Convert.ToUInt32(Common.Number.BytesToUInt64(retPackOpts.Data).Value);
}
/// <summary>
/// 向指定的 JTAG 设备地址写入数据到 FIFO
/// </summary>
/// <param name="devAddr">目标设备地址</param>
/// <param name="data">要写入的数据</param>
/// <param name="delayMilliseconds">写入后的延迟时间(毫秒)</param>
/// <returns>包含接收数据包的异步结果</returns>
public async ValueTask<Result<RecvDataPackage>> WriteFIFO(UInt32 devAddr, UInt32 data, UInt32 delayMilliseconds = 0)
async ValueTask<Result<bool>> WriteFIFO
(UInt32 devAddr, UInt32 data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0)
{
var ret = false;
var opts = new SendAddrPackOptions();
opts.BurstType = BurstType.FixedBurst;
opts.BurstLength = 0;
opts.CommandID = 0;
opts.Address = devAddr;
// Write Jtag State Register
opts.IsWrite = true;
ret = await UDPClientPool.SendAddrPackAsync(ep, new SendAddrPackage(opts));
if (!ret) return new(new Exception("Send 1st address package failed!"));
// Send Data Package
ret = await UDPClientPool.SendDataPackAsync(ep,
new SendDataPackage(Common.Number.NumberToBytes(data, 4).Value));
if (!ret) return new(new Exception("Send data package failed!"));
// Check Msg Bus
if (!MsgBus.IsRunning)
return new(new Exception("Message bus not working!"));
// Wait for Write Ack
var udpWriteAck = await MsgBus.UDPServer.WaitForAckAsync(address, port);
if (!udpWriteAck.IsSuccessful) return new(udpWriteAck.Error);
else if (!udpWriteAck.Value.IsSuccessful)
return new(new Exception("Send address package failed"));
// Delay some time before read register
await Task.Delay(TimeSpan.FromMilliseconds(delayMilliseconds));
// Read Jtag State Register
opts.IsWrite = false;
opts.Address = JtagAddr.STATE;
ret = await UDPClientPool.SendAddrPackAsync(ep, new SendAddrPackage(opts));
if (!ret) return new(new Exception("Send 2rd Address Package Failed!"));
// Wait for Read Data
var udpDataResp = await MsgBus.UDPServer.WaitForDataAsync(address, port);
if (!udpDataResp.IsSuccessful) return new(udpDataResp.Error);
else if (!udpDataResp.Value.IsSuccessful)
return new(new Exception("Send address package failed"));
return udpDataResp.Value;
}
async ValueTask<Result<RecvDataPackage>> WriteFIFO(UInt32 devAddr, byte[] dataArray, UInt32 delayMilliseconds = 0)
{
var ret = false;
var opts = new SendAddrPackOptions();
opts.BurstType = BurstType.FixedBurst;
opts.CommandID = 0;
opts.Address = devAddr;
// Check Msg Bus
if (!MsgBus.IsRunning)
return new(new Exception("Message bus not working!"));
var writeTimes = dataArray.Length / (256 * (32 / 8)) + 1;
for (var i = 0; i < writeTimes; i++)
{
// Sperate Data Array
var isLastData = i == writeTimes - 1;
var sendDataArray =
isLastData ?
dataArray[(i * (256 * (32 / 8)))..] :
dataArray[(i * (256 * (32 / 8)))..((i + 1) * (256 * (32 / 8)))];
// Write Jtag State Register
opts.IsWrite = true;
opts.BurstLength = ((byte)(sendDataArray.Length / 4 - 1));
ret = await UDPClientPool.SendAddrPackAsync(ep, new SendAddrPackage(opts));
if (!ret) return new(new Exception("Send 1st address package failed!"));
// Send Data Package
ret = await UDPClientPool.SendDataPackAsync(ep, new SendDataPackage(sendDataArray));
if (!ret) return new(new Exception("Send data package failed!"));
// Wait for Write Ack
var udpWriteAck = await MsgBus.UDPServer.WaitForAckAsync(address, port);
if (!udpWriteAck.IsSuccessful) return new(udpWriteAck.Error);
else if (!udpWriteAck.Value.IsSuccessful)
return new(new Exception("Send address package failed"));
var ret = await UDPClientPool.WriteAddr(this.ep, devAddr, data, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception("Write FIFO failed"));
}
// Delay some time before read register
await Task.Delay(TimeSpan.FromMilliseconds(delayMilliseconds));
// Read Jtag State Register
opts.IsWrite = false;
opts.BurstLength = 0;
opts.Address = JtagAddr.STATE;
ret = await UDPClientPool.SendAddrPackAsync(ep, new SendAddrPackage(opts));
if (!ret) return new(new Exception("Send 2rd Address Package Failed!"));
// Wait for Read Data
var udpDataResp = await MsgBus.UDPServer.WaitForDataAsync(address, port);
if (!udpDataResp.IsSuccessful) return new(udpDataResp.Error);
else if (!udpDataResp.Value.IsSuccessful)
return new(new Exception("Send address package failed"));
return udpDataResp.Value;
}
async ValueTask<Result<bool>> WriteFIFO
(UInt32 devAddr, UInt32 data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0)
{
var ret = false;
var retPack = await WriteFIFO(devAddr, data, delayMilliseconds);
if (!retPack.IsSuccessful) return new(retPack.Error);
if (retPack.Value.Options.Data is null)
return new(new Exception($"Data is Null, package: {retPack.Value.Options.ToString()}"));
var retPackLen = retPack.Value.Options.Data.Length;
if (retPackLen != 4)
return new(new Exception($"RecvDataPackage BodyData Length not Equal to 4: Total {retPackLen} bytes"));
if (Common.Number.BitsCheck(
Common.Number.BytesToUInt64(retPack.Value.Options.Data).Value, result, resultMask))
ret = true;
return ret;
{
var ret = await UDPClientPool.ReadAddrWithWait(this.ep, JtagAddr.STATE, result, resultMask, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
}
}
async ValueTask<Result<bool>> WriteFIFO
(UInt32 devAddr, byte[] data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0)
{
var ret = false;
var retPack = await WriteFIFO(devAddr, data, delayMilliseconds);
if (retPack.Value.Options.Data is null)
return new(new Exception($"Data is Null, package: {retPack.Value.Options.ToString()}"));
var retPackLen = retPack.Value.Options.Data.Length;
if (retPackLen != 4)
return new(new Exception($"RecvDataPackage BodyData Length not Equal to 4: Total {retPackLen} bytes"));
if (Common.Number.BitsCheck(
Common.Number.BytesToUInt64(retPack.Value.Options.Data).Value, result, resultMask))
ret = true;
return ret;
}
async ValueTask<Result<bool>> WaitForWriteFIFO
(UInt32 devAddr, byte[] data, UInt32 result,
UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 timeout = 10_000, UInt32 cycle = 500)
{
{
var wrRet = await WriteFIFO(devAddr, data, result, resultMask);
if (!wrRet.IsSuccessful) return new(wrRet.Error);
if (wrRet.Value) return true;
var ret = await UDPClientPool.WriteAddr(this.ep, devAddr, data, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception("Write FIFO failed"));
}
// Wait some time
var ret = false;
var startTime = DateTime.Now;
var isTimeout = false;
var timeleft = TimeSpan.FromMilliseconds(timeout);
while (!isTimeout)
// Delay some time before read register
await Task.Delay(TimeSpan.FromMilliseconds(delayMilliseconds));
{
// Check whether timeout
var elapsed = DateTime.Now - startTime;
isTimeout = elapsed >= TimeSpan.FromMilliseconds(timeout);
if (isTimeout) break;
timeleft = TimeSpan.FromMilliseconds(timeout) - elapsed;
// Check FIFO
var retPack = await ReadFIFO(JtagAddr.STATE);
if (Common.Number.BitsCheck(retPack.Value, result, resultMask))
{
ret = true;
break;
}
// Wait
await Task.Delay(TimeSpan.FromMilliseconds(cycle));
var ret = await UDPClientPool.ReadAddrWithWait(this.ep, JtagAddr.STATE, result, resultMask, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
}
return ret;
}
/// <summary>
/// 清除所有 JTAG 寄存器
/// </summary>
@ -718,17 +563,16 @@ public class Jtag
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Write CMD_JTAG_LOAD_DR_CAREI Failed"));
}
{
var ret = await WaitForWriteFIFO(
var ret = await WriteFIFO(
JtagAddr.WRITE_DATA,
bytesArray, 0x01_00_00_00,
JtagState.CMD_EXEC_FINISH,
timeout, cycle);
JtagState.CMD_EXEC_FINISH);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Write Data Failed"));
return ret.Value;
}
return true;
}
async ValueTask<Result<UInt32>> LoadDRCareOutput(UInt32 UInt32Num)
@ -753,7 +597,7 @@ public class Jtag
var ret = await WriteFIFO(
JtagAddr.WRITE_CMD,
Common.Number.MultiBitsToNumber(JtagCmd.CMD_JTAG_LOAD_DR_CAREO, JtagCmd.LEN_CMD_JTAG, 32 * UInt32Num, 28).Value,
0x01_00_00_00, JtagState.CMD_EXEC_FINISH);
JtagState.CMD_EXEC_FINISH, JtagState.CMD_EXEC_FINISH);
if (ret.Value)
{
@ -955,11 +799,10 @@ public class Jtag
ret = await ExecRDCmd(JtagCmd.JTAG_DR_SAMPLE);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_JRST Failed"));
else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_SAMPLE Failed"));
var retData = await LoadDRCareOutputArray(((uint)(portNum % 32 == 0 ? portNum / 32 : portNum / 32 + 1)));
if (!retData.IsSuccessful)
return new(new Exception("Read Status Reg Failed"));
if (!retData.IsSuccessful) return new(retData.Error);
ret = await CloseTest();
if (!ret.IsSuccessful) return new(ret.Error);

View File

@ -1,7 +1,10 @@
<script setup lang="ts">
import Navbar from "./components/Navbar.vue";
import Dialog from "./components/Dialog.vue";
import { ref, provide, onMounted } from "vue";
import { ref, provide, computed, onMounted } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
//
const isDarkMode = ref(
@ -42,6 +45,10 @@ provide("theme", {
isDarkMode,
toggleTheme,
});
const currentRoutePath = computed(() => {
return router.currentRoute.value.path;
});
</script>
<template>
@ -54,7 +61,7 @@ provide("theme", {
<main>
<RouterView />
</main>
<footer class="footer footer-center p-4 bg-base-300 text-base-content">
<footer v-if="currentRoutePath != '/project'" class="footer footer-center p-4 bg-base-300 text-base-content">
<div>
<p>Copyright © 2023 - All right reserved by OurEDA</p>
</div>

1
src/assets/doc.svg Normal file
View File

@ -0,0 +1 @@
<svg t="1747137150839" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5910" width="200" height="200"><path d="M224 831.936V192.096L223.808 192H576v159.936c0 35.328 28.736 64.064 64.064 64.064h159.712c0.032 0.512 0.224 1.184 0.224 1.664L800.256 832 224 831.936zM757.664 352L640 351.936V224.128L757.664 352z m76.064-11.872l-163.872-178.08C651.712 142.336 619.264 128 592.672 128H223.808A64.032 64.032 0 0 0 160 192.096v639.84A64 64 0 0 0 223.744 896h576.512A64 64 0 0 0 864 831.872V417.664c0-25.856-12.736-58.464-30.272-77.536z" fill="#3E3A39" p-id="5911"></path><path d="M640 512h-256a32 32 0 0 0 0 64h256a32 32 0 0 0 0-64M640 672h-256a32 32 0 0 0 0 64h256a32 32 0 0 0 0-64" fill="#3E3A39" p-id="5912"></path></svg>

After

Width:  |  Height:  |  Size: 759 B

1
src/assets/refresh.svg Normal file
View File

@ -0,0 +1 @@
<svg t="1747136937808" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4865" width="200" height="200"><path d="M894.481158 505.727133c0 49.589418-9.711176 97.705276-28.867468 143.007041-18.501376 43.74634-44.98454 83.031065-78.712713 116.759237-33.728172 33.728172-73.012897 60.211337-116.759237 78.712713-45.311998 19.156292-93.417623 28.877701-143.007041 28.877701s-97.695043-9.721409-142.996808-28.877701c-43.756573-18.501376-83.031065-44.98454-116.76947-78.712713-33.728172-33.728172-60.211337-73.012897-78.712713-116.759237-19.156292-45.301765-28.867468-93.417623-28.867468-143.007041 0-49.579185 9.711176-97.695043 28.867468-142.996808 18.501376-43.74634 44.98454-83.031065 78.712713-116.759237 33.738405-33.728172 73.012897-60.211337 116.76947-78.712713 45.301765-19.166525 93.40739-28.877701 142.996808-28.877701 52.925397 0 104.008842 11.010775 151.827941 32.745798 46.192042 20.977777 86.909395 50.79692 121.016191 88.608084 4.389984 4.860704 8.646937 9.854439 12.781094 14.97097l0-136.263453c0-11.307533 9.168824-20.466124 20.466124-20.466124 11.307533 0 20.466124 9.15859 20.466124 20.466124l0 183.64253c0 5.433756-2.148943 10.632151-5.986341 14.46955-3.847631 3.837398-9.046027 5.996574-14.479783 5.996574l-183.64253-0.020466c-11.307533 0-20.466124-9.168824-20.466124-20.466124 0-11.307533 9.168824-20.466124 20.466124-20.466124l132.293025 0.020466c-3.960195-4.952802-8.063653-9.782807-12.289907-14.479783-30.320563-33.605376-66.514903-60.098773-107.549481-78.753645-42.467207-19.289322-87.850837-29.072129-134.902456-29.072129-87.195921 0-169.172981 33.9533-230.816946 95.597265-61.654198 61.654198-95.597265 143.621025-95.597265 230.816946s33.943067 169.172981 95.597265 230.816946c61.643965 61.654198 143.621025 95.607498 230.816946 95.607498s169.172981-33.9533 230.816946-95.607498c61.654198-61.643965 95.597265-143.621025 95.597265-230.816946 0-11.2973 9.168824-20.466124 20.466124-20.466124C885.322567 485.261009 894.481158 494.429833 894.481158 505.727133z" p-id="4866"></path></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -3,35 +3,39 @@
interface Props {
title: string;
isExpanded?: boolean;
status?: 'default' | 'success' | 'error';
status?: "default" | "success" | "error";
}
const props = withDefaults(defineProps<Props>(), {
isExpanded: false,
status: 'default'
status: "default",
});
const emit = defineEmits<{
(e: 'update:isExpanded', value: boolean): void
(e: "update:isExpanded", value: boolean): void;
}>();
// /
const toggleExpand = () => {
emit('update:isExpanded', !props.isExpanded);
emit("update:isExpanded", !props.isExpanded);
};
//
const enter = (element: Element, done: () => void) => {
if (element instanceof HTMLElement) {
const height = element.scrollHeight;
element.style.height = '0px';
element.style.height = "0px";
//
element.offsetHeight;
element.style.height = height + 'px';
element.addEventListener('transitionend', () => {
done();
}, { once: true });
element.style.height = height + "px";
element.addEventListener(
"transitionend",
() => {
done();
},
{ once: true },
);
} else {
done();
}
@ -39,21 +43,25 @@ const enter = (element: Element, done: () => void) => {
const afterEnter = (element: Element) => {
if (element instanceof HTMLElement) {
element.style.height = 'auto';
element.style.height = "auto";
}
};
const leave = (element: Element, done: () => void) => {
if (element instanceof HTMLElement) {
const height = element.scrollHeight;
element.style.height = height + 'px';
element.style.height = height + "px";
//
element.offsetHeight;
element.style.height = '0px';
element.addEventListener('transitionend', () => {
done();
}, { once: true });
element.style.height = "0px";
element.addEventListener(
"transitionend",
() => {
done();
},
{ once: true },
);
} else {
done();
}
@ -61,17 +69,12 @@ const leave = (element: Element, done: () => void) => {
</script>
<template>
<div class="section" :class="[`status-${status}`]">
<div class="section-header" @click="toggleExpand">
<div class="section m-4 shadow-xl" :class="[`status-${status}`]">
<div class="section-header bg-primary text-primary-content" @click="toggleExpand">
<h2>{{ title }}</h2>
<span class="expand-icon" :class="{ 'is-expanded': isExpanded }"></span>
</div>
<transition
name="collapse"
@enter="enter"
@after-enter="afterEnter"
@leave="leave"
>
<transition name="collapse" @enter="enter" @after-enter="afterEnter" @leave="leave">
<div v-show="isExpanded" class="section-content">
<div class="section-inner">
<slot></slot>
@ -88,7 +91,6 @@ const leave = (element: Element, done: () => void) => {
border-radius: var(--radius-md, 0.375rem);
overflow: hidden;
background-color: hsl(var(--b1));
box-shadow: var(--shadow-sm, 0 1px 2px 0 rgba(0, 0, 0, 0.05));
transition: all var(--transition-normal, 0.3s);
}
@ -119,26 +121,58 @@ const leave = (element: Element, done: () => void) => {
}
@keyframes borderPulseSuccess {
0% { border-color: hsl(var(--b3)); box-shadow: 0px 0px 0px transparent;}
50% { border-color: hsl(var(--su)); box-shadow: 0px 0px 5px hsl(var(--su));}
100% { border-color: hsl(var(--su)); box-shadow: 0px 0px 3px hsl(var(--su));}
0% {
border-color: hsl(var(--b3));
box-shadow: 0px 0px 0px transparent;
}
50% {
border-color: hsl(var(--su));
box-shadow: 0px 0px 5px hsl(var(--su));
}
100% {
border-color: hsl(var(--su));
box-shadow: 0px 0px 3px hsl(var(--su));
}
}
@keyframes borderPulseError {
0% { border-color: hsl(var(--b3)); box-shadow: 0px 0px 0px transparent;}
50% { border-color: hsl(var(--er)); box-shadow: 0px 0px 5px hsl(var(--er));}
100% { border-color: hsl(var(--er)); box-shadow: 0px 0px 3px hsl(var(--er));}
0% {
border-color: hsl(var(--b3));
box-shadow: 0px 0px 0px transparent;
}
50% {
border-color: hsl(var(--er));
box-shadow: 0px 0px 5px hsl(var(--er));
}
100% {
border-color: hsl(var(--er));
box-shadow: 0px 0px 3px hsl(var(--er));
}
}
@keyframes borderPulseInfo {
0% { border-color: hsl(var(--b3)); box-shadow: 0px 0px 0px transparent;}
50% { border-color: hsl(var(--in)); box-shadow: 0px 0px 5px hsl(var(--in));}
100% { border-color: hsl(var(--in)); box-shadow: 0px 0px 3px hsl(var(--in));}
0% {
border-color: hsl(var(--b3));
box-shadow: 0px 0px 0px transparent;
}
50% {
border-color: hsl(var(--in));
box-shadow: 0px 0px 5px hsl(var(--in));
}
100% {
border-color: hsl(var(--in));
box-shadow: 0px 0px 3px hsl(var(--in));
}
}
.section-header {
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 0.75rem);
background-color: hsl(var(--b1));
cursor: pointer;
display: flex;
justify-content: space-between;
@ -148,10 +182,6 @@ const leave = (element: Element, done: () => void) => {
transition: all var(--transition-normal, 0.3s);
}
.section-header:hover {
background-color: hsl(var(--b2));
}
.section-header h2 {
margin: 0;
font-size: 1.1em;
@ -188,10 +218,12 @@ const leave = (element: Element, done: () => void) => {
}
.content-wrapper {
overflow: visible; /* 允许内容溢出 */
overflow: visible;
/* 允许内容溢出 */
}
.card-body {
overflow: visible; /* 允许内容溢出 */
overflow: visible;
/* 允许内容溢出 */
}
</style>

View File

@ -49,17 +49,6 @@
测试功能
</router-link>
</li>
<li class="my-1 hover:translate-x-1 transition-all duration-300">
<router-link to="/test/jtag" class="text-base font-medium">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-70" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
<line x1="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line>
</svg>
JTAG测试
</router-link>
</li>
<li class="my-1 hover:translate-x-1 transition-all duration-300">
<a href="http://localhost:5000/swagger" target="_self" rel="noopener noreferrer"
class="text-base font-medium">

View File

@ -4,51 +4,50 @@
<div v-else>
<div class="mb-4 pb-4 border-b border-base-300">
<h4 class="font-semibold text-lg mb-1">{{ componentData?.type }}</h4>
<p class="text-xs text-base-content opacity-70">ID: {{ componentData?.id }}</p>
<p class="text-xs text-base-content opacity-70">类型: {{ componentData?.type }}</p>
<p class="text-xs text-base-content opacity-70">
ID: {{ componentData?.id }}
</p>
<p class="text-xs text-base-content opacity-70">
类型: {{ componentData?.type }}
</p>
</div>
<!-- 通用属性部分 -->
<CollapsibleSection
title="通用属性"
v-model:isExpanded="generalPropsExpanded"
status="default"
>
<CollapsibleSection title="通用属性" v-model:isExpanded="generalPropsExpanded" status="default">
<div class="space-y-4">
<div v-for="prop in getGeneralProps()" :key="prop.name" class="form-control">
<label class="label">
<span class="label-text">{{ prop.label || prop.name }}</span>
</label>
<!-- 根据 prop 类型选择输入控件 -->
<input
v-if="prop.type === 'number'"
type="number"
:placeholder="prop.label || prop.name"
class="input input-bordered input-sm w-full"
:value="getPropValue(componentData, prop.name)"
:disabled="prop.isReadOnly"
:min="prop.min"
:max="prop.max"
:step="prop.step"
@input="updateDirectProp(componentData.id, prop.name, parseFloat(($event.target as HTMLInputElement).value) || prop.default)"
/>
<input
v-else-if="prop.type === 'string'"
type="text"
:placeholder="prop.label || prop.name"
class="input input-bordered input-sm w-full"
:value="getPropValue(componentData, prop.name)"
:disabled="prop.isReadOnly"
@input="updateDirectProp(componentData.id, prop.name, ($event.target as HTMLInputElement).value)"
/>
<input v-if="prop.type === 'number'" type="number" :placeholder="prop.label || prop.name"
class="input input-bordered input-sm w-full" :value="getPropValue(componentData, prop.name)"
:disabled="prop.isReadOnly" :min="prop.min" :max="prop.max" :step="prop.step" @input="
updateDirectProp(
componentData.id,
prop.name,
parseFloat(($event.target as HTMLInputElement).value) ||
prop.default,
)
" />
<input v-else-if="prop.type === 'string'" type="text" :placeholder="prop.label || prop.name"
class="input input-bordered input-sm w-full" :value="getPropValue(componentData, prop.name)"
:disabled="prop.isReadOnly" @input="
updateDirectProp(
componentData.id,
prop.name,
($event.target as HTMLInputElement).value,
)
" />
<div v-else-if="prop.type === 'boolean'" class="flex items-center">
<input
type="checkbox"
class="checkbox checkbox-sm mr-2"
:checked="getPropValue(componentData, prop.name)"
:disabled="prop.isReadOnly"
@change="updateDirectProp(componentData.id, prop.name, ($event.target as HTMLInputElement).checked)"
/>
<input type="checkbox" class="checkbox checkbox-sm mr-2" :checked="getPropValue(componentData, prop.name)"
:disabled="prop.isReadOnly" @change="
updateDirectProp(
componentData.id,
prop.name,
($event.target as HTMLInputElement).checked,
)
" />
<span>{{ prop.label || prop.name }}</span>
</div>
</div>
@ -56,74 +55,69 @@
</CollapsibleSection>
<!-- 组件特有属性部分 -->
<CollapsibleSection
title="组件特有属性"
v-model:isExpanded="componentPropsExpanded"
status="default"
class="mt-4"
>
<CollapsibleSection title="组件特有属性" v-model:isExpanded="componentPropsExpanded" status="default" class="mt-4">
<div v-if="componentConfig && componentConfig.props" class="space-y-4">
<div v-for="prop in getComponentProps()" :key="prop.name" class="form-control">
<label class="label">
<span class="label-text">{{ prop.label || prop.name }}</span>
</label>
<!-- 根据 prop 类型选择输入控件 -->
<input
v-if="prop.type === 'string'"
type="text"
:placeholder="prop.label || prop.name"
class="input input-bordered input-sm w-full"
:value="componentData?.attrs?.[prop.name]"
:disabled="prop.isReadOnly"
@input="updateProp(componentData.id, prop.name, ($event.target as HTMLInputElement).value)"
/>
<input
v-else-if="prop.type === 'number'"
type="number"
:placeholder="prop.label || prop.name"
class="input input-bordered input-sm w-full"
:value="componentData?.attrs?.[prop.name]"
:disabled="prop.isReadOnly"
:min="prop.min"
:max="prop.max"
:step="prop.step"
@input="updateProp(componentData.id, prop.name, parseFloat(($event.target as HTMLInputElement).value) || prop.default)"
/>
<input v-if="prop.type === 'string'" type="text" :placeholder="prop.label || prop.name"
class="input input-bordered input-sm w-full" :value="componentData?.attrs?.[prop.name]"
:disabled="prop.isReadOnly" @input="
updateProp(
componentData.id,
prop.name,
($event.target as HTMLInputElement).value,
)
" />
<input v-else-if="prop.type === 'number'" type="number" :placeholder="prop.label || prop.name"
class="input input-bordered input-sm w-full" :value="componentData?.attrs?.[prop.name]"
:disabled="prop.isReadOnly" :min="prop.min" :max="prop.max" :step="prop.step" @input="
updateProp(
componentData.id,
prop.name,
parseFloat(($event.target as HTMLInputElement).value) ||
prop.default,
)
" />
<div v-else-if="prop.type === 'boolean'" class="flex items-center">
<input
type="checkbox"
class="checkbox checkbox-sm mr-2"
:checked="componentData?.attrs?.[prop.name]"
:disabled="prop.isReadOnly"
@change="updateProp(componentData.id, prop.name, ($event.target as HTMLInputElement).checked)"
/>
<input type="checkbox" class="checkbox checkbox-sm mr-2" :checked="componentData?.attrs?.[prop.name]"
:disabled="prop.isReadOnly" @change="
updateProp(
componentData.id,
prop.name,
($event.target as HTMLInputElement).checked,
)
" />
<span>{{ prop.label || prop.name }}</span>
</div>
<select
v-else-if="prop.type === 'select' && prop.options"
class="select select-bordered select-sm w-full"
:value="componentData?.attrs?.[prop.name]"
:disabled="prop.isReadOnly"
@change="(event) => {
const selectElement = event.target as HTMLSelectElement;
const value = selectElement.value;
if (componentData) {
updateProp(componentData.id, prop.name, value);
</div>
<select v-else-if="prop.type === 'select' && prop.options" class="select select-bordered select-sm w-full"
:value="componentData?.attrs?.[prop.name]" :disabled="prop.isReadOnly" @change="
(event) => {
const selectElement = event.target as HTMLSelectElement;
const value = selectElement.value;
if (componentData) {
updateProp(componentData.id, prop.name, value);
}
}
}"
>
">
<option v-for="option in prop.options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
<p v-else class="text-xs text-warning">不支持的属性类型: {{ prop.type }}</p>
<p v-else class="text-xs text-warning">
不支持的属性类型: {{ prop.type }}
</p>
</div>
</div>
<div v-else-if="componentData && !componentConfig" class="text-base-content opacity-70 text-sm">
正在加载组件配置...
</div>
<div v-else-if="componentData && componentConfig && getComponentProps().length === 0" class="text-base-content opacity-70 text-sm">
<div v-else-if="
componentData && componentConfig && getComponentProps().length === 0
" class="text-base-content opacity-70 text-sm">
此组件没有特有属性可配置
</div>
</CollapsibleSection>
@ -132,13 +126,13 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import CollapsibleSection from './CollapsibleSection.vue';
import { type DiagramPart } from '@/components/diagramManager';
import {
type PropertyConfig,
getPropValue
} from '@/components/equipments/componentConfig';
import { ref } from "vue";
import CollapsibleSection from "./CollapsibleSection.vue";
import { type DiagramPart } from "@/components/diagramManager";
import {
type PropertyConfig,
getPropValue,
} from "@/components/equipments/componentConfig";
//
const props = defineProps<{
@ -148,8 +142,13 @@ const props = defineProps<{
//
const emit = defineEmits<{
(e: 'updateProp', componentId: string, propName: string, value: any): void;
(e: 'updateDirectProp', componentId: string, propName: string, value: any): void;
(e: "updateProp", componentId: string, propName: string, value: any): void;
(
e: "updateDirectProp",
componentId: string,
propName: string,
value: any,
): void;
}>();
//
@ -158,23 +157,32 @@ const componentPropsExpanded = ref(true);
//
function updateProp(componentId: string, propName: string, value: any) {
emit('updateProp', componentId, propName, value);
emit("updateProp", componentId, propName, value);
}
//
function updateDirectProp(componentId: string, propName: string, value: any) {
emit('updateDirectProp', componentId, propName, value);
emit("updateDirectProp", componentId, propName, value);
}
//
function getGeneralProps(): PropertyConfig[] {
return props.componentConfig?.props.filter(p => p.isDirectProp && p.name !== 'pins') || [];
return (
props.componentConfig?.props.filter(
(p) => p.isDirectProp && p.name !== "pins",
) || []
);
}
//
function getComponentProps(): PropertyConfig[] {
return props.componentConfig?.props.filter(p => !p.isDirectProp && p.name !== 'pins') || [];
return (
props.componentConfig?.props.filter(
(p) => !p.isDirectProp && p.name !== "pins",
) || []
);
}
</script>
<style scoped>

View File

@ -1,68 +1,64 @@
<template> <div class="property-panel">
<CollapsibleSection
title="基本属性"
v-model:isExpanded="propertySectionExpanded"
status="default"
>
<PropertyEditor
:componentData="componentData"
:componentConfig="componentConfig"
@updateProp="(componentId, propName, value) => $emit('updateProp', componentId, propName, value)"
@updateDirectProp="(componentId, propName, value) => $emit('updateDirectProp', componentId, propName, value)"
/>
<template>
<div class="property-panel">
<CollapsibleSection title="基本属性" v-model:isExpanded="propertySectionExpanded" status="default">
<PropertyEditor :componentData="componentData" :componentConfig="componentConfig" @updateProp="
(componentId, propName, value) =>
$emit('updateProp', componentId, propName, value)
" @updateDirectProp="
(componentId, propName, value) =>
$emit('updateDirectProp', componentId, propName, value)
" />
</CollapsibleSection>
<!-- 信号发生器DDS特殊属性编辑器 -->
<div v-if="isDDSComponent">
<DDSPropertyEditor
v-model="ddsProperties"
@update:modelValue="updateDDSProperties"
/>
<DDSPropertyEditor v-model="ddsProperties" @update:modelValue="updateDDSProperties" />
</div>
<!-- 如果选中的组件有pins属性则显示引脚配置区域 -->
<CollapsibleSection
v-if="hasPinsProperty"
title="引脚配置"
v-model:isExpanded="pinsSectionExpanded"
status="default"
>
<CollapsibleSection v-if="hasPinsProperty" title="引脚配置" v-model:isExpanded="pinsSectionExpanded" status="default">
<div class="space-y-4 p-2">
<!-- 显示现有的pins -->
<div v-for="(pin, index) in componentPins" :key="index" class="pin-item p-2 border rounded-md bg-base-200">
<div class="font-medium mb-2">引脚 #{{ index + 1 }}</div>
<div class="grid grid-cols-2 gap-2">
<div class="form-control">
<label class="label">
<span class="label-text text-xs">ID</span>
</label>
<input
type="text"
v-model="componentPins[index].pinId"
class="input input-bordered input-sm w-full"
placeholder="引脚ID"
@change="updatePins"
/>
<input type="text" v-model="componentPins[index].pinId" class="input input-bordered input-sm w-full"
placeholder="引脚ID" @change="updatePins" />
</div>
<div class="form-control">
<label class="label">
<span class="label-text text-xs">约束条件</span>
</label>
<input
type="text"
v-model="componentPins[index].constraint"
class="input input-bordered input-sm w-full"
placeholder="约束条件"
@change="updatePins"
/>
<input type="text" v-model="componentPins[index].constraint" class="input input-bordered input-sm w-full"
placeholder="约束条件" @change="updatePins" />
</div>
</div>
</div>
</div>
</CollapsibleSection>
<CollapsibleSection title="组件功能" v-model:isExpanded="componentCapsExpanded" status="default" class="mt-4">
<div v-if="componentData && componentData.type">
<component
v-if="capabilityComponent"
:is="capabilityComponent"
v-bind="componentData.attrs"
/>
<div v-else class="text-gray-400">
该组件没有提供特殊功能
</div>
</div>
<div v-else class="text-gray-400">
选择元件以查看其功能
</div>
</CollapsibleSection>
<!-- 未来可以在这里添加更多的分区 -->
<!-- 例如
<CollapsibleSection
@ -77,120 +73,258 @@
</template>
<script setup lang="ts">
import { type DiagramPart } from '@/components/diagramManager';
import { type PropertyConfig } from '@/components/equipments/componentConfig';
import CollapsibleSection from './CollapsibleSection.vue';
import PropertyEditor from './PropertyEditor.vue';
import DDSPropertyEditor from './equipments/DDSPropertyEditor.vue';
import { ref, computed, watch } from 'vue';
//
import { type DiagramPart } from "@/components/diagramManager"; //
import { type PropertyConfig } from "@/components/equipments/componentConfig"; //
import CollapsibleSection from "./CollapsibleSection.vue"; //
import PropertyEditor from "./PropertyEditor.vue"; //
import DDSPropertyEditor from "./equipments/DDSPropertyEditor.vue"; // DDS
import { ref, computed, watch, shallowRef, markRaw, h, createApp } from "vue"; // VueAPI
import { isNull, isUndefined } from "lodash";
import type { JSX } from "vue/jsx-runtime";
// Pin
//
interface Pin {
pinId: string;
constraint: string;
x: number;
y: number;
pinId: string; // ID
constraint: string; //
x: number; // X
y: number; // Y
}
//
//
const props = defineProps<{
componentData: DiagramPart | null;
componentConfig: { props: PropertyConfig[] } | null;
componentData: DiagramPart | null; //
componentConfig: { props: PropertyConfig[] } | null; //
}>();
//
const propertySectionExpanded = ref(true);
const pinsSectionExpanded = ref(false);
const wireSectionExpanded = ref(false);
//
const propertySectionExpanded = ref(true); //
const pinsSectionExpanded = ref(false); //
const componentCapsExpanded = ref(true); //
const wireSectionExpanded = ref(false); // 线
// DDS
// DDS
const ddsProperties = ref({
frequency: 1000,
phase: 0,
waveform: 'sine',
customWaveformPoints: []
frequency: 1000, // 1000Hz
phase: 0, // 0
waveform: "sine", //
customWaveformPoints: [], //
});
// pins
// pins
const componentPins = ref<Pin[]>([]);
// pins
watch(() => props.componentData?.attrs?.pins, (newPins) => {
if (newPins) {
componentPins.value = JSON.parse(JSON.stringify(newPins));
} else {
componentPins.value = [];
}
}, { deep: true, immediate: true });
// pinspins
watch(
() => props.componentData?.attrs?.pins,
(newPins) => {
if (newPins) {
//
componentPins.value = JSON.parse(JSON.stringify(newPins));
} else {
componentPins.value = [];
}
},
{ deep: true, immediate: true }, //
);
// DDS
watch(() => props.componentData?.attrs, (newAttrs) => {
if (newAttrs && isDDSComponent.value) {
ddsProperties.value = {
frequency: newAttrs.frequency || 1000,
phase: newAttrs.phase || 0,
waveform: newAttrs.waveform || 'sine',
customWaveformPoints: newAttrs.customWaveformPoints || []
};
}
}, { deep: true, immediate: true });
watch(
() => props.componentData?.attrs,
(newAttrs) => {
if (newAttrs && isDDSComponent.value) {
// DDS
ddsProperties.value = {
frequency: newAttrs.frequency || 1000,
phase: newAttrs.phase || 0,
waveform: newAttrs.waveform || "sine",
customWaveformPoints: newAttrs.customWaveformPoints || [],
};
}
},
{ deep: true, immediate: true }, //
);
// pins
const hasPinsProperty = computed(() => {
if (!props.componentData || !props.componentData.attrs) {
return false;
}
// pins
// 1pins
if (props.componentConfig && props.componentConfig.props) {
return props.componentConfig.props.some(prop => prop.name === 'pins' && prop.isArrayType);
return props.componentConfig.props.some(
(prop) => prop.name === "pins" && prop.isArrayType,
);
}
// attrspins
return 'pins' in props.componentData.attrs;
// 2attrspins
return "pins" in props.componentData.attrs;
});
// DDS
const isDDSComponent = computed(() => {
return props.componentData?.type === 'DDS';
return props.componentData?.type === "DDS";
});
//
//
const emit = defineEmits<{
(e: 'updateProp', componentId: string, propName: string, value: any): void;
(e: 'updateDirectProp', componentId: string, propName: string, value: any): void;
//
(e: "updateProp", componentId: string, propName: string, value: any): void;
//
(
e: "updateDirectProp",
componentId: string,
propName: string,
value: any,
): void;
}>();
// pins
// pins
function updatePins() {
if (props.componentData && props.componentData.id) {
emit('updateProp', props.componentData.id, 'pins', componentPins.value);
// pins
emit("updateProp", props.componentData.id, "pins", componentPins.value);
}
}
// DDS
watch(() => props.componentData?.attrs, (newAttrs) => {
if (newAttrs && isDDSComponent.value) {
ddsProperties.value = {
frequency: newAttrs.frequency || 1000,
phase: newAttrs.phase || 0,
waveform: newAttrs.waveform || 'sine',
customWaveformPoints: newAttrs.customWaveformPoints || []
};
}
}, { deep: true, immediate: true });
// DDS
// DDS
function updateDDSProperties(newProperties: any) {
//
ddsProperties.value = newProperties;
if (props.componentData && props.componentData.id) {
//
emit('updateProp', props.componentData.id, 'frequency', newProperties.frequency);
emit('updateProp', props.componentData.id, 'phase', newProperties.phase);
emit('updateProp', props.componentData.id, 'waveform', newProperties.waveform);
emit('updateProp', props.componentData.id, 'customWaveformPoints', newProperties.customWaveformPoints);
//
emit(
"updateProp",
props.componentData.id,
"frequency",
newProperties.frequency,
);
emit("updateProp", props.componentData.id, "phase", newProperties.phase);
emit(
"updateProp",
props.componentData.id,
"waveform",
newProperties.waveform,
);
emit(
"updateProp",
props.componentData.id,
"customWaveformPoints",
newProperties.customWaveformPoints,
);
}
}
//
const capabilityComponent = shallowRef(null);
//
async function getExposedCapabilities(componentType: string) {
try {
//
const module = await import(`./equipments/${componentType}.vue`);
const Component = module.default;
// div
const tempDiv = document.createElement('div');
//
let exposedMethods: any = null;
const app = createApp({
render() {
return h(Component, {
ref: (el: any) => {
if (el) {
//
exposedMethods = el;
}
}
});
}
});
//
const vm = app.mount(tempDiv);
//
await new Promise(resolve => setTimeout(resolve, 0));
// getCapabilities
if (exposedMethods && typeof exposedMethods.getCapabilities === 'function') {
//
const CapabilityComponent = exposedMethods.getCapabilities();
// DOM
app.unmount();
tempDiv.remove();
return CapabilityComponent;
}
// DOM
app.unmount();
tempDiv.remove();
return null;
} catch (error) {
console.error(`获取${componentType}能力页面失败:`, error);
return null;
}
}
//
watch(
() => props.componentData,
async (newComponentData) => {
if (newComponentData && newComponentData.type) {
try {
//
const capsComponent = await getExposedCapabilities(newComponentData.type);
if (capsComponent) {
capabilityComponent.value = markRaw(capsComponent);
console.log(`已从实例加载${newComponentData.type}组件的能力页面`);
return;
}
// 退
const module = await import(`./equipments/${newComponentData.type}.vue`);
if (
(module.default && typeof module.default.getCapabilities === "function") ||
typeof module.getCapabilities === "function"
) {
const getCapsFn =
typeof module.getCapabilities === "function"
? module.getCapabilities
: module.default.getCapabilities;
const moduleCapComponent = getCapsFn();
if (moduleCapComponent) {
capabilityComponent.value = markRaw(moduleCapComponent);
console.log(`已从模块加载${newComponentData.type}组件的能力页面`);
return;
}
}
capabilityComponent.value = null;
console.log(`组件${newComponentData.type}没有提供getCapabilities方法`);
} catch (error) {
console.error(`加载组件${newComponentData.type}能力页面失败:`, error);
capabilityComponent.value = null;
}
} else {
capabilityComponent.value = null;
}
},
{ immediate: true }
);
// hasComponentCaps
const hasComponentCaps = computed(() => {
return capabilityComponent.value !== null;
});
</script>
<style scoped>

View File

@ -6,7 +6,7 @@
<!-- Input File -->
<fieldset class="fieldset w-full">
<legend class="fieldset-legend text-sm">选择或拖拽上传文件</legend>
<input type="file" class="file-input w-full" @change="handleFileChange" />
<input type="file" class="file-input w-full" :value="fileInput" @change="handleFileChange" />
<label class="fieldset-label">文件最大容量: {{ maxMemory }}MB</label>
</fieldset>
@ -20,17 +20,38 @@
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { computed, ref } from "vue";
import { useEquipments } from "@/stores/equipments";
import { useDialogStore } from "@/stores/dialog";
import { isNull, isUndefined } from "lodash";
interface Props {
uploadEvent?: Function;
downloadEvent?: Function;
maxMemory?: number;
defaultFile?: string;
}
const props = withDefaults(defineProps<Props>(), {
maxMemory: 4,
});
const emits = defineEmits<{
finishedUpload: [file: File];
}>();
const dialog = useDialogStore();
const eqps = useEquipments();
const buttonText = computed(() => {
return isUndefined(props.downloadEvent) ? "上传" : "上传并下载";
});
var bitstream: File | null = null;
// var bitstream: File | null = null;
const bitstream = defineModel<File | undefined>();
const fileInput = computed(() => {
return !isUndefined(bitstream.value) ? bitstream.value.name : "";
});
function handleFileChange(event: Event): void {
const target = event.target as HTMLInputElement;
@ -40,15 +61,10 @@ function handleFileChange(event: Event): void {
return;
}
bitstream = file;
bitstream.value = file;
}
function checkFile(file: File | null): boolean {
if (isNull(file)) {
dialog.error(`未选择文件`);
return false;
}
function checkFile(file: File): boolean {
const maxBytes = props.maxMemory! * 1024 * 1024; // MB
if (file.size > maxBytes) {
dialog.error(`文件大小超过最大限制: ${props.maxMemory}MB`);
@ -58,18 +74,25 @@ function checkFile(file: File | null): boolean {
}
async function handleClick(event: Event): Promise<void> {
if (!checkFile(bitstream)) return;
if (isNull(bitstream.value) || isUndefined(bitstream.value)) {
dialog.error(`未选择文件`);
return;
}
if (!checkFile(bitstream.value)) return;
if (isUndefined(props.uploadEvent)) {
dialog.error("无法上传");
return;
}
// Upload
// Upload - bitstream.valuebitstream
try {
const ret = await props.uploadEvent(event, bitstream);
const ret = await props.uploadEvent(event, bitstream.value);
if (isUndefined(props.downloadEvent)) {
if (ret) dialog.info("上传成功");
else dialog.error("上传失败");
if (ret) {
dialog.info("上传成功");
emits("finishedUpload", bitstream.value);
} else dialog.error("上传失败");
return;
}
} catch (e) {
@ -89,15 +112,6 @@ async function handleClick(event: Event): Promise<void> {
}
}
interface Props {
uploadEvent?: Function;
downloadEvent?: Function;
maxMemory?: number;
}
const props = withDefaults(defineProps<Props>(), {
maxMemory: 4,
});
</script>
<style scoped lang="postcss">

View File

@ -1,3 +1,5 @@
import type { JSX } from "vue/jsx-runtime";
// 定义 diagram.json 的类型结构
export interface DiagramData {
version: number;
@ -15,6 +17,7 @@ export interface DiagramPart {
x: number;
y: number;
attrs: Record<string, any>;
capsPage?: JSX.Element;
rotate: number;
group: string;
positionlock: boolean;

View File

@ -1,45 +1,65 @@
<template> <div class="eth-component" :style="{ width: width + 'px', height: height + 'px' }">
<img
src="../equipments/svg/eth.svg"
:width="width"
:height="height"
alt="以太网接口"
class="svg-image"
draggable="false"
/>
<template>
<div class="eth-component" :style="{ width: width + 'px', height: height + 'px' }">
<img src="../equipments/svg/eth.svg" :width="width" :height="height" alt="以太网接口" class="svg-image"
draggable="false" />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { computed } from "vue";
interface Props {
size?: number;
devIPAddress?: string;
devPort?: string;
}
const props = withDefaults(defineProps<Props>(), {
size: 1
});
const props = withDefaults(defineProps<Props>(), getDefaultProps());
//
const width = computed(() => 100 * props.size);
const height = computed(() => 60 * props.size);
//
defineExpose({
getInfo: () => ({
size: props.size,
type: "ETH",
}),
// getPinPosition
getPinPosition: () => null,
});
</script>
<script lang="ts">
// props
export function getDefaultProps() {
return {
size: 1,
devIPAddress: "127.0.0.1",
devPort: "1234",
};
}
</script>
<style scoped>
.eth-component {
display: block;
user-select: none;
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE/Edge */
-webkit-user-select: none;
/* Safari */
-moz-user-select: none;
/* Firefox */
-ms-user-select: none;
/* IE/Edge */
}
.svg-image {
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none; /* 禁止鼠标交互 */
pointer-events: none;
/* 禁止鼠标交互 */
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;

View File

@ -1,54 +1,190 @@
<template>
<div class="motherboard-container" :style="{ width: width + 'px', height: height + 'px', position: 'relative' }">
<svg
xmlns="http://www.w3.org/2000/svg"
:width="width"
:height="height"
:viewBox="`0 0 800 600`"
class="motherboard-svg"
>
<image
href="../equipments/svg/motherboard.svg"
width="100%"
height="100%"
preserveAspectRatio="xMidYMid meet"
/>
<div class="motherboard-container" :style="{
width: width + 'px',
height: height + 'px',
position: 'relative',
}">
<svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height" :viewBox="`0 0 800 600`"
class="motherboard-svg">
<image href="../equipments/svg/motherboard.svg" width="100%" height="100%" preserveAspectRatio="xMidYMid meet" />
</svg>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
<script setup lang="tsx">
import { JtagClient, type FileParameter } from "@/APIClient";
import z from "zod";
import { useEquipments } from "@/stores/equipments";
import UploadCard from "@/components/UploadCard.vue";
import { useDialogStore } from "@/stores/dialog";
import { toNumber, toString } from "lodash";
import { computed, ref, defineComponent, watch } from "vue";
//
interface MotherBoardProps {
size?: number;
jtagAddr?: string;
jtagPort?: string;
}
const props = withDefaults(defineProps<MotherBoardProps>(), {
size: 1
});
const props = withDefaults(defineProps<MotherBoardProps>(), getDefaultProps());
//
const width = computed(() => 800 * props.size);
const height = computed(() => 600 * props.size);
const eqps = useEquipments();
watch([props.jtagAddr, props.jtagPort], () => {
eqps.jtagIPAddr = props.jtagAddr;
eqps.jtagPort = props.jtagPort;
});
//
defineExpose({
getInfo: () => ({
size: props.size,
type: 'motherboard'
type: "motherboard",
}),
// getPinPosition
getPinPosition: () => null
getPinPosition: () => null,
getCapabilities: () => {
// JSX
return defineComponent({
name: "MotherBoardCapabilities",
props: {
jtagAddr: {
type: String,
default: props.jtagAddr,
},
jtagPort: {
type: String,
default: props.jtagPort,
},
},
setup(props) {
const jtagController = new JtagClient();
const dialog = useDialogStore();
// 使
const jtagIDCode = ref("");
const boardAddress = computed(() => props.jtagAddr);
const boardPort = computed(() => toNumber(props.jtagPort));
async function uploadBitstream(event: Event, bitstream: File) {
if (!boardAddress.value || !boardPort.value) {
dialog.error("开发板地址或端口空缺");
return;
}
const fileParam = {
data: bitstream,
fileName: bitstream.name,
};
try {
const resp = await jtagController.uploadBitstream(
boardAddress.value,
fileParam,
);
return resp;
} catch (e) {
dialog.error("上传错误");
}
}
async function downloadBitstream() {
if (!boardAddress.value || !boardPort.value) {
dialog.error("开发板地址或端口空缺");
return;
}
try {
const resp = await jtagController.downloadBitstream(
boardAddress.value,
boardPort.value,
);
return resp;
} catch (e) {
dialog.error("上传错误");
console.error(e);
}
}
watch([boardAddress, boardPort], () => {
if (
z.string().ip().safeParse(boardAddress).success &&
z.number().positive().safeParse(boardPort).success
)
getIDCode();
});
async function getIDCode() {
if (!boardAddress.value || !boardPort.value) {
dialog.error("开发板地址或端口空缺");
return;
}
try {
const resp = await jtagController.getDeviceIDCode(
boardAddress.value,
boardPort.value,
);
jtagIDCode.value = toString(resp);
} catch (e) {
dialog.error("获取IDCode错误");
console.error(e);
}
}
return () => (
<div>
<h1 class="font-bold text-center text-2xl">Jtag</h1>
<div class="flex">
<p class="grow">IDCode: {jtagIDCode.value}</p>
<button class="btn btn-circle w-8 h-8" onClick={getIDCode}>
<svg
class="icon opacity-70"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4865"
width="200"
height="200"
>
<path
d="M894.481158 505.727133c0 49.589418-9.711176 97.705276-28.867468 143.007041-18.501376 43.74634-44.98454 83.031065-78.712713 116.759237-33.728172 33.728172-73.012897 60.211337-116.759237 78.712713-45.311998 19.156292-93.417623 28.877701-143.007041 28.877701s-97.695043-9.721409-142.996808-28.877701c-43.756573-18.501376-83.031065-44.98454-116.76947-78.712713-33.728172-33.728172-60.211337-73.012897-78.712713-116.759237-19.156292-45.301765-28.867468-93.417623-28.867468-143.007041 0-49.579185 9.711176-97.695043 28.867468-142.996808 18.501376-43.74634 44.98454-83.031065 78.712713-116.759237 33.738405-33.728172 73.012897-60.211337 116.76947-78.712713 45.301765-19.166525 93.40739-28.877701 142.996808-28.877701 52.925397 0 104.008842 11.010775 151.827941 32.745798 46.192042 20.977777 86.909395 50.79692 121.016191 88.608084 4.389984 4.860704 8.646937 9.854439 12.781094 14.97097l0-136.263453c0-11.307533 9.168824-20.466124 20.466124-20.466124 11.307533 0 20.466124 9.15859 20.466124 20.466124l0 183.64253c0 5.433756-2.148943 10.632151-5.986341 14.46955-3.847631 3.837398-9.046027 5.996574-14.479783 5.996574l-183.64253-0.020466c-11.307533 0-20.466124-9.168824-20.466124-20.466124 0-11.307533 9.168824-20.466124 20.466124-20.466124l132.293025 0.020466c-3.960195-4.952802-8.063653-9.782807-12.289907-14.479783-30.320563-33.605376-66.514903-60.098773-107.549481-78.753645-42.467207-19.289322-87.850837-29.072129-134.902456-29.072129-87.195921 0-169.172981 33.9533-230.816946 95.597265-61.654198 61.654198-95.597265 143.621025-95.597265 230.816946s33.943067 169.172981 95.597265 230.816946c61.643965 61.654198 143.621025 95.607498 230.816946 95.607498s169.172981-33.9533 230.816946-95.607498c61.654198-61.643965 95.597265-143.621025 95.597265-230.816946 0-11.2973 9.168824-20.466124 20.466124-20.466124C885.322567 485.261009 894.481158 494.429833 894.481158 505.727133z"
p-id="4866"
></path>
</svg>
</button>
</div>
<div class="divider"></div>
<UploadCard
class="bg-base-200"
upload-event={uploadBitstream}
download-event={downloadBitstream}
defaultFile={eqps.jtagBitstream}
onFinishedUpload={(file: File) => {
eqps.jtagBitstream = file;
}}
>
{" "}
</UploadCard>
</div>
);
},
});
},
});
</script>
<script lang="ts">
<script lang="tsx">
// props
export function getDefaultProps() {
return {
size: 1
size: 1,
jtagAddr: "127.0.0.1",
jtagPort: "1234",
};
}
</script>
@ -58,14 +194,18 @@ export function getDefaultProps() {
display: block;
position: relative;
user-select: none;
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE/Edge */
-webkit-user-select: none;
/* Safari */
-moz-user-select: none;
/* Firefox */
-ms-user-select: none;
/* IE/Edge */
}
.motherboard-svg {
width: 100%;
height: 100%;
pointer-events: none; /* 禁止鼠标交互 */
pointer-events: none;
/* 禁止鼠标交互 */
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,6 @@ import { createWebHistory, createRouter } from "vue-router";
import LoginView from "../views/LoginView.vue";
import UserView from "../views/UserView.vue";
import TestView from "../views/TestView.vue";
import JtagTest from "../views/JtagTest.vue";
import ProjectView from "../views/ProjectView.vue";
import HomeView from "@/views/HomeView.vue";
import AdminView from "@/views/AdminView.vue";
@ -12,7 +11,6 @@ const routes = [
{ path: "/login", name: "Login", component: LoginView },
{ path: "/user", name: "User", component: UserView },
{ path: "/test", name: "Test", component: TestView },
{ path: "/test/jtag", name: "JtagTest", component: JtagTest },
{ path: "/project", name: "Project", component: ProjectView },
{ path: "/admin", name: "Admin", component: AdminView },
];

View File

@ -1,5 +1,4 @@
import { ref, reactive } from 'vue';
// 约束电平状态类型
export type ConstraintLevel = 'high' | 'low' | 'undefined';

22
src/stores/equipments.ts Normal file
View File

@ -0,0 +1,22 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import { isUndefined } from 'lodash';
export const useEquipments = defineStore('equipments', () => {
const jtagIPAddr = ref("127.0.0.1")
const jtagPort = ref("1234")
const jtagBitstream = ref<File | undefined>()
const remoteUpdateIPAddr = ref("127.0.0.1")
const remoteUpdatePort = ref("1234")
const remoteUpdateBitstream = ref<File | undefined>()
return {
jtagIPAddr,
jtagPort,
jtagBitstream,
remoteUpdateIPAddr,
remoteUpdatePort,
remoteUpdateBitstream,
}
})

View File

@ -1,45 +0,0 @@
<template>
<div class="h-screen flex flex-col overflow-hidden">
<!-- 主要内容 -->
<div class="flex-1 flex justify-center">
<div class="h-full w-32"></div>
<div class="h-full w-[70%] shadow-2xl flex items-start p-4">
<button class="btn btn-primary h-10 w-30">获取ID Code</button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from "vue";
const eventSource = ref<EventSource>();
const initSource = () => {
eventSource.value = new EventSource("http://localhost:500/api/log/example");
eventSource.value.onmessage = (event) => {
console.log("收到消息内容是:", event.data);
};
eventSource.value.onerror = (error) => {
console.error("SSE 连接出错:", error);
eventSource.value!.close();
};
};
onMounted(() => {
initSource();
});
onUnmounted(() => {
if (eventSource) {
eventSource.value?.close();
}
});
</script>
<style scoped lang="postcss">
@import "../assets/main.css";
</style>

View File

@ -1,80 +1,65 @@
<template>
<div class="h-screen flex flex-col overflow-hidden">
<div class="flex flex-1 overflow-hidden relative">
<!-- 左侧图形化区域 -->
<!-- 左侧图形化区域 -->
<div class="relative bg-base-200 overflow-hidden h-full" :style="{ width: leftPanelWidth + '%' }">
<DiagramCanvas
ref="diagramCanvas"
:componentModules="componentModules"
@component-selected="handleComponentSelected"
@component-moved="handleComponentMoved"
@component-delete="handleComponentDelete"
@wire-created="handleWireCreated"
@wire-deleted="handleWireDeleted"
@diagram-updated="handleDiagramUpdated"
@open-components="openComponentsMenu"
@load-component-module="handleLoadComponentModule"
/>
<DiagramCanvas ref="diagramCanvas" :componentModules="componentModules"
@component-selected="handleComponentSelected" @component-moved="handleComponentMoved"
@component-delete="handleComponentDelete" @wire-created="handleWireCreated" @wire-deleted="handleWireDeleted"
@diagram-updated="handleDiagramUpdated" @open-components="openComponentsMenu"
@load-component-module="handleLoadComponentModule" />
</div>
<!-- 拖拽分割线 -->
<div
class="resizer cursor-col-resize bg-base-300 hover:bg-primary hover:opacity-70 active:bg-primary active:opacity-90 transition-colors"
@mousedown="startResize"
></div>
class="resizer bg-base-100 hover:bg-primary hover:opacity-70 active:bg-primary active:opacity-90 transition-colors"
@mousedown="startResize"></div>
<!-- 右侧编辑区域 -->
<div class="bg-base-100 h-full overflow-hidden flex flex-col" :style="{ width: (100 - leftPanelWidth) + '%' }">
<div class="overflow-y-auto p-4 flex-1">
<PropertyPanel
:componentData="selectedComponentData"
:componentConfig="selectedComponentConfig"
@updateProp="updateComponentProp"
@updateDirectProp="updateComponentDirectProp"
/>
<div class="bg-base-200 h-full overflow-hidden flex flex-col" :style="{ width: 100 - leftPanelWidth + '%' }">
<div class="overflow-y-auto flex-1">
<PropertyPanel :componentData="selectedComponentData" :componentConfig="selectedComponentConfig"
@updateProp="updateComponentProp" @updateDirectProp="updateComponentDirectProp" />
</div>
</div>
</div> <!-- 元器件选择组件 -->
<ComponentSelector
:open="showComponentsMenu"
@update:open="showComponentsMenu = $event"
@add-component="handleAddComponent"
@add-template="handleAddTemplate"
@close="showComponentsMenu = false"
/>
</div>
<!-- 元器件选择组件 -->
<ComponentSelector :open="showComponentsMenu" @update:open="showComponentsMenu = $event"
@add-component="handleAddComponent" @add-template="handleAddTemplate" @close="showComponentsMenu = false" />
</div>
</template>
<script setup lang="ts">
// wokwi-elements
// import "@wokwi/elements"; // wokwi
import { ref, reactive, computed, onMounted, onUnmounted, defineAsyncComponent, shallowRef } from 'vue'; // defineAsyncComponent shallowRef
import DiagramCanvas from '@/components/DiagramCanvas.vue';
import ComponentSelector from '@/components/ComponentSelector.vue';
import PropertyPanel from '@/components/PropertyPanel.vue';
import type { DiagramData, DiagramPart, ConnectionArray } from '@/components/diagramManager';
import { validateDiagramData } from '@/components/diagramManager';
import {
type PropertyConfig,
generatePropertyConfigs,
generatePropsFromDefault,
generatePropsFromAttrs,
getPropValue
} from '@/components/equipments/componentConfig'; //
import { ref, computed, onMounted, onUnmounted, shallowRef } from "vue"; // defineAsyncComponent shallowRef
import DiagramCanvas from "@/components/DiagramCanvas.vue";
import ComponentSelector from "@/components/ComponentSelector.vue";
import PropertyPanel from "@/components/PropertyPanel.vue";
import type { DiagramData, DiagramPart } from "@/components/diagramManager";
import {
type PropertyConfig,
generatePropertyConfigs,
generatePropsFromDefault,
generatePropsFromAttrs,
} from "@/components/equipments/componentConfig"; //
// --- ---
const showComponentsMenu = ref(false);
const diagramData = ref<DiagramData>({
version: 1,
author: 'admin',
editor: 'me',
author: "admin",
editor: "me",
parts: [],
connections: []
connections: [],
});
const selectedComponentId = ref<string | null>(null);
const selectedComponentData = computed(() => {
return diagramData.value.parts.find(p => p.id === selectedComponentId.value) || null;
return (
diagramData.value.parts.find((p) => p.id === selectedComponentId.value) ||
null
);
});
const diagramCanvas = ref(null);
@ -88,7 +73,9 @@ interface ComponentModule {
}
const componentModules = shallowRef<Record<string, ComponentModule>>({});
const selectedComponentConfig = shallowRef<{ props: PropertyConfig[] } | null>(null); //
const selectedComponentConfig = shallowRef<{ props: PropertyConfig[] } | null>(
null,
); //
//
async function loadComponentModule(type: string) {
@ -96,13 +83,13 @@ async function loadComponentModule(type: string) {
try {
// src/components/equipments/ type
const module = await import(`../components/equipments/${type}.vue`);
// 使 markRaw
componentModules.value = {
...componentModules.value,
[type]: module
[type]: module,
};
console.log(`Loaded module for ${type}:`, module);
} catch (error) {
console.error(`Failed to load component module ${type}:`, error);
@ -114,7 +101,7 @@ async function loadComponentModule(type: string) {
//
async function handleLoadComponentModule(type: string) {
console.log('Handling load component module request for:', type);
console.log("Handling load component module request for:", type);
await loadComponentModule(type);
}
@ -125,35 +112,37 @@ const isResizing = ref(false);
//
function startResize(e: MouseEvent) {
isResizing.value = true;
document.addEventListener('mousemove', onResize);
document.addEventListener('mouseup', stopResize);
document.addEventListener("mousemove", onResize);
document.addEventListener("mouseup", stopResize);
e.preventDefault(); //
}
function onResize(e: MouseEvent) {
if (!isResizing.value) return;
//
const container = document.querySelector('.flex-1.overflow-hidden') as HTMLElement;
const container = document.querySelector(
".flex-1.overflow-hidden",
) as HTMLElement;
if (!container) return;
const containerWidth = container.clientWidth;
const mouseX = e.clientX;
//
let newWidth = (mouseX / containerWidth) * 100;
//
newWidth = Math.max(20, Math.min(newWidth, 80));
//
leftPanelWidth.value = newWidth;
}
function stopResize() {
isResizing.value = false;
document.removeEventListener('mousemove', onResize);
document.removeEventListener('mouseup', stopResize);
document.removeEventListener("mousemove", onResize);
document.removeEventListener("mouseup", stopResize);
}
// --- ---
@ -162,42 +151,65 @@ function openComponentsMenu() {
}
// ComponentSelector
async function handleAddComponent(componentData: { type: string; name: string; props: Record<string, any> }) {
async function handleAddComponent(componentData: {
type: string;
name: string;
props: Record<string, any>;
}) {
// 便使
await loadComponentModule(componentData.type);
const componentModule = await loadComponentModule(componentData.type);
//
const canvasInstance = diagramCanvas.value as any;
//
let position = { x: 100, y: 100 };
let scale = 1;
try {
if (canvasInstance && canvasInstance.getCanvasPosition && canvasInstance.getScale) {
if (
canvasInstance &&
canvasInstance.getCanvasPosition &&
canvasInstance.getScale
) {
position = canvasInstance.getCanvasPosition();
scale = canvasInstance.getScale();
//
const canvasContainer = canvasInstance.$el as HTMLElement;
if (canvasContainer) {
//
const viewportWidth = canvasContainer.clientWidth;
const viewportHeight = canvasContainer.clientHeight;
//
position.x = (viewportWidth / 2 - position.x) / scale;
position.y = (viewportHeight / 2 - position.y) / scale;
}
}
} catch (error) {
console.error('获取画布位置时出错:', error);
console.error("获取画布位置时出错:", error);
}
//
const offsetX = Math.floor(Math.random() * 100) - 50;
const offsetY = Math.floor(Math.random() * 100) - 50;
//
let capsPage = null;
if (
componentModule &&
componentModule.default &&
typeof componentModule.default.getCapabilities === "function"
) {
try {
capsPage = componentModule.default.getCapabilities();
console.log(`获取到${componentData.type}组件的能力页面`);
} catch (error) {
console.error(`获取${componentData.type}组件能力页面失败:`, error);
}
}
// 使diagramManager
const newComponent: DiagramPart = {
id: `component-${Date.now()}`,
@ -205,17 +217,22 @@ async function handleAddComponent(componentData: { type: string; name: string; p
x: Math.round(position.x + offsetX),
y: Math.round(position.y + offsetY),
attrs: componentData.props,
capsPage: capsPage, //
rotate: 0,
group: '',
group: "",
positionlock: false,
hidepins: true,
isOn: true,
index: 0
index: 0,
};
console.log('添加新组件:', newComponent);
console.log("添加新组件:", newComponent);
//
if (canvasInstance && canvasInstance.getDiagramData && canvasInstance.updateDiagramDataDirectly) {
if (
canvasInstance &&
canvasInstance.getDiagramData &&
canvasInstance.updateDiagramDataDirectly
) {
const currentData = canvasInstance.getDiagramData();
currentData.parts.push(newComponent);
canvasInstance.updateDiagramDataDirectly(currentData);
@ -223,81 +240,114 @@ async function handleAddComponent(componentData: { type: string; name: string; p
}
//
async function handleAddTemplate(templateData: { id: string; name: string; template: any }) {
console.log('添加模板:', templateData);
console.log('=== 模板组件数量:', templateData.template?.parts?.length || 0);
//
async function handleAddTemplate(templateData: {
id: string;
name: string;
template: any;
}) {
console.log("添加模板:", templateData);
console.log("=== 模板组件数量:", templateData.template?.parts?.length || 0);
//
const canvasInstance = diagramCanvas.value as any;
if (!canvasInstance || !canvasInstance.getDiagramData || !canvasInstance.updateDiagramDataDirectly) {
console.error('没有可用的画布实例添加模板');
if (
!canvasInstance ||
!canvasInstance.getDiagramData ||
!canvasInstance.updateDiagramDataDirectly
) {
console.error("没有可用的画布实例添加模板");
return;
}
//
const currentData = canvasInstance.getDiagramData();
console.log('=== 当前图表组件数量:', currentData.parts.length);
console.log("=== 当前图表组件数量:", currentData.parts.length);
// IDID
const idPrefix = `template-${Date.now()}-`;
//
//
if (templateData.template && templateData.template.parts) {
//
let viewportCenter = { x: 300, y: 200 }; //
try {
if (canvasInstance && canvasInstance.getCanvasPosition && canvasInstance.getScale) {
if (
canvasInstance &&
canvasInstance.getCanvasPosition &&
canvasInstance.getScale
) {
const position = canvasInstance.getCanvasPosition();
const scale = canvasInstance.getScale();
//
const canvasContainer = canvasInstance.$el as HTMLElement;
if (canvasContainer) {
//
const viewportWidth = canvasContainer.clientWidth;
const viewportHeight = canvasContainer.clientHeight;
// (handleAddComponent)
viewportCenter.x = (viewportWidth / 2 - position.x) / scale;
viewportCenter.y = (viewportHeight / 2 - position.y) / scale;
console.log(`=== 计算的视口中心: x=${viewportCenter.x}, y=${viewportCenter.y}, scale=${scale}`);
console.log(
`=== 计算的视口中心: x=${viewportCenter.x}, y=${viewportCenter.y}, scale=${scale}`,
);
}
}
} catch (error) {
console.error('获取视口中心位置时出错:', error);
console.error("获取视口中心位置时出错:", error);
}
console.log('=== 使用视口中心添加模板组件:', viewportCenter);
console.log("=== 使用视口中心添加模板组件:", viewportCenter);
//
const mainPart = templateData.template.parts[0];
//
const newParts = templateData.template.parts.map((part: any) => {
// ID
const newPart = JSON.parse(JSON.stringify(part));
newPart.id = `${idPrefix}${part.id}`;
//
if (typeof newPart.x === 'number' && typeof newPart.y === 'number') {
const oldX = newPart.x;
const oldY = newPart.y;
//
const relativeX = part.x - mainPart.x;
const relativeY = part.y - mainPart.y;
//
newPart.x = viewportCenter.x + relativeX;
newPart.y = viewportCenter.y + relativeY;
console.log(`=== 组件[${newPart.id}]位置调整: (${oldX},${oldY}) -> (${newPart.x},${newPart.y})`);
}
return newPart;
});
const newParts = await Promise.all(
templateData.template.parts.map(async (part: any) => {
// ID
const newPart = JSON.parse(JSON.stringify(part));
newPart.id = `${idPrefix}${part.id}`;
//
try {
const componentModule = await loadComponentModule(part.type);
if (
componentModule &&
componentModule.default &&
typeof componentModule.default.getCapabilities === "function"
) {
newPart.capsPage = componentModule.default.getCapabilities();
console.log(`加载模板组件${part.type}组件的能力页面成功`);
}
} catch (error) {
console.error(`加载模板组件${part.type}的能力页面失败:`, error);
}
//
if (typeof newPart.x === "number" && typeof newPart.y === "number") {
const oldX = newPart.x;
const oldY = newPart.y;
//
const relativeX = part.x - mainPart.x;
const relativeY = part.y - mainPart.y;
//
newPart.x = viewportCenter.x + relativeX;
newPart.y = viewportCenter.y + relativeY;
console.log(
`=== 组件[${newPart.id}]位置调整: (${oldX},${oldY}) -> (${newPart.x},${newPart.y})`,
);
}
return newPart;
}),
);
//
currentData.parts.push(...newParts);
//
if (templateData.template.connections) {
// IDID
@ -305,45 +355,47 @@ async function handleAddTemplate(templateData: { id: string; name: string; templ
templateData.template.parts.forEach((part: any) => {
idMap[part.id] = `${idPrefix}${part.id}`;
});
// ID
const newConnections = templateData.template.connections.map((conn: any) => {
// ( [from, to, type, path])
if (Array.isArray(conn)) {
const [from, to, type, path] = conn;
// IDID
const fromParts = from.split(':');
const toParts = to.split(':');
if (fromParts.length === 2 && toParts.length === 2) {
const fromComponentId = fromParts[0];
const fromPinId = fromParts[1];
const toComponentId = toParts[0];
const toPinId = toParts[1];
// 使ID
const newFrom = `${idMap[fromComponentId] || fromComponentId}:${fromPinId}`;
const newTo = `${idMap[toComponentId] || toComponentId}:${toPinId}`;
return [newFrom, newTo, type, path];
const newConnections = templateData.template.connections.map(
(conn: any) => {
// ( [from, to, type, path])
if (Array.isArray(conn)) {
const [from, to, type, path] = conn;
// IDID
const fromParts = from.split(":");
const toParts = to.split(":");
if (fromParts.length === 2 && toParts.length === 2) {
const fromComponentId = fromParts[0];
const fromPinId = fromParts[1];
const toComponentId = toParts[0];
const toPinId = toParts[1];
// 使ID
const newFrom = `${idMap[fromComponentId] || fromComponentId}:${fromPinId}`;
const newTo = `${idMap[toComponentId] || toComponentId}:${toPinId}`;
return [newFrom, newTo, type, path];
}
}
}
return conn; //
});
return conn; //
},
);
//
currentData.connections.push(...newConnections);
}
//
//
canvasInstance.updateDiagramDataDirectly(currentData);
console.log('=== 更新图表数据完成,新组件数量:', currentData.parts.length);
console.log("=== 更新图表数据完成,新组件数量:", currentData.parts.length);
//
showToast(`已添加 ${templateData.name} 模板`, 'success');
showToast(`已添加 ${templateData.name} 模板`, "success");
} else {
console.error('模板格式错误缺少parts数组');
showToast('模板格式错误', 'error');
console.error("模板格式错误缺少parts数组");
showToast("模板格式错误", "error");
}
}
@ -355,36 +407,44 @@ async function handleComponentSelected(componentData: DiagramPart | null) {
if (componentData) {
//
const moduleRef = await loadComponentModule(componentData.type);
if (moduleRef) {
try {
// 使
const propConfigs: PropertyConfig[] = [];
// 1. - DiagramPart
const directPropConfigs = generatePropertyConfigs(componentData);
propConfigs.push(...directPropConfigs);
// 2. 使getDefaultProps
if (typeof moduleRef.getDefaultProps === 'function') {
if (typeof moduleRef.getDefaultProps === "function") {
// getDefaultProps
const defaultProps = moduleRef.getDefaultProps();
const defaultPropConfigs = generatePropsFromDefault(defaultProps);
propConfigs.push(...defaultPropConfigs);
selectedComponentConfig.value = { props: propConfigs };
console.log(`Built config for ${componentData.type} from getDefaultProps:`, selectedComponentConfig.value);
console.log(
`Built config for ${componentData.type} from getDefaultProps:`,
selectedComponentConfig.value,
);
} else {
console.warn(`Component ${componentData.type} does not export getDefaultProps method.`);
console.warn(
`Component ${componentData.type} does not export getDefaultProps method.`,
);
//
const attrs = componentData.attrs || {};
const attrPropConfigs = generatePropsFromAttrs(attrs);
propConfigs.push(...attrPropConfigs);
selectedComponentConfig.value = { props: propConfigs };
}
} catch (error) {
console.error(`Error building config for ${componentData.type}:`, error);
console.error(
`Error building config for ${componentData.type}:`,
error,
);
selectedComponentConfig.value = { props: [] };
}
} else {
@ -397,12 +457,12 @@ async function handleComponentSelected(componentData: DiagramPart | null) {
//
function handleDiagramUpdated(data: DiagramData) {
diagramData.value = data;
console.log('Diagram data updated:', data);
console.log("Diagram data updated:", data);
}
//
function handleComponentMoved(moveData: { id: string; x: number; y: number }) {
const part = diagramData.value.parts.find(p => p.id === moveData.id);
const part = diagramData.value.parts.find((p) => p.id === moveData.id);
if (part) {
part.x = moveData.x;
part.y = moveData.y;
@ -412,63 +472,79 @@ function handleComponentMoved(moveData: { id: string; x: number; y: number }) {
//
function handleComponentDelete(componentId: string) {
//
const component = diagramData.value.parts.find(p => p.id === componentId);
const component = diagramData.value.parts.find((p) => p.id === componentId);
if (!component) return;
// ID
const componentsToDelete: string[] = [componentId];
//
if (component.group && component.group !== '') {
if (component.group && component.group !== "") {
const groupMembers = diagramData.value.parts.filter(
p => p.group === component.group && p.id !== componentId
(p) => p.group === component.group && p.id !== componentId,
);
// ID
componentsToDelete.push(...groupMembers.map(p => p.id));
console.log(`删除组件 ${componentId} 及其组 ${component.group} 中的 ${groupMembers.length} 个组件`);
componentsToDelete.push(...groupMembers.map((p) => p.id));
console.log(
`删除组件 ${componentId} 及其组 ${component.group} 中的 ${groupMembers.length} 个组件`,
);
}
//
diagramData.value.parts = diagramData.value.parts.filter(
p => !componentsToDelete.includes(p.id)
(p) => !componentsToDelete.includes(p.id),
);
//
diagramData.value.connections = diagramData.value.connections.filter(
connection => {
(connection) => {
for (const id of componentsToDelete) {
if (connection[0].startsWith(`${id}:`) || connection[1].startsWith(`${id}:`)) {
if (
connection[0].startsWith(`${id}:`) ||
connection[1].startsWith(`${id}:`)
) {
return false;
}
}
return true;
}
},
);
//
if (selectedComponentId.value && componentsToDelete.includes(selectedComponentId.value)) {
if (
selectedComponentId.value &&
componentsToDelete.includes(selectedComponentId.value)
) {
selectedComponentId.value = null;
selectedComponentConfig.value = null;
}
}
//
function updateComponentProp(componentId: string, propName: string, value: any) {
function updateComponentProp(
componentId: string,
propName: string,
value: any,
) {
const canvasInstance = diagramCanvas.value as any;
if (!canvasInstance || !canvasInstance.getDiagramData || !canvasInstance.updateDiagramDataDirectly) {
console.error('没有可用的画布实例进行属性更新');
if (
!canvasInstance ||
!canvasInstance.getDiagramData ||
!canvasInstance.updateDiagramDataDirectly
) {
console.error("没有可用的画布实例进行属性更新");
return;
}
// value使
if (value !== null && typeof value === 'object' && 'value' in value) {
if (value !== null && typeof value === "object" && "value" in value) {
value = value.value;
}
const currentData = canvasInstance.getDiagramData();
const part = currentData.parts.find((p: DiagramPart) => p.id === componentId);
if (part) {
//
if (propName in part) {
@ -480,29 +556,45 @@ function updateComponentProp(componentId: string, propName: string, value: any)
}
part.attrs[propName] = value;
}
canvasInstance.updateDiagramDataDirectly(currentData);
console.log(`更新组件${componentId}的属性${propName}为:`, value, typeof value);
console.log(
`更新组件${componentId}的属性${propName}为:`,
value,
typeof value,
);
}
}
//
function updateComponentDirectProp(componentId: string, propName: string, value: any) {
function updateComponentDirectProp(
componentId: string,
propName: string,
value: any,
) {
const canvasInstance = diagramCanvas.value as any;
if (!canvasInstance || !canvasInstance.getDiagramData || !canvasInstance.updateDiagramDataDirectly) {
console.error('没有可用的画布实例进行属性更新');
if (
!canvasInstance ||
!canvasInstance.getDiagramData ||
!canvasInstance.updateDiagramDataDirectly
) {
console.error("没有可用的画布实例进行属性更新");
return;
}
const currentData = canvasInstance.getDiagramData();
const part = currentData.parts.find((p: DiagramPart) => p.id === componentId);
if (part) {
// @ts-ignore:
part[propName] = value;
canvasInstance.updateDiagramDataDirectly(currentData);
console.log(`更新组件${componentId}的直接属性${propName}为:`, value, typeof value);
console.log(
`更新组件${componentId}的直接属性${propName}为:`,
value,
typeof value,
);
}
}
@ -510,12 +602,12 @@ function updateComponentDirectProp(componentId: string, propName: string, value:
// 线
function handleWireCreated(wireData: any) {
console.log('Wire created:', wireData);
console.log("Wire created:", wireData);
}
// 线
function handleWireDeleted(wireId: string) {
console.log('Wire deleted:', wireId);
console.log("Wire deleted:", wireId);
}
// diagram
@ -529,10 +621,15 @@ function exportDiagram() {
// --- ---
const showNotification = ref(false);
const notificationMessage = ref('');
const notificationType = ref<'success' | 'error' | 'info'>('info');
const notificationMessage = ref("");
const notificationType = ref<"success" | "error" | "info">("info");
function showToast(message: string, type: 'success' | 'error' | 'info' = 'info', duration = 3000) { const canvasInstance = diagramCanvas.value as any;
function showToast(
message: string,
type: "success" | "error" | "info" = "info",
duration = 3000,
) {
const canvasInstance = diagramCanvas.value as any;
if (canvasInstance && canvasInstance.showToast) {
canvasInstance.showToast(message, type, duration);
} else {
@ -540,7 +637,7 @@ function showToast(message: string, type: 'success' | 'error' | 'info' = 'info',
notificationMessage.value = message;
notificationType.value = type;
showNotification.value = true;
//
setTimeout(() => {
showNotification.value = false;
@ -555,33 +652,33 @@ function showToast(message: string, type: 'success' | 'error' | 'info' = 'info',
// --- ---
onMounted(async () => {
//
console.log('ProjectView mounted, diagram canvas ref:', diagramCanvas.value);
console.log("ProjectView mounted, diagram canvas ref:", diagramCanvas.value);
//
const canvasInstance = diagramCanvas.value as any;
if (canvasInstance && canvasInstance.getDiagramData) {
diagramData.value = canvasInstance.getDiagramData();
// 使
const componentTypes = new Set<string>();
diagramData.value.parts.forEach(part => {
diagramData.value.parts.forEach((part) => {
componentTypes.add(part.type);
});
console.log('Preloading component modules:', Array.from(componentTypes));
console.log("Preloading component modules:", Array.from(componentTypes));
//
await Promise.all(
Array.from(componentTypes).map(type => loadComponentModule(type))
Array.from(componentTypes).map((type) => loadComponentModule(type)),
);
console.log('All component modules loaded');
console.log("All component modules loaded");
}
});
onUnmounted(() => {
document.removeEventListener('mousemove', onResize);
document.removeEventListener('mouseup', stopResize);
document.removeEventListener("mousemove", onResize);
document.removeEventListener("mouseup", stopResize);
});
</script>
@ -593,13 +690,13 @@ onUnmounted(() => {
.resizer {
width: 6px;
height: 100%;
background-color: hsl(var(--b3));
cursor: col-resize;
transition: background-color 0.3s;
z-index: 10;
}
.resizer:hover, .resizer:active {
.resizer:hover,
.resizer:active {
width: 6px;
}
@ -618,6 +715,7 @@ onUnmounted(() => {
opacity: 0;
transform: translateX(30px);
}
to {
opacity: 1;
transform: translateX(0);
@ -625,7 +723,8 @@ onUnmounted(() => {
}
/* 确保滚动行为仅在需要时出现 */
html, body {
html,
body {
overflow: hidden;
height: 100%;
margin: 0;

View File

@ -6,7 +6,7 @@
</div>
<div class="divider"></div>
<div class="w-full">
<div class="collapse bg-primary border-base-300 border">
<div class="collapse bg-primary">
<input type="checkbox" />
<div class="collapse-title font-semibold text-lg text-white">
自定义开发板参数
@ -73,7 +73,7 @@ async function uploadBitstream(event: Event, bitstream: File) {
boardAddress.value,
fileParam,
);
return resp;
return resp
} catch (e) {
dialog.error("上传错误");
}

View File

@ -7,5 +7,8 @@
{
"path": "./tsconfig.app.json"
}
]
],
"compilerOptions": {
"jsx": "preserve"
}
}

View File

@ -6,7 +6,6 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
import tailwindcss from '@tailwindcss/postcss'
import autoprefixer from 'autoprefixer'
import basicSsl from '@vitejs/plugin-basic-ssl'
// https://vite.dev/config/
export default defineConfig({
@ -21,7 +20,6 @@ export default defineConfig({
}),
vueJsx(),
vueDevTools(),
// basicSsl()
],
resolve: {
alias: {