591 lines
18 KiB
Vue
591 lines
18 KiB
Vue
<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>
|