feat: add public account overview visibility
This commit is contained in:
+82
-7
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user