feat: move admin entry to header
Deploy staging to test server / deploy (push) Successful in 3m5s

This commit is contained in:
linmeng
2026-07-29 12:44:31 +08:00
parent 435e628806
commit ce4054fb78
4 changed files with 27 additions and 20 deletions
+7 -1
View File
@@ -367,7 +367,13 @@ button:disabled { cursor: default; opacity: .45; }
.chat-header span { color: var(--color-ink-secondary); margin-top: var(--space-1); font-size: 12px; }
.status { display: inline-block; margin: 0 6px 1px 0; border-radius: 50%; background: var(--color-success); width: 7px; height: 7px; margin-right: var(--space-2); }
.status-loading { background: var(--color-action); }
.credit-button { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 11px; cursor: pointer; font-size: 13px; font-variant-numeric: tabular-nums; transition: background-color 120ms ease-out, transform 120ms ease-out; min-width: 64px; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas-soft); color: var(--color-ink-secondary); font-weight: 500; }
.chat-header > .chat-header-actions { min-width: auto; display: flex; flex: 0 0 auto; align-items: center; gap: var(--space-2); }
.admin-button, .credit-button { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas-soft); color: var(--color-ink-secondary); transition: background-color 120ms ease-out, border-color 120ms ease-out, color 120ms ease-out, transform 120ms ease-out; }
.admin-button { width: 44px; flex: 0 0 44px; }
.admin-button svg { width: 18px; height: 18px; }
.admin-button:hover, .credit-button:hover { border-color: color-mix(in srgb, var(--color-action) 35%, var(--color-border)); background: var(--color-action-soft); color: var(--color-action-hover); }
.admin-button:active, .credit-button:active { transform: translateY(1px); }
.credit-button { gap: 6px; min-width: 64px; padding: 0 11px; cursor: pointer; font-size: 13px; font-variant-numeric: tabular-nums; font-weight: 500; }
.conversation { min-width: 0; min-height: 0; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; scrollbar-gutter: stable; padding-bottom: var(--composer-reserve); background: var(--color-canvas); }
.conversation.is-empty { display: grid; place-items: center; padding: var(--space-8); }
+12 -5
View File
@@ -2,7 +2,7 @@
import Link from "next/link";
import dynamic from "next/dynamic";
import { ArrowUp, ArrowUpRight, Sparkles, Square, X } from "lucide-react";
import { ArrowUp, ArrowUpRight, ShieldCheck, Sparkles, Square, X } from "lucide-react";
import { useGSAP } from "@gsap/react";
import { gsap } from "gsap";
import { useEffect, useRef, useState } from "react";
@@ -2875,10 +2875,17 @@ export default function Home() {
? "正在校正出生时间"
: personalChartAvailable ? "基于星盘证据回答" : "回答一般占星知识"}</span>
</div>
<button className="credit-button" ref={creditTrigger} type="button" onClick={() => openAccountDialog("redeem", creditTrigger.current)} aria-label={account ? `余额 ${account.credits} 点,兑换点数` : accountError || "读取余额中"}>
<Sparkles className="credit-icon" aria-hidden="true" />
<span>{account ? account.credits : "—"}</span>
</button>
<div className="chat-header-actions">
{account.isAdmin && account.adminUrl ? (
<Link className="admin-button" href={account.adminUrl} aria-label="后台管理" title="后台管理">
<ShieldCheck aria-hidden="true" />
</Link>
) : null}
<button className="credit-button" ref={creditTrigger} type="button" onClick={() => openAccountDialog("redeem", creditTrigger.current)} aria-label={account ? `余额 ${account.credits} 点,兑换点数` : accountError || "读取余额中"}>
<Sparkles className="credit-icon" aria-hidden="true" />
<span>{account ? account.credits : "—"}</span>
</button>
</div>
</header>
{!rectificationSurfaceOpen && (
-7
View File
@@ -1,11 +1,9 @@
"use client";
import { Popover } from "@base-ui/react/popover";
import Link from "next/link";
import {
ChevronRight,
Gift,
KeyRound,
LogOut,
MessageSquareText,
Plus,
@@ -37,8 +35,6 @@ export type SidebarAccount = {
name: string;
email: string;
credits: number;
isAdmin: boolean;
adminUrl: string | null;
initial: string;
};
@@ -215,9 +211,6 @@ export function AppSidebar({
<button className="account-menu-item" role="menuitem" type="button" onClick={() => handleAccountAction(onOpenRedeem)}>
<Gift aria-hidden="true" /><span></span><small>{account.credits} </small>
</button>
{account.isAdmin && account.adminUrl && <Link className="account-menu-item" href={account.adminUrl} role="menuitem" onClick={() => onAccountMenuOpenChange(false)}>
<KeyRound aria-hidden="true" /><span></span><ChevronRight aria-hidden="true" />
</Link>}
<div className="account-menu-separator" role="separator" />
<button className="account-menu-item account-menu-danger" role="menuitem" type="button" onClick={() => handleAccountAction(onOpenLogout)}>
<LogOut aria-hidden="true" /><span>退</span>
+8 -7
View File
@@ -154,16 +154,17 @@ test("keeps app sidebar props as product data and callbacks", () => {
assert.doesNotMatch(appSidebar, /supabase|fetch\(|\/api\//i);
});
test("uses the server-provided same-origin admin entry URL", () => {
test("renders the server-provided admin entry beside the credit control", () => {
const appSidebar = readProjectFile("src/components/app-sidebar.tsx");
const page = readProjectFile("src/app/page.tsx");
assert.match(appSidebar, /adminUrl: string \| null/);
assert.match(appSidebar, /account\.isAdmin && account\.adminUrl/);
assert.match(appSidebar, /href=\{account\.adminUrl\}/);
assert.match(appSidebar, />后台管理</);
assert.doesNotMatch(appSidebar, /href="\/admin\/codes"/);
assert.match(page, /adminUrl: account\.adminUrl/);
assert.doesNotMatch(appSidebar, /adminUrl|account\.isAdmin|后台管理|KeyRound/);
assert.match(page, /className="chat-header-actions"/);
assert.match(page, /account\.isAdmin && account\.adminUrl/);
assert.match(page, /className="admin-button" href=\{account\.adminUrl\}/);
assert.match(page, /aria-label="后台管理" title="后台管理"/);
assert.match(page, /<ShieldCheck aria-hidden="true" \/>/);
assert.match(page, /className="admin-button"[\s\S]*className="credit-button"/);
});
test("composes the chat page with the app sidebar shell", () => {