feat: 更加完善实验板管理面板,前后端分离

This commit is contained in:
SikongJueluo 2025-07-12 13:37:02 +08:00
parent e0619eb9a3
commit 50ffd491fe
No known key found for this signature in database
7 changed files with 1092 additions and 577 deletions

View File

@ -89,6 +89,7 @@ try
{
options.AddPolicy("Users", policy => policy
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
);
});

View File

@ -0,0 +1,251 @@
<template>
<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>
<!-- 端口号 -->
<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>
<!-- 操作按钮 -->
<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>
<!-- 点击背景关闭 -->
<form method="dialog" class="modal-backdrop">
<button type="button" @click="handleCancel">close</button>
</form>
</dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { useBoardManager } from './BoardManager';
// Props Emits
interface Props {
visible: boolean;
}
interface Emits {
(e: 'update:visible', value: boolean): void;
(e: 'success'): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
// 使 BoardManager
const boardManager = useBoardManager()!;
//
const form = reactive({
name: '',
ipAddr: '',
port: 1234
});
//
const errors = reactive({
name: '',
ipAddr: '',
port: ''
});
//
const isSubmitting = 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]?)$/;
//
function validateForm(): boolean {
//
errors.name = '';
errors.ipAddr = '';
errors.port = '';
let isValid = true;
//
if (!form.name.trim()) {
errors.name = '请输入实验板名称';
isValid = false;
} else if (form.name.trim().length < 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 = '端口号必须是整数';
isValid = false;
}
return isValid;
}
//
function resetForm() {
form.name = '';
form.ipAddr = '';
form.port = 1234;
errors.name = '';
errors.ipAddr = '';
errors.port = '';
}
//
function handleCancel() {
if (!isSubmitting.value) {
emit('update:visible', false);
resetForm();
}
}
//
async function handleSubmit() {
if (!validateForm()) {
return;
}
isSubmitting.value = true;
try {
const success = await boardManager.addBoard(
form.name.trim(),
form.ipAddr.trim(),
form.port
);
if (success) {
emit('success');
resetForm();
}
} catch (error) {
console.error('添加实验板失败:', error);
} finally {
isSubmitting.value = false;
}
}
//
watch(() => props.visible, (newVisible) => {
if (newVisible) {
resetForm();
}
});
</script>
<style scoped lang="postcss">
@import "@/assets/main.css";
.form-control {
@apply w-full;
}
.label-text {
@apply font-medium;
}
.input-error {
@apply border-error;
}
.text-error {
@apply text-red-500;
}
.loading {
@apply opacity-50 cursor-not-allowed;
}
</style>

View File

@ -1,485 +0,0 @@
<template>
<div class="flex flex-row justify-between items-center">
<h1 class="text-3xl font-bold mb-6">FPGA 设备管理</h1>
<button
class="btn btn-ghost text-error hover:underline"
@click="toggleEditMode"
>
编辑
</button>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="flex flex-row justify-between items-center mb-4">
<h2 class="card-title">IP 地址列表</h2>
<button class="btn btn-ghost" @click="boardManager.refreshData">
刷新
</button>
</div>
<!-- 搜索和列控制 -->
<div class="flex items-center py-4 gap-4">
<input
type="text"
placeholder="筛选 IP 地址..."
class="input input-bordered max-w-sm"
:value="table.getColumn('devAddr')?.getFilterValue() as string"
@input="
table
.getColumn('devAddr')
?.setFilterValue(($event.target as HTMLInputElement).value)
"
/>
<div class="dropdown dropdown-end ml-auto">
<div tabindex="0" role="button" class="btn btn-outline">
列显示
<svg
class="w-4 h-4 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m19 9-7 7-7-7"
></path>
</svg>
</div>
<ul
tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
>
<li
v-for="column in table
.getAllColumns()
.filter((column) => column.getCanHide())"
:key="column.id"
>
<label class="label cursor-pointer">
<span class="label-text capitalize">{{ column.id }}</span>
<input
type="checkbox"
class="checkbox checkbox-sm"
:checked="column.getIsVisible()"
@change="
column.toggleVisibility(
!!($event.target as HTMLInputElement).checked,
)
"
/>
</label>
</li>
</ul>
</div>
</div>
<!-- 表格 -->
<div class="overflow-x-auto border border-base-300 rounded-lg">
<table class="table w-full">
<thead>
<tr
v-for="headerGroup in table.getHeaderGroups()"
:key="headerGroup.id"
class="bg-base-300"
>
<th v-for="header in headerGroup.headers" :key="header.id">
<FlexRender
v-if="!header.isPlaceholder"
:render="header.column.columnDef.header"
:props="header.getContext()"
/>
</th>
</tr>
</thead>
<tbody>
<template v-if="table.getRowModel().rows?.length">
<template v-for="row in table.getRowModel().rows" :key="row.id">
<tr
class="hover"
:class="{ 'bg-primary/10': row.getIsSelected() }"
>
<td v-for="cell in row.getVisibleCells()" :key="cell.id">
<FlexRender
:render="cell.column.columnDef.cell"
:props="cell.getContext()"
/>
</td>
</tr>
<tr v-if="row.getIsExpanded()">
<td :colspan="row.getAllCells().length" class="bg-base-200">
<div class="p-4">
<pre class="text-sm">{{
JSON.stringify(row.original, null, 2)
}}</pre>
</div>
</td>
</tr>
</template>
</template>
<tr v-else>
<td
:colspan="columns.length"
class="h-24 text-center text-base-content/60"
>
暂无数据
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页控制 -->
<div class="flex items-center justify-between py-4">
<div class="text-sm text-base-content/60">
已选择 {{ table.getFilteredSelectedRowModel().rows.length }} /
{{ table.getFilteredRowModel().rows.length }}
</div>
<div class="flex gap-2">
<button
class="btn btn-outline btn-sm"
:disabled="!table.getCanPreviousPage()"
@click="table.previousPage()"
>
上一页
</button>
<button
class="btn btn-outline btn-sm"
:disabled="!table.getCanNextPage()"
@click="table.nextPage()"
>
下一页
</button>
</div>
</div>
<div class="mt-6 bg-base-300 p-4 rounded-lg">
<p class="text-sm opacity-80">
<span class="font-semibold text-error">提示</span>
请谨慎操作FPGA固化和热启动功能确保上传的位流文件无误以避免设备损坏
</p>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type {
ColumnDef,
ColumnFiltersState,
ExpandedState,
SortingState,
VisibilityState,
} from "@tanstack/vue-table";
import {
FlexRender,
getCoreRowModel,
getExpandedRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from "@tanstack/vue-table";
import { h, onMounted, ref } from "vue";
import { useProvideBoardManager, type BoardData } from "./BoardManager";
// 使 BoardManager
const boardManager = useProvideBoardManager()!;
//
const isEditMode = ref(false);
function toggleEditMode() {
isEditMode.value = !isEditMode.value;
}
//
const columns: ColumnDef<BoardData>[] = [
{
id: "select",
header: ({ table }) =>
h("input", {
type: "checkbox",
class: "checkbox",
checked:
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() ? "indeterminate" : false),
onChange: (event: Event) =>
table.toggleAllPageRowsSelected(
!!(event.target as HTMLInputElement).checked,
),
}),
cell: ({ row }) =>
h("input", {
type: "checkbox",
class: "checkbox",
checked: row.getIsSelected(),
onChange: (event: Event) =>
row.toggleSelected(!!(event.target as HTMLInputElement).checked),
}),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "devAddr",
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);
},
},
{
accessorKey: "port",
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());
},
enableHiding: true,
},
{
accessorKey: "id",
header: "设备ID",
cell: ({ row }) =>
h("span", { class: "font-mono text-xs" }, row.original.id),
enableHiding: true,
},
{
accessorKey: "status",
header: "状态",
cell: ({ row }) => {
const device = row.original;
const statusText = device.status === 0 ? "忙碌" : "可用";
const statusClass = device.status === 0 ? "badge-warning" : "badge-success";
return h("span", {
class: `badge ${statusClass} min-w-15`
}, statusText);
},
enableHiding: true,
},
{
accessorKey: "version",
header: "版本号",
cell: ({ row }) =>
h("span", { class: "font-mono" }, row.original.firmVersion),
},
{
accessorKey: "defaultBitstream",
header: "默认启动位流",
cell: ({ row }) => {
const device = row.original;
return h(
"select",
{
class: "select select-bordered select-sm w-full",
value: device.defaultBitstream,
onChange: (e: Event) => {
device.defaultBitstream = (e.target as HTMLSelectElement).value;
},
},
[
h("option", { value: "黄金位流" }, "黄金位流"),
h("option", { value: "应用位流1" }, "应用位流1"),
h("option", { value: "应用位流2" }, "应用位流2"),
h("option", { value: "应用位流3" }, "应用位流3"),
],
);
},
},
{
id: "goldBitstream",
header: "黄金位流",
cell: ({ row }) => {
const device = row.original;
return h("input", {
type: "file",
class: "file-input file-input-primary file-input-sm",
onChange: (e: Event) =>
boardManager.handleFileChange(e, device, "goldBitstreamFile"),
});
},
},
{
id: "appBitstream1",
header: "应用位流1",
cell: ({ row }) => {
const device = row.original;
return h("input", {
type: "file",
class: "file-input file-input-secondary file-input-sm",
onChange: (e: Event) =>
boardManager.handleFileChange(e, device, "appBitstream1File"),
});
},
},
{
id: "appBitstream2",
header: "应用位流2",
cell: ({ row }) => {
const device = row.original;
return h("input", {
type: "file",
class: "file-input file-input-accent file-input-sm",
onChange: (e: Event) =>
boardManager.handleFileChange(e, device, "appBitstream2File"),
});
},
},
{
id: "appBitstream3",
header: "应用位流3",
cell: ({ row }) => {
const device = row.original;
return h("input", {
type: "file",
class: "file-input file-input-info file-input-sm",
onChange: (e: Event) =>
boardManager.handleFileChange(e, device, "appBitstream3File"),
});
},
},
{
id: "actions",
header: "操作",
cell: ({ row }) => {
const device = row.original;
return h("div", { class: "flex gap-2 min-w-30" }, [
h(
"button",
{
class: "btn btn-warning btn-sm",
onClick: () =>
boardManager.uploadAndDownloadBitstreams(
device,
device.goldBitstreamFile,
device.appBitstream1File,
device.appBitstream2File,
device.appBitstream3File,
),
},
"固化",
),
h(
"button",
{
class: "btn btn-success btn-sm",
onClick: () =>
boardManager.hotresetBitstream(
device,
boardManager.getSelectedBitstreamNum(device.defaultBitstream),
),
},
"热启动",
),
]);
},
enableHiding: false,
},
];
// TanStack Table
const sorting = ref<SortingState>([]);
const columnFilters = ref<ColumnFiltersState>([]);
const columnVisibility = ref<VisibilityState>({
// ID
port: false,
id: false,
status: false,
});
const rowSelection = ref({});
const expanded = ref<ExpandedState>({});
//
const table = useVueTable({
get data() {
return boardManager.boards.value;
},
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getExpandedRowModel: getExpandedRowModel(),
onSortingChange: (updaterOrValue) => {
if (typeof updaterOrValue === "function") {
sorting.value = updaterOrValue(sorting.value);
} else {
sorting.value = updaterOrValue;
}
},
onColumnFiltersChange: (updaterOrValue) => {
if (typeof updaterOrValue === "function") {
columnFilters.value = updaterOrValue(columnFilters.value);
} else {
columnFilters.value = updaterOrValue;
}
},
onColumnVisibilityChange: (updaterOrValue) => {
if (typeof updaterOrValue === "function") {
columnVisibility.value = updaterOrValue(columnVisibility.value);
} else {
columnVisibility.value = updaterOrValue;
}
},
onRowSelectionChange: (updaterOrValue) => {
if (typeof updaterOrValue === "function") {
rowSelection.value = updaterOrValue(rowSelection.value);
} else {
rowSelection.value = updaterOrValue;
}
},
onExpandedChange: (updaterOrValue) => {
if (typeof updaterOrValue === "function") {
expanded.value = updaterOrValue(expanded.value);
} else {
expanded.value = updaterOrValue;
}
},
state: {
get sorting() {
return sorting.value;
},
get columnFilters() {
return columnFilters.value;
},
get columnVisibility() {
return columnVisibility.value;
},
get rowSelection() {
return rowSelection.value;
},
get expanded() {
return expanded.value;
},
},
});
onMounted(() => {
//
boardManager.fetchBoardsData();
});
</script>
<style scoped lang="postcss">
@import "@/assets/main.css";
</style>

View File

@ -1,8 +1,6 @@
import { ref } from "vue";
import { createInjectionState } from "@vueuse/core";
import { RemoteUpdateClient, DataClient, Board } from "@/APIClient";
import { useDialogStore } from "@/stores/dialog";
import { useAlertStore } from "@/components/Alert";
import { Common } from "@/utils/Common";
import { isUndefined } from "lodash";
import { AuthManager } from "@/utils/AuthManager";
@ -17,9 +15,6 @@ export interface BoardData extends Board {
}
const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
const dialog = useDialogStore();
const alert = useAlertStore();
// 远程升级相关参数
const devPort = 1234;
const remoteUpdater = new RemoteUpdateClient();
@ -36,13 +31,14 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
return 0;
}
// 获取所有板卡信息(管理员权限)- 不显示提示信息,供内部调用
async function fetchBoardsData(): Promise<boolean> {
// 获取所有板卡信息(管理员权限)
async function getAllBoards(): Promise<{ success: boolean; error?: string }> {
try {
// 验证管理员权限
const hasAdminAuth = await AuthManager.verifyAdminAuth();
if (!hasAdminAuth) {
return false;
console.error("权限验证失败");
return { success: false, error: "权限不足" };
}
const client = AuthManager.createAuthenticatedClient();
@ -63,109 +59,98 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
toJSON: board.toJSON?.bind(board),
};
});
return true;
console.log("获取板卡信息成功", result.length);
return { success: true };
} else {
return false;
console.error("获取板卡信息失败:返回结果为空");
return { success: false, error: "获取板卡信息失败" };
}
} catch (e) {
console.error(e);
return false;
} catch (e: any) {
console.error("获取板卡信息异常:", e);
return { success: false, error: e.message || "获取板卡信息异常" };
}
}
// 获取所有板卡信息(管理员权限)- 显示提示信息,供外部调用
async function getAllBoards(): Promise<boolean> {
const result = await fetchBoardsData();
if (result) {
dialog.info("获取板卡信息成功");
} else {
dialog.warn("获取板卡信息失败");
}
return result;
}
// 新增板卡(管理员权限)
async function addBoard(
name: string,
ipAddr: string,
port: number,
): Promise<boolean> {
): Promise<{ success: boolean; error?: string; boardId?: string }> {
try {
// 验证管理员权限
const hasAdminAuth = await AuthManager.verifyAdminAuth();
if (!hasAdminAuth) {
dialog.error("需要管理员权限");
return false;
console.error("权限验证失败");
return { success: false, error: "权限不足" };
}
// 验证输入参数
if (!name || !ipAddr || !port) {
dialog.error("请填写完整的板卡信息");
return false;
console.error("参数验证失败", { name, ipAddr, port });
return { success: false, error: "参数不完整" };
}
const client = AuthManager.createAuthenticatedClient();
const boardId = await client.addBoard(name, ipAddr, port);
if (boardId) {
// 静默刷新板卡列表,不显示重复提示
await fetchBoardsData();
dialog.info("新增板卡成功");
return true;
console.log("新增板卡成功", { boardId, name, ipAddr, port });
// 刷新板卡列表
await getAllBoards();
return { success: true};
} else {
dialog.warn("新增板卡失败");
return false;
console.error("新增板卡失败返回ID为空");
return { success: false, error: "新增板卡失败" };
}
} catch (e: any) {
console.error("新增板卡异常:", e);
if (e.status === 401) {
dialog.error("权限不足,需要管理员权限");
return { success: false, error: "权限不足" };
} else if (e.status === 400) {
dialog.error("输入参数错误");
return { success: false, error: "输入参数错误" };
} else {
dialog.error("新增板卡失败");
return { success: false, error: e.message || "新增板卡异常" };
}
console.error(e);
return false;
}
}
// 删除板卡(管理员权限)
async function deleteBoard(boardId: string): Promise<boolean> {
async function deleteBoard(boardId: string): Promise<{ success: boolean; error?: string }> {
try {
// 验证管理员权限
const hasAdminAuth = await AuthManager.verifyAdminAuth();
if (!hasAdminAuth) {
dialog.error("需要管理员权限");
return false;
console.error("权限验证失败");
return { success: false, error: "权限不足" };
}
if (!boardId) {
dialog.error("板卡ID不能为空");
return false;
console.error("板卡ID为空");
return { success: false, error: "板卡ID不能为空" };
}
const client = AuthManager.createAuthenticatedClient();
const result = await client.deleteBoard(boardId);
if (result > 0) {
// 静默刷新板卡列表,不显示重复提示
await fetchBoardsData();
dialog.info("删除板卡成功");
return true;
console.log("删除板卡成功", { boardId, deletedCount: result });
// 刷新板卡列表
await getAllBoards();
return { success: true };
} else {
dialog.warn("删除板卡失败");
return false;
console.error("删除板卡失败影响行数为0");
return { success: false, error: "删除板卡失败" };
}
} catch (e: any) {
console.error("删除板卡异常:", e);
if (e.status === 401) {
dialog.error("权限不足,需要管理员权限");
return { success: false, error: "权限不足" };
} else if (e.status === 400) {
dialog.error("输入参数错误");
return { success: false, error: "输入参数错误" };
} else {
dialog.error("删除板卡失败");
return { success: false, error: e.message || "删除板卡异常" };
}
console.error(e);
return false;
}
}
@ -176,20 +161,23 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
appBitstream1?: File,
appBitstream2?: File,
appBitstream3?: File,
) {
): Promise<{ success: boolean; error?: string }> {
let cnt = 0;
if (!isUndefined(goldBitstream)) cnt++;
if (!isUndefined(appBitstream1)) cnt++;
if (!isUndefined(appBitstream2)) cnt++;
if (!isUndefined(appBitstream3)) cnt++;
if (cnt === 0) {
dialog.error("未选择比特流");
return;
console.error("未选择比特流文件");
return { success: false, error: "未选择比特流文件" };
}
try {
console.log("开始上传比特流", { boardIp: board.ipAddr, fileCount: cnt });
const uploadResult = await remoteUpdater.uploadBitstreams(
board.ipAddr, // 使用板卡的IP地址
board.ipAddr,
Common.toFileParameterOrNull(goldBitstream),
Common.toFileParameterOrNull(appBitstream1),
Common.toFileParameterOrNull(appBitstream2),
@ -197,10 +185,12 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
);
if (!uploadResult) {
dialog.warn("上传比特流出错");
return;
console.error("上传比特流失败");
return { success: false, error: "上传比特流失败" };
}
console.log("比特流上传成功,开始固化");
const downloadResult = await remoteUpdater.downloadMultiBitstreams(
board.ipAddr,
board.port,
@ -208,47 +198,42 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
);
if (downloadResult != cnt) {
dialog.warn("固化比特流出错");
console.error("固化比特流失败", { expected: cnt, actual: downloadResult });
return { success: false, error: "固化比特流失败" };
} else {
dialog.info("固化比特流成功");
console.log("固化比特流成功", { count: downloadResult });
return { success: true };
}
} catch (e) {
dialog.error("比特流上传错误");
console.error(e);
} catch (e: any) {
console.error("比特流操作异常:", e);
return { success: false, error: e.message || "比特流操作异常" };
}
}
// 热启动位流
async function hotresetBitstream(board: BoardData, bitstreamNum: number) {
async function hotresetBitstream(
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) {
dialog.info("切换比特流成功");
console.log("热启动比特流成功");
return { success: true };
} else {
dialog.error("切换比特流失败");
console.error("热启动比特流失败");
return { success: false, error: "热启动比特流失败" };
}
} catch (e) {
dialog.error("切换比特流失败");
console.error(e);
}
}
// 刷新板卡数据 - 简化逻辑,避免重复提示
async function refreshData() {
try {
const result = await fetchBoardsData();
if (result) {
alert?.info("刷新数据成功");
} else {
alert?.error("获取板卡信息失败");
}
} catch (e) {
alert?.error("获取数据失败");
console.error(e);
} catch (e: any) {
console.error("热启动比特流异常:", e);
return { success: false, error: e.message || "热启动比特流异常" };
}
}
@ -268,6 +253,7 @@ 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 });
}
}
@ -275,11 +261,9 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
boards,
uploadAndDownloadBitstreams,
hotresetBitstream,
refreshData,
handleFileChange,
getSelectedBitstreamNum,
getAllBoards,
fetchBoardsData,
addBoard,
deleteBoard,
};

View File

@ -0,0 +1,220 @@
<template>
<div class="flex flex-row justify-between items-center">
<h1 class="text-3xl font-bold mb-6">FPGA 设备管理</h1>
<button
class="btn btn-ghost text-error hover:underline"
@click="tableManager.toggleEditMode"
>
编辑
</button>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="flex flex-row justify-between items-center mb-4">
<h2 class="card-title">IP 地址列表</h2>
<button class="btn btn-ghost" @click="tableManager.getAllBoards">
刷新
</button>
</div>
<!-- 搜索和列控制 -->
<div class="flex items-center py-4 gap-4">
<input
type="text"
placeholder="筛选 IP 地址..."
class="input input-bordered max-w-sm"
:value="tableManager.getColumnByKey('devAddr')?.getFilterValue() as string"
@input="
tableManager.getColumnByKey('devAddr')
?.setFilterValue(($event.target as HTMLInputElement).value)
"
/>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-outline">
列显示
<svg
class="w-4 h-4 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m19 9-7 7-7-7"
></path>
</svg>
</div>
<ul
tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
>
<li
v-for="column in tableManager.getAllHideableColumns()"
:key="column.id"
>
<label class="label cursor-pointer">
<span class="label-text capitalize">{{ column.id }}</span>
<input
type="checkbox"
class="checkbox checkbox-sm"
:checked="column.getIsVisible()"
@change="
column.toggleVisibility(
!!($event.target as HTMLInputElement).checked,
)
"
/>
</label>
</li>
</ul>
</div>
<div class="flex gap-2 ml-auto">
<button
class="btn btn-primary"
:disabled="!tableManager.isEditMode.value"
@click="showAddBoardDialog = true"
>
新增实验板
</button>
<button
class="btn btn-error"
:disabled="!tableManager.isEditMode.value"
@click="tableManager.deleteSelectedBoards"
>
删除选中
</button>
</div>
</div>
<!-- 表格 -->
<div class="overflow-x-auto border border-base-300 rounded-lg">
<table class="table w-full">
<thead>
<tr
v-for="headerGroup in tableManager.getHeaderGroups()"
:key="headerGroup.id"
class="bg-base-300"
>
<th v-for="header in headerGroup.headers" :key="header.id">
<FlexRender
v-if="!header.isPlaceholder"
:render="header.column.columnDef.header"
:props="header.getContext()"
/>
</th>
</tr>
</thead>
<tbody>
<template v-if="tableManager.getRowModel().rows?.length">
<template v-for="row in tableManager.getRowModel().rows" :key="row.id">
<tr
class="hover"
:class="{ 'bg-primary/10': row.getIsSelected() }"
>
<td v-for="cell in row.getVisibleCells()" :key="cell.id">
<FlexRender
:render="cell.column.columnDef.cell"
:props="cell.getContext()"
/>
</td>
</tr>
<tr v-if="row.getIsExpanded()">
<td :colspan="row.getAllCells().length" class="bg-base-200">
<div class="p-4">
<pre class="text-sm">{{
JSON.stringify(row.original, null, 2)
}}</pre>
</div>
</td>
</tr>
</template>
</template>
<tr v-else>
<td
:colspan="tableManager.columns.length"
class="h-24 text-center text-base-content/60"
>
暂无数据
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页控制 -->
<div class="flex items-center justify-between py-4">
<div class="text-sm text-base-content/60">
已选择 {{ tableManager.getSelectedRows().length }} /
{{ tableManager.getAllRows().length }}
</div>
<div class="flex gap-2">
<button
class="btn btn-outline btn-sm"
:disabled="!tableManager.canPreviousPage()"
@click="tableManager.previousPage()"
>
上一页
</button>
<button
class="btn btn-outline btn-sm"
:disabled="!tableManager.canNextPage()"
@click="tableManager.nextPage()"
>
下一页
</button>
</div>
</div>
<div class="mt-6 bg-base-300 p-4 rounded-lg">
<p class="text-sm opacity-80">
<span class="font-semibold text-error">提示</span>
请谨慎操作FPGA固化和热启动功能确保上传的位流文件无误以避免设备损坏
</p>
</div>
</div>
</div>
<!-- 新增实验板对话框 -->
<AddBoardDialog
v-model:visible="showAddBoardDialog"
@success="handleAddBoardSuccess"
/>
</template>
<script lang="ts" setup>
import { FlexRender } from "@tanstack/vue-table";
import { onMounted, ref } from "vue";
import { useProvideBoardManager } from "./BoardManager";
import { useProvideBoardTableManager } from "./BoardTableManager";
import AddBoardDialog from "./AddBoardDialog.vue";
// 使 BoardManager
const boardManager = useProvideBoardManager()!;
// 使
const tableManager = useProvideBoardTableManager()!;
//
const showAddBoardDialog = ref(false);
//
const handleAddBoardSuccess = () => {
showAddBoardDialog.value = false;
// BoardManager.addBoard
};
onMounted(() => {
//
boardManager.getAllBoards();
});
</script>
<style scoped lang="postcss">
@import "@/assets/main.css";
</style>

View File

@ -0,0 +1,544 @@
import type {
ColumnDef,
ColumnFiltersState,
ExpandedState,
SortingState,
VisibilityState,
} from "@tanstack/vue-table";
import {
getCoreRowModel,
getExpandedRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from "@tanstack/vue-table";
import { h, ref, computed, version } from "vue";
import { createInjectionState } from "@vueuse/core";
import type { BoardData } from "./BoardManager";
import { useBoardManager } from "./BoardManager";
import { useDialogStore } from "@/stores/dialog";
const [useProvideBoardTableManager, useBoardTableManager] =
createInjectionState(() => {
// 从BoardManager获取数据和方法
const boardManager = useBoardManager()!;
const dialog = useDialogStore();
// 编辑状态
const isEditMode = ref(false);
// 表格状态管理
const sorting = ref<SortingState>([]);
const columnFilters = ref<ColumnFiltersState>([]);
const columnVisibility = ref<VisibilityState>({
// 默认隐藏端口、ID、状态列和板卡名称列
port: false,
id: false,
status: false,
version: false,
});
const rowSelection = ref({});
const expanded = ref<ExpandedState>({});
// 表格列定义
const columns: ColumnDef<BoardData>[] = [
{
id: "select",
header: ({ table }) =>
h("input", {
type: "checkbox",
class: "checkbox",
checked:
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() ? "indeterminate" : false),
onChange: (event: Event) =>
table.toggleAllPageRowsSelected(
!!(event.target as HTMLInputElement).checked,
),
}),
cell: ({ row }) =>
h("input", {
type: "checkbox",
class: "checkbox",
checked: row.getIsSelected(),
onChange: (event: Event) =>
row.toggleSelected(!!(event.target as HTMLInputElement).checked),
}),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "boardName",
header: "板卡名称",
cell: ({ row }) => {
const device = row.original;
return isEditMode.value
? h("input", {
type: "text",
class: "input input-sm w-full",
value: device.boardName,
onInput: (e: Event) => {
device.boardName = (e.target as HTMLInputElement).value;
},
})
: h("span", { class: "font-medium" }, device.boardName);
},
enableHiding: true,
},
{
accessorKey: "devAddr",
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);
},
},
{
accessorKey: "port",
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());
},
enableHiding: true,
},
{
accessorKey: "id",
header: "设备ID",
cell: ({ row }) =>
h("span", { class: "font-mono text-xs" }, row.original.id),
enableHiding: true,
},
{
accessorKey: "status",
header: "状态",
cell: ({ row }) => {
const device = row.original;
const statusText = device.status === 0 ? "忙碌" : "可用";
const statusClass =
device.status === 0 ? "badge-warning" : "badge-success";
return h(
"span",
{
class: `badge ${statusClass} min-w-15`,
},
statusText,
);
},
enableHiding: true,
},
{
accessorKey: "version",
header: "版本号",
cell: ({ row }) =>
h("span", { class: "font-mono" }, row.original.firmVersion),
},
{
accessorKey: "defaultBitstream",
header: "默认启动位流",
cell: ({ row }) => {
const device = row.original;
return h(
"select",
{
class: "select select-bordered select-sm w-full",
value: device.defaultBitstream,
onChange: (e: Event) => {
device.defaultBitstream = (e.target as HTMLSelectElement).value;
},
},
[
h("option", { value: "黄金位流" }, "黄金位流"),
h("option", { value: "应用位流1" }, "应用位流1"),
h("option", { value: "应用位流2" }, "应用位流2"),
h("option", { value: "应用位流3" }, "应用位流3"),
],
);
},
},
{
id: "goldBitstream",
header: "黄金位流",
cell: ({ row }) => {
const device = row.original;
return h("input", {
type: "file",
class: "file-input file-input-primary file-input-sm",
onChange: (e: Event) =>
boardManager.handleFileChange(e, device, "goldBitstreamFile"),
});
},
},
{
id: "appBitstream1",
header: "应用位流1",
cell: ({ row }) => {
const device = row.original;
return h("input", {
type: "file",
class: "file-input file-input-secondary file-input-sm",
onChange: (e: Event) =>
boardManager.handleFileChange(e, device, "appBitstream1File"),
});
},
},
{
id: "appBitstream2",
header: "应用位流2",
cell: ({ row }) => {
const device = row.original;
return h("input", {
type: "file",
class: "file-input file-input-accent file-input-sm",
onChange: (e: Event) =>
boardManager.handleFileChange(e, device, "appBitstream2File"),
});
},
},
{
id: "appBitstream3",
header: "应用位流3",
cell: ({ row }) => {
const device = row.original;
return h("input", {
type: "file",
class: "file-input file-input-info file-input-sm",
onChange: (e: Event) =>
boardManager.handleFileChange(e, device, "appBitstream3File"),
});
},
},
{
id: "actions",
header: "操作",
cell: ({ row }) => {
const device = row.original;
// 根据编辑模式显示不同的按钮
if (isEditMode.value) {
return h(
"div",
{
class: ["flex gap-2", { "min-w-30": !isEditMode.value }],
},
[
h(
"button",
{
class: "btn btn-error btn-sm",
onClick: async () => {
const confirmed = confirm(
`确定要删除设备 ${device.ipAddr} 吗?`,
);
if (confirmed) {
await deleteBoard(device.id);
}
},
},
"删除",
),
],
);
} else {
return h("div", { class: "flex gap-2 min-w-30" }, [
h(
"button",
{
class: "btn btn-warning btn-sm",
onClick: () =>
uploadAndDownloadBitstreams(
device,
device.goldBitstreamFile,
device.appBitstream1File,
device.appBitstream2File,
device.appBitstream3File,
),
},
"固化",
),
h(
"button",
{
class: "btn btn-success btn-sm",
onClick: () =>
hotresetBitstream(
device,
boardManager.getSelectedBitstreamNum(
device.defaultBitstream,
),
),
},
"热启动",
),
]);
}
},
enableHiding: false,
},
];
// 创建表格实例
const table = useVueTable({
get data() {
return boardManager.boards.value;
},
get columns() {
return columns;
},
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getExpandedRowModel: getExpandedRowModel(),
onSortingChange: (updaterOrValue) => {
if (typeof updaterOrValue === "function") {
sorting.value = updaterOrValue(sorting.value);
} else {
sorting.value = updaterOrValue;
}
},
onColumnFiltersChange: (updaterOrValue) => {
if (typeof updaterOrValue === "function") {
columnFilters.value = updaterOrValue(columnFilters.value);
} else {
columnFilters.value = updaterOrValue;
}
},
onColumnVisibilityChange: (updaterOrValue) => {
if (typeof updaterOrValue === "function") {
columnVisibility.value = updaterOrValue(columnVisibility.value);
} else {
columnVisibility.value = updaterOrValue;
}
},
onRowSelectionChange: (updaterOrValue) => {
if (typeof updaterOrValue === "function") {
rowSelection.value = updaterOrValue(rowSelection.value);
} else {
rowSelection.value = updaterOrValue;
}
},
onExpandedChange: (updaterOrValue) => {
if (typeof updaterOrValue === "function") {
expanded.value = updaterOrValue(expanded.value);
} else {
expanded.value = updaterOrValue;
}
},
state: {
get sorting() {
return sorting.value;
},
get columnFilters() {
return columnFilters.value;
},
get columnVisibility() {
return columnVisibility.value;
},
get rowSelection() {
return rowSelection.value;
},
get expanded() {
return expanded.value;
},
},
});
// UI层的API封装方法 - 添加dialog提示
// 获取所有板卡信息
async function getAllBoards(): Promise<boolean> {
const result = await boardManager.getAllBoards();
if (result.success) {
dialog?.info("获取板卡信息成功");
} else {
dialog?.error(result.error || "获取板卡信息失败");
}
return result.success;
}
// 新增板卡
async function addBoard(name: string, ipAddr: string, port: number): Promise<boolean> {
const result = await boardManager.addBoard(name, ipAddr, port);
if (result.success) {
dialog?.info("新增板卡成功");
} else {
dialog?.error(result.error || "新增板卡失败");
}
return result.success;
}
// 删除板卡
async function deleteBoard(boardId: string): Promise<boolean> {
const result = await boardManager.deleteBoard(boardId);
if (result.success) {
dialog?.info("删除板卡成功");
} else {
dialog?.error(result.error || "删除板卡失败");
}
return result.success;
}
// 上传并固化位流
async function uploadAndDownloadBitstreams(
board: BoardData,
goldBitstream?: File,
appBitstream1?: File,
appBitstream2?: File,
appBitstream3?: File,
): Promise<boolean> {
const result = await boardManager.uploadAndDownloadBitstreams(
board,
goldBitstream,
appBitstream1,
appBitstream2,
appBitstream3,
);
if (result.success) {
dialog?.info("固化比特流成功");
} else {
dialog?.error(result.error || "固化比特流失败");
}
return result.success;
}
// 热启动位流
async function hotresetBitstream(board: BoardData, bitstreamNum: number): Promise<boolean> {
const result = await boardManager.hotresetBitstream(board, bitstreamNum);
if (result.success) {
dialog?.info("切换比特流成功");
} else {
dialog?.error(result.error || "切换比特流失败");
}
return result.success;
}
// 表格操作方法
const getSelectedRows = () => table.getFilteredSelectedRowModel().rows;
const getAllRows = () => table.getFilteredRowModel().rows;
const getColumnByKey = (key: string) => table.getColumn(key);
const getAllHideableColumns = () =>
table.getAllColumns().filter((column) => column.getCanHide());
const getHeaderGroups = () => table.getHeaderGroups();
const getRowModel = () => table.getRowModel();
const canPreviousPage = () => table.getCanPreviousPage();
const canNextPage = () => table.getCanNextPage();
const previousPage = () => table.previousPage();
const nextPage = () => table.nextPage();
// 编辑模式控制
const toggleEditMode = () => {
isEditMode.value = !isEditMode.value;
};
// 删除选中的实验板
const deleteSelectedBoards = async () => {
const selectedRows = getSelectedRows();
if (selectedRows.length === 0) {
dialog?.warn("请先选择要删除的实验板");
return false;
}
const boardNames = selectedRows
.map((row) => row.original.boardName || row.original.ipAddr)
.join("、");
const confirmed = confirm(
`确定要删除以下 ${selectedRows.length} 个实验板吗?\n${boardNames}`,
);
if (!confirmed) {
return false;
}
let successCount = 0;
let failCount = 0;
// 批量删除
for (const row of selectedRows) {
const board = row.original;
const result = await boardManager.deleteBoard(board.id);
if (result.success) {
successCount++;
} else {
failCount++;
}
}
// 清空选择状态
rowSelection.value = {};
// 显示结果提示
if (failCount === 0) {
dialog?.info(`成功删除 ${successCount} 个实验板`);
} else if (successCount === 0) {
dialog?.error(
`删除失败,共 ${failCount} 个实验板删除失败`,
);
} else {
dialog?.warn(
`部分删除成功:成功 ${successCount} 个,失败 ${failCount}`,
);
}
return successCount > 0;
};
return {
// 表格实例
table,
// 列定义
columns,
// 表格操作方法
getSelectedRows,
getAllRows,
getColumnByKey,
getAllHideableColumns,
getHeaderGroups,
getRowModel,
canPreviousPage,
canNextPage,
previousPage,
nextPage,
deleteSelectedBoards,
// 状态
sorting,
columnFilters,
columnVisibility,
rowSelection,
expanded,
// 编辑模式
isEditMode,
toggleEditMode,
// UI层封装的API方法
getAllBoards,
addBoard,
deleteBoard,
uploadAndDownloadBitstreams,
hotresetBitstream,
// BoardManager 的引用
boardManager,
};
});
export { useProvideBoardTableManager, useBoardTableManager };

View File

@ -20,14 +20,14 @@
<p>这里是用户信息页面的内容</p>
</div>
<div v-else-if="activePage === 100">
<BoardControl />
<BoardTable />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import BoardControl from "./BoardControl.vue";
import BoardTable from "./BoardTable.vue";
import { toNumber } from "lodash";
import { onMounted, ref } from "vue";
import { AuthManager } from "@/utils/AuthManager";