feat: 更加完善实验板管理面板,前后端分离
This commit is contained in:
parent
e0619eb9a3
commit
50ffd491fe
|
@ -89,6 +89,7 @@ try
|
|||
{
|
||||
options.AddPolicy("Users", policy => policy
|
||||
.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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>
|
|
@ -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 };
|
|
@ -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";
|
||||
|
|
Loading…
Reference in New Issue