feat(desktop): show live device name in shell and keep stored info fresh
Sidebar now pulls the device name from the runtime snapshot (the client the
backend sees) instead of the stale copy persisted at login, so a rebinding or
hostname change is visible without relogin. Session composable resolves a more
accurate fallback device name/OS from userAgentData and silently re-persists
desktopClient.{device_name,os,cpu_arch} on boot when the resolved values differ
from what's stored. Also replaces the text logout link with an icon button and
adds a status dot to the device tag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
LinkOutlined,
|
||||
RobotOutlined,
|
||||
ToolOutlined,
|
||||
LogoutOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
|
||||
import { useDesktopRuntime } from "../composables/useDesktopRuntime";
|
||||
@@ -73,8 +74,9 @@ const navItems = computed(() => {
|
||||
|
||||
const operatorName = computed(() => session.value?.user?.name ?? "未命名操作员");
|
||||
const operatorEmail = computed(() => session.value?.user?.email ?? "");
|
||||
const liveClientLabel = computed(() => snapshot.value?.clients[0]?.deviceName ?? null);
|
||||
const clientLabel = computed(
|
||||
() => session.value?.desktopClient?.device_name ?? "当前设备尚未注册 client",
|
||||
() => liveClientLabel.value ?? session.value?.desktopClient?.device_name ?? "当前设备尚未注册 client",
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -119,15 +121,23 @@ const clientLabel = computed(
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-actions-row">
|
||||
<span class="device-tag">{{ clientLabel }}</span>
|
||||
<button type="button" class="logout-link" @click="logout">退出</button>
|
||||
<a-tooltip :title="clientLabel" placement="top">
|
||||
<div class="device-tag">
|
||||
<span class="status-dot"></span>
|
||||
<span class="device-name-text">{{ clientLabel }}</span>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="退出登录" placement="top">
|
||||
<button type="button" class="logout-action-btn" @click="logout">
|
||||
<LogoutOutlined />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<div class="footer-row">
|
||||
<span>Version</span>
|
||||
<strong>{{ snapshot?.app.version ?? "0.1.0" }}</strong>
|
||||
<div class="footer-bottom-row">
|
||||
<span class="version-label">Version</span>
|
||||
<strong class="version-value">{{ snapshot?.app.version ?? "0.1.0" }}</strong>
|
||||
</div>
|
||||
<p v-if="error" class="error-text">{{ error }}</p>
|
||||
</footer>
|
||||
@@ -299,10 +309,17 @@ h1 {
|
||||
}
|
||||
|
||||
.user-profile-panel {
|
||||
padding: 14px;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02), 0 2px 4px -2px rgba(0, 0, 0, 0.01);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.user-profile-panel:hover {
|
||||
border-color: #cbd5e1;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.03), 0 2px 4px -2px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.user-info-row {
|
||||
@@ -312,21 +329,25 @@ h1 {
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: #e0e7ff;
|
||||
color: #4f46e5;
|
||||
background: #0f172a;
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.user-details {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.user-details strong {
|
||||
@@ -337,75 +358,99 @@ h1 {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.user-details p {
|
||||
margin: 2px 0 0;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.user-actions-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 14px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px dashed #e2e8f0;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.device-tag {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: #475569;
|
||||
background: #f1f5f9;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.logout-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #ef4444;
|
||||
color: #4b5563;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.device-tag .status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #10b981;
|
||||
box-shadow: 0 0 0 2px #d1fae5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.device-name-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.logout-action-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
color: #94a3b8;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.logout-link:hover {
|
||||
.logout-action-btn:hover {
|
||||
background: #fef2f2;
|
||||
color: #ef4444;
|
||||
border-color: #fee2e2;
|
||||
}
|
||||
|
||||
.footer-row span {
|
||||
display: block;
|
||||
color: #595959;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.footer-row {
|
||||
.footer-bottom-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.footer-row strong {
|
||||
.version-label {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.version-value {
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
color: #1a1a1a;
|
||||
font-weight: 600;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
margin: 0;
|
||||
color: #ff4d4f;
|
||||
color: #ef4444;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
|
||||
@@ -167,10 +167,46 @@ function getOrCreateClientRegistrationID(user: UserInfo): string {
|
||||
return generated;
|
||||
}
|
||||
|
||||
function resolveFallbackPlatform(): string {
|
||||
const currentNavigator = globalThis.navigator as Navigator & {
|
||||
userAgentData?: { platform?: string };
|
||||
};
|
||||
return currentNavigator.userAgentData?.platform || currentNavigator.platform || "unknown";
|
||||
}
|
||||
|
||||
function resolveFallbackOS(platform: string): string {
|
||||
const normalized = platform.toLowerCase();
|
||||
if (normalized.includes("mac")) {
|
||||
return "darwin";
|
||||
}
|
||||
if (normalized.includes("win")) {
|
||||
return "windows";
|
||||
}
|
||||
if (normalized.includes("linux")) {
|
||||
return "linux";
|
||||
}
|
||||
return normalized || "unknown";
|
||||
}
|
||||
|
||||
function resolveFallbackDeviceName(platform: string): string {
|
||||
const normalized = platform.toLowerCase();
|
||||
if (normalized.includes("mac")) {
|
||||
return "Current Mac";
|
||||
}
|
||||
if (normalized.includes("win")) {
|
||||
return "Current Windows PC";
|
||||
}
|
||||
if (normalized.includes("linux")) {
|
||||
return "Current Linux Desktop";
|
||||
}
|
||||
return "Current Desktop";
|
||||
}
|
||||
|
||||
async function resolveDeviceInfo(): Promise<{ device_name: string; os: string; cpu_arch: string }> {
|
||||
const platform = resolveFallbackPlatform();
|
||||
const fallback = {
|
||||
device_name: `Desktop ${navigator.platform || "unknown"}`,
|
||||
os: navigator.platform || "unknown",
|
||||
device_name: resolveFallbackDeviceName(platform),
|
||||
os: resolveFallbackOS(platform),
|
||||
cpu_arch: "unknown",
|
||||
};
|
||||
|
||||
@@ -186,6 +222,37 @@ async function resolveDeviceInfo(): Promise<{ device_name: string; os: string; c
|
||||
return fallback;
|
||||
}
|
||||
|
||||
async function syncStoredDesktopClientDeviceInfo(): Promise<void> {
|
||||
const current = session.value;
|
||||
if (typeof window === "undefined" || current?.mode !== "authenticated" || !current.desktopClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
const device = await resolveDeviceInfo();
|
||||
if (session.value !== current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const desktopClient = current.desktopClient;
|
||||
if (
|
||||
desktopClient.device_name === device.device_name
|
||||
&& desktopClient.os === device.os
|
||||
&& desktopClient.cpu_arch === device.cpu_arch
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
persistSession({
|
||||
...current,
|
||||
desktopClient: {
|
||||
...desktopClient,
|
||||
device_name: device.device_name,
|
||||
os: device.os,
|
||||
cpu_arch: device.cpu_arch,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function registerPayload(user: UserInfo): Promise<DesktopClientRegisterRequest> {
|
||||
const device = await resolveDeviceInfo();
|
||||
return {
|
||||
@@ -337,6 +404,10 @@ async function logout(): Promise<void> {
|
||||
persistSession(null);
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
void syncStoredDesktopClientDeviceInfo();
|
||||
}
|
||||
|
||||
export function useDesktopSession() {
|
||||
return {
|
||||
session: readonly(session),
|
||||
|
||||
Reference in New Issue
Block a user