feat: 更加完善实验板管理面板,前后端分离
This commit is contained in:
		@@ -89,6 +89,7 @@ try
 | 
			
		||||
    {
 | 
			
		||||
        options.AddPolicy("Users", policy => policy
 | 
			
		||||
            .AllowAnyOrigin()
 | 
			
		||||
            .AllowAnyMethod()
 | 
			
		||||
            .AllowAnyHeader()
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										251
									
								
								src/views/User/AddBoardDialog.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										251
									
								
								src/views/User/AddBoardDialog.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,251 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <dialog class="modal" :class="{ 'modal-open': visible }">
 | 
			
		||||
    <div class="modal-box w-96 max-w-md">
 | 
			
		||||
      <h3 class="text-lg font-bold mb-4">新增实验板</h3>
 | 
			
		||||
      
 | 
			
		||||
      <form @submit.prevent="handleSubmit" class="space-y-4">
 | 
			
		||||
        <!-- 实验板名称 -->
 | 
			
		||||
        <div class="form-control">
 | 
			
		||||
          <label class="label">
 | 
			
		||||
            <span class="label-text">实验板名称 <span class="text-error">*</span></span>
 | 
			
		||||
          </label>
 | 
			
		||||
          <input
 | 
			
		||||
            v-model="form.name"
 | 
			
		||||
            type="text"
 | 
			
		||||
            placeholder="请输入实验板名称"
 | 
			
		||||
            class="input input-bordered"
 | 
			
		||||
            :class="{ 'input-error': errors.name }"
 | 
			
		||||
            required
 | 
			
		||||
          />
 | 
			
		||||
          <label v-if="errors.name" class="label">
 | 
			
		||||
            <span class="label-text-alt text-error">{{ errors.name }}</span>
 | 
			
		||||
          </label>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- IP 地址 -->
 | 
			
		||||
        <div class="form-control">
 | 
			
		||||
          <label class="label">
 | 
			
		||||
            <span class="label-text">IP 地址 <span class="text-error">*</span></span>
 | 
			
		||||
          </label>
 | 
			
		||||
          <input
 | 
			
		||||
            v-model="form.ipAddr"
 | 
			
		||||
            type="text"
 | 
			
		||||
            placeholder="例如:192.168.1.100"
 | 
			
		||||
            class="input input-bordered"
 | 
			
		||||
            :class="{ 'input-error': errors.ipAddr }"
 | 
			
		||||
            required
 | 
			
		||||
          />
 | 
			
		||||
          <label v-if="errors.ipAddr" class="label">
 | 
			
		||||
            <span class="label-text-alt text-error">{{ errors.ipAddr }}</span>
 | 
			
		||||
          </label>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- 端口号 -->
 | 
			
		||||
        <div class="form-control">
 | 
			
		||||
          <label class="label">
 | 
			
		||||
            <span class="label-text">端口号 <span class="text-error">*</span></span>
 | 
			
		||||
          </label>
 | 
			
		||||
          <input
 | 
			
		||||
            v-model.number="form.port"
 | 
			
		||||
            type="number"
 | 
			
		||||
            placeholder="例如:1234"
 | 
			
		||||
            min="1"
 | 
			
		||||
            max="65535"
 | 
			
		||||
            class="input input-bordered"
 | 
			
		||||
            :class="{ 'input-error': errors.port }"
 | 
			
		||||
            required
 | 
			
		||||
          />
 | 
			
		||||
          <label v-if="errors.port" class="label">
 | 
			
		||||
            <span class="label-text-alt text-error">{{ errors.port }}</span>
 | 
			
		||||
          </label>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- 操作按钮 -->
 | 
			
		||||
        <div class="modal-action">
 | 
			
		||||
          <button
 | 
			
		||||
            type="button"
 | 
			
		||||
            class="btn btn-ghost"
 | 
			
		||||
            @click="handleCancel"
 | 
			
		||||
            :disabled="isSubmitting"
 | 
			
		||||
          >
 | 
			
		||||
            取消
 | 
			
		||||
          </button>
 | 
			
		||||
          <button
 | 
			
		||||
            type="submit"
 | 
			
		||||
            class="btn btn-primary"
 | 
			
		||||
            :class="{ 'loading': isSubmitting }"
 | 
			
		||||
            :disabled="isSubmitting"
 | 
			
		||||
          >
 | 
			
		||||
            {{ isSubmitting ? '添加中...' : '确认添加' }}
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </form>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <!-- 点击背景关闭 -->
 | 
			
		||||
    <form method="dialog" class="modal-backdrop">
 | 
			
		||||
      <button type="button" @click="handleCancel">close</button>
 | 
			
		||||
    </form>
 | 
			
		||||
  </dialog>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, reactive, watch } from 'vue';
 | 
			
		||||
import { useBoardManager } from './BoardManager';
 | 
			
		||||
 | 
			
		||||
// Props 和 Emits
 | 
			
		||||
interface Props {
 | 
			
		||||
  visible: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Emits {
 | 
			
		||||
  (e: 'update:visible', value: boolean): void;
 | 
			
		||||
  (e: 'success'): void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<Props>();
 | 
			
		||||
const emit = defineEmits<Emits>();
 | 
			
		||||
 | 
			
		||||
// 使用 BoardManager
 | 
			
		||||
const boardManager = useBoardManager()!;
 | 
			
		||||
 | 
			
		||||
// 表单数据
 | 
			
		||||
const form = reactive({
 | 
			
		||||
  name: '',
 | 
			
		||||
  ipAddr: '',
 | 
			
		||||
  port: 1234
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 表单错误
 | 
			
		||||
const errors = reactive({
 | 
			
		||||
  name: '',
 | 
			
		||||
  ipAddr: '',
 | 
			
		||||
  port: ''
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 提交状态
 | 
			
		||||
const isSubmitting = ref(false);
 | 
			
		||||
 | 
			
		||||
// IP地址验证正则
 | 
			
		||||
const IP_REGEX = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
 | 
			
		||||
 | 
			
		||||
// 验证表单
 | 
			
		||||
function validateForm(): boolean {
 | 
			
		||||
  // 清空之前的错误
 | 
			
		||||
  errors.name = '';
 | 
			
		||||
  errors.ipAddr = '';
 | 
			
		||||
  errors.port = '';
 | 
			
		||||
 | 
			
		||||
  let isValid = true;
 | 
			
		||||
 | 
			
		||||
  // 验证名称
 | 
			
		||||
  if (!form.name.trim()) {
 | 
			
		||||
    errors.name = '请输入实验板名称';
 | 
			
		||||
    isValid = false;
 | 
			
		||||
  } else if (form.name.trim().length < 2) {
 | 
			
		||||
    errors.name = '实验板名称至少需要2个字符';
 | 
			
		||||
    isValid = false;
 | 
			
		||||
  } else if (form.name.trim().length > 50) {
 | 
			
		||||
    errors.name = '实验板名称不能超过50个字符';
 | 
			
		||||
    isValid = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 验证IP地址
 | 
			
		||||
  if (!form.ipAddr.trim()) {
 | 
			
		||||
    errors.ipAddr = '请输入IP地址';
 | 
			
		||||
    isValid = false;
 | 
			
		||||
  } else if (!IP_REGEX.test(form.ipAddr.trim())) {
 | 
			
		||||
    errors.ipAddr = '请输入有效的IP地址格式';
 | 
			
		||||
    isValid = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 验证端口号
 | 
			
		||||
  if (!form.port) {
 | 
			
		||||
    errors.port = '请输入端口号';
 | 
			
		||||
    isValid = false;
 | 
			
		||||
  } else if (form.port < 1 || form.port > 65535) {
 | 
			
		||||
    errors.port = '端口号必须在1-65535之间';
 | 
			
		||||
    isValid = false;
 | 
			
		||||
  } else if (!Number.isInteger(form.port)) {
 | 
			
		||||
    errors.port = '端口号必须是整数';
 | 
			
		||||
    isValid = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return isValid;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 重置表单
 | 
			
		||||
function resetForm() {
 | 
			
		||||
  form.name = '';
 | 
			
		||||
  form.ipAddr = '';
 | 
			
		||||
  form.port = 1234;
 | 
			
		||||
  errors.name = '';
 | 
			
		||||
  errors.ipAddr = '';
 | 
			
		||||
  errors.port = '';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理取消
 | 
			
		||||
function handleCancel() {
 | 
			
		||||
  if (!isSubmitting.value) {
 | 
			
		||||
    emit('update:visible', false);
 | 
			
		||||
    resetForm();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理提交
 | 
			
		||||
async function handleSubmit() {
 | 
			
		||||
  if (!validateForm()) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isSubmitting.value = true;
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const success = await boardManager.addBoard(
 | 
			
		||||
      form.name.trim(),
 | 
			
		||||
      form.ipAddr.trim(),
 | 
			
		||||
      form.port
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (success) {
 | 
			
		||||
      emit('success');
 | 
			
		||||
      resetForm();
 | 
			
		||||
    }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('添加实验板失败:', error);
 | 
			
		||||
  } finally {
 | 
			
		||||
    isSubmitting.value = false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 监听对话框显示状态,重置表单
 | 
			
		||||
watch(() => props.visible, (newVisible) => {
 | 
			
		||||
  if (newVisible) {
 | 
			
		||||
    resetForm();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="postcss">
 | 
			
		||||
@import "@/assets/main.css";
 | 
			
		||||
 | 
			
		||||
.form-control {
 | 
			
		||||
  @apply w-full;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.label-text {
 | 
			
		||||
  @apply font-medium;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.input-error {
 | 
			
		||||
  @apply border-error;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-error {
 | 
			
		||||
  @apply text-red-500;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.loading {
 | 
			
		||||
  @apply opacity-50 cursor-not-allowed;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -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,
 | 
			
		||||
  };
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										220
									
								
								src/views/User/BoardTable.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										220
									
								
								src/views/User/BoardTable.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,220 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="flex flex-row justify-between items-center">
 | 
			
		||||
    <h1 class="text-3xl font-bold mb-6">FPGA 设备管理</h1>
 | 
			
		||||
    <button
 | 
			
		||||
      class="btn btn-ghost text-error hover:underline"
 | 
			
		||||
      @click="tableManager.toggleEditMode"
 | 
			
		||||
    >
 | 
			
		||||
      编辑
 | 
			
		||||
    </button>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="card bg-base-100 shadow-xl">
 | 
			
		||||
    <div class="card-body">
 | 
			
		||||
      <div class="flex flex-row justify-between items-center mb-4">
 | 
			
		||||
        <h2 class="card-title">IP 地址列表</h2>
 | 
			
		||||
        <button class="btn btn-ghost" @click="tableManager.getAllBoards">
 | 
			
		||||
          刷新
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- 搜索和列控制 -->
 | 
			
		||||
      <div class="flex items-center py-4 gap-4">
 | 
			
		||||
        <input
 | 
			
		||||
          type="text"
 | 
			
		||||
          placeholder="筛选 IP 地址..."
 | 
			
		||||
          class="input input-bordered max-w-sm"
 | 
			
		||||
          :value="tableManager.getColumnByKey('devAddr')?.getFilterValue() as string"
 | 
			
		||||
          @input="
 | 
			
		||||
            tableManager.getColumnByKey('devAddr')
 | 
			
		||||
              ?.setFilterValue(($event.target as HTMLInputElement).value)
 | 
			
		||||
          "
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <div class="dropdown dropdown-end">
 | 
			
		||||
          <div tabindex="0" role="button" class="btn btn-outline">
 | 
			
		||||
            列显示
 | 
			
		||||
            <svg
 | 
			
		||||
              class="w-4 h-4 ml-2"
 | 
			
		||||
              fill="none"
 | 
			
		||||
              stroke="currentColor"
 | 
			
		||||
              viewBox="0 0 24 24"
 | 
			
		||||
            >
 | 
			
		||||
              <path
 | 
			
		||||
                stroke-linecap="round"
 | 
			
		||||
                stroke-linejoin="round"
 | 
			
		||||
                stroke-width="2"
 | 
			
		||||
                d="m19 9-7 7-7-7"
 | 
			
		||||
              ></path>
 | 
			
		||||
            </svg>
 | 
			
		||||
          </div>
 | 
			
		||||
          <ul
 | 
			
		||||
            tabindex="0"
 | 
			
		||||
            class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
 | 
			
		||||
          >
 | 
			
		||||
            <li
 | 
			
		||||
              v-for="column in tableManager.getAllHideableColumns()"
 | 
			
		||||
              :key="column.id"
 | 
			
		||||
            >
 | 
			
		||||
              <label class="label cursor-pointer">
 | 
			
		||||
                <span class="label-text capitalize">{{ column.id }}</span>
 | 
			
		||||
                <input
 | 
			
		||||
                  type="checkbox"
 | 
			
		||||
                  class="checkbox checkbox-sm"
 | 
			
		||||
                  :checked="column.getIsVisible()"
 | 
			
		||||
                  @change="
 | 
			
		||||
                    column.toggleVisibility(
 | 
			
		||||
                      !!($event.target as HTMLInputElement).checked,
 | 
			
		||||
                    )
 | 
			
		||||
                  "
 | 
			
		||||
                />
 | 
			
		||||
              </label>
 | 
			
		||||
            </li>
 | 
			
		||||
          </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="flex gap-2 ml-auto">
 | 
			
		||||
          <button 
 | 
			
		||||
            class="btn btn-primary"
 | 
			
		||||
            :disabled="!tableManager.isEditMode.value"
 | 
			
		||||
            @click="showAddBoardDialog = true"
 | 
			
		||||
          >
 | 
			
		||||
            新增实验板
 | 
			
		||||
          </button>
 | 
			
		||||
 | 
			
		||||
          <button 
 | 
			
		||||
            class="btn btn-error"
 | 
			
		||||
            :disabled="!tableManager.isEditMode.value"
 | 
			
		||||
            @click="tableManager.deleteSelectedBoards"
 | 
			
		||||
          >
 | 
			
		||||
            删除选中
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- 表格 -->
 | 
			
		||||
      <div class="overflow-x-auto border border-base-300 rounded-lg">
 | 
			
		||||
        <table class="table w-full">
 | 
			
		||||
          <thead>
 | 
			
		||||
            <tr
 | 
			
		||||
              v-for="headerGroup in tableManager.getHeaderGroups()"
 | 
			
		||||
              :key="headerGroup.id"
 | 
			
		||||
              class="bg-base-300"
 | 
			
		||||
            >
 | 
			
		||||
              <th v-for="header in headerGroup.headers" :key="header.id">
 | 
			
		||||
                <FlexRender
 | 
			
		||||
                  v-if="!header.isPlaceholder"
 | 
			
		||||
                  :render="header.column.columnDef.header"
 | 
			
		||||
                  :props="header.getContext()"
 | 
			
		||||
                />
 | 
			
		||||
              </th>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </thead>
 | 
			
		||||
          <tbody>
 | 
			
		||||
            <template v-if="tableManager.getRowModel().rows?.length">
 | 
			
		||||
              <template v-for="row in tableManager.getRowModel().rows" :key="row.id">
 | 
			
		||||
                <tr
 | 
			
		||||
                  class="hover"
 | 
			
		||||
                  :class="{ 'bg-primary/10': row.getIsSelected() }"
 | 
			
		||||
                >
 | 
			
		||||
                  <td v-for="cell in row.getVisibleCells()" :key="cell.id">
 | 
			
		||||
                    <FlexRender
 | 
			
		||||
                      :render="cell.column.columnDef.cell"
 | 
			
		||||
                      :props="cell.getContext()"
 | 
			
		||||
                    />
 | 
			
		||||
                  </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr v-if="row.getIsExpanded()">
 | 
			
		||||
                  <td :colspan="row.getAllCells().length" class="bg-base-200">
 | 
			
		||||
                    <div class="p-4">
 | 
			
		||||
                      <pre class="text-sm">{{
 | 
			
		||||
                        JSON.stringify(row.original, null, 2)
 | 
			
		||||
                      }}</pre>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
              </template>
 | 
			
		||||
            </template>
 | 
			
		||||
            <tr v-else>
 | 
			
		||||
              <td
 | 
			
		||||
                :colspan="tableManager.columns.length"
 | 
			
		||||
                class="h-24 text-center text-base-content/60"
 | 
			
		||||
              >
 | 
			
		||||
                暂无数据
 | 
			
		||||
              </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- 分页控制 -->
 | 
			
		||||
      <div class="flex items-center justify-between py-4">
 | 
			
		||||
        <div class="text-sm text-base-content/60">
 | 
			
		||||
          已选择 {{ tableManager.getSelectedRows().length }} /
 | 
			
		||||
          {{ tableManager.getAllRows().length }} 行
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="flex gap-2">
 | 
			
		||||
          <button
 | 
			
		||||
            class="btn btn-outline btn-sm"
 | 
			
		||||
            :disabled="!tableManager.canPreviousPage()"
 | 
			
		||||
            @click="tableManager.previousPage()"
 | 
			
		||||
          >
 | 
			
		||||
            上一页
 | 
			
		||||
          </button>
 | 
			
		||||
          <button
 | 
			
		||||
            class="btn btn-outline btn-sm"
 | 
			
		||||
            :disabled="!tableManager.canNextPage()"
 | 
			
		||||
            @click="tableManager.nextPage()"
 | 
			
		||||
          >
 | 
			
		||||
            下一页
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="mt-6 bg-base-300 p-4 rounded-lg">
 | 
			
		||||
        <p class="text-sm opacity-80">
 | 
			
		||||
          <span class="font-semibold text-error">提示:</span>
 | 
			
		||||
          请谨慎操作FPGA固化和热启动功能,确保上传的位流文件无误,以避免设备损坏。
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <!-- 新增实验板对话框 -->
 | 
			
		||||
  <AddBoardDialog 
 | 
			
		||||
    v-model:visible="showAddBoardDialog"
 | 
			
		||||
    @success="handleAddBoardSuccess"
 | 
			
		||||
  />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { FlexRender } from "@tanstack/vue-table";
 | 
			
		||||
import { onMounted, ref } from "vue";
 | 
			
		||||
import { useProvideBoardManager } from "./BoardManager";
 | 
			
		||||
import { useProvideBoardTableManager } from "./BoardTableManager";
 | 
			
		||||
import AddBoardDialog from "./AddBoardDialog.vue";
 | 
			
		||||
 | 
			
		||||
// 使用 BoardManager
 | 
			
		||||
const boardManager = useProvideBoardManager()!;
 | 
			
		||||
 | 
			
		||||
// 使用表格管理器(不再需要参数)
 | 
			
		||||
const tableManager = useProvideBoardTableManager()!;
 | 
			
		||||
 | 
			
		||||
// 新增实验板对话框显示状态
 | 
			
		||||
const showAddBoardDialog = ref(false);
 | 
			
		||||
 | 
			
		||||
// 处理新增实验板成功事件
 | 
			
		||||
const handleAddBoardSuccess = () => {
 | 
			
		||||
  showAddBoardDialog.value = false;
 | 
			
		||||
  // 刷新数据在 BoardManager.addBoard 中已经处理
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  // 初始化数据
 | 
			
		||||
  boardManager.getAllBoards();
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="postcss">
 | 
			
		||||
@import "@/assets/main.css";
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										544
									
								
								src/views/User/BoardTableManager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										544
									
								
								src/views/User/BoardTableManager.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,544 @@
 | 
			
		||||
import type {
 | 
			
		||||
  ColumnDef,
 | 
			
		||||
  ColumnFiltersState,
 | 
			
		||||
  ExpandedState,
 | 
			
		||||
  SortingState,
 | 
			
		||||
  VisibilityState,
 | 
			
		||||
} from "@tanstack/vue-table";
 | 
			
		||||
import {
 | 
			
		||||
  getCoreRowModel,
 | 
			
		||||
  getExpandedRowModel,
 | 
			
		||||
  getFilteredRowModel,
 | 
			
		||||
  getPaginationRowModel,
 | 
			
		||||
  getSortedRowModel,
 | 
			
		||||
  useVueTable,
 | 
			
		||||
} from "@tanstack/vue-table";
 | 
			
		||||
import { h, ref, computed, version } from "vue";
 | 
			
		||||
import { createInjectionState } from "@vueuse/core";
 | 
			
		||||
import type { BoardData } from "./BoardManager";
 | 
			
		||||
import { useBoardManager } from "./BoardManager";
 | 
			
		||||
import { useDialogStore } from "@/stores/dialog";
 | 
			
		||||
 | 
			
		||||
const [useProvideBoardTableManager, useBoardTableManager] =
 | 
			
		||||
  createInjectionState(() => {
 | 
			
		||||
    // 从BoardManager获取数据和方法
 | 
			
		||||
    const boardManager = useBoardManager()!;
 | 
			
		||||
  
 | 
			
		||||
    const dialog = useDialogStore();
 | 
			
		||||
 | 
			
		||||
    // 编辑状态
 | 
			
		||||
    const isEditMode = ref(false);
 | 
			
		||||
 | 
			
		||||
    // 表格状态管理
 | 
			
		||||
    const sorting = ref<SortingState>([]);
 | 
			
		||||
    const columnFilters = ref<ColumnFiltersState>([]);
 | 
			
		||||
    const columnVisibility = ref<VisibilityState>({
 | 
			
		||||
      // 默认隐藏端口、ID、状态列和板卡名称列
 | 
			
		||||
      port: false,
 | 
			
		||||
      id: false,
 | 
			
		||||
      status: false,
 | 
			
		||||
      version: false,
 | 
			
		||||
    });
 | 
			
		||||
    const rowSelection = ref({});
 | 
			
		||||
    const expanded = ref<ExpandedState>({});
 | 
			
		||||
 | 
			
		||||
    // 表格列定义
 | 
			
		||||
    const columns: ColumnDef<BoardData>[] = [
 | 
			
		||||
      {
 | 
			
		||||
        id: "select",
 | 
			
		||||
        header: ({ table }) =>
 | 
			
		||||
          h("input", {
 | 
			
		||||
            type: "checkbox",
 | 
			
		||||
            class: "checkbox",
 | 
			
		||||
            checked:
 | 
			
		||||
              table.getIsAllPageRowsSelected() ||
 | 
			
		||||
              (table.getIsSomePageRowsSelected() ? "indeterminate" : false),
 | 
			
		||||
            onChange: (event: Event) =>
 | 
			
		||||
              table.toggleAllPageRowsSelected(
 | 
			
		||||
                !!(event.target as HTMLInputElement).checked,
 | 
			
		||||
              ),
 | 
			
		||||
          }),
 | 
			
		||||
        cell: ({ row }) =>
 | 
			
		||||
          h("input", {
 | 
			
		||||
            type: "checkbox",
 | 
			
		||||
            class: "checkbox",
 | 
			
		||||
            checked: row.getIsSelected(),
 | 
			
		||||
            onChange: (event: Event) =>
 | 
			
		||||
              row.toggleSelected(!!(event.target as HTMLInputElement).checked),
 | 
			
		||||
          }),
 | 
			
		||||
        enableSorting: false,
 | 
			
		||||
        enableHiding: false,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        accessorKey: "boardName",
 | 
			
		||||
        header: "板卡名称",
 | 
			
		||||
        cell: ({ row }) => {
 | 
			
		||||
          const device = row.original;
 | 
			
		||||
          return isEditMode.value
 | 
			
		||||
            ? h("input", {
 | 
			
		||||
                type: "text",
 | 
			
		||||
                class: "input input-sm w-full",
 | 
			
		||||
                value: device.boardName,
 | 
			
		||||
                onInput: (e: Event) => {
 | 
			
		||||
                  device.boardName = (e.target as HTMLInputElement).value;
 | 
			
		||||
                },
 | 
			
		||||
              })
 | 
			
		||||
            : h("span", { class: "font-medium" }, device.boardName);
 | 
			
		||||
        },
 | 
			
		||||
        enableHiding: true,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        accessorKey: "devAddr",
 | 
			
		||||
        header: "IP 地址",
 | 
			
		||||
        cell: ({ row }) => {
 | 
			
		||||
          const device = row.original;
 | 
			
		||||
          return isEditMode.value
 | 
			
		||||
            ? h("input", {
 | 
			
		||||
                type: "text",
 | 
			
		||||
                class: "input input-sm w-full",
 | 
			
		||||
                value: device.ipAddr,
 | 
			
		||||
                onInput: (e: Event) => {
 | 
			
		||||
                  device.ipAddr = (e.target as HTMLInputElement).value;
 | 
			
		||||
                },
 | 
			
		||||
              })
 | 
			
		||||
            : h("span", { class: "font-medium" }, device.ipAddr);
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        accessorKey: "port",
 | 
			
		||||
        header: "端口",
 | 
			
		||||
        cell: ({ row }) => {
 | 
			
		||||
          const device = row.original;
 | 
			
		||||
          return isEditMode.value
 | 
			
		||||
            ? h("input", {
 | 
			
		||||
                type: "number",
 | 
			
		||||
                class: "input input-sm w-full",
 | 
			
		||||
                value: device.port,
 | 
			
		||||
                onInput: (e: Event) => {
 | 
			
		||||
                  device.port = parseInt((e.target as HTMLInputElement).value);
 | 
			
		||||
                },
 | 
			
		||||
              })
 | 
			
		||||
            : h("span", { class: "font-mono" }, device.port.toString());
 | 
			
		||||
        },
 | 
			
		||||
        enableHiding: true,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        accessorKey: "id",
 | 
			
		||||
        header: "设备ID",
 | 
			
		||||
        cell: ({ row }) =>
 | 
			
		||||
          h("span", { class: "font-mono text-xs" }, row.original.id),
 | 
			
		||||
        enableHiding: true,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        accessorKey: "status",
 | 
			
		||||
        header: "状态",
 | 
			
		||||
        cell: ({ row }) => {
 | 
			
		||||
          const device = row.original;
 | 
			
		||||
          const statusText = device.status === 0 ? "忙碌" : "可用";
 | 
			
		||||
          const statusClass =
 | 
			
		||||
            device.status === 0 ? "badge-warning" : "badge-success";
 | 
			
		||||
          return h(
 | 
			
		||||
            "span",
 | 
			
		||||
            {
 | 
			
		||||
              class: `badge ${statusClass} min-w-15`,
 | 
			
		||||
            },
 | 
			
		||||
            statusText,
 | 
			
		||||
          );
 | 
			
		||||
        },
 | 
			
		||||
        enableHiding: true,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        accessorKey: "version",
 | 
			
		||||
        header: "版本号",
 | 
			
		||||
        cell: ({ row }) =>
 | 
			
		||||
          h("span", { class: "font-mono" }, row.original.firmVersion),
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        accessorKey: "defaultBitstream",
 | 
			
		||||
        header: "默认启动位流",
 | 
			
		||||
        cell: ({ row }) => {
 | 
			
		||||
          const device = row.original;
 | 
			
		||||
          return h(
 | 
			
		||||
            "select",
 | 
			
		||||
            {
 | 
			
		||||
              class: "select select-bordered select-sm w-full",
 | 
			
		||||
              value: device.defaultBitstream,
 | 
			
		||||
              onChange: (e: Event) => {
 | 
			
		||||
                device.defaultBitstream = (e.target as HTMLSelectElement).value;
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
            [
 | 
			
		||||
              h("option", { value: "黄金位流" }, "黄金位流"),
 | 
			
		||||
              h("option", { value: "应用位流1" }, "应用位流1"),
 | 
			
		||||
              h("option", { value: "应用位流2" }, "应用位流2"),
 | 
			
		||||
              h("option", { value: "应用位流3" }, "应用位流3"),
 | 
			
		||||
            ],
 | 
			
		||||
          );
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        id: "goldBitstream",
 | 
			
		||||
        header: "黄金位流",
 | 
			
		||||
        cell: ({ row }) => {
 | 
			
		||||
          const device = row.original;
 | 
			
		||||
          return h("input", {
 | 
			
		||||
            type: "file",
 | 
			
		||||
            class: "file-input file-input-primary file-input-sm",
 | 
			
		||||
            onChange: (e: Event) =>
 | 
			
		||||
              boardManager.handleFileChange(e, device, "goldBitstreamFile"),
 | 
			
		||||
          });
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        id: "appBitstream1",
 | 
			
		||||
        header: "应用位流1",
 | 
			
		||||
        cell: ({ row }) => {
 | 
			
		||||
          const device = row.original;
 | 
			
		||||
          return h("input", {
 | 
			
		||||
            type: "file",
 | 
			
		||||
            class: "file-input file-input-secondary file-input-sm",
 | 
			
		||||
            onChange: (e: Event) =>
 | 
			
		||||
              boardManager.handleFileChange(e, device, "appBitstream1File"),
 | 
			
		||||
          });
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        id: "appBitstream2",
 | 
			
		||||
        header: "应用位流2",
 | 
			
		||||
        cell: ({ row }) => {
 | 
			
		||||
          const device = row.original;
 | 
			
		||||
          return h("input", {
 | 
			
		||||
            type: "file",
 | 
			
		||||
            class: "file-input file-input-accent file-input-sm",
 | 
			
		||||
            onChange: (e: Event) =>
 | 
			
		||||
              boardManager.handleFileChange(e, device, "appBitstream2File"),
 | 
			
		||||
          });
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        id: "appBitstream3",
 | 
			
		||||
        header: "应用位流3",
 | 
			
		||||
        cell: ({ row }) => {
 | 
			
		||||
          const device = row.original;
 | 
			
		||||
          return h("input", {
 | 
			
		||||
            type: "file",
 | 
			
		||||
            class: "file-input file-input-info file-input-sm",
 | 
			
		||||
            onChange: (e: Event) =>
 | 
			
		||||
              boardManager.handleFileChange(e, device, "appBitstream3File"),
 | 
			
		||||
          });
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        id: "actions",
 | 
			
		||||
        header: "操作",
 | 
			
		||||
        cell: ({ row }) => {
 | 
			
		||||
          const device = row.original;
 | 
			
		||||
 | 
			
		||||
          // 根据编辑模式显示不同的按钮
 | 
			
		||||
          if (isEditMode.value) {
 | 
			
		||||
            return h(
 | 
			
		||||
              "div",
 | 
			
		||||
              {
 | 
			
		||||
                class: ["flex gap-2", { "min-w-30": !isEditMode.value }],
 | 
			
		||||
              },
 | 
			
		||||
              [
 | 
			
		||||
                h(
 | 
			
		||||
                  "button",
 | 
			
		||||
                  {
 | 
			
		||||
                    class: "btn btn-error btn-sm",
 | 
			
		||||
                    onClick: async () => {
 | 
			
		||||
                      const confirmed = confirm(
 | 
			
		||||
                        `确定要删除设备 ${device.ipAddr} 吗?`,
 | 
			
		||||
                      );
 | 
			
		||||
                      if (confirmed) {
 | 
			
		||||
                        await deleteBoard(device.id);
 | 
			
		||||
                      }
 | 
			
		||||
                    },
 | 
			
		||||
                  },
 | 
			
		||||
                  "删除",
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            );
 | 
			
		||||
          } else {
 | 
			
		||||
            return h("div", { class: "flex gap-2 min-w-30" }, [
 | 
			
		||||
              h(
 | 
			
		||||
                "button",
 | 
			
		||||
                {
 | 
			
		||||
                  class: "btn btn-warning btn-sm",
 | 
			
		||||
                  onClick: () =>
 | 
			
		||||
                    uploadAndDownloadBitstreams(
 | 
			
		||||
                      device,
 | 
			
		||||
                      device.goldBitstreamFile,
 | 
			
		||||
                      device.appBitstream1File,
 | 
			
		||||
                      device.appBitstream2File,
 | 
			
		||||
                      device.appBitstream3File,
 | 
			
		||||
                    ),
 | 
			
		||||
                },
 | 
			
		||||
                "固化",
 | 
			
		||||
              ),
 | 
			
		||||
              h(
 | 
			
		||||
                "button",
 | 
			
		||||
                {
 | 
			
		||||
                  class: "btn btn-success btn-sm",
 | 
			
		||||
                  onClick: () =>
 | 
			
		||||
                    hotresetBitstream(
 | 
			
		||||
                      device,
 | 
			
		||||
                      boardManager.getSelectedBitstreamNum(
 | 
			
		||||
                        device.defaultBitstream,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                },
 | 
			
		||||
                "热启动",
 | 
			
		||||
              ),
 | 
			
		||||
            ]);
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        enableHiding: false,
 | 
			
		||||
      },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // 创建表格实例
 | 
			
		||||
    const table = useVueTable({
 | 
			
		||||
      get data() {
 | 
			
		||||
        return boardManager.boards.value;
 | 
			
		||||
      },
 | 
			
		||||
      get columns() {
 | 
			
		||||
        return columns;
 | 
			
		||||
      },
 | 
			
		||||
      getCoreRowModel: getCoreRowModel(),
 | 
			
		||||
      getPaginationRowModel: getPaginationRowModel(),
 | 
			
		||||
      getSortedRowModel: getSortedRowModel(),
 | 
			
		||||
      getFilteredRowModel: getFilteredRowModel(),
 | 
			
		||||
      getExpandedRowModel: getExpandedRowModel(),
 | 
			
		||||
      onSortingChange: (updaterOrValue) => {
 | 
			
		||||
        if (typeof updaterOrValue === "function") {
 | 
			
		||||
          sorting.value = updaterOrValue(sorting.value);
 | 
			
		||||
        } else {
 | 
			
		||||
          sorting.value = updaterOrValue;
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      onColumnFiltersChange: (updaterOrValue) => {
 | 
			
		||||
        if (typeof updaterOrValue === "function") {
 | 
			
		||||
          columnFilters.value = updaterOrValue(columnFilters.value);
 | 
			
		||||
        } else {
 | 
			
		||||
          columnFilters.value = updaterOrValue;
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      onColumnVisibilityChange: (updaterOrValue) => {
 | 
			
		||||
        if (typeof updaterOrValue === "function") {
 | 
			
		||||
          columnVisibility.value = updaterOrValue(columnVisibility.value);
 | 
			
		||||
        } else {
 | 
			
		||||
          columnVisibility.value = updaterOrValue;
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      onRowSelectionChange: (updaterOrValue) => {
 | 
			
		||||
        if (typeof updaterOrValue === "function") {
 | 
			
		||||
          rowSelection.value = updaterOrValue(rowSelection.value);
 | 
			
		||||
        } else {
 | 
			
		||||
          rowSelection.value = updaterOrValue;
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      onExpandedChange: (updaterOrValue) => {
 | 
			
		||||
        if (typeof updaterOrValue === "function") {
 | 
			
		||||
          expanded.value = updaterOrValue(expanded.value);
 | 
			
		||||
        } else {
 | 
			
		||||
          expanded.value = updaterOrValue;
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      state: {
 | 
			
		||||
        get sorting() {
 | 
			
		||||
          return sorting.value;
 | 
			
		||||
        },
 | 
			
		||||
        get columnFilters() {
 | 
			
		||||
          return columnFilters.value;
 | 
			
		||||
        },
 | 
			
		||||
        get columnVisibility() {
 | 
			
		||||
          return columnVisibility.value;
 | 
			
		||||
        },
 | 
			
		||||
        get rowSelection() {
 | 
			
		||||
          return rowSelection.value;
 | 
			
		||||
        },
 | 
			
		||||
        get expanded() {
 | 
			
		||||
          return expanded.value;
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // UI层的API封装方法 - 添加dialog提示
 | 
			
		||||
    
 | 
			
		||||
    // 获取所有板卡信息
 | 
			
		||||
    async function getAllBoards(): Promise<boolean> {
 | 
			
		||||
      const result = await boardManager.getAllBoards();
 | 
			
		||||
      if (result.success) {
 | 
			
		||||
        dialog?.info("获取板卡信息成功");
 | 
			
		||||
      } else {
 | 
			
		||||
        dialog?.error(result.error || "获取板卡信息失败");
 | 
			
		||||
      }
 | 
			
		||||
      return result.success;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 新增板卡
 | 
			
		||||
    async function addBoard(name: string, ipAddr: string, port: number): Promise<boolean> {
 | 
			
		||||
      const result = await boardManager.addBoard(name, ipAddr, port);
 | 
			
		||||
      if (result.success) {
 | 
			
		||||
        dialog?.info("新增板卡成功");
 | 
			
		||||
      } else {
 | 
			
		||||
        dialog?.error(result.error || "新增板卡失败");
 | 
			
		||||
      }
 | 
			
		||||
      return result.success;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 删除板卡
 | 
			
		||||
    async function deleteBoard(boardId: string): Promise<boolean> {
 | 
			
		||||
      const result = await boardManager.deleteBoard(boardId);
 | 
			
		||||
      if (result.success) {
 | 
			
		||||
        dialog?.info("删除板卡成功");
 | 
			
		||||
      } else {
 | 
			
		||||
        dialog?.error(result.error || "删除板卡失败");
 | 
			
		||||
      }
 | 
			
		||||
      return result.success;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 上传并固化位流
 | 
			
		||||
    async function uploadAndDownloadBitstreams(
 | 
			
		||||
      board: BoardData,
 | 
			
		||||
      goldBitstream?: File,
 | 
			
		||||
      appBitstream1?: File,
 | 
			
		||||
      appBitstream2?: File,
 | 
			
		||||
      appBitstream3?: File,
 | 
			
		||||
    ): Promise<boolean> {
 | 
			
		||||
      const result = await boardManager.uploadAndDownloadBitstreams(
 | 
			
		||||
        board,
 | 
			
		||||
        goldBitstream,
 | 
			
		||||
        appBitstream1,
 | 
			
		||||
        appBitstream2,
 | 
			
		||||
        appBitstream3,
 | 
			
		||||
      );
 | 
			
		||||
      if (result.success) {
 | 
			
		||||
        dialog?.info("固化比特流成功");
 | 
			
		||||
      } else {
 | 
			
		||||
        dialog?.error(result.error || "固化比特流失败");
 | 
			
		||||
      }
 | 
			
		||||
      return result.success;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 热启动位流
 | 
			
		||||
    async function hotresetBitstream(board: BoardData, bitstreamNum: number): Promise<boolean> {
 | 
			
		||||
      const result = await boardManager.hotresetBitstream(board, bitstreamNum);
 | 
			
		||||
      if (result.success) {
 | 
			
		||||
        dialog?.info("切换比特流成功");
 | 
			
		||||
      } else {
 | 
			
		||||
        dialog?.error(result.error || "切换比特流失败");
 | 
			
		||||
      }
 | 
			
		||||
      return result.success;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 表格操作方法
 | 
			
		||||
    const getSelectedRows = () => table.getFilteredSelectedRowModel().rows;
 | 
			
		||||
    const getAllRows = () => table.getFilteredRowModel().rows;
 | 
			
		||||
    const getColumnByKey = (key: string) => table.getColumn(key);
 | 
			
		||||
    const getAllHideableColumns = () =>
 | 
			
		||||
      table.getAllColumns().filter((column) => column.getCanHide());
 | 
			
		||||
    const getHeaderGroups = () => table.getHeaderGroups();
 | 
			
		||||
    const getRowModel = () => table.getRowModel();
 | 
			
		||||
    const canPreviousPage = () => table.getCanPreviousPage();
 | 
			
		||||
    const canNextPage = () => table.getCanNextPage();
 | 
			
		||||
    const previousPage = () => table.previousPage();
 | 
			
		||||
    const nextPage = () => table.nextPage();
 | 
			
		||||
 | 
			
		||||
    // 编辑模式控制
 | 
			
		||||
    const toggleEditMode = () => {
 | 
			
		||||
      isEditMode.value = !isEditMode.value;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // 删除选中的实验板
 | 
			
		||||
    const deleteSelectedBoards = async () => {
 | 
			
		||||
      const selectedRows = getSelectedRows();
 | 
			
		||||
 | 
			
		||||
      if (selectedRows.length === 0) {
 | 
			
		||||
        dialog?.warn("请先选择要删除的实验板");
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const boardNames = selectedRows
 | 
			
		||||
        .map((row) => row.original.boardName || row.original.ipAddr)
 | 
			
		||||
        .join("、");
 | 
			
		||||
      const confirmed = confirm(
 | 
			
		||||
        `确定要删除以下 ${selectedRows.length} 个实验板吗?\n${boardNames}`,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (!confirmed) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      let successCount = 0;
 | 
			
		||||
      let failCount = 0;
 | 
			
		||||
 | 
			
		||||
      // 批量删除
 | 
			
		||||
      for (const row of selectedRows) {
 | 
			
		||||
        const board = row.original;
 | 
			
		||||
        const result = await boardManager.deleteBoard(board.id);
 | 
			
		||||
        if (result.success) {
 | 
			
		||||
          successCount++;
 | 
			
		||||
        } else {
 | 
			
		||||
          failCount++;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // 清空选择状态
 | 
			
		||||
      rowSelection.value = {};
 | 
			
		||||
 | 
			
		||||
      // 显示结果提示
 | 
			
		||||
      if (failCount === 0) {
 | 
			
		||||
        dialog?.info(`成功删除 ${successCount} 个实验板`);
 | 
			
		||||
      } else if (successCount === 0) {
 | 
			
		||||
        dialog?.error(
 | 
			
		||||
          `删除失败,共 ${failCount} 个实验板删除失败`,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        dialog?.warn(
 | 
			
		||||
          `部分删除成功:成功 ${successCount} 个,失败 ${failCount} 个`,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return successCount > 0;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      // 表格实例
 | 
			
		||||
      table,
 | 
			
		||||
      // 列定义
 | 
			
		||||
      columns,
 | 
			
		||||
      // 表格操作方法
 | 
			
		||||
      getSelectedRows,
 | 
			
		||||
      getAllRows,
 | 
			
		||||
      getColumnByKey,
 | 
			
		||||
      getAllHideableColumns,
 | 
			
		||||
      getHeaderGroups,
 | 
			
		||||
      getRowModel,
 | 
			
		||||
      canPreviousPage,
 | 
			
		||||
      canNextPage,
 | 
			
		||||
      previousPage,
 | 
			
		||||
      nextPage,
 | 
			
		||||
      deleteSelectedBoards,
 | 
			
		||||
      // 状态
 | 
			
		||||
      sorting,
 | 
			
		||||
      columnFilters,
 | 
			
		||||
      columnVisibility,
 | 
			
		||||
      rowSelection,
 | 
			
		||||
      expanded,
 | 
			
		||||
      // 编辑模式
 | 
			
		||||
      isEditMode,
 | 
			
		||||
      toggleEditMode,
 | 
			
		||||
      // UI层封装的API方法
 | 
			
		||||
      getAllBoards,
 | 
			
		||||
      addBoard,
 | 
			
		||||
      deleteBoard,
 | 
			
		||||
      uploadAndDownloadBitstreams,
 | 
			
		||||
      hotresetBitstream,
 | 
			
		||||
      // BoardManager 的引用
 | 
			
		||||
      boardManager,
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
export { useProvideBoardTableManager, useBoardTableManager };
 | 
			
		||||
@@ -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";
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user