feat: 更加完善实验板管理面板,前后端分离
This commit is contained in:
parent
e0619eb9a3
commit
50ffd491fe
|
@ -89,6 +89,7 @@ try
|
||||||
{
|
{
|
||||||
options.AddPolicy("Users", policy => policy
|
options.AddPolicy("Users", policy => policy
|
||||||
.AllowAnyOrigin()
|
.AllowAnyOrigin()
|
||||||
|
.AllowAnyMethod()
|
||||||
.AllowAnyHeader()
|
.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 { ref } from "vue";
|
||||||
import { createInjectionState } from "@vueuse/core";
|
import { createInjectionState } from "@vueuse/core";
|
||||||
import { RemoteUpdateClient, DataClient, Board } from "@/APIClient";
|
import { RemoteUpdateClient, DataClient, Board } from "@/APIClient";
|
||||||
import { useDialogStore } from "@/stores/dialog";
|
|
||||||
import { useAlertStore } from "@/components/Alert";
|
|
||||||
import { Common } from "@/utils/Common";
|
import { Common } from "@/utils/Common";
|
||||||
import { isUndefined } from "lodash";
|
import { isUndefined } from "lodash";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
|
@ -17,9 +15,6 @@ export interface BoardData extends Board {
|
||||||
}
|
}
|
||||||
|
|
||||||
const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
|
const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
|
||||||
const dialog = useDialogStore();
|
|
||||||
const alert = useAlertStore();
|
|
||||||
|
|
||||||
// 远程升级相关参数
|
// 远程升级相关参数
|
||||||
const devPort = 1234;
|
const devPort = 1234;
|
||||||
const remoteUpdater = new RemoteUpdateClient();
|
const remoteUpdater = new RemoteUpdateClient();
|
||||||
|
@ -36,13 +31,14 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取所有板卡信息(管理员权限)- 不显示提示信息,供内部调用
|
// 获取所有板卡信息(管理员权限)
|
||||||
async function fetchBoardsData(): Promise<boolean> {
|
async function getAllBoards(): Promise<{ success: boolean; error?: string }> {
|
||||||
try {
|
try {
|
||||||
// 验证管理员权限
|
// 验证管理员权限
|
||||||
const hasAdminAuth = await AuthManager.verifyAdminAuth();
|
const hasAdminAuth = await AuthManager.verifyAdminAuth();
|
||||||
if (!hasAdminAuth) {
|
if (!hasAdminAuth) {
|
||||||
return false;
|
console.error("权限验证失败");
|
||||||
|
return { success: false, error: "权限不足" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = AuthManager.createAuthenticatedClient();
|
const client = AuthManager.createAuthenticatedClient();
|
||||||
|
@ -63,109 +59,98 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
|
||||||
toJSON: board.toJSON?.bind(board),
|
toJSON: board.toJSON?.bind(board),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return true;
|
console.log("获取板卡信息成功", result.length);
|
||||||
|
return { success: true };
|
||||||
} else {
|
} else {
|
||||||
return false;
|
console.error("获取板卡信息失败:返回结果为空");
|
||||||
|
return { success: false, error: "获取板卡信息失败" };
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error("获取板卡信息异常:", e);
|
||||||
return false;
|
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(
|
async function addBoard(
|
||||||
name: string,
|
name: string,
|
||||||
ipAddr: string,
|
ipAddr: string,
|
||||||
port: number,
|
port: number,
|
||||||
): Promise<boolean> {
|
): Promise<{ success: boolean; error?: string; boardId?: string }> {
|
||||||
try {
|
try {
|
||||||
// 验证管理员权限
|
// 验证管理员权限
|
||||||
const hasAdminAuth = await AuthManager.verifyAdminAuth();
|
const hasAdminAuth = await AuthManager.verifyAdminAuth();
|
||||||
if (!hasAdminAuth) {
|
if (!hasAdminAuth) {
|
||||||
dialog.error("需要管理员权限");
|
console.error("权限验证失败");
|
||||||
return false;
|
return { success: false, error: "权限不足" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证输入参数
|
// 验证输入参数
|
||||||
if (!name || !ipAddr || !port) {
|
if (!name || !ipAddr || !port) {
|
||||||
dialog.error("请填写完整的板卡信息");
|
console.error("参数验证失败", { name, ipAddr, port });
|
||||||
return false;
|
return { success: false, error: "参数不完整" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = AuthManager.createAuthenticatedClient();
|
const client = AuthManager.createAuthenticatedClient();
|
||||||
const boardId = await client.addBoard(name, ipAddr, port);
|
const boardId = await client.addBoard(name, ipAddr, port);
|
||||||
|
|
||||||
if (boardId) {
|
if (boardId) {
|
||||||
// 静默刷新板卡列表,不显示重复提示
|
console.log("新增板卡成功", { boardId, name, ipAddr, port });
|
||||||
await fetchBoardsData();
|
// 刷新板卡列表
|
||||||
dialog.info("新增板卡成功");
|
await getAllBoards();
|
||||||
return true;
|
return { success: true};
|
||||||
} else {
|
} else {
|
||||||
dialog.warn("新增板卡失败");
|
console.error("新增板卡失败:返回ID为空");
|
||||||
return false;
|
return { success: false, error: "新增板卡失败" };
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
console.error("新增板卡异常:", e);
|
||||||
if (e.status === 401) {
|
if (e.status === 401) {
|
||||||
dialog.error("权限不足,需要管理员权限");
|
return { success: false, error: "权限不足" };
|
||||||
} else if (e.status === 400) {
|
} else if (e.status === 400) {
|
||||||
dialog.error("输入参数错误");
|
return { success: false, error: "输入参数错误" };
|
||||||
} else {
|
} 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 {
|
try {
|
||||||
// 验证管理员权限
|
// 验证管理员权限
|
||||||
const hasAdminAuth = await AuthManager.verifyAdminAuth();
|
const hasAdminAuth = await AuthManager.verifyAdminAuth();
|
||||||
if (!hasAdminAuth) {
|
if (!hasAdminAuth) {
|
||||||
dialog.error("需要管理员权限");
|
console.error("权限验证失败");
|
||||||
return false;
|
return { success: false, error: "权限不足" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!boardId) {
|
if (!boardId) {
|
||||||
dialog.error("板卡ID不能为空");
|
console.error("板卡ID为空");
|
||||||
return false;
|
return { success: false, error: "板卡ID不能为空" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = AuthManager.createAuthenticatedClient();
|
const client = AuthManager.createAuthenticatedClient();
|
||||||
const result = await client.deleteBoard(boardId);
|
const result = await client.deleteBoard(boardId);
|
||||||
|
|
||||||
if (result > 0) {
|
if (result > 0) {
|
||||||
// 静默刷新板卡列表,不显示重复提示
|
console.log("删除板卡成功", { boardId, deletedCount: result });
|
||||||
await fetchBoardsData();
|
// 刷新板卡列表
|
||||||
dialog.info("删除板卡成功");
|
await getAllBoards();
|
||||||
return true;
|
return { success: true };
|
||||||
} else {
|
} else {
|
||||||
dialog.warn("删除板卡失败");
|
console.error("删除板卡失败:影响行数为0");
|
||||||
return false;
|
return { success: false, error: "删除板卡失败" };
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
console.error("删除板卡异常:", e);
|
||||||
if (e.status === 401) {
|
if (e.status === 401) {
|
||||||
dialog.error("权限不足,需要管理员权限");
|
return { success: false, error: "权限不足" };
|
||||||
} else if (e.status === 400) {
|
} else if (e.status === 400) {
|
||||||
dialog.error("输入参数错误");
|
return { success: false, error: "输入参数错误" };
|
||||||
} else {
|
} else {
|
||||||
dialog.error("删除板卡失败");
|
return { success: false, error: e.message || "删除板卡异常" };
|
||||||
}
|
}
|
||||||
console.error(e);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,20 +161,23 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
|
||||||
appBitstream1?: File,
|
appBitstream1?: File,
|
||||||
appBitstream2?: File,
|
appBitstream2?: File,
|
||||||
appBitstream3?: File,
|
appBitstream3?: File,
|
||||||
) {
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
let cnt = 0;
|
let cnt = 0;
|
||||||
if (!isUndefined(goldBitstream)) cnt++;
|
if (!isUndefined(goldBitstream)) cnt++;
|
||||||
if (!isUndefined(appBitstream1)) cnt++;
|
if (!isUndefined(appBitstream1)) cnt++;
|
||||||
if (!isUndefined(appBitstream2)) cnt++;
|
if (!isUndefined(appBitstream2)) cnt++;
|
||||||
if (!isUndefined(appBitstream3)) cnt++;
|
if (!isUndefined(appBitstream3)) cnt++;
|
||||||
|
|
||||||
if (cnt === 0) {
|
if (cnt === 0) {
|
||||||
dialog.error("未选择比特流");
|
console.error("未选择比特流文件");
|
||||||
return;
|
return { success: false, error: "未选择比特流文件" };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log("开始上传比特流", { boardIp: board.ipAddr, fileCount: cnt });
|
||||||
|
|
||||||
const uploadResult = await remoteUpdater.uploadBitstreams(
|
const uploadResult = await remoteUpdater.uploadBitstreams(
|
||||||
board.ipAddr, // 使用板卡的IP地址
|
board.ipAddr,
|
||||||
Common.toFileParameterOrNull(goldBitstream),
|
Common.toFileParameterOrNull(goldBitstream),
|
||||||
Common.toFileParameterOrNull(appBitstream1),
|
Common.toFileParameterOrNull(appBitstream1),
|
||||||
Common.toFileParameterOrNull(appBitstream2),
|
Common.toFileParameterOrNull(appBitstream2),
|
||||||
|
@ -197,10 +185,12 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!uploadResult) {
|
if (!uploadResult) {
|
||||||
dialog.warn("上传比特流出错");
|
console.error("上传比特流失败");
|
||||||
return;
|
return { success: false, error: "上传比特流失败" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("比特流上传成功,开始固化");
|
||||||
|
|
||||||
const downloadResult = await remoteUpdater.downloadMultiBitstreams(
|
const downloadResult = await remoteUpdater.downloadMultiBitstreams(
|
||||||
board.ipAddr,
|
board.ipAddr,
|
||||||
board.port,
|
board.port,
|
||||||
|
@ -208,47 +198,42 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (downloadResult != cnt) {
|
if (downloadResult != cnt) {
|
||||||
dialog.warn("固化比特流出错");
|
console.error("固化比特流失败", { expected: cnt, actual: downloadResult });
|
||||||
|
return { success: false, error: "固化比特流失败" };
|
||||||
} else {
|
} else {
|
||||||
dialog.info("固化比特流成功");
|
console.log("固化比特流成功", { count: downloadResult });
|
||||||
|
return { success: true };
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
dialog.error("比特流上传错误");
|
console.error("比特流操作异常:", e);
|
||||||
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 {
|
try {
|
||||||
|
console.log("开始热启动比特流", { boardIp: board.ipAddr, bitstreamNum });
|
||||||
|
|
||||||
const ret = await remoteUpdater.hotResetBitstream(
|
const ret = await remoteUpdater.hotResetBitstream(
|
||||||
board.ipAddr,
|
board.ipAddr,
|
||||||
board.port,
|
board.port,
|
||||||
bitstreamNum,
|
bitstreamNum,
|
||||||
);
|
);
|
||||||
if (ret) {
|
|
||||||
dialog.info("切换比特流成功");
|
|
||||||
} else {
|
|
||||||
dialog.error("切换比特流失败");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
dialog.error("切换比特流失败");
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 刷新板卡数据 - 简化逻辑,避免重复提示
|
if (ret) {
|
||||||
async function refreshData() {
|
console.log("热启动比特流成功");
|
||||||
try {
|
return { success: true };
|
||||||
const result = await fetchBoardsData();
|
|
||||||
if (result) {
|
|
||||||
alert?.info("刷新数据成功");
|
|
||||||
} else {
|
} else {
|
||||||
alert?.error("获取板卡信息失败");
|
console.error("热启动比特流失败");
|
||||||
|
return { success: false, error: "热启动比特流失败" };
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
alert?.error("获取数据失败");
|
console.error("热启动比特流异常:", e);
|
||||||
console.error(e);
|
return { success: false, error: e.message || "热启动比特流异常" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,6 +253,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
|
||||||
const file = target.files?.[0];
|
const file = target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
(board as any)[fileKey] = file;
|
(board as any)[fileKey] = file;
|
||||||
|
console.log(`文件选择成功`, { boardIp: board.ipAddr, fileKey, fileName: file.name });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,11 +261,9 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
|
||||||
boards,
|
boards,
|
||||||
uploadAndDownloadBitstreams,
|
uploadAndDownloadBitstreams,
|
||||||
hotresetBitstream,
|
hotresetBitstream,
|
||||||
refreshData,
|
|
||||||
handleFileChange,
|
handleFileChange,
|
||||||
getSelectedBitstreamNum,
|
getSelectedBitstreamNum,
|
||||||
getAllBoards,
|
getAllBoards,
|
||||||
fetchBoardsData,
|
|
||||||
addBoard,
|
addBoard,
|
||||||
deleteBoard,
|
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>
|
<p>这里是用户信息页面的内容。</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="activePage === 100">
|
<div v-else-if="activePage === 100">
|
||||||
<BoardControl />
|
<BoardTable />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import BoardControl from "./BoardControl.vue";
|
import BoardTable from "./BoardTable.vue";
|
||||||
import { toNumber } from "lodash";
|
import { toNumber } from "lodash";
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
|
|
Loading…
Reference in New Issue