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 build
npm run preview npm run preview
dev: dev-server
# 测试服务器 # 测试服务器
dev-server: _show-dir dev-server: _show-dir
cd server && dotnet run cd server && dotnet run --watch
# 运行网页客户端 # 运行网页客户端
dev-web: dev-web:

14
package-lock.json generated
View File

@ -25,7 +25,6 @@
"@tailwindcss/postcss": "^4.0.12", "@tailwindcss/postcss": "^4.0.12",
"@tsconfig/node22": "^22.0.0", "@tsconfig/node22": "^22.0.0",
"@types/node": "^22.13.4", "@types/node": "^22.13.4",
"@vitejs/plugin-basic-ssl": "^2.0.0",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1", "@vitejs/plugin-vue-jsx": "^4.1.1",
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.7.0",
@ -1647,19 +1646,6 @@
"undici-types": "~6.21.0" "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": { "node_modules/@vitejs/plugin-vue": {
"version": "5.2.3", "version": "5.2.3",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz", "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", "@tailwindcss/postcss": "^4.0.12",
"@tsconfig/node22": "^22.0.0", "@tsconfig/node22": "^22.0.0",
"@types/node": "^22.13.4", "@types/node": "^22.13.4",
"@vitejs/plugin-basic-ssl": "^2.0.0",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1", "@vitejs/plugin-vue-jsx": "^4.1.1",
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.7.0",

View File

@ -192,26 +192,6 @@ public class JtagController : ControllerBase
return "This is Jtag Controller"; 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> /// <summary>
/// 获取Jtag ID Code /// 获取Jtag ID Code
/// </summary> /// </summary>
@ -349,6 +329,11 @@ public class JtagController : ControllerBase
return TypedResults.InternalServerError(retBuffer.Error); return TypedResults.InternalServerError(retBuffer.Error);
revBuffer = retBuffer.Value; revBuffer = retBuffer.Value;
for (int i = 0; i < revBuffer.Length; i++)
{
revBuffer[i] = Common.Number.ReverseBits(revBuffer[i]);
}
await memoryStream.WriteAsync(revBuffer, 0, bytesRead); await memoryStream.WriteAsync(revBuffer, 0, bytesRead);
totalBytesRead += bytesRead; totalBytesRead += bytesRead;
} }

View File

@ -432,199 +432,44 @@ public class Jtag
return Convert.ToUInt32(Common.Number.BytesToUInt64(retPackOpts.Data).Value); 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)
{
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"));
}
// 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 async ValueTask<Result<bool>> WriteFIFO
(UInt32 devAddr, UInt32 data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0) (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); var ret = await UDPClientPool.WriteAddr(this.ep, devAddr, data, this.timeout);
if (!retPack.IsSuccessful) return new(retPack.Error); if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception("Write FIFO failed"));
}
if (retPack.Value.Options.Data is null) // Delay some time before read register
return new(new Exception($"Data is Null, package: {retPack.Value.Options.ToString()}")); await Task.Delay(TimeSpan.FromMilliseconds(delayMilliseconds));
var retPackLen = retPack.Value.Options.Data.Length; {
if (retPackLen != 4) var ret = await UDPClientPool.ReadAddrWithWait(this.ep, JtagAddr.STATE, result, resultMask, this.timeout);
return new(new Exception($"RecvDataPackage BodyData Length not Equal to 4: Total {retPackLen} bytes")); if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
if (Common.Number.BitsCheck( }
Common.Number.BytesToUInt64(retPack.Value.Options.Data).Value, result, resultMask))
ret = true;
return ret;
} }
async ValueTask<Result<bool>> WriteFIFO async ValueTask<Result<bool>> WriteFIFO
(UInt32 devAddr, byte[] data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0) (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 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));
{ {
var wrRet = await WriteFIFO(devAddr, data, result, resultMask); var ret = await UDPClientPool.ReadAddrWithWait(this.ep, JtagAddr.STATE, result, resultMask, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
if (!wrRet.IsSuccessful) return new(wrRet.Error); return ret.Value;
if (wrRet.Value) return true;
} }
// Wait some time
var ret = false;
var startTime = DateTime.Now;
var isTimeout = false;
var timeleft = TimeSpan.FromMilliseconds(timeout);
while (!isTimeout)
{
// 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));
}
return ret;
}
/// <summary> /// <summary>
/// 清除所有 JTAG 寄存器 /// 清除所有 JTAG 寄存器
/// </summary> /// </summary>
@ -718,17 +563,16 @@ public class Jtag
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Write CMD_JTAG_LOAD_DR_CAREI Failed")); 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, JtagAddr.WRITE_DATA,
bytesArray, 0x01_00_00_00, bytesArray, 0x01_00_00_00,
JtagState.CMD_EXEC_FINISH, JtagState.CMD_EXEC_FINISH);
timeout, cycle);
if (!ret.IsSuccessful) return new(ret.Error); 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) async ValueTask<Result<UInt32>> LoadDRCareOutput(UInt32 UInt32Num)
@ -753,7 +597,7 @@ public class Jtag
var ret = await WriteFIFO( var ret = await WriteFIFO(
JtagAddr.WRITE_CMD, JtagAddr.WRITE_CMD,
Common.Number.MultiBitsToNumber(JtagCmd.CMD_JTAG_LOAD_DR_CAREO, JtagCmd.LEN_CMD_JTAG, 32 * UInt32Num, 28).Value, 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) if (ret.Value)
{ {
@ -955,11 +799,10 @@ public class Jtag
ret = await ExecRDCmd(JtagCmd.JTAG_DR_SAMPLE); ret = await ExecRDCmd(JtagCmd.JTAG_DR_SAMPLE);
if (!ret.IsSuccessful) return new(ret.Error); 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))); var retData = await LoadDRCareOutputArray(((uint)(portNum % 32 == 0 ? portNum / 32 : portNum / 32 + 1)));
if (!retData.IsSuccessful) if (!retData.IsSuccessful) return new(retData.Error);
return new(new Exception("Read Status Reg Failed"));
ret = await CloseTest(); ret = await CloseTest();
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);

View File

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

View File

@ -49,17 +49,6 @@
测试功能 测试功能
</router-link> </router-link>
</li> </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"> <li class="my-1 hover:translate-x-1 transition-all duration-300">
<a href="http://localhost:5000/swagger" target="_self" rel="noopener noreferrer" <a href="http://localhost:5000/swagger" target="_self" rel="noopener noreferrer"
class="text-base font-medium"> class="text-base font-medium">

View File

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

View File

@ -1,32 +1,22 @@
<template> <div class="property-panel"> <template>
<CollapsibleSection <div class="property-panel">
title="基本属性" <CollapsibleSection title="基本属性" v-model:isExpanded="propertySectionExpanded" status="default">
v-model:isExpanded="propertySectionExpanded" <PropertyEditor :componentData="componentData" :componentConfig="componentConfig" @updateProp="
status="default" (componentId, propName, value) =>
> $emit('updateProp', componentId, propName, value)
<PropertyEditor " @updateDirectProp="
:componentData="componentData" (componentId, propName, value) =>
:componentConfig="componentConfig" $emit('updateDirectProp', componentId, propName, value)
@updateProp="(componentId, propName, value) => $emit('updateProp', componentId, propName, value)" " />
@updateDirectProp="(componentId, propName, value) => $emit('updateDirectProp', componentId, propName, value)"
/>
</CollapsibleSection> </CollapsibleSection>
<!-- 信号发生器DDS特殊属性编辑器 --> <!-- 信号发生器DDS特殊属性编辑器 -->
<div v-if="isDDSComponent"> <div v-if="isDDSComponent">
<DDSPropertyEditor <DDSPropertyEditor v-model="ddsProperties" @update:modelValue="updateDDSProperties" />
v-model="ddsProperties"
@update:modelValue="updateDDSProperties"
/>
</div> </div>
<!-- 如果选中的组件有pins属性则显示引脚配置区域 --> <!-- 如果选中的组件有pins属性则显示引脚配置区域 -->
<CollapsibleSection <CollapsibleSection v-if="hasPinsProperty" title="引脚配置" v-model:isExpanded="pinsSectionExpanded" status="default">
v-if="hasPinsProperty"
title="引脚配置"
v-model:isExpanded="pinsSectionExpanded"
status="default"
>
<div class="space-y-4 p-2"> <div class="space-y-4 p-2">
<!-- 显示现有的pins --> <!-- 显示现有的pins -->
<div v-for="(pin, index) in componentPins" :key="index" class="pin-item p-2 border rounded-md bg-base-200"> <div v-for="(pin, index) in componentPins" :key="index" class="pin-item p-2 border rounded-md bg-base-200">
@ -37,29 +27,35 @@
<label class="label"> <label class="label">
<span class="label-text text-xs">ID</span> <span class="label-text text-xs">ID</span>
</label> </label>
<input <input type="text" v-model="componentPins[index].pinId" class="input input-bordered input-sm w-full"
type="text" placeholder="引脚ID" @change="updatePins" />
v-model="componentPins[index].pinId"
class="input input-bordered input-sm w-full"
placeholder="引脚ID"
@change="updatePins"
/>
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text text-xs">约束条件</span> <span class="label-text text-xs">约束条件</span>
</label> </label>
<input <input type="text" v-model="componentPins[index].constraint" class="input input-bordered input-sm w-full"
type="text" placeholder="约束条件" @change="updatePins" />
v-model="componentPins[index].constraint" </div>
class="input input-bordered input-sm w-full" </div>
placeholder="约束条件" </div>
@change="updatePins" </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> </div>
</div> <div v-else class="text-gray-400">
选择元件以查看其功能
</div> </div>
</CollapsibleSection> </CollapsibleSection>
@ -77,63 +73,77 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { type DiagramPart } from '@/components/diagramManager'; //
import { type PropertyConfig } from '@/components/equipments/componentConfig'; import { type DiagramPart } from "@/components/diagramManager"; //
import CollapsibleSection from './CollapsibleSection.vue'; import { type PropertyConfig } from "@/components/equipments/componentConfig"; //
import PropertyEditor from './PropertyEditor.vue'; import CollapsibleSection from "./CollapsibleSection.vue"; //
import DDSPropertyEditor from './equipments/DDSPropertyEditor.vue'; import PropertyEditor from "./PropertyEditor.vue"; //
import { ref, computed, watch } from '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 { interface Pin {
pinId: string; pinId: string; // ID
constraint: string; constraint: string; //
x: number; x: number; // X
y: number; y: number; // Y
} }
// //
const props = defineProps<{ const props = defineProps<{
componentData: DiagramPart | null; componentData: DiagramPart | null; //
componentConfig: { props: PropertyConfig[] } | null; componentConfig: { props: PropertyConfig[] } | null; //
}>(); }>();
// //
const propertySectionExpanded = ref(true); const propertySectionExpanded = ref(true); //
const pinsSectionExpanded = ref(false); const pinsSectionExpanded = ref(false); //
const wireSectionExpanded = ref(false); const componentCapsExpanded = ref(true); //
const wireSectionExpanded = ref(false); // 线
// DDS // DDS
const ddsProperties = ref({ const ddsProperties = ref({
frequency: 1000, frequency: 1000, // 1000Hz
phase: 0, phase: 0, // 0
waveform: 'sine', waveform: "sine", //
customWaveformPoints: [] customWaveformPoints: [], //
}); });
// pins // pins
const componentPins = ref<Pin[]>([]); const componentPins = ref<Pin[]>([]);
// pins // pinspins
watch(() => props.componentData?.attrs?.pins, (newPins) => { watch(
() => props.componentData?.attrs?.pins,
(newPins) => {
if (newPins) { if (newPins) {
//
componentPins.value = JSON.parse(JSON.stringify(newPins)); componentPins.value = JSON.parse(JSON.stringify(newPins));
} else { } else {
componentPins.value = []; componentPins.value = [];
} }
}, { deep: true, immediate: true }); },
{ deep: true, immediate: true }, //
);
// DDS // DDS
watch(() => props.componentData?.attrs, (newAttrs) => { watch(
() => props.componentData?.attrs,
(newAttrs) => {
if (newAttrs && isDDSComponent.value) { if (newAttrs && isDDSComponent.value) {
// DDS
ddsProperties.value = { ddsProperties.value = {
frequency: newAttrs.frequency || 1000, frequency: newAttrs.frequency || 1000,
phase: newAttrs.phase || 0, phase: newAttrs.phase || 0,
waveform: newAttrs.waveform || 'sine', waveform: newAttrs.waveform || "sine",
customWaveformPoints: newAttrs.customWaveformPoints || [] customWaveformPoints: newAttrs.customWaveformPoints || [],
}; };
} }
}, { deep: true, immediate: true }); },
{ deep: true, immediate: true }, //
);
// pins // pins
const hasPinsProperty = computed(() => { const hasPinsProperty = computed(() => {
@ -141,56 +151,180 @@ const hasPinsProperty = computed(() => {
return false; return false;
} }
// pins // 1pins
if (props.componentConfig && props.componentConfig.props) { 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 // 2attrspins
return 'pins' in props.componentData.attrs; return "pins" in props.componentData.attrs;
}); });
// DDS // DDS
const isDDSComponent = computed(() => { const isDDSComponent = computed(() => {
return props.componentData?.type === 'DDS'; return props.componentData?.type === "DDS";
}); });
// //
const emit = defineEmits<{ 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() { function updatePins() {
if (props.componentData && props.componentData.id) { if (props.componentData && props.componentData.id) {
emit('updateProp', props.componentData.id, 'pins', componentPins.value); // pins
emit("updateProp", props.componentData.id, "pins", componentPins.value);
} }
} }
// DDS // 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
function updateDDSProperties(newProperties: any) { function updateDDSProperties(newProperties: any) {
//
ddsProperties.value = newProperties; ddsProperties.value = newProperties;
if (props.componentData && props.componentData.id) { if (props.componentData && props.componentData.id) {
// //
emit('updateProp', props.componentData.id, 'frequency', newProperties.frequency); emit(
emit('updateProp', props.componentData.id, 'phase', newProperties.phase); "updateProp",
emit('updateProp', props.componentData.id, 'waveform', newProperties.waveform); props.componentData.id,
emit('updateProp', props.componentData.id, 'customWaveformPoints', newProperties.customWaveformPoints); "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> </script>
<style scoped> <style scoped>

View File

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

View File

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

View File

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

View File

@ -1,54 +1,190 @@
<template> <template>
<div class="motherboard-container" :style="{ width: width + 'px', height: height + 'px', position: 'relative' }"> <div class="motherboard-container" :style="{
<svg width: width + 'px',
xmlns="http://www.w3.org/2000/svg" height: height + 'px',
:width="width" position: 'relative',
:height="height" }">
:viewBox="`0 0 800 600`" <svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height" :viewBox="`0 0 800 600`"
class="motherboard-svg" class="motherboard-svg">
> <image href="../equipments/svg/motherboard.svg" width="100%" height="100%" preserveAspectRatio="xMidYMid meet" />
<image
href="../equipments/svg/motherboard.svg"
width="100%"
height="100%"
preserveAspectRatio="xMidYMid meet"
/>
</svg> </svg>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="tsx">
import { computed } from 'vue'; 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 { interface MotherBoardProps {
size?: number; size?: number;
jtagAddr?: string;
jtagPort?: string;
} }
const props = withDefaults(defineProps<MotherBoardProps>(), { const props = withDefaults(defineProps<MotherBoardProps>(), getDefaultProps());
size: 1
});
// //
const width = computed(() => 800 * props.size); const width = computed(() => 800 * props.size);
const height = computed(() => 600 * 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({ defineExpose({
getInfo: () => ({ getInfo: () => ({
size: props.size, size: props.size,
type: 'motherboard' type: "motherboard",
}), }),
// getPinPosition // 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>
<script lang="ts"> <script lang="tsx">
// props // props
export function getDefaultProps() { export function getDefaultProps() {
return { return {
size: 1 size: 1,
jtagAddr: "127.0.0.1",
jtagPort: "1234",
}; };
} }
</script> </script>
@ -58,14 +194,18 @@ export function getDefaultProps() {
display: block; display: block;
position: relative; position: relative;
user-select: none; user-select: none;
-webkit-user-select: none; /* Safari */ -webkit-user-select: none;
-moz-user-select: none; /* Firefox */ /* Safari */
-ms-user-select: none; /* IE/Edge */ -moz-user-select: none;
/* Firefox */
-ms-user-select: none;
/* IE/Edge */
} }
.motherboard-svg { .motherboard-svg {
width: 100%; width: 100%;
height: 100%; height: 100%;
pointer-events: none; /* 禁止鼠标交互 */ pointer-events: none;
/* 禁止鼠标交互 */
} }
</style> </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 LoginView from "../views/LoginView.vue";
import UserView from "../views/UserView.vue"; import UserView from "../views/UserView.vue";
import TestView from "../views/TestView.vue"; import TestView from "../views/TestView.vue";
import JtagTest from "../views/JtagTest.vue";
import ProjectView from "../views/ProjectView.vue"; import ProjectView from "../views/ProjectView.vue";
import HomeView from "@/views/HomeView.vue"; import HomeView from "@/views/HomeView.vue";
import AdminView from "@/views/AdminView.vue"; import AdminView from "@/views/AdminView.vue";
@ -12,7 +11,6 @@ const routes = [
{ path: "/login", name: "Login", component: LoginView }, { path: "/login", name: "Login", component: LoginView },
{ path: "/user", name: "User", component: UserView }, { path: "/user", name: "User", component: UserView },
{ path: "/test", name: "Test", component: TestView }, { path: "/test", name: "Test", component: TestView },
{ path: "/test/jtag", name: "JtagTest", component: JtagTest },
{ path: "/project", name: "Project", component: ProjectView }, { path: "/project", name: "Project", component: ProjectView },
{ path: "/admin", name: "Admin", component: AdminView }, { path: "/admin", name: "Admin", component: AdminView },
]; ];

View File

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

@ -3,78 +3,63 @@
<div class="flex flex-1 overflow-hidden relative"> <div class="flex flex-1 overflow-hidden relative">
<!-- 左侧图形化区域 --> <!-- 左侧图形化区域 -->
<div class="relative bg-base-200 overflow-hidden h-full" :style="{ width: leftPanelWidth + '%' }"> <div class="relative bg-base-200 overflow-hidden h-full" :style="{ width: leftPanelWidth + '%' }">
<DiagramCanvas <DiagramCanvas ref="diagramCanvas" :componentModules="componentModules"
ref="diagramCanvas" @component-selected="handleComponentSelected" @component-moved="handleComponentMoved"
:componentModules="componentModules" @component-delete="handleComponentDelete" @wire-created="handleWireCreated" @wire-deleted="handleWireDeleted"
@component-selected="handleComponentSelected" @diagram-updated="handleDiagramUpdated" @open-components="openComponentsMenu"
@component-moved="handleComponentMoved" @load-component-module="handleLoadComponentModule" />
@component-delete="handleComponentDelete"
@wire-created="handleWireCreated"
@wire-deleted="handleWireDeleted"
@diagram-updated="handleDiagramUpdated"
@open-components="openComponentsMenu"
@load-component-module="handleLoadComponentModule"
/>
</div> </div>
<!-- 拖拽分割线 --> <!-- 拖拽分割线 -->
<div <div
class="resizer cursor-col-resize bg-base-300 hover:bg-primary hover:opacity-70 active:bg-primary active:opacity-90 transition-colors" class="resizer bg-base-100 hover:bg-primary hover:opacity-70 active:bg-primary active:opacity-90 transition-colors"
@mousedown="startResize" @mousedown="startResize"></div>
></div>
<!-- 右侧编辑区域 --> <!-- 右侧编辑区域 -->
<div class="bg-base-100 h-full overflow-hidden flex flex-col" :style="{ width: (100 - leftPanelWidth) + '%' }"> <div class="bg-base-200 h-full overflow-hidden flex flex-col" :style="{ width: 100 - leftPanelWidth + '%' }">
<div class="overflow-y-auto p-4 flex-1"> <div class="overflow-y-auto flex-1">
<PropertyPanel <PropertyPanel :componentData="selectedComponentData" :componentConfig="selectedComponentConfig"
:componentData="selectedComponentData" @updateProp="updateComponentProp" @updateDirectProp="updateComponentDirectProp" />
:componentConfig="selectedComponentConfig"
@updateProp="updateComponentProp"
@updateDirectProp="updateComponentDirectProp"
/>
</div> </div>
</div> </div>
</div> <!-- 元器件选择组件 --> </div>
<ComponentSelector <!-- 元器件选择组件 -->
:open="showComponentsMenu" <ComponentSelector :open="showComponentsMenu" @update:open="showComponentsMenu = $event"
@update:open="showComponentsMenu = $event" @add-component="handleAddComponent" @add-template="handleAddTemplate" @close="showComponentsMenu = false" />
@add-component="handleAddComponent"
@add-template="handleAddTemplate"
@close="showComponentsMenu = false"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// wokwi-elements // wokwi-elements
// import "@wokwi/elements"; // wokwi // import "@wokwi/elements"; // wokwi
import { ref, reactive, computed, onMounted, onUnmounted, defineAsyncComponent, shallowRef } from 'vue'; // defineAsyncComponent shallowRef import { ref, computed, onMounted, onUnmounted, shallowRef } from "vue"; // defineAsyncComponent shallowRef
import DiagramCanvas from '@/components/DiagramCanvas.vue'; import DiagramCanvas from "@/components/DiagramCanvas.vue";
import ComponentSelector from '@/components/ComponentSelector.vue'; import ComponentSelector from "@/components/ComponentSelector.vue";
import PropertyPanel from '@/components/PropertyPanel.vue'; import PropertyPanel from "@/components/PropertyPanel.vue";
import type { DiagramData, DiagramPart, ConnectionArray } from '@/components/diagramManager'; import type { DiagramData, DiagramPart } from "@/components/diagramManager";
import { validateDiagramData } from '@/components/diagramManager';
import { import {
type PropertyConfig, type PropertyConfig,
generatePropertyConfigs, generatePropertyConfigs,
generatePropsFromDefault, generatePropsFromDefault,
generatePropsFromAttrs, generatePropsFromAttrs,
getPropValue } from "@/components/equipments/componentConfig"; //
} from '@/components/equipments/componentConfig'; //
// --- --- // --- ---
const showComponentsMenu = ref(false); const showComponentsMenu = ref(false);
const diagramData = ref<DiagramData>({ const diagramData = ref<DiagramData>({
version: 1, version: 1,
author: 'admin', author: "admin",
editor: 'me', editor: "me",
parts: [], parts: [],
connections: [] connections: [],
}); });
const selectedComponentId = ref<string | null>(null); const selectedComponentId = ref<string | null>(null);
const selectedComponentData = computed(() => { 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); const diagramCanvas = ref(null);
@ -88,7 +73,9 @@ interface ComponentModule {
} }
const componentModules = shallowRef<Record<string, 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) { async function loadComponentModule(type: string) {
@ -100,7 +87,7 @@ async function loadComponentModule(type: string) {
// 使 markRaw // 使 markRaw
componentModules.value = { componentModules.value = {
...componentModules.value, ...componentModules.value,
[type]: module [type]: module,
}; };
console.log(`Loaded module for ${type}:`, module); console.log(`Loaded module for ${type}:`, module);
@ -114,7 +101,7 @@ async function loadComponentModule(type: string) {
// //
async function handleLoadComponentModule(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); await loadComponentModule(type);
} }
@ -125,8 +112,8 @@ const isResizing = ref(false);
// //
function startResize(e: MouseEvent) { function startResize(e: MouseEvent) {
isResizing.value = true; isResizing.value = true;
document.addEventListener('mousemove', onResize); document.addEventListener("mousemove", onResize);
document.addEventListener('mouseup', stopResize); document.addEventListener("mouseup", stopResize);
e.preventDefault(); // e.preventDefault(); //
} }
@ -134,7 +121,9 @@ function onResize(e: MouseEvent) {
if (!isResizing.value) return; 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; if (!container) return;
const containerWidth = container.clientWidth; const containerWidth = container.clientWidth;
@ -152,8 +141,8 @@ function onResize(e: MouseEvent) {
function stopResize() { function stopResize() {
isResizing.value = false; isResizing.value = false;
document.removeEventListener('mousemove', onResize); document.removeEventListener("mousemove", onResize);
document.removeEventListener('mouseup', stopResize); document.removeEventListener("mouseup", stopResize);
} }
// --- --- // --- ---
@ -162,9 +151,13 @@ function openComponentsMenu() {
} }
// ComponentSelector // 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; const canvasInstance = diagramCanvas.value as any;
@ -174,7 +167,11 @@ async function handleAddComponent(componentData: { type: string; name: string; p
let scale = 1; let scale = 1;
try { try {
if (canvasInstance && canvasInstance.getCanvasPosition && canvasInstance.getScale) { if (
canvasInstance &&
canvasInstance.getCanvasPosition &&
canvasInstance.getScale
) {
position = canvasInstance.getCanvasPosition(); position = canvasInstance.getCanvasPosition();
scale = canvasInstance.getScale(); scale = canvasInstance.getScale();
@ -191,13 +188,28 @@ async function handleAddComponent(componentData: { type: string; name: string; p
} }
} }
} catch (error) { } catch (error) {
console.error('获取画布位置时出错:', error); console.error("获取画布位置时出错:", error);
} }
// //
const offsetX = Math.floor(Math.random() * 100) - 50; const offsetX = Math.floor(Math.random() * 100) - 50;
const offsetY = 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 // 使diagramManager
const newComponent: DiagramPart = { const newComponent: DiagramPart = {
id: `component-${Date.now()}`, id: `component-${Date.now()}`,
@ -205,17 +217,22 @@ async function handleAddComponent(componentData: { type: string; name: string; p
x: Math.round(position.x + offsetX), x: Math.round(position.x + offsetX),
y: Math.round(position.y + offsetY), y: Math.round(position.y + offsetY),
attrs: componentData.props, attrs: componentData.props,
capsPage: capsPage, //
rotate: 0, rotate: 0,
group: '', group: "",
positionlock: false, positionlock: false,
hidepins: true, hidepins: true,
isOn: 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(); const currentData = canvasInstance.getDiagramData();
currentData.parts.push(newComponent); currentData.parts.push(newComponent);
canvasInstance.updateDiagramDataDirectly(currentData); canvasInstance.updateDiagramDataDirectly(currentData);
@ -223,19 +240,27 @@ async function handleAddComponent(componentData: { type: string; name: string; p
} }
// //
async function handleAddTemplate(templateData: { id: string; name: string; template: any }) { async function handleAddTemplate(templateData: {
console.log('添加模板:', templateData); id: string;
console.log('=== 模板组件数量:', templateData.template?.parts?.length || 0); name: string;
template: any;
}) {
console.log("添加模板:", templateData);
console.log("=== 模板组件数量:", templateData.template?.parts?.length || 0);
// //
const canvasInstance = diagramCanvas.value as any; const canvasInstance = diagramCanvas.value as any;
if (!canvasInstance || !canvasInstance.getDiagramData || !canvasInstance.updateDiagramDataDirectly) { if (
console.error('没有可用的画布实例添加模板'); !canvasInstance ||
!canvasInstance.getDiagramData ||
!canvasInstance.updateDiagramDataDirectly
) {
console.error("没有可用的画布实例添加模板");
return; return;
} }
// //
const currentData = canvasInstance.getDiagramData(); const currentData = canvasInstance.getDiagramData();
console.log('=== 当前图表组件数量:', currentData.parts.length); console.log("=== 当前图表组件数量:", currentData.parts.length);
// IDID // IDID
const idPrefix = `template-${Date.now()}-`; const idPrefix = `template-${Date.now()}-`;
@ -244,7 +269,11 @@ async function handleAddTemplate(templateData: { id: string; name: string; templ
// //
let viewportCenter = { x: 300, y: 200 }; // let viewportCenter = { x: 300, y: 200 }; //
try { try {
if (canvasInstance && canvasInstance.getCanvasPosition && canvasInstance.getScale) { if (
canvasInstance &&
canvasInstance.getCanvasPosition &&
canvasInstance.getScale
) {
const position = canvasInstance.getCanvasPosition(); const position = canvasInstance.getCanvasPosition();
const scale = canvasInstance.getScale(); const scale = canvasInstance.getScale();
@ -258,26 +287,44 @@ async function handleAddTemplate(templateData: { id: string; name: string; templ
// (handleAddComponent) // (handleAddComponent)
viewportCenter.x = (viewportWidth / 2 - position.x) / scale; viewportCenter.x = (viewportWidth / 2 - position.x) / scale;
viewportCenter.y = (viewportHeight / 2 - position.y) / 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) { } catch (error) {
console.error('获取视口中心位置时出错:', error); console.error("获取视口中心位置时出错:", error);
} }
console.log('=== 使用视口中心添加模板组件:', viewportCenter); console.log("=== 使用视口中心添加模板组件:", viewportCenter);
// //
const mainPart = templateData.template.parts[0]; const mainPart = templateData.template.parts[0];
// //
const newParts = templateData.template.parts.map((part: any) => { const newParts = await Promise.all(
templateData.template.parts.map(async (part: any) => {
// ID // ID
const newPart = JSON.parse(JSON.stringify(part)); const newPart = JSON.parse(JSON.stringify(part));
newPart.id = `${idPrefix}${part.id}`; 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') { if (typeof newPart.x === "number" && typeof newPart.y === "number") {
const oldX = newPart.x; const oldX = newPart.x;
const oldY = newPart.y; const oldY = newPart.y;
@ -289,11 +336,14 @@ async function handleAddTemplate(templateData: { id: string; name: string; templ
newPart.x = viewportCenter.x + relativeX; newPart.x = viewportCenter.x + relativeX;
newPart.y = viewportCenter.y + relativeY; newPart.y = viewportCenter.y + relativeY;
console.log(`=== 组件[${newPart.id}]位置调整: (${oldX},${oldY}) -> (${newPart.x},${newPart.y})`); console.log(
`=== 组件[${newPart.id}]位置调整: (${oldX},${oldY}) -> (${newPart.x},${newPart.y})`,
);
} }
return newPart; return newPart;
}); }),
);
// //
currentData.parts.push(...newParts); currentData.parts.push(...newParts);
@ -307,14 +357,15 @@ async function handleAddTemplate(templateData: { id: string; name: string; templ
}); });
// ID // ID
const newConnections = templateData.template.connections.map((conn: any) => { const newConnections = templateData.template.connections.map(
(conn: any) => {
// ( [from, to, type, path]) // ( [from, to, type, path])
if (Array.isArray(conn)) { if (Array.isArray(conn)) {
const [from, to, type, path] = conn; const [from, to, type, path] = conn;
// IDID // IDID
const fromParts = from.split(':'); const fromParts = from.split(":");
const toParts = to.split(':'); const toParts = to.split(":");
if (fromParts.length === 2 && toParts.length === 2) { if (fromParts.length === 2 && toParts.length === 2) {
const fromComponentId = fromParts[0]; const fromComponentId = fromParts[0];
@ -330,20 +381,21 @@ async function handleAddTemplate(templateData: { id: string; name: string; templ
} }
} }
return conn; // return conn; //
}); },
);
// //
currentData.connections.push(...newConnections); currentData.connections.push(...newConnections);
} }
// //
canvasInstance.updateDiagramDataDirectly(currentData); canvasInstance.updateDiagramDataDirectly(currentData);
console.log('=== 更新图表数据完成,新组件数量:', currentData.parts.length); console.log("=== 更新图表数据完成,新组件数量:", currentData.parts.length);
// //
showToast(`已添加 ${templateData.name} 模板`, 'success'); showToast(`已添加 ${templateData.name} 模板`, "success");
} else { } else {
console.error('模板格式错误缺少parts数组'); console.error("模板格式错误缺少parts数组");
showToast('模板格式错误', 'error'); showToast("模板格式错误", "error");
} }
} }
@ -366,16 +418,21 @@ async function handleComponentSelected(componentData: DiagramPart | null) {
propConfigs.push(...directPropConfigs); propConfigs.push(...directPropConfigs);
// 2. 使getDefaultProps // 2. 使getDefaultProps
if (typeof moduleRef.getDefaultProps === 'function') { if (typeof moduleRef.getDefaultProps === "function") {
// getDefaultProps // getDefaultProps
const defaultProps = moduleRef.getDefaultProps(); const defaultProps = moduleRef.getDefaultProps();
const defaultPropConfigs = generatePropsFromDefault(defaultProps); const defaultPropConfigs = generatePropsFromDefault(defaultProps);
propConfigs.push(...defaultPropConfigs); propConfigs.push(...defaultPropConfigs);
selectedComponentConfig.value = { props: propConfigs }; 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 { } 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 attrs = componentData.attrs || {};
const attrPropConfigs = generatePropsFromAttrs(attrs); const attrPropConfigs = generatePropsFromAttrs(attrs);
@ -384,7 +441,10 @@ async function handleComponentSelected(componentData: DiagramPart | null) {
selectedComponentConfig.value = { props: propConfigs }; selectedComponentConfig.value = { props: propConfigs };
} }
} catch (error) { } catch (error) {
console.error(`Error building config for ${componentData.type}:`, error); console.error(
`Error building config for ${componentData.type}:`,
error,
);
selectedComponentConfig.value = { props: [] }; selectedComponentConfig.value = { props: [] };
} }
} else { } else {
@ -397,12 +457,12 @@ async function handleComponentSelected(componentData: DiagramPart | null) {
// //
function handleDiagramUpdated(data: DiagramData) { function handleDiagramUpdated(data: DiagramData) {
diagramData.value = data; diagramData.value = data;
console.log('Diagram data updated:', data); console.log("Diagram data updated:", data);
} }
// //
function handleComponentMoved(moveData: { id: string; x: number; y: number }) { 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) { if (part) {
part.x = moveData.x; part.x = moveData.x;
part.y = moveData.y; part.y = moveData.y;
@ -412,57 +472,73 @@ function handleComponentMoved(moveData: { id: string; x: number; y: number }) {
// //
function handleComponentDelete(componentId: string) { 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; if (!component) return;
// ID // ID
const componentsToDelete: string[] = [componentId]; const componentsToDelete: string[] = [componentId];
// //
if (component.group && component.group !== '') { if (component.group && component.group !== "") {
const groupMembers = diagramData.value.parts.filter( const groupMembers = diagramData.value.parts.filter(
p => p.group === component.group && p.id !== componentId (p) => p.group === component.group && p.id !== componentId,
); );
// ID // ID
componentsToDelete.push(...groupMembers.map(p => p.id)); componentsToDelete.push(...groupMembers.map((p) => p.id));
console.log(`删除组件 ${componentId} 及其组 ${component.group} 中的 ${groupMembers.length} 个组件`); console.log(
`删除组件 ${componentId} 及其组 ${component.group} 中的 ${groupMembers.length} 个组件`,
);
} }
// //
diagramData.value.parts = diagramData.value.parts.filter( diagramData.value.parts = diagramData.value.parts.filter(
p => !componentsToDelete.includes(p.id) (p) => !componentsToDelete.includes(p.id),
); );
// //
diagramData.value.connections = diagramData.value.connections.filter( diagramData.value.connections = diagramData.value.connections.filter(
connection => { (connection) => {
for (const id of componentsToDelete) { 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 false;
} }
} }
return true; return true;
} },
); );
// //
if (selectedComponentId.value && componentsToDelete.includes(selectedComponentId.value)) { if (
selectedComponentId.value &&
componentsToDelete.includes(selectedComponentId.value)
) {
selectedComponentId.value = null; selectedComponentId.value = null;
selectedComponentConfig.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; const canvasInstance = diagramCanvas.value as any;
if (!canvasInstance || !canvasInstance.getDiagramData || !canvasInstance.updateDiagramDataDirectly) { if (
console.error('没有可用的画布实例进行属性更新'); !canvasInstance ||
!canvasInstance.getDiagramData ||
!canvasInstance.updateDiagramDataDirectly
) {
console.error("没有可用的画布实例进行属性更新");
return; return;
} }
// value使 // value使
if (value !== null && typeof value === 'object' && 'value' in value) { if (value !== null && typeof value === "object" && "value" in value) {
value = value.value; value = value.value;
} }
@ -482,15 +558,27 @@ function updateComponentProp(componentId: string, propName: string, value: any)
} }
canvasInstance.updateDiagramDataDirectly(currentData); 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; const canvasInstance = diagramCanvas.value as any;
if (!canvasInstance || !canvasInstance.getDiagramData || !canvasInstance.updateDiagramDataDirectly) { if (
console.error('没有可用的画布实例进行属性更新'); !canvasInstance ||
!canvasInstance.getDiagramData ||
!canvasInstance.updateDiagramDataDirectly
) {
console.error("没有可用的画布实例进行属性更新");
return; return;
} }
@ -502,7 +590,11 @@ function updateComponentDirectProp(componentId: string, propName: string, value:
part[propName] = value; part[propName] = value;
canvasInstance.updateDiagramDataDirectly(currentData); 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) { function handleWireCreated(wireData: any) {
console.log('Wire created:', wireData); console.log("Wire created:", wireData);
} }
// 线 // 线
function handleWireDeleted(wireId: string) { function handleWireDeleted(wireId: string) {
console.log('Wire deleted:', wireId); console.log("Wire deleted:", wireId);
} }
// diagram // diagram
@ -529,10 +621,15 @@ function exportDiagram() {
// --- --- // --- ---
const showNotification = ref(false); const showNotification = ref(false);
const notificationMessage = ref(''); const notificationMessage = ref("");
const notificationType = ref<'success' | 'error' | 'info'>('info'); 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) { if (canvasInstance && canvasInstance.showToast) {
canvasInstance.showToast(message, type, duration); canvasInstance.showToast(message, type, duration);
} else { } else {
@ -555,7 +652,7 @@ function showToast(message: string, type: 'success' | 'error' | 'info' = 'info',
// --- --- // --- ---
onMounted(async () => { 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; const canvasInstance = diagramCanvas.value as any;
@ -564,24 +661,24 @@ onMounted(async () => {
// 使 // 使
const componentTypes = new Set<string>(); const componentTypes = new Set<string>();
diagramData.value.parts.forEach(part => { diagramData.value.parts.forEach((part) => {
componentTypes.add(part.type); componentTypes.add(part.type);
}); });
console.log('Preloading component modules:', Array.from(componentTypes)); console.log("Preloading component modules:", Array.from(componentTypes));
// //
await Promise.all( 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(() => { onUnmounted(() => {
document.removeEventListener('mousemove', onResize); document.removeEventListener("mousemove", onResize);
document.removeEventListener('mouseup', stopResize); document.removeEventListener("mouseup", stopResize);
}); });
</script> </script>
@ -593,13 +690,13 @@ onUnmounted(() => {
.resizer { .resizer {
width: 6px; width: 6px;
height: 100%; height: 100%;
background-color: hsl(var(--b3));
cursor: col-resize; cursor: col-resize;
transition: background-color 0.3s; transition: background-color 0.3s;
z-index: 10; z-index: 10;
} }
.resizer:hover, .resizer:active { .resizer:hover,
.resizer:active {
width: 6px; width: 6px;
} }
@ -618,6 +715,7 @@ onUnmounted(() => {
opacity: 0; opacity: 0;
transform: translateX(30px); transform: translateX(30px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateX(0); transform: translateX(0);
@ -625,7 +723,8 @@ onUnmounted(() => {
} }
/* 确保滚动行为仅在需要时出现 */ /* 确保滚动行为仅在需要时出现 */
html, body { html,
body {
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
margin: 0; margin: 0;

View File

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

View File

@ -7,5 +7,8 @@
{ {
"path": "./tsconfig.app.json" "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 vueDevTools from 'vite-plugin-vue-devtools'
import tailwindcss from '@tailwindcss/postcss' import tailwindcss from '@tailwindcss/postcss'
import autoprefixer from 'autoprefixer' import autoprefixer from 'autoprefixer'
import basicSsl from '@vitejs/plugin-basic-ssl'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
@ -21,7 +20,6 @@ export default defineConfig({
}), }),
vueJsx(), vueJsx(),
vueDevTools(), vueDevTools(),
// basicSsl()
], ],
resolve: { resolve: {
alias: { alias: {