fea: 前端完成动态ip与动态mac的适配

This commit is contained in:
SikongJueluo 2025-07-17 21:18:59 +08:00
parent 0f4386457d
commit 80b6dfb38d
No known key found for this signature in database
4 changed files with 372 additions and 175 deletions

View File

@ -10,6 +10,7 @@ import {
TutorialClient, TutorialClient,
UDPClient, UDPClient,
LogicAnalyzerClient, LogicAnalyzerClient,
NetConfigClient,
} from "@/APIClient"; } from "@/APIClient";
// 支持的客户端类型联合类型 // 支持的客户端类型联合类型
@ -24,7 +25,8 @@ type SupportedClient =
| RemoteUpdateClient | RemoteUpdateClient
| TutorialClient | TutorialClient
| LogicAnalyzerClient | LogicAnalyzerClient
| UDPClient; | UDPClient
| NetConfigClient;
export class AuthManager { export class AuthManager {
// 存储token到localStorage // 存储token到localStorage
@ -157,6 +159,10 @@ export class AuthManager {
return AuthManager.createAuthenticatedClient(LogicAnalyzerClient); return AuthManager.createAuthenticatedClient(LogicAnalyzerClient);
} }
public static createAuthenticatedNetConfigClient(): NetConfigClient {
return AuthManager.createAuthenticatedClient(NetConfigClient);
}
// 登录函数 // 登录函数
public static async login( public static async login(
username: string, username: string,

View File

@ -74,8 +74,6 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
// 新增板卡(管理员权限) // 新增板卡(管理员权限)
async function addBoard( async function addBoard(
name: string, name: string,
ipAddr: string,
port: number,
): Promise<{ success: boolean; error?: string; boardId?: string }> { ): Promise<{ success: boolean; error?: string; boardId?: string }> {
try { try {
// 验证管理员权限 // 验证管理员权限
@ -86,19 +84,19 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
} }
// 验证输入参数 // 验证输入参数
if (!name || !ipAddr || !port) { if (!name) {
console.error("参数验证失败", { name, ipAddr, port }); console.error("参数验证失败", { name });
return { success: false, error: "参数不完整" }; return { success: false, error: "参数不完整" };
} }
const client = AuthManager.createAuthenticatedDataClient(); const client = AuthManager.createAuthenticatedDataClient();
const boardId = await client.addBoard(name, ipAddr, port); const boardId = await client.addBoard(name);
if (boardId) { if (boardId) {
console.log("新增板卡成功", { boardId, name, ipAddr, port }); console.log("新增板卡成功", { boardId, name});
// 刷新板卡列表 // 刷新板卡列表
await getAllBoards(); await getAllBoards();
return { success: true}; return { success: true };
} else { } else {
console.error("新增板卡失败返回ID为空"); console.error("新增板卡失败返回ID为空");
return { success: false, error: "新增板卡失败" }; return { success: false, error: "新增板卡失败" };
@ -116,7 +114,9 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
} }
// 删除板卡(管理员权限) // 删除板卡(管理员权限)
async function deleteBoard(boardId: string): Promise<{ success: boolean; error?: string }> { async function deleteBoard(
boardId: string,
): Promise<{ success: boolean; error?: string }> {
try { try {
// 验证管理员权限 // 验证管理员权限
const hasAdminAuth = await AuthManager.verifyAdminAuth(); const hasAdminAuth = await AuthManager.verifyAdminAuth();
@ -198,7 +198,10 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
); );
if (downloadResult != cnt) { if (downloadResult != cnt) {
console.error("固化比特流失败", { expected: cnt, actual: downloadResult }); console.error("固化比特流失败", {
expected: cnt,
actual: downloadResult,
});
return { success: false, error: "固化比特流失败" }; return { success: false, error: "固化比特流失败" };
} else { } else {
console.log("固化比特流成功", { count: downloadResult }); console.log("固化比特流成功", { count: downloadResult });
@ -213,7 +216,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
// 热启动位流 // 热启动位流
async function hotresetBitstream( async function hotresetBitstream(
board: BoardData, board: BoardData,
bitstreamNum: number bitstreamNum: number,
): Promise<{ success: boolean; error?: string }> { ): Promise<{ success: boolean; error?: string }> {
try { try {
console.log("开始热启动比特流", { boardIp: board.ipAddr, bitstreamNum }); console.log("开始热启动比特流", { boardIp: board.ipAddr, bitstreamNum });
@ -253,7 +256,11 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
const file = target.files?.[0]; const file = target.files?.[0];
if (file) { if (file) {
(board as any)[fileKey] = file; (board as any)[fileKey] = file;
console.log(`文件选择成功`, { boardIp: board.ipAddr, fileKey, fileName: file.name }); console.log(`文件选择成功`, {
boardIp: board.ipAddr,
fileKey,
fileName: file.name,
});
} }
} }

View File

@ -3,61 +3,73 @@
<div class="modal-box w-96 max-w-md"> <div class="modal-box w-96 max-w-md">
<h3 class="text-lg font-bold mb-4">新增实验板</h3> <h3 class="text-lg font-bold mb-4">新增实验板</h3>
<form @submit.prevent="handleSubmit" class="space-y-4"> <!-- 步骤1: 输入板卡名称 -->
<!-- 实验板名称 --> <div v-if="currentStep === 'input'" class="space-y-4">
<div class="form-control"> <form @submit.prevent="handleSubmit">
<label class="label"> <!-- 实验板名称 -->
<span class="label-text">实验板名称 <span class="text-error">*</span></span> <div class="form-control">
</label> <label class="label">
<input <span class="label-text"
v-model="form.name" >实验板名称 <span class="text-error">*</span></span
type="text" >
placeholder="请输入实验板名称" </label>
class="input input-bordered" <input
:class="{ 'input-error': errors.name }" v-model="form.name"
required type="text"
/> placeholder="请输入实验板名称"
<label v-if="errors.name" class="label"> class="input input-bordered"
<span class="label-text-alt text-error">{{ errors.name }}</span> :class="{ 'input-error': errors.name }"
</label> required
</div> />
<label v-if="errors.name" class="label">
<span class="label-text-alt text-error">{{ errors.name }}</span>
</label>
</div>
<!-- IP 地址 --> <!-- 操作按钮 -->
<div class="form-control"> <div class="modal-action">
<label class="label"> <button
<span class="label-text">IP 地址 <span class="text-error">*</span></span> type="button"
</label> class="btn btn-ghost"
<input @click="handleCancel"
v-model="form.ipAddr" :disabled="isSubmitting"
type="text" >
placeholder="例如192.168.1.100" 取消
class="input input-bordered" </button>
:class="{ 'input-error': errors.ipAddr }" <button
required type="submit"
/> class="btn btn-primary"
<label v-if="errors.ipAddr" class="label"> :class="{ loading: isSubmitting }"
<span class="label-text-alt text-error">{{ errors.ipAddr }}</span> :disabled="isSubmitting"
</label> >
</div> {{ isSubmitting ? "添加中..." : "确认添加" }}
</button>
</div>
</form>
</div>
<!-- 端口号 --> <!-- 步骤2: 等待配对 -->
<div class="form-control"> <div v-else-if="currentStep === 'pairing'" class="space-y-4">
<label class="label"> <div class="text-center">
<span class="label-text">端口号 <span class="text-error">*</span></span> <div class="alert alert-info">
</label> <div class="flex items-center">
<input <svg
v-model.number="form.port" class="w-5 h-5 mr-2"
type="number" fill="none"
placeholder="例如1234" stroke="currentColor"
min="1" viewBox="0 0 24 24"
max="65535" xmlns="http://www.w3.org/2000/svg"
class="input input-bordered" >
:class="{ 'input-error': errors.port }" <path
required stroke-linecap="round"
/> stroke-linejoin="round"
<label v-if="errors.port" class="label"> stroke-width="2"
<span class="label-text-alt text-error">{{ errors.port }}</span> d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
</label> />
</svg>
<span>请打开实验板配对模式</span>
</div>
</div>
</div> </div>
<!-- 操作按钮 --> <!-- 操作按钮 -->
@ -65,21 +77,82 @@
<button <button
type="button" type="button"
class="btn btn-ghost" class="btn btn-ghost"
@click="handleCancel" @click="handleCancelPairing"
:disabled="isSubmitting" :disabled="isConfiguring"
> >
取消 取消
</button> </button>
<button <button
type="submit" type="button"
class="btn btn-primary" class="btn btn-primary"
:class="{ 'loading': isSubmitting }" @click="handlePairingConfirm"
:disabled="isSubmitting" :disabled="isConfiguring"
> >
{{ isSubmitting ? '添加中...' : '确认添加' }} 已开启
</button> </button>
</div> </div>
</form> </div>
<!-- 步骤3: 配置网络 -->
<div v-else-if="currentStep === 'configuring'" class="space-y-4">
<div class="text-center">
<div class="alert alert-warning">
<div class="flex items-center">
<div class="loading loading-spinner loading-sm mr-2"></div>
<span>正在分配网络...</span>
</div>
</div>
</div>
</div>
<!-- 步骤4: 显示配置结果 -->
<div v-else-if="currentStep === 'result'" class="space-y-4">
<div class="text-center">
<div class="alert alert-success">
<div class="flex items-center">
<svg
class="w-5 h-5 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
<span>实验板配置成功</span>
</div>
</div>
</div>
<!-- 网络配置信息 -->
<div v-if="networkConfig" class="space-y-2">
<h4 class="font-semibold">网络配置信息</h4>
<div class="bg-base-200 p-3 rounded">
<div class="text-sm space-y-1">
<div><span class="font-medium">主机IP:</span> {{ networkConfig.hostIP }}</div>
<div><span class="font-medium">板卡IP:</span> {{ networkConfig.boardIP }}</div>
<div><span class="font-medium">主机MAC:</span> {{ networkConfig.hostMAC }}</div>
<div><span class="font-medium">板卡MAC:</span> {{ networkConfig.boardMAC }}</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="modal-action">
<button
type="button"
class="btn btn-primary"
@click="handleSuccess"
>
确认
</button>
</div>
</div>
</div> </div>
<!-- 点击背景关闭 --> <!-- 点击背景关闭 -->
@ -90,8 +163,10 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, watch } from 'vue'; import { ref, reactive, watch } from "vue";
import { useBoardManager } from '../../utils/BoardManager'; import { AuthManager } from "../../utils/AuthManager";
import { useAlertStore } from "../../components/Alert";
import type { NetworkConfigDto } from "../../APIClient";
// Props Emits // Props Emits
interface Props { interface Props {
@ -99,75 +174,53 @@ interface Props {
} }
interface Emits { interface Emits {
(e: 'update:visible', value: boolean): void; (e: "update:visible", value: boolean): void;
(e: 'success'): void; (e: "success"): void;
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
// 使 BoardManager // 使 Alert
const boardManager = useBoardManager()!; const alertStore = useAlertStore();
//
const currentStep = ref<'input' | 'pairing' | 'configuring' | 'result'>('input');
// //
const form = reactive({ const form = reactive({
name: 'Board1', name: "Board1",
ipAddr: '169.254.103.0',
port: 1234
}); });
// //
const errors = reactive({ const errors = reactive({
name: '', name: "",
ipAddr: '',
port: ''
}); });
// //
const isSubmitting = ref(false); const isSubmitting = ref(false);
const isConfiguring = ref(false);
// IP //
const IP_REGEX = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; const addedBoardId = ref<string>("");
const networkConfig = ref<NetworkConfigDto | null>(null);
// //
function validateForm(): boolean { function validateForm(): boolean {
// //
errors.name = ''; errors.name = "";
errors.ipAddr = '';
errors.port = '';
let isValid = true; let isValid = true;
// //
if (!form.name.trim()) { if (!form.name.trim()) {
errors.name = '请输入实验板名称'; errors.name = "请输入实验板名称";
isValid = false; isValid = false;
} else if (form.name.trim().length < 2) { } else if (form.name.trim().length < 2) {
errors.name = '实验板名称至少需要2个字符'; errors.name = "实验板名称至少需要2个字符";
isValid = false; isValid = false;
} else if (form.name.trim().length > 50) { } else if (form.name.trim().length > 50) {
errors.name = '实验板名称不能超过50个字符'; errors.name = "实验板名称不能超过50个字符";
isValid = false;
}
// IP
if (!form.ipAddr.trim()) {
errors.ipAddr = '请输入IP地址';
isValid = false;
} else if (!IP_REGEX.test(form.ipAddr.trim())) {
errors.ipAddr = '请输入有效的IP地址格式';
isValid = false;
}
//
if (!form.port) {
errors.port = '请输入端口号';
isValid = false;
} else if (form.port < 1 || form.port > 65535) {
errors.port = '端口号必须在1-65535之间';
isValid = false;
} else if (!Number.isInteger(form.port)) {
errors.port = '端口号必须是整数';
isValid = false; isValid = false;
} }
@ -176,18 +229,17 @@ function validateForm(): boolean {
// //
function resetForm() { function resetForm() {
form.name = 'Board1'; form.name = "Board1";
form.ipAddr = '169.254.103.0'; errors.name = "";
form.port = 1234; currentStep.value = 'input';
errors.name = ''; addedBoardId.value = "";
errors.ipAddr = ''; networkConfig.value = null;
errors.port = '';
} }
// //
function handleCancel() { function handleCancel() {
if (!isSubmitting.value) { if (!isSubmitting.value && !isConfiguring.value) {
emit('update:visible', false); emit("update:visible", false);
resetForm(); resetForm();
} }
} }
@ -201,29 +253,120 @@ async function handleSubmit() {
isSubmitting.value = true; isSubmitting.value = true;
try { try {
const success = await boardManager.addBoard( // AuthManager DataClient
form.name.trim(), const dataClient = AuthManager.createAuthenticatedDataClient();
form.ipAddr.trim(),
form.port
);
if (success) { //
emit('success'); const boardId = await dataClient.addBoard(form.name.trim());
resetForm();
if (boardId) {
addedBoardId.value = boardId;
currentStep.value = 'pairing';
alertStore?.success("板卡添加成功,请开启配对模式");
} else {
alertStore?.error("板卡添加失败");
} }
} catch (error) { } catch (error) {
console.error('添加实验板失败:', error); console.error("添加实验板失败:", error);
alertStore?.error("添加实验板失败");
} finally { } finally {
isSubmitting.value = false; isSubmitting.value = false;
} }
} }
// //
watch(() => props.visible, (newVisible) => { async function handleCancelPairing() {
if (newVisible) { if (!addedBoardId.value) return;
try {
// AuthManager DataClient
const dataClient = AuthManager.createAuthenticatedDataClient();
//
await dataClient.deleteBoard(addedBoardId.value);
alertStore?.info("已取消添加实验板");
emit("update:visible", false);
resetForm(); resetForm();
} catch (error) {
console.error("删除板卡失败:", error);
alertStore?.error("删除板卡失败");
} }
}); }
//
async function handlePairingConfirm() {
if (!addedBoardId.value) return;
isConfiguring.value = true;
currentStep.value = 'configuring';
try {
// AuthManager
const dataClient = AuthManager.createAuthenticatedDataClient();
const netConfigClient = AuthManager.createAuthenticatedNetConfigClient();
//
const boardInfo = await dataClient.getBoardByID(addedBoardId.value);
if (!boardInfo) {
throw new Error("无法获取板卡信息");
}
// IPMAC
await netConfigClient.updateHostIP();
await netConfigClient.updateHostMAC();
// IPMAC
await netConfigClient.setBoardIP(boardInfo.ipAddr);
await netConfigClient.setBoardMAC(boardInfo.macAddr);
//
const networkInfo = await netConfigClient.getNetworkConfig();
if (networkInfo) {
networkConfig.value = networkInfo;
currentStep.value = 'result';
alertStore?.success("实验板配置成功");
} else {
throw new Error("无法获取网络配置信息");
}
} catch (error) {
console.error("配置实验板失败:", error);
alertStore?.error("配置实验板失败");
//
try {
const dataClient = AuthManager.createAuthenticatedDataClient();
await dataClient.deleteBoard(addedBoardId.value);
} catch (deleteError) {
console.error("删除板卡失败:", deleteError);
}
//
currentStep.value = 'input';
} finally {
isConfiguring.value = false;
}
}
//
function handleSuccess() {
emit("success");
emit("update:visible", false);
resetForm();
}
//
watch(
() => props.visible,
(newVisible) => {
if (newVisible) {
resetForm();
}
},
);
</script> </script>
<style scoped lang="postcss"> <style scoped lang="postcss">
@ -248,4 +391,20 @@ watch(() => props.visible, (newVisible) => {
.loading { .loading {
@apply opacity-50 cursor-not-allowed; @apply opacity-50 cursor-not-allowed;
} }
.alert {
@apply rounded-lg p-4;
}
.alert-info {
@apply bg-blue-50 text-blue-800 border border-blue-200;
}
.alert-warning {
@apply bg-yellow-50 text-yellow-800 border border-yellow-200;
}
.alert-success {
@apply bg-green-50 text-green-800 border border-green-200;
}
</style> </style>

View File

@ -37,7 +37,9 @@ const [useProvideBoardTableManager, useBoardTableManager] =
port: false, port: false,
id: false, id: false,
status: false, status: false,
version: false, firmVersion: false,
macAddr: false,
occupiedUserName: false,
}); });
const rowSelection = ref({}); const rowSelection = ref({});
const expanded = ref<ExpandedState>({}); const expanded = ref<ExpandedState>({});
@ -88,20 +90,12 @@ const [useProvideBoardTableManager, useBoardTableManager] =
enableHiding: true, enableHiding: true,
}, },
{ {
accessorKey: "devAddr", accessorKey: "ipAddr",
header: "IP 地址", header: "IP 地址",
cell: ({ row }) => { cell: ({ row }) => {
const device = row.original; const device = row.original;
return isEditMode.value // IP地址设置为不可更改
? h("input", { return h("span", { class: "font-mono" }, device.ipAddr);
type: "text",
class: "input input-sm w-full",
value: device.ipAddr,
onInput: (e: Event) => {
device.ipAddr = (e.target as HTMLInputElement).value;
},
})
: h("span", { class: "font-medium" }, device.ipAddr);
}, },
}, },
{ {
@ -109,16 +103,28 @@ const [useProvideBoardTableManager, useBoardTableManager] =
header: "端口", header: "端口",
cell: ({ row }) => { cell: ({ row }) => {
const device = row.original; const device = row.original;
return isEditMode.value // 端口设置为不可更改
? h("input", { return h("span", { class: "font-mono" }, device.port.toString());
type: "number", },
class: "input input-sm w-full", enableHiding: true,
value: device.port, },
onInput: (e: Event) => { {
device.port = parseInt((e.target as HTMLInputElement).value); accessorKey: "macAddr",
}, header: "MAC 地址",
}) cell: ({ row }) => {
: h("span", { class: "font-mono" }, device.port.toString()); const device = row.original;
return h("span", { class: "font-mono text-sm" }, device.macAddr);
},
enableHiding: true,
},
{
accessorKey: "occupiedUserName",
header: "占用用户",
cell: ({ row }) => {
const device = row.original;
const userName = device.occupiedUserName || "未占用";
const userClass = device.occupiedUserName ? "text-warning" : "text-success";
return h("span", { class: `font-medium ${userClass}` }, userName);
}, },
enableHiding: true, enableHiding: true,
}, },
@ -134,9 +140,27 @@ const [useProvideBoardTableManager, useBoardTableManager] =
header: "状态", header: "状态",
cell: ({ row }) => { cell: ({ row }) => {
const device = row.original; const device = row.original;
const statusText = device.status === 0 ? "忙碌" : "可用"; let statusText = "";
const statusClass = let statusClass = "";
device.status === 0 ? "badge-warning" : "badge-success";
switch (device.status) {
case 0: // Disabled
statusText = "禁用";
statusClass = "badge-error";
break;
case 1: // Busy
statusText = "忙碌";
statusClass = "badge-warning";
break;
case 2: // Available
statusText = "可用";
statusClass = "badge-success";
break;
default:
statusText = "未知";
statusClass = "badge-neutral";
}
return h( return h(
"span", "span",
{ {
@ -148,10 +172,11 @@ const [useProvideBoardTableManager, useBoardTableManager] =
enableHiding: true, enableHiding: true,
}, },
{ {
accessorKey: "version", accessorKey: "firmVersion",
header: "版本", header: "固件版本",
cell: ({ row }) => cell: ({ row }) =>
h("span", { class: "font-mono" }, row.original.firmVersion), h("span", { class: "font-mono text-sm" }, row.original.firmVersion),
enableHiding: true,
}, },
{ {
accessorKey: "defaultBitstream", accessorKey: "defaultBitstream",
@ -248,7 +273,7 @@ const [useProvideBoardTableManager, useBoardTableManager] =
class: "btn btn-error btn-sm", class: "btn btn-error btn-sm",
onClick: async () => { onClick: async () => {
const confirmed = confirm( const confirmed = confirm(
`确定要删除设备 ${device.ipAddr} 吗?`, `确定要删除设备 ${device.boardName || device.ipAddr} 吗?`,
); );
if (confirmed) { if (confirmed) {
await deleteBoard(device.id); await deleteBoard(device.id);
@ -378,8 +403,8 @@ const [useProvideBoardTableManager, useBoardTableManager] =
} }
// 新增板卡 // 新增板卡
async function addBoard(name: string, ipAddr: string, port: number): Promise<boolean> { async function addBoard(name: string): Promise<boolean> {
const result = await boardManager.addBoard(name, ipAddr, port); const result = await boardManager.addBoard(name);
if (result.success) { if (result.success) {
dialog?.info("新增板卡成功"); dialog?.info("新增板卡成功");
} else { } else {