FPGA_WebLab/src/views/User/UserInfo.vue

591 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="space-y-6">
<!-- 页面标题 -->
<div class="flex items-center gap-3">
<User class="w-8 h-8 text-primary" />
<h1 class="text-3xl font-bold">用户信息</h1>
<!-- 刷新按钮图标 -->
<button
@click="refreshAllInfo"
class="btn btn-ghost btn-sm ml-auto"
:disabled="loading"
title="刷新信息"
>
<RefreshCw class="w-5 h-5" :class="{ 'animate-spin': loading }" />
</button>
</div>
<!-- 全局加载状态 -->
<div v-if="loading" class="flex justify-center items-center py-12">
<span class="loading loading-spinner loading-lg text-primary m-2"> </span>
加载中...
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="alert alert-error">
<AlertCircle class="w-5 h-5" />
<span>{{ error }}</span>
<button @click="refreshAllInfo" class="btn btn-sm btn-outline">
<RefreshCw class="w-4 h-4" />
重试
</button>
</div>
<!-- 用户信息内容 -->
<div v-else class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 用户基本信息卡片 -->
<div class="card bg-base-200 shadow-lg">
<div class="card-body">
<div class="flex items-center gap-3 mb-4">
<UserCircle class="w-6 h-6 text-primary" />
<h2 class="card-title">基本信息</h2>
</div>
<div class="space-y-4">
<!-- 用户ID -->
<div class="flex items-center gap-3 p-3 bg-base-100 rounded-lg">
<IdCard class="w-5 h-5 text-secondary" />
<div class="flex-1">
<div class="text-sm text-base-content/70">用户ID</div>
<div class="font-mono text-sm">{{ userInfo?.id || "N/A" }}</div>
</div>
<button
@click="copyToClipboard(userInfo?.id)"
class="btn btn-ghost btn-sm"
title="复制ID"
>
<Copy class="w-4 h-4" />
</button>
</div>
<!-- 用户名 -->
<div class="flex items-center gap-3 p-3 bg-base-100 rounded-lg">
<User class="w-5 h-5 text-secondary" />
<div class="flex-1">
<div class="text-sm text-base-content/70">用户名</div>
<div class="font-semibold">{{ userInfo?.name || "N/A" }}</div>
</div>
</div>
<!-- 邮箱 -->
<div class="flex items-center gap-3 p-3 bg-base-100 rounded-lg">
<Mail class="w-5 h-5 text-secondary" />
<div class="flex-1">
<div class="text-sm text-base-content/70">邮箱地址</div>
<div class="font-mono text-sm">
{{ userInfo?.eMail || "N/A" }}
</div>
</div>
<button
@click="copyToClipboard(userInfo?.eMail)"
class="btn btn-ghost btn-sm"
title="复制邮箱"
>
<Copy class="w-4 h-4" />
</button>
</div>
<!-- 账户状态 -->
<div class="flex items-center gap-3 p-3 bg-base-100 rounded-lg">
<Shield class="w-5 h-5 text-secondary" />
<div class="flex-1">
<div class="text-sm text-base-content/70">账户状态</div>
<div class="badge badge-success">已认证</div>
</div>
</div>
<!-- 绑定过期时间 -->
<div
v-if="userInfo?.boardExpireTime"
class="flex items-center gap-3 p-3 bg-base-100 rounded-lg"
>
<Clock class="w-5 h-5 text-secondary" />
<div class="flex-1">
<div class="text-sm text-base-content/70">绑定过期时间</div>
<div class="font-mono text-sm">
{{ formatExpireTime(userInfo.boardExpireTime) }}
</div>
<div
class="text-xs mt-1"
:class="getExpireTimeStatusClass(userInfo.boardExpireTime)"
>
{{ getExpireTimeStatus(userInfo.boardExpireTime) }}
</div>
</div>
<div
class="badge badge-sm"
:class="getExpireTimeBadgeClass(userInfo.boardExpireTime)"
>
{{ getTimeRemaining(userInfo.boardExpireTime) }}
</div>
</div>
</div>
</div>
</div>
<!-- 实验板信息卡片 -->
<div class="card bg-base-200 shadow-lg">
<div class="card-body">
<div class="flex items-center justify-between gap-3 mb-4">
<div class="flex items-center gap-3">
<Cpu class="w-6 h-6 text-primary" />
<h2 class="card-title">绑定实验板</h2>
</div>
<!-- 操作按钮 - 只有在有绑定实验板时才显示 -->
<div v-if="boardInfo" class="flex items-center gap-3">
<button
@click="testBoardConnection"
class="btn btn-secondary btn-sm"
:disabled="testingConnection"
>
<Zap
class="w-4 h-4"
:class="{ 'animate-pulse': testingConnection }"
/>
{{ testingConnection ? "测试中..." : "测试连接" }}
</button>
<button
@click="unbindBoard"
class="btn btn-error btn-outline btn-sm"
:disabled="unbindingBoard"
>
<Unlink2
class="w-4 h-4"
:class="{ 'animate-pulse': unbindingBoard }"
/>
{{ unbindingBoard ? "解绑中..." : "解绑实验板" }}
</button>
</div>
</div>
<!-- 无实验板绑定 -->
<div v-if="!boardInfo" class="text-center py-8">
<Unlink class="w-12 h-12 text-base-content/50 mx-auto mb-4" />
<div class="text-base-content/70 mb-4">暂无绑定的实验板</div>
<!-- 申请实验板按钮 -->
<button
@click="applyBoard"
class="btn btn-primary"
:disabled="applyingBoard"
>
<Plus
class="w-4 h-4"
:class="{ 'animate-pulse': applyingBoard }"
/>
{{ applyingBoard ? "申请中..." : "申请实验板" }}
</button>
</div>
<!-- 实验板信息 -->
<div v-else class="space-y-4">
<!-- 实验板ID -->
<div class="flex items-center gap-3 p-3 bg-base-100 rounded-lg">
<IdCard class="w-5 h-5 text-secondary" />
<div class="flex-1">
<div class="text-sm text-base-content/70">实验板ID</div>
<div class="font-mono text-sm">
{{ boardInfo?.id || "N/A" }}
</div>
</div>
<button
@click="copyToClipboard(boardInfo?.id)"
class="btn btn-ghost btn-sm"
title="复制ID"
>
<Copy class="w-4 h-4" />
</button>
</div>
<!-- 实验板名称 -->
<div class="flex items-center gap-3 p-3 bg-base-100 rounded-lg">
<Tag class="w-5 h-5 text-secondary" />
<div class="flex-1">
<div class="text-sm text-base-content/70">实验板名称</div>
<div class="font-semibold">
{{ boardInfo?.boardName || "N/A" }}
</div>
</div>
</div>
<!-- IP地址 -->
<div class="flex items-center gap-3 p-3 bg-base-100 rounded-lg">
<Globe class="w-5 h-5 text-secondary" />
<div class="flex-1">
<div class="text-sm text-base-content/70">IP地址</div>
<div class="font-mono text-sm">
{{ boardInfo?.ipAddr || "N/A" }}
</div>
</div>
<button
@click="copyToClipboard(boardInfo?.ipAddr)"
class="btn btn-ghost btn-sm"
title="复制IP地址"
>
<Copy class="w-4 h-4" />
</button>
</div>
<!-- 端口 -->
<div class="flex items-center gap-3 p-3 bg-base-100 rounded-lg">
<Server class="w-5 h-5 text-secondary" />
<div class="flex-1">
<div class="text-sm text-base-content/70">端口</div>
<div class="font-mono text-sm">
{{ boardInfo?.port || "N/A" }}
</div>
</div>
</div>
<!-- 状态 -->
<div class="flex items-center gap-3 p-3 bg-base-100 rounded-lg">
<Activity class="w-5 h-5 text-secondary" />
<div class="flex-1">
<div class="text-sm text-base-content/70">状态</div>
<div
class="badge"
:class="getBoardStatusClass(boardInfo?.status)"
>
{{ getBoardStatusText(boardInfo?.status) }}
</div>
</div>
</div>
<!-- 固件版本 -->
<div class="flex items-center gap-3 p-3 bg-base-100 rounded-lg">
<Settings class="w-5 h-5 text-secondary" />
<div class="flex-1">
<div class="text-sm text-base-content/70">固件版本</div>
<div class="font-mono text-sm">
{{ boardInfo?.firmVersion || "N/A" }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 使用自定义 Alert 组件 -->
<Alert />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { AuthManager } from "@/utils/AuthManager";
import { UserInfo, Board, BoardStatus } from "@/APIClient";
import { Alert, useAlertStore } from "@/components/Alert";
import {
User,
UserCircle,
Mail,
IdCard,
Copy,
Shield,
Cpu,
Globe,
Server,
Activity,
Settings,
Tag,
RefreshCw,
AlertCircle,
Unlink,
Unlink2,
Zap,
Plus,
Clock,
} from "lucide-vue-next";
// 响应式数据
const loading = ref(false);
const error = ref("");
const userInfo = ref<UserInfo | null>(null);
const boardInfo = ref<Board | null>(null);
// 操作状态
const testingConnection = ref(false);
const unbindingBoard = ref(false);
const applyingBoard = ref(false);
// 使用自定义 Alert
const alertStore = useAlertStore();
// 加载实验板信息
const loadBoardInfo = async () => {
if (!userInfo.value?.boardID) {
boardInfo.value = null;
return;
}
try {
const client = AuthManager.createAuthenticatedDataClient();
boardInfo.value = await client.getBoardByID(userInfo.value.boardID);
} catch (err) {
console.error("加载实验板信息失败:", err);
boardInfo.value = null;
}
};
// 统一的信息加载函数(合并了原来的 loadUserInfo 和 refreshAllInfo
const loadUserInfo = async (showSuccessMessage = false) => {
loading.value = true;
error.value = "";
try {
await new Promise((resolve) => setTimeout(resolve, 200));
const client = AuthManager.createAuthenticatedDataClient();
userInfo.value = await client.getUserInfo();
// 如果有绑定的实验板ID加载实验板信息
if (userInfo.value?.boardID) {
await loadBoardInfo();
} else {
boardInfo.value = null;
}
if (showSuccessMessage) {
alertStore?.success("信息刷新成功");
}
} catch (err) {
console.error("加载用户信息失败:", err);
error.value = "加载用户信息失败,请检查网络连接或重新登录";
if (showSuccessMessage) {
alertStore?.error("刷新信息失败,请检查网络连接");
}
} finally {
loading.value = false;
}
};
// 刷新所有信息(调用统一的加载函数,显示成功消息)
const refreshAllInfo = async () => {
await loadUserInfo(true);
};
// 申请实验板
const applyBoard = async () => {
applyingBoard.value = true;
alertStore?.info("正在申请实验板...");
try {
const client = AuthManager.createAuthenticatedDataClient();
// 获取可用的实验板
const availableBoard = await client.getAvailableBoard(undefined);
if (availableBoard) {
alertStore?.success(`成功申请到实验板: ${availableBoard.boardName}`);
// 重新加载用户信息以获取最新的绑定状态
await loadUserInfo();
} else {
alertStore?.warn("当前没有可用的实验板,请稍后再试");
}
} catch (err: any) {
console.error("申请实验板失败:", err);
// 根据错误状态码提供更友好的错误信息
if (err?.status === 404) {
alertStore?.warn("当前没有可用的实验板,请稍后再试");
} else if (err?.status === 400) {
alertStore?.error("您已经绑定了实验板,无需重复申请");
} else {
alertStore?.error("申请实验板失败,请检查网络连接或稍后重试");
}
} finally {
applyingBoard.value = false;
}
};
// 测试实验板连接
const testBoardConnection = async () => {
if (!boardInfo.value) return;
testingConnection.value = true;
alertStore?.info("正在测试连接...");
try {
const jtagClient = AuthManager.createAuthenticatedJtagClient();
// 使用JTAG客户端读取设备ID Code
const idCode = await jtagClient.getDeviceIDCode(
boardInfo.value.ipAddr,
boardInfo.value.port,
);
// 检查ID Code是否有效非0xFFFFFFFF表示连接成功
if (idCode !== 0xffffffff && idCode !== 0) {
alertStore?.success(
`连接测试成功设备ID: 0x${idCode.toString(16).toUpperCase()}`,
);
} else {
alertStore?.warn("连接测试失败,未检测到有效设备");
}
} catch (err) {
console.error("连接测试失败:", err);
alertStore?.error("连接测试失败,请检查实验板是否在线");
} finally {
testingConnection.value = false;
}
};
// 解绑实验板
const unbindBoard = async () => {
if (!boardInfo.value) return;
// 确认对话框
if (!confirm("确定要解绑当前实验板吗?解绑后需要重新绑定才能使用。")) {
return;
}
unbindingBoard.value = true;
alertStore?.info("正在解绑实验板...");
try {
const client = AuthManager.createAuthenticatedDataClient();
const success = await client.unbindBoard();
if (success) {
alertStore?.success("实验板解绑成功");
// 清空实验板信息并重新加载用户信息
boardInfo.value = null;
await loadUserInfo();
} else {
alertStore?.error("实验板解绑失败");
}
} catch (err) {
console.error("解绑实验板失败:", err);
alertStore?.error("解绑实验板失败,请稍后重试");
} finally {
unbindingBoard.value = false;
}
};
// 复制到剪贴板
const copyToClipboard = async (text?: string) => {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
alertStore?.success("已复制到剪贴板");
} catch (err) {
alertStore?.error("复制失败");
}
};
// 时间相关的工具函数
const formatExpireTime = (expireTime: Date) => {
return new Date(expireTime).toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
};
const getTimeRemaining = (expireTime: Date) => {
const now = new Date();
const expire = new Date(expireTime);
const timeDiff = expire.getTime() - now.getTime();
if (timeDiff <= 0) {
return "已过期";
}
const hours = Math.floor(timeDiff / (1000 * 60 * 60));
const minutes = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60));
if (hours > 0) {
return `剩余 ${hours}小时${minutes}分钟`;
} else {
return `剩余 ${minutes}分钟`;
}
};
// 获取过期时间相关状态的统一函数
const getExpireTimeInfo = (expireTime: Date) => {
const now = new Date();
const expire = new Date(expireTime);
const timeDiff = expire.getTime() - now.getTime();
if (timeDiff <= 0) {
return {
status: "已过期",
statusClass: "text-error",
badgeClass: "badge-error",
};
} else if (timeDiff <= 30 * 60 * 1000) {
return {
status: "即将过期",
statusClass: "text-warning",
badgeClass: "badge-warning",
};
} else if (timeDiff <= 60 * 60 * 1000) {
return {
status: "临近过期",
statusClass: "text-warning",
badgeClass: "badge-warning",
};
} else {
return {
status: "正常",
statusClass: "text-success",
badgeClass: "badge-success",
};
}
};
// 使用统一函数的便捷方法
const getExpireTimeStatus = (expireTime: Date) =>
getExpireTimeInfo(expireTime).status;
const getExpireTimeStatusClass = (expireTime: Date) =>
getExpireTimeInfo(expireTime).statusClass;
const getExpireTimeBadgeClass = (expireTime: Date) =>
getExpireTimeInfo(expireTime).badgeClass;
// 获取实验板状态相关信息的统一函数
const getBoardStatusInfo = (status?: BoardStatus) => {
switch (status) {
case BoardStatus.Available:
return { text: "可用", class: "badge-success" };
case BoardStatus.Busy:
return { text: "使用中", class: "badge-warning" };
default:
return { text: "未知", class: "badge-neutral" };
}
};
// 使用统一函数的便捷方法
const getBoardStatusClass = (status?: BoardStatus) =>
getBoardStatusInfo(status).class;
const getBoardStatusText = (status?: BoardStatus) =>
getBoardStatusInfo(status).text;
// 组件挂载时加载数据
onMounted(() => {
loadUserInfo();
});
</script>
<style scoped>
/* 添加一些自定义样式优化 */
.card {
transition: all 0.2s ease;
}
.card:hover {
transform: translateY(-2px);
}
/* 响应式优化 */
@media (max-width: 768px) {
.grid-cols-1.lg\:grid-cols-2 {
grid-template-columns: 1fr;
}
}
</style>