feat: add public account overview visibility

This commit is contained in:
2026-08-14 14:38:22 +08:00
parent bf37999833
commit 915d26d51d
14 changed files with 660 additions and 62 deletions
+82 -7
View File
@@ -2,6 +2,7 @@ import React, { lazy, Suspense, useEffect, useRef, useState } from "react";
import { createRoot } from "react-dom/client";
import {
BrowserRouter,
Link,
Navigate,
Route,
Routes,
@@ -18,6 +19,7 @@ import {
Flame,
Github,
Gauge,
LogIn,
Mail,
LogOut,
Moon,
@@ -83,7 +85,13 @@ function App() {
{authenticated ? (
<Route path="*" element={<Shell version={status.version} />} />
) : (
<Route path="*" element={<Login version={status.version} />} />
<>
<Route path="/login" element={<Login version={status.version} />} />
<Route
path="*"
element={<PublicShell version={status.version} />}
/>
</>
)}
</Routes>
</BrowserRouter>
@@ -206,10 +214,32 @@ function Login({ version }: { version: string }) {
</label>
{e && <p className="error">{e}</p>}
<button>登录</button>
<p className="hint public-login-link">
<Link to="/">查看公开总览</Link>
</p>
</form>
</main>
);
}
function PublicShell({ version }: { version: string }) {
const nav = useNavigate();
return (
<div className="public-app">
<header className="public-header">
<Brand version={version} />
<button className="secondary" onClick={() => nav("/login")}>
<LogIn /> 登录后配置
</button>
</header>
<main className="public-content">
<Routes>
<Route path="/" element={<Dashboard publicView />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</main>
</div>
);
}
function Shell({ version }: { version: string }) {
const { logout } = useAuth();
const nav = useNavigate();
@@ -358,7 +388,7 @@ const emptyDashboardCardState = (): DashboardCardState => ({
refresh: "idle",
});
function Dashboard() {
function Dashboard({ publicView = false }: { publicView?: boolean }) {
const [accounts, setAccounts] = useState<Account[] | null>(null),
[cards, setCards] = useState<Record<number, DashboardCardState>>({}),
[expanded, setExpanded] = useState<Record<number, boolean>>({}),
@@ -539,7 +569,12 @@ function Dashboard() {
if (!accounts.length)
return (
<>
<Header title="用量总览" sub="请先在设置中心添加账号" />
<Header
title="用量总览"
sub={
publicView ? "当前还没有可公开展示的账号" : "请先在设置中心添加账号"
}
/>
<div className="panel empty">尚未添加 Codex 账号</div>
</>
);
@@ -569,6 +604,7 @@ function Dashboard() {
key={account.id}
account={account}
state={cards[account.id] || emptyDashboardCardState()}
publicView={publicView}
expanded={Boolean(expanded[account.id])}
onToggle={() =>
setExpanded((current) => ({
@@ -587,12 +623,14 @@ function Dashboard() {
function AccountOverviewCard({
account,
state,
publicView,
expanded,
onToggle,
onRefresh,
}: {
account: Account;
state: DashboardCardState;
publicView: boolean;
expanded: boolean;
onToggle: () => void;
onRefresh: () => void;
@@ -619,7 +657,7 @@ function AccountOverviewCard({
<span className="account-summary-identity">
<strong>{title}</strong>
<span>
{email ? maskEmail(email) : "尚未登录"} ·{" "}
{publicView ? "公开只读" : email ? maskEmail(email) : "尚未登录"} ·{" "}
{connected ? "已连接" : "未连接"}
</span>
</span>
@@ -638,6 +676,7 @@ function AccountOverviewCard({
dashboard={dashboard}
state={state}
onRefresh={onRefresh}
publicView={publicView}
/>
) : (
<div className="account-detail-state">
@@ -701,10 +740,12 @@ function AccountDashboardDetails({
dashboard,
state,
onRefresh,
publicView,
}: {
dashboard: Dash;
state: DashboardCardState;
onRefresh: () => void;
publicView: boolean;
}) {
return (
<>
@@ -747,9 +788,11 @@ function AccountDashboardDetails({
icon={<Mail />}
label="登录邮箱"
value={
dashboard.account.email
? maskEmail(dashboard.account.email)
: "尚未连接"
publicView
? "登录后查看"
: dashboard.account.email
? maskEmail(dashboard.account.email)
: "尚未连接"
}
/>
<AccountInfo
@@ -1124,6 +1167,7 @@ function CodexSettings() {
[deviceLogin, setDeviceLogin] = useState<DeviceLogin | null>(null),
[active, setActive] = useState(0),
[newKind, setNewKind] = useState<"personal" | "team">("team"),
[newPublicVisible, setNewPublicVisible] = useState(false),
[busy, setBusy] = useState(false),
[err, setErr] = useState("");
const load = async (signal?: AbortSignal) => {
@@ -1195,6 +1239,7 @@ function CodexSettings() {
const x = await post("accounts", (value) => decodeAccounts([value])[0], {
displayName: `账号 ${xs.length + 1}`,
expectedKind: newKind,
publicVisible: newPublicVisible,
});
await load();
setActive(x.id);
@@ -1238,6 +1283,14 @@ function CodexSettings() {
<option value="personal">个人订阅</option>
</select>
</label>
<label className="checks">
<input
type="checkbox"
checked={newPublicVisible}
onChange={(e) => setNewPublicVisible(e.target.checked)}
/>
公开显示到未登录总览
</label>
<button disabled={busy} onClick={add}>
{busy && active === 0 ? "服务启动中…" : "添加账号"}
</button>
@@ -1263,6 +1316,7 @@ function CodexSettings() {
await put(`accounts/${x.id}`, decodeOK, {
displayName: name,
expectedKind: x.expectedKind,
publicVisible: x.publicVisible,
});
void load().catch((error) =>
setErr(toErrorMessage(error)),
@@ -1296,6 +1350,7 @@ function CodexSettings() {
await put(`accounts/${x.id}`, decodeOK, {
displayName: x.displayName,
expectedKind: e.target.value,
publicVisible: x.publicVisible,
});
void load().catch((error) => setErr(toErrorMessage(error)));
}}
@@ -1305,6 +1360,26 @@ function CodexSettings() {
<option value="team">Team / Business</option>
</select>
</label>
<label className="checks">
<input
type="checkbox"
checked={x.publicVisible}
onChange={async (e) => {
try {
setErr("");
await put(`accounts/${x.id}`, decodeOK, {
displayName: x.displayName,
expectedKind: x.expectedKind,
publicVisible: e.target.checked,
});
await load();
} catch (q) {
setErr(toErrorMessage(q));
}
}}
/>
公开显示到未登录总览
</label>
<div className="account-buttons">
<button
className="secondary"
+44
View File
@@ -221,6 +221,35 @@ button svg {
min-height: 100vh;
min-height: 100dvh;
}
.public-app {
min-height: 100vh;
min-height: 100dvh;
}
.public-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 20px;
width: 100%;
max-width: 1500px;
margin: 0 auto 0;
padding: 24px 42px;
border-bottom: 1px solid var(--border);
}
.public-header button {
white-space: nowrap;
}
.public-content {
width: 100%;
max-width: 1500px;
margin: 0 auto;
padding: 42px;
}
.public-login-link {
margin: -4px 0 0;
text-align: center;
}
.mobile-topbar,
.mobile-bottom-nav {
display: none;
@@ -939,6 +968,21 @@ a {
padding: 22px max(14px, env(safe-area-inset-right)) 24px
max(14px, env(safe-area-inset-left));
}
.public-header {
min-height: calc(58px + env(safe-area-inset-top));
padding: env(safe-area-inset-top) 12px 0 16px;
}
.public-header .logo {
min-width: 0;
gap: 7px;
}
.public-header .logo svg {
flex: none;
}
.public-content {
padding: 22px max(14px, env(safe-area-inset-right)) 24px
max(14px, env(safe-area-inset-left));
}
header {
align-items: stretch;
flex-direction: column;
+2
View File
@@ -26,6 +26,7 @@ export interface Account {
email: string | null;
planType: string | null;
expectedKind: ExpectedKind;
publicVisible: boolean;
actualKind: "unknown" | "personal" | "team";
validationStatus: ValidationStatus;
possibleDuplicate: boolean;
@@ -144,6 +145,7 @@ export const decodeAccount: Decoder<Account> = (value) => {
["any", "personal", "team"],
"expectedKind",
),
publicVisible: boolean(x.publicVisible, "publicVisible"),
actualKind: enumValue(
x.actualKind,
["unknown", "personal", "team"],