diff --git a/web/src/components/pages/showcase-shell.render.test.tsx b/web/src/components/pages/showcase-shell.render.test.tsx
index 1bc8ede..cc71a8c 100644
--- a/web/src/components/pages/showcase-shell.render.test.tsx
+++ b/web/src/components/pages/showcase-shell.render.test.tsx
@@ -1,9 +1,15 @@
/** @vitest-environment jsdom */
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
-import { afterEach, describe, expect, it, vi } from 'vitest'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ShowcaseShell } from './showcase-shell'
+const authState = { isAuthenticated: false }
+
+vi.mock('#/hooks/useAuth', () => ({
+ useAuth: () => ({ isAuthenticated: authState.isAuthenticated }),
+}))
+
vi.mock('@tanstack/react-router', () => ({
useNavigate: () => vi.fn(),
}))
@@ -19,6 +25,10 @@ vi.mock('#/components/preview/file-viewer', () => ({
),
}))
+beforeEach(() => {
+ authState.isAuthenticated = false
+})
+
afterEach(() => {
cleanup()
})
@@ -37,6 +47,13 @@ describe('ShowcaseShell UI', () => {
created_at: '2026-09-16T12:00:00Z',
}
+ const mockInactiveVersion = {
+ ...mockVersion,
+ id: 'ver_2',
+ version: 'v1.1.0',
+ is_active: false,
+ }
+
const mockPage = {
id: 'page_1',
project_id: 'proj_1',
@@ -74,8 +91,9 @@ describe('ShowcaseShell UI', () => {
},
]
- it('renders top Header and floating Bar with capsule header', () => {
- render(
+ function renderShell(isAuthenticated: boolean, versions = [mockVersion]) {
+ authState.isAuthenticated = isAuthenticated
+ return render(
{
page={mockPage}
version={mockVersion}
files={mockFiles}
- versions={[mockVersion]}
+ versions={versions}
/>,
)
+ }
+
+ it('renders top Header and floating Bar with capsule header', () => {
+ renderShell(true)
// 顶部 Header
expect(screen.getByText('Lumina')).toBeTruthy()
@@ -122,4 +144,56 @@ describe('ShowcaseShell UI', () => {
fireEvent.pointerDown(document.body)
expect(screen.queryByText('元信息')).toBeNull()
})
+
+ it('S-01: hides meta, version list, fork and console entries from anonymous visitors', () => {
+ renderShell(false)
+
+ fireEvent.click(
+ screen.getByRole('button', { name: '展开或折叠页面管理卡片' }),
+ )
+
+ // 匿名访客仅保留「可渲染页面」与「高级资源」文件切换视图
+ expect(screen.getByText('可渲染页面')).toBeTruthy()
+ expect(screen.getAllByText('index.html').length).toBeGreaterThan(0)
+ fireEvent.click(screen.getByText('高级资源'))
+ expect(screen.getByText('style.css')).toBeTruthy()
+
+ // 管理入口彻底隐藏
+ expect(screen.queryByText('元信息')).toBeNull()
+ expect(screen.queryByText('版本')).toBeNull()
+ expect(screen.queryByText('当前生效: v1.0.0')).toBeNull()
+ expect(screen.queryByText('设为生效')).toBeNull()
+ expect(screen.queryByText('复制当前路径')).toBeNull()
+ expect(screen.queryByText('Fork 到新预览')).toBeNull()
+ expect(screen.queryByText('管理员端口 · 页面安全设置')).toBeNull()
+ })
+
+ it('S-01: keeps version switching available for authenticated visitors', () => {
+ renderShell(true, [mockVersion, mockInactiveVersion])
+
+ fireEvent.click(
+ screen.getByRole('button', { name: '展开或折叠页面管理卡片' }),
+ )
+
+ expect(screen.getByText('元信息')).toBeTruthy()
+ expect(screen.getByText('版本')).toBeTruthy()
+ expect(screen.getByText('v1.1.0')).toBeTruthy()
+ expect(screen.getByText('设为生效')).toBeTruthy()
+ expect(screen.getByText('Fork 到新预览')).toBeTruthy()
+ expect(screen.getByText('管理员端口 · 页面安全设置')).toBeTruthy()
+ })
+
+ it('Q-03: expands the floating card with a top-down slide instead of zoom', () => {
+ renderShell(true)
+
+ fireEvent.click(
+ screen.getByRole('button', { name: '展开或折叠页面管理卡片' }),
+ )
+
+ const card = screen.getByText('可渲染页面').closest('div.animate-in')
+ expect(card).not.toBeNull()
+ expect(card?.classList.contains('slide-in-from-top-2')).toBe(true)
+ expect(card?.classList.contains('fade-in')).toBe(true)
+ expect(card?.classList.contains('zoom-in-95')).toBe(false)
+ })
})
diff --git a/web/src/components/pages/showcase-shell.tsx b/web/src/components/pages/showcase-shell.tsx
index e383675..0509b93 100644
--- a/web/src/components/pages/showcase-shell.tsx
+++ b/web/src/components/pages/showcase-shell.tsx
@@ -4,6 +4,7 @@ import { Copy, GitBranch, GripVertical, Settings2 } from 'lucide-react'
import { toast } from 'sonner'
import { PagesBrandHeader } from '#/components/pages/brand-header'
import { PreviewFileViewer } from '#/components/preview/file-viewer'
+import { useAuth } from '#/hooks/useAuth'
import { previewKindFromFilename } from '#/lib/preview-file'
import { useForkPage, useSwitchActiveVersion } from '#/hooks/usePages'
import type {
@@ -62,6 +63,7 @@ export function ShowcaseShell({
versions: PageVersionItem[]
}) {
const navigate = useNavigate()
+ const { isAuthenticated } = useAuth()
const [open, setOpen] = useState(false)
const [advanced, setAdvanced] = useState(false)
const [pos, setPos] = useState({ x: 20, y: 16 })
@@ -233,72 +235,76 @@ export function ShowcaseShell({
- {/* 展开悬浮面板卡片(挂在 Bar 容器内部,跟随 Bar 悬浮展开) */}
+ {/* 展开悬浮面板卡片(挂在 Bar 容器内部,跟随 Bar 悬浮展开,自上而下流动) */}
{open ? (
-
- {/* 元信息区 */}
-
-
- 元信息
-
-
- {page.title}
-
-
- {page.access_mode === 'password'
- ? '🔒 密码保护'
- : '🌐 公开访问'}{' '}
- · 不可变快照
-
-
-
- {/* 生效指针与版本历史 */}
-
-
+
+ {/* 元信息区(登录态可见;匿名访客只保留文件切换视图) */}
+ {isAuthenticated ? (
+
- 版本
+ 元信息
-
- 当前生效: {version.version}
-
-
-
- {versions.map((item) => (
- -
-
+ {page.title}
+
+
+ {page.access_mode === 'password'
+ ? '🔒 密码保护'
+ : '🌐 公开访问'}{' '}
+ · 不可变快照
+
+
+ ) : null}
+
+ {/* 生效指针与版本历史(管理入口,仅登录态可见) */}
+ {isAuthenticated ? (
+
+
+
+ 版本
+
+
+ 当前生效: {version.version}
+
+
+
+ {versions.map((item) => (
+ -
- {item.version}
-
- {!item.is_active ? (
-
- ) : (
-
- 当前
+ {item.version}
- )}
-
- ))}
-
-
+ {!item.is_active ? (
+
+ ) : (
+
+ 当前
+
+ )}
+
+ ))}
+
+
+ ) : null}
{/* 可渲染页面直切区 */}
@@ -356,43 +362,45 @@ export function ShowcaseShell({
) : null}
- {/* 底部快捷操作条 */}
-
-
-
+
+ 管理员端口 · 页面安全设置
+
+
+ ) : null}
) : null}
diff --git a/web/src/routes/console.tsx b/web/src/routes/console.tsx
index 21c753c..b583805 100644
--- a/web/src/routes/console.tsx
+++ b/web/src/routes/console.tsx
@@ -1,4 +1,9 @@
-import { createFileRoute, Outlet, redirect, useLocation } from '@tanstack/react-router'
+import {
+ createFileRoute,
+ Outlet,
+ redirect,
+ useLocation,
+} from '@tanstack/react-router'
import { motion, AnimatePresence } from 'motion/react'
import {
SidebarProvider,
@@ -19,7 +24,9 @@ export const Route = createFileRoute('/console')({
if (!token && !refreshToken) {
throw redirect({
to: '/auth/login',
- search: { redirect: getSafeRedirect(location.href, '/console/dashboard') },
+ search: {
+ redirect: getSafeRedirect(location.href, '/console/dashboard'),
+ },
})
}
},
@@ -27,11 +34,10 @@ export const Route = createFileRoute('/console')({
})
const headerVariants = {
- hidden: { opacity: 0, x: 20 },
+ hidden: { opacity: 0 },
visible: {
opacity: 1,
- x: 0,
- transition: { duration: 0.35, ease },
+ transition: { duration: 0.3, ease },
},
}
diff --git a/web/src/routes/interact/thank.tsx b/web/src/routes/interact/thank.tsx
index 8c3ebc4..da17837 100644
--- a/web/src/routes/interact/thank.tsx
+++ b/web/src/routes/interact/thank.tsx
@@ -3,83 +3,76 @@ import { motion } from 'motion/react'
import { ArrowRight, CheckCircle2, MessageSquare } from 'lucide-react'
import { Button } from '@lumina/components/ui/button'
-import { staggerContainer, staggerItem, staggerItemLeft } from '@lumina/components/motion'
+import { staggerContainer, staggerItem } from '@lumina/components/motion'
export const Route = createFileRoute('/interact/thank')({
- component: ThankPage,
+ component: ThankPage,
})
function ThankPage() {
- const navigate = useNavigate()
+ const navigate = useNavigate()
- return (
-
-
-
-
-
-
+ return (
+
+
+
+
+
+
-
- 感谢您的回答
-
-
- 本次会话已归档,所有问答记录已保存。
- 请回到 AI Agent 继续后续的开发工作。
-
-
+
+ 感谢您的回答
+
+
+ 本次会话已归档,所有问答记录已保存。 请回到 AI Agent
+ 继续后续的开发工作。
+
+
-
-
-
-
-
- 会话已结束
-
-
- AI Agent 已归档此会话,后续问题将需要创建新的会话进行交互。
-
-
-
-
+
+
+
+
+
会话已结束
+
+ AI Agent 已归档此会话,后续问题将需要创建新的会话进行交互。
+
+
+
+
-
-
- navigate({ to: '/interact', replace: true })
- }
- className="w-full bg-sea-ink text-foam hover:bg-lagoon-deep"
- >
- 返回大厅
-
-
- navigate({ to: '/console/qa', replace: true })}
- className="w-full text-xs text-sea-ink-soft"
- >
- 查看历史会话
-
-
-
-
- )
+
+ navigate({ to: '/interact', replace: true })}
+ className="w-full bg-sea-ink text-foam hover:bg-lagoon-deep"
+ >
+ 返回大厅
+
+
+ navigate({ to: '/console/qa', replace: true })}
+ className="w-full text-xs text-sea-ink-soft"
+ >
+ 查看历史会话
+
+
+
+
+ )
}