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

This commit is contained in:
2025-07-17 21:18:59 +08:00
parent 0f4386457d
commit 80b6dfb38d
4 changed files with 372 additions and 175 deletions

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("无法获取板卡信息");
}
// 更新主机IP和主机MAC
await netConfigClient.updateHostIP();
await netConfigClient.updateHostMAC();
// 设置板卡IP和MAC
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 {