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

View File

@ -74,8 +74,6 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
// 新增板卡(管理员权限)
async function addBoard(
name: string,
ipAddr: string,
port: number,
): Promise<{ success: boolean; error?: string; boardId?: string }> {
try {
// 验证管理员权限
@ -86,19 +84,19 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
}
// 验证输入参数
if (!name || !ipAddr || !port) {
console.error("参数验证失败", { name, ipAddr, port });
if (!name) {
console.error("参数验证失败", { name });
return { success: false, error: "参数不完整" };
}
const client = AuthManager.createAuthenticatedDataClient();
const boardId = await client.addBoard(name, ipAddr, port);
const boardId = await client.addBoard(name);
if (boardId) {
console.log("新增板卡成功", { boardId, name, ipAddr, port });
console.log("新增板卡成功", { boardId, name});
// 刷新板卡列表
await getAllBoards();
return { success: true};
return { success: true };
} else {
console.error("新增板卡失败返回ID为空");
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 {
// 验证管理员权限
const hasAdminAuth = await AuthManager.verifyAdminAuth();
@ -167,7 +167,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
if (!isUndefined(appBitstream1)) cnt++;
if (!isUndefined(appBitstream2)) cnt++;
if (!isUndefined(appBitstream3)) cnt++;
if (cnt === 0) {
console.error("未选择比特流文件");
return { success: false, error: "未选择比特流文件" };
@ -175,7 +175,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
try {
console.log("开始上传比特流", { boardIp: board.ipAddr, fileCount: cnt });
const uploadResult = await remoteUpdater.uploadBitstreams(
board.ipAddr,
toFileParameterOrNull(goldBitstream),
@ -198,7 +198,10 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
);
if (downloadResult != cnt) {
console.error("固化比特流失败", { expected: cnt, actual: downloadResult });
console.error("固化比特流失败", {
expected: cnt,
actual: downloadResult,
});
return { success: false, error: "固化比特流失败" };
} else {
console.log("固化比特流成功", { count: downloadResult });
@ -212,18 +215,18 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
// 热启动位流
async function hotresetBitstream(
board: BoardData,
bitstreamNum: number
board: BoardData,
bitstreamNum: number,
): Promise<{ success: boolean; error?: string }> {
try {
console.log("开始热启动比特流", { boardIp: board.ipAddr, bitstreamNum });
const ret = await remoteUpdater.hotResetBitstream(
board.ipAddr,
board.port,
bitstreamNum,
);
if (ret) {
console.log("热启动比特流成功");
return { success: true };
@ -253,7 +256,11 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
const file = target.files?.[0];
if (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

@ -2,62 +2,74 @@
<dialog class="modal" :class="{ 'modal-open': visible }">
<div class="modal-box w-96 max-w-md">
<h3 class="text-lg font-bold mb-4">新增实验板</h3>
<form @submit.prevent="handleSubmit" class="space-y-4">
<!-- 实验板名称 -->
<div class="form-control">
<label class="label">
<span class="label-text">实验板名称 <span class="text-error">*</span></span>
</label>
<input
v-model="form.name"
type="text"
placeholder="请输入实验板名称"
class="input input-bordered"
:class="{ 'input-error': errors.name }"
required
/>
<label v-if="errors.name" class="label">
<span class="label-text-alt text-error">{{ errors.name }}</span>
</label>
</div>
<!-- IP 地址 -->
<div class="form-control">
<label class="label">
<span class="label-text">IP 地址 <span class="text-error">*</span></span>
</label>
<input
v-model="form.ipAddr"
type="text"
placeholder="例如192.168.1.100"
class="input input-bordered"
:class="{ 'input-error': errors.ipAddr }"
required
/>
<label v-if="errors.ipAddr" class="label">
<span class="label-text-alt text-error">{{ errors.ipAddr }}</span>
</label>
</div>
<!-- 步骤1: 输入板卡名称 -->
<div v-if="currentStep === 'input'" class="space-y-4">
<form @submit.prevent="handleSubmit">
<!-- 实验板名称 -->
<div class="form-control">
<label class="label">
<span class="label-text"
>实验板名称 <span class="text-error">*</span></span
>
</label>
<input
v-model="form.name"
type="text"
placeholder="请输入实验板名称"
class="input input-bordered"
:class="{ 'input-error': errors.name }"
required
/>
<label v-if="errors.name" class="label">
<span class="label-text-alt text-error">{{ errors.name }}</span>
</label>
</div>
<!-- 端口号 -->
<div class="form-control">
<label class="label">
<span class="label-text">端口号 <span class="text-error">*</span></span>
</label>
<input
v-model.number="form.port"
type="number"
placeholder="例如1234"
min="1"
max="65535"
class="input input-bordered"
:class="{ 'input-error': errors.port }"
required
/>
<label v-if="errors.port" class="label">
<span class="label-text-alt text-error">{{ errors.port }}</span>
</label>
<!-- 操作按钮 -->
<div class="modal-action">
<button
type="button"
class="btn btn-ghost"
@click="handleCancel"
:disabled="isSubmitting"
>
取消
</button>
<button
type="submit"
class="btn btn-primary"
:class="{ loading: isSubmitting }"
:disabled="isSubmitting"
>
{{ isSubmitting ? "添加中..." : "确认添加" }}
</button>
</div>
</form>
</div>
<!-- 步骤2: 等待配对 -->
<div v-else-if="currentStep === 'pairing'" class="space-y-4">
<div class="text-center">
<div class="alert alert-info">
<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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>请打开实验板配对模式</span>
</div>
</div>
</div>
<!-- 操作按钮 -->
@ -65,23 +77,84 @@
<button
type="button"
class="btn btn-ghost"
@click="handleCancel"
:disabled="isSubmitting"
@click="handleCancelPairing"
:disabled="isConfiguring"
>
取消
</button>
<button
type="submit"
type="button"
class="btn btn-primary"
:class="{ 'loading': isSubmitting }"
:disabled="isSubmitting"
@click="handlePairingConfirm"
:disabled="isConfiguring"
>
{{ isSubmitting ? '添加中...' : '确认添加' }}
已开启
</button>
</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>
<!-- 点击背景关闭 -->
<form method="dialog" class="modal-backdrop">
<button type="button" @click="handleCancel">close</button>
@ -90,8 +163,10 @@
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { useBoardManager } from '../../utils/BoardManager';
import { ref, reactive, watch } from "vue";
import { AuthManager } from "../../utils/AuthManager";
import { useAlertStore } from "../../components/Alert";
import type { NetworkConfigDto } from "../../APIClient";
// Props Emits
interface Props {
@ -99,75 +174,53 @@ interface Props {
}
interface Emits {
(e: 'update:visible', value: boolean): void;
(e: 'success'): void;
(e: "update:visible", value: boolean): void;
(e: "success"): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
// 使 BoardManager
const boardManager = useBoardManager()!;
// 使 Alert
const alertStore = useAlertStore();
//
const currentStep = ref<'input' | 'pairing' | 'configuring' | 'result'>('input');
//
const form = reactive({
name: 'Board1',
ipAddr: '169.254.103.0',
port: 1234
name: "Board1",
});
//
const errors = reactive({
name: '',
ipAddr: '',
port: ''
name: "",
});
//
//
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 {
//
errors.name = '';
errors.ipAddr = '';
errors.port = '';
errors.name = "";
let isValid = true;
//
if (!form.name.trim()) {
errors.name = '请输入实验板名称';
errors.name = "请输入实验板名称";
isValid = false;
} else if (form.name.trim().length < 2) {
errors.name = '实验板名称至少需要2个字符';
errors.name = "实验板名称至少需要2个字符";
isValid = false;
} else if (form.name.trim().length > 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 = '端口号必须是整数';
errors.name = "实验板名称不能超过50个字符";
isValid = false;
}
@ -176,18 +229,17 @@ function validateForm(): boolean {
//
function resetForm() {
form.name = 'Board1';
form.ipAddr = '169.254.103.0';
form.port = 1234;
errors.name = '';
errors.ipAddr = '';
errors.port = '';
form.name = "Board1";
errors.name = "";
currentStep.value = 'input';
addedBoardId.value = "";
networkConfig.value = null;
}
//
function handleCancel() {
if (!isSubmitting.value) {
emit('update:visible', false);
if (!isSubmitting.value && !isConfiguring.value) {
emit("update:visible", false);
resetForm();
}
}
@ -201,29 +253,120 @@ async function handleSubmit() {
isSubmitting.value = true;
try {
const success = await boardManager.addBoard(
form.name.trim(),
form.ipAddr.trim(),
form.port
);
if (success) {
emit('success');
resetForm();
// AuthManager DataClient
const dataClient = AuthManager.createAuthenticatedDataClient();
//
const boardId = await dataClient.addBoard(form.name.trim());
if (boardId) {
addedBoardId.value = boardId;
currentStep.value = 'pairing';
alertStore?.success("板卡添加成功,请开启配对模式");
} else {
alertStore?.error("板卡添加失败");
}
} catch (error) {
console.error('添加实验板失败:', error);
console.error("添加实验板失败:", error);
alertStore?.error("添加实验板失败");
} finally {
isSubmitting.value = false;
}
}
//
watch(() => props.visible, (newVisible) => {
if (newVisible) {
//
async function handleCancelPairing() {
if (!addedBoardId.value) return;
try {
// AuthManager DataClient
const dataClient = AuthManager.createAuthenticatedDataClient();
//
await dataClient.deleteBoard(addedBoardId.value);
alertStore?.info("已取消添加实验板");
emit("update:visible", false);
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>
<style scoped lang="postcss">
@ -248,4 +391,20 @@ watch(() => props.visible, (newVisible) => {
.loading {
@apply opacity-50 cursor-not-allowed;
}
</style>
.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>

View File

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