Show WeCom machine name after brand title on shell and login.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-02 11:43:02 +08:00
parent 9427d9c429
commit ead3545df8
5 changed files with 72 additions and 5 deletions
+11
View File
@@ -98,16 +98,27 @@ async def login(
) )
@router.get("/branding")
async def branding() -> dict:
"""登录页/顶栏展示用(无敏感信息):与企微通知同一机器名。"""
from ..notify import wecom
return {"machine_name": wecom.wecom_machine_name() or ""}
@router.get("/me") @router.get("/me")
async def me( async def me(
username: Annotated[str, Depends(require_user)], username: Annotated[str, Depends(require_user)],
settings: Annotated[Settings, Depends(get_settings)], settings: Annotated[Settings, Depends(get_settings)],
) -> dict: ) -> dict:
from ..notify import wecom
return { return {
"username": username, "username": username,
"env_name": settings.env_name, "env_name": settings.env_name,
"mode": settings.mode, "mode": settings.mode,
"sim": settings.is_sim, "sim": settings.is_sim,
"machine_name": wecom.wecom_machine_name() or "",
} }
+25 -3
View File
@@ -1,7 +1,8 @@
import { type ReactNode, useEffect } from "react"; import { type ReactNode, useEffect, useState } from "react";
import { NavLink, Navigate, Route, Routes } from "react-router-dom"; import { NavLink, Navigate, Route, Routes } from "react-router-dom";
import { import {
clearSession, clearSession,
fetchMachineName,
getToken, getToken,
getUsername, getUsername,
startAuthTokenAutoRefresh, startAuthTokenAutoRefresh,
@@ -22,6 +23,17 @@ const NAV = [
function Shell({ children }: { children: ReactNode }) { function Shell({ children }: { children: ReactNode }) {
const user = getUsername(); const user = getUsername();
const [machineName, setMachineName] = useState("");
useEffect(() => {
let cancelled = false;
void fetchMachineName().then((name) => {
if (!cancelled) setMachineName(name);
});
return () => {
cancelled = true;
};
}, []);
const machineTag = machineName ? `${machineName}` : "";
return ( return (
<div className="app-shell"> <div className="app-shell">
<div className="app-container"> <div className="app-container">
@@ -35,8 +47,18 @@ function Shell({ children }: { children: ReactNode }) {
width={28} width={28}
height={28} height={28}
/> />
<span className="brand-full"></span> <span className="brand-full">
<span className="brand-short"></span>
{machineTag ? (
<span className="brand-machine">{machineTag}</span>
) : null}
</span>
<span className="brand-short">
{machineTag ? (
<span className="brand-machine">{machineTag}</span>
) : null}
</span>
</div> </div>
<div className="brand-actions"> <div className="brand-actions">
<span className="meta mono topnav-user">{user}</span> <span className="meta mono topnav-user">{user}</span>
+12
View File
@@ -32,6 +32,18 @@ export function getUsername(): string | null {
return localStorage.getItem(USER_KEY); return localStorage.getItem(USER_KEY);
} }
/** 与企微通知同一机器名(公开接口,登录前可用)。 */
export async function fetchMachineName(): Promise<string> {
try {
const res = await fetch(`${getApiBase()}/api/auth/branding`);
if (!res.ok) return "";
const data = (await res.json()) as { machine_name?: string };
return String(data.machine_name || "").trim();
} catch {
return "";
}
}
function tokenExpiresAtMs(): number | null { function tokenExpiresAtMs(): number | null {
const raw = localStorage.getItem(TOKEN_EXP_KEY); const raw = localStorage.getItem(TOKEN_EXP_KEY);
if (!raw) return null; if (!raw) return null;
+18 -2
View File
@@ -1,6 +1,6 @@
import { FormEvent, useEffect, useState } from "react"; import { FormEvent, useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
import { login, setSession } from "../api/client"; import { fetchMachineName, login, setSession } from "../api/client";
type ExchangeRes = { type ExchangeRes = {
token: string; token: string;
@@ -16,6 +16,17 @@ export default function LoginPage() {
const [err, setErr] = useState(""); const [err, setErr] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [ticketBusy, setTicketBusy] = useState(false); const [ticketBusy, setTicketBusy] = useState(false);
const [machineName, setMachineName] = useState("");
useEffect(() => {
let cancelled = false;
void fetchMachineName().then((name) => {
if (!cancelled) setMachineName(name);
});
return () => {
cancelled = true;
};
}, []);
useEffect(() => { useEffect(() => {
const ticket = (params.get("ticket") || "").trim(); const ticket = (params.get("ticket") || "").trim();
@@ -91,7 +102,12 @@ export default function LoginPage() {
width={48} width={48}
height={48} height={48}
/> />
<h1></h1> <h1>
{machineName ? (
<span className="brand-machine">{machineName}</span>
) : null}
</h1>
</div> </div>
{err ? <div className="err">{err}</div> : null} {err ? <div className="err">{err}</div> : null}
<div className="field"> <div className="field">
+6
View File
@@ -124,6 +124,12 @@ input {
background: transparent; background: transparent;
} }
.brand-machine {
margin-left: 0.15em;
font-weight: 700;
color: var(--accent);
}
.brand-actions { .brand-actions {
display: flex; display: flex;
align-items: center; align-items: center;