feat: init

This commit is contained in:
Simon
2025-09-29 16:33:15 +08:00
parent e8041e0582
commit 847620b5e8
98 changed files with 20166 additions and 0 deletions

1
pages/README.md Normal file
View File

@@ -0,0 +1 @@
# Landing Page & Docs

View File

@@ -0,0 +1,19 @@
export default function BetaNotice() {
return (
<div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4 mb-8">
<div className="flex items-start">
<div className="flex-shrink-0">
<span className="text-xl">🚧</span>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-orange-800 dark:text-orange-200 mb-1">
Beta
</h3>
<p className="text-sm text-orange-700 dark:text-orange-300">
</p>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,126 @@
/**
* 代码编辑器组件,模拟现代代码编辑器的外观
*/
import React from 'react'
import HighlightSyntax from './HighlightSyntax'
interface CodeEditorProps {
code: string
language?: string
title?: string
showLineNumbers?: boolean
showHeader?: boolean
showFooter?: boolean
className?: string
}
const CodeEditor: React.FC<CodeEditorProps> = ({
code,
language = 'javascript',
title,
showLineNumbers = false,
showHeader = false,
showFooter = false,
className = '',
}) => {
const lines = code.split('\n')
// 使用 Tailwind 的 dark: 前缀实现自动主题切换
const containerClasses =
'bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 border-gray-300 dark:border-gray-700'
const headerClasses = 'bg-gray-100 dark:bg-gray-800 border-gray-300 dark:border-gray-700'
const headerTextClasses = 'text-gray-700 dark:text-gray-300'
const languageTextClasses = 'text-gray-600 dark:text-gray-400'
const lineNumbersClasses =
'bg-gray-100 dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-600 dark:text-gray-500'
const codeAreaClasses = 'bg-white dark:bg-gray-900'
const footerClasses =
'bg-gray-100 dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-600 dark:text-gray-400'
const copyButtonClasses =
'bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-white'
return (
<div
className={`group relative ${containerClasses} rounded-xl border shadow-2xl overflow-hidden ${className}`}
>
{/* 编辑器顶部栏 */}
{showHeader && (
<div className={`flex items-center justify-between px-4 py-3 ${headerClasses} border-b`}>
<div className="flex items-center space-x-3">
{/* 窗口控制按钮 */}
<div className="flex space-x-2">
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
<div className="w-3 h-3 bg-yellow-500 rounded-full"></div>
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
</div>
{title && (
<span className={`text-sm ${headerTextClasses} font-medium ml-2`}>{title}</span>
)}
</div>
<div className="flex items-center space-x-2">
<span className={`text-xs ${languageTextClasses} uppercase tracking-wide`}>
{language}
</span>
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
</div>
</div>
)}
{/* 代码内容区域 */}
<div className="relative">
<div className="flex">
{/* 行号 */}
{showLineNumbers && (
<div className={`flex-shrink-0 px-4 py-4 ${lineNumbersClasses} border-r select-none`}>
<div className="text-xs font-mono leading-6">
{lines.map((_, index) => (
<div key={index} className="text-right">
{index + 1}
</div>
))}
</div>
</div>
)}
{/* 代码内容 */}
<div className={`flex-1 px-4 py-4 ${codeAreaClasses} overflow-x-auto`}>
<div className="text-sm font-mono leading-6">
<HighlightSyntax code={code} />
</div>
</div>
</div>
{/* 复制按钮 */}
<button
onClick={() => {
navigator.clipboard.writeText(code).catch(console.error)
}}
className={`absolute top-3 right-3 p-2 ${copyButtonClasses} rounded-lg transition-all duration-200 opacity-0 group-hover:opacity-100`}
title="复制代码"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</button>
</div>
{/* 底部状态栏 */}
{showFooter && (
<div className={`px-4 py-2 ${footerClasses} border-t`}>
<div className="flex items-center justify-between text-xs">
<span>{lines.length} lines</span>
<span>UTF-8</span>
</div>
</div>
)}
</div>
)
}
export default CodeEditor

View File

@@ -0,0 +1,95 @@
import { ReactNode } from 'react'
import { Link, useLocation } from 'wouter'
interface DocsLayoutProps {
children: ReactNode
}
interface NavItem {
title: string
path: string
}
interface NavSection {
title: string
items: NavItem[]
}
const navigationSections: NavSection[] = [
{
title: 'Introduction',
items: [
{ title: 'Overview', path: '/docs/introduction/overview' },
{ title: 'Quick Start', path: '/docs/introduction/quick-start' },
{ title: '使用限制', path: '/docs/introduction/limitations' },
],
},
{
title: 'Features',
items: [
{ title: '模型接入', path: '/docs/features/model-integration' },
{ title: '安全与权限', path: '/docs/features/security-permissions' },
{ title: '数据脱敏', path: '/docs/features/data-masking' },
{ title: '知识库注入', path: '/docs/features/knowledge-injection' },
{ title: '自定义工具', path: '/docs/features/custom-tools' },
],
},
{
title: 'Integration',
items: [
{ title: 'CDN 引入', path: '/docs/integration/cdn-setup' },
{ title: '配置选项', path: '/docs/integration/configuration' },
{ title: 'API 参考', path: '/docs/integration/api-reference' },
{ title: '最佳实践', path: '/docs/integration/best-practices' },
{ title: '接入第三方 Agent', path: '/docs/integration/third-party-agent' },
],
},
]
export default function DocsLayout({ children }: DocsLayoutProps) {
const [location] = useLocation()
return (
<div className="max-w-7xl mx-auto px-6 py-8">
<div className="flex gap-8">
{/* Sidebar */}
<aside className="w-64 flex-shrink-0" role="complementary" aria-label="文档导航">
<div className="sticky top-8">
<nav className="space-y-8" role="navigation" aria-label="文档章节">
{navigationSections.map((section) => (
<section key={section.title}>
<h3 className="font-semibold uppercase tracking-wider mb-3">{section.title}</h3>
<ul className="space-y-2" role="list">
{section.items.map((item) => {
const isActive = location === item.path
return (
<li key={item.path}>
<Link
href={item.path}
className={`block px-3 py-2 rounded-lg transition-colors duration-200 ${
isActive
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 font-medium'
: ' hover:text-foreground hover:bg-gray-100 dark:hover:bg-gray-800'
}`}
aria-current={isActive ? 'page' : undefined}
>
{item.title}
</Link>
</li>
)
})}
</ul>
</section>
))}
</nav>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 min-w-0" id="main-content" role="main">
<div className="prose prose-lg dark:prose-invert max-w-none">{children}</div>
</main>
</div>
</div>
)
}

View File

@@ -0,0 +1,91 @@
import { Link } from 'wouter'
export default function Footer() {
return (
<footer
className="bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700"
role="contentinfo"
>
<div className="max-w-7xl mx-auto px-6 py-12">
<div className="grid md:grid-cols-3 gap-8">
{/* Brand */}
<section className="space-y-4">
<div className="flex items-center space-x-3">
<div
className="w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center"
aria-hidden="true"
>
<span className="text-white font-bold">P</span>
</div>
<h3 className="text-lg font-bold text-foreground">page-agent</h3>
</div>
<p className="text-foreground/80 leading-relaxed">
Web AI
<br />
</p>
</section>
{/* Links */}
<section className="space-y-4">
<h4 className="font-semibold text-foreground uppercase tracking-wider"></h4>
<nav className="space-y-2" role="navigation" aria-label="页脚导航">
<Link
href="/docs/introduction/overview"
className="block text-foreground/80 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
>
</Link>
<Link
href="TODO"
target="_blank"
rel="noopener noreferrer"
className="block text-foreground/80 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
aria-label="查看源码(在新窗口打开)"
>
</Link>
<Link
href="/docs/introduction/quick-start"
className="block text-foreground/80 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
>
</Link>
</nav>
</section>
{/* Contact */}
<section className="space-y-4">
<h4 className="font-semibold text-foreground uppercase tracking-wider"></h4>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<span className="text-foreground/80 text-sm">:</span>
<span className="text-blue-600 dark:text-blue-400 font-medium">@TODO</span>
</div>
</div>
</section>
</div>
{/* Bottom */}
<div className="mt-8 pt-8 border-t border-gray-200 dark:border-gray-700">
<div className="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
<p className="text-foreground/80 text-sm">© 2025 page-agent. All rights reserved.</p>
<div className="flex items-center space-x-6">
<a
href="TODO"
target="_blank"
rel="noopener noreferrer"
className="text-foreground/80 hover:text-foreground transition-colors duration-200"
aria-label="访问 GitHub 仓库"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
</a>
</div>
</div>
</div>
</div>
</footer>
)
}

View File

@@ -0,0 +1,74 @@
import { Link } from 'wouter'
export default function Header() {
return (
<header
className="relative z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-700"
role="banner"
>
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex items-center justify-between">
{/* Logo */}
<Link href="/" className="flex items-center space-x-3 group" aria-label="page-agent 首页">
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-200">
<span className="text-white font-bold text-2xl lg:text-2xl" aria-hidden="true">
P
</span>
</div>
<div>
<span className="text-xl font-bold text-foreground">page-agent</span>
<p className="text-xs text-foreground/80">UI Agent in your webpage</p>
</div>
</Link>
{/* Navigation */}
<nav
className="hidden md:flex items-center space-x-8"
role="navigation"
aria-label="主导航"
>
<Link
href="/docs/introduction/overview"
className="text-foreground/80 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
>
</Link>
<a
href="TODO"
target="_blank"
rel="noopener noreferrer"
className="text-foreground/80 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
aria-label="查看源码(在新窗口打开)"
>
</a>
</nav>
{/* Mobile menu button */}
<button
type="button"
className="md:hidden p-2 rounded-lg text-foreground/80 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200"
aria-label="打开移动端菜单"
aria-expanded="false"
aria-controls="mobile-menu"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</button>
</div>
</div>
</header>
)
}

View File

@@ -0,0 +1,21 @@
.syntax {
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
font-family: monospace;
}
.keyword {
color: #d73a49;
font-weight: 600;
}
.string {
color: #1d6eca;
}
.number {
color: #00c583;
}
.comment {
color: #6a737d;
font-style: italic;
}

View File

@@ -0,0 +1,77 @@
/**
* js 语法高亮组件,适合在文章中演示代码片段
*/
import React from 'react'
import styles from './HighlightSyntax.module.css'
interface HighlightSyntaxProps {
code: string
}
const keywords =
'async|await|function|const|let|var|if|else|for|while|return|try|catch|finally|class|extends|from|import|export|default|undefined|throw|true|false|null|this|new|in|of|instanceof|break|continue|switch|case|default|do|while|with|yield'
// 语法高亮函数,先整体提取字符串/注释等token再高亮
function highlightSyntax(code: string): string {
// 先转义HTML特殊字符
const escaped = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
// 单行字符串,所有反斜杠双重转义,保证正则安全
const pattern = new RegExp(
'("([^"\\\\]|\\\\.)*"|\'([^\'\\\\]|\\\\.)*\'|`([^`\\\\]|\\\\.)*`|//[^\\n]*|/\\*[\\s\\S]*?\\*/|\\b\\d+\\.?\\d*\\b|\\b(?:' +
keywords +
')\\b)',
'g'
)
const tokens: string[] = []
let lastIndex = 0
let match: RegExpExecArray | null
while ((match = pattern.exec(escaped)) !== null) {
if (match.index > lastIndex) {
tokens.push(...escaped.slice(lastIndex, match.index).split(/([ \t\n\r.])/))
}
tokens.push(match[0])
lastIndex = pattern.lastIndex
}
if (lastIndex < escaped.length) {
tokens.push(...escaped.slice(lastIndex).split(/([ \t\n\r.])/))
}
const highlighted = tokens
.map((token) => {
if (
/^"([^"\\]|\\.)*"$/.test(token) ||
/^'([^'\\]|\\.)*'$/.test(token) ||
/^`([^`\\]|\\.)*`$/.test(token)
) {
return `<span style="color: #1d6eca;">${token}</span>`
}
if (/^\b\d+\.?\d*\b$/.test(token)) {
return `<span style="color: #00c583;">${token}</span>`
}
if (/^\/\/.*$/.test(token)) {
return `<span style="color: #6a737d; font-style: italic;">${token}</span>`
}
if (/^\/\*[\s\S]*?\*\/$/.test(token)) {
return `<span style="color: #6a737d; font-style: italic;">${token}</span>`
}
if (new RegExp(`\\b(?:${keywords})\\b`).test(token)) {
return `<span style="color: #d73a49; font-weight: 600;">${token}</span>`
}
return token
})
.join('')
return highlighted
}
const HighlightSyntaxClient: React.FC<HighlightSyntaxProps> = ({ code }) => {
const htmlContent = highlightSyntax(code)
// eslint-disable-next-line react-dom/no-dangerously-set-innerhtml
return <code className={styles.syntax} dangerouslySetInnerHTML={{ __html: htmlContent }} />
}
export default HighlightSyntaxClient

View File

@@ -0,0 +1,164 @@
.console {
display: flex;
flex-direction: column;
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-family: monospace;
font-size: 12px;
line-height: 1;
overflow: hidden;
scroll-behavior: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.historyArea {
flex: 1;
overflow-y: auto;
padding: 12px;
background-color: #fafafa;
min-height: 200px;
display: flex;
flex-direction: column;
scroll-behavior: contain;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: #d0d0d0;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: #b0b0b0;
}
.historyItem {
display: flex;
align-items: center;
font-size: 12px;
line-height: 1;
padding-bottom: 6px;
border-bottom: #ccdeeebd 1px solid;
margin-bottom: 6px;
flex: 0 0 auto;
&:last-child {
margin-bottom: 0;
border-bottom: none;
}
&.input {
}
&.output {
}
.content {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
flex: 1;
font-family: inherit;
font-size: inherit;
line-height: inherit;
color: #2563eb;
}
/* 错误样式 */
&.error .content {
color: #dc2626;
background-color: #fef2f2;
padding: 4px 8px;
border-radius: 4px;
border-left: 3px solid #dc2626;
}
}
}
.prompt {
display: flex;
height: 100%;
align-items: flex-start;
width: 12px;
color: #666;
margin-right: 8px;
font-weight: 500;
flex-shrink: 0;
user-select: none;
}
.executing {
color: #f59e0b;
font-style: italic;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.inputArea {
display: flex;
align-items: center;
padding: 12px;
background-color: #ffffff;
border-top: 1px solid #e0e0e0;
.prompt {
margin-top: 8px;
}
.input {
flex: auto;
border: none;
outline: none;
background: transparent;
color: #333;
resize: none;
line-height: 20px;
}
.input::placeholder {
color: #999;
font-style: italic;
}
.input:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.console {
font-size: 12px;
border-radius: 6px;
}
.historyArea,
.inputLine {
padding: 8px;
}
.prompt {
margin-right: 6px;
}
}

View File

@@ -0,0 +1,369 @@
/**
* JS 调试台,适合在文档中直接让用户运行代码,并且实时查看运行结果
*/
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-base-to-string */
import { KeyboardEvent, forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
import HighlightSyntax from './HighlightSyntax'
import styles from './JSConsole.module.css'
// 全局console拦截管理器
class ConsoleInterceptor {
private static instance: ConsoleInterceptor
private subscribers = new Set<(type: string, args: unknown[]) => void>()
private originalConsole: {
log: typeof console.log
warn: typeof console.warn
error: typeof console.error
}
private isIntercepting = false
private constructor() {
this.originalConsole = {
log: console.log.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
}
}
static getInstance() {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!ConsoleInterceptor.instance) {
ConsoleInterceptor.instance = new ConsoleInterceptor()
}
return ConsoleInterceptor.instance
}
subscribe(callback: (type: string, args: unknown[]) => void) {
this.subscribers.add(callback)
this.startIntercepting()
}
unsubscribe(callback: (type: string, args: unknown[]) => void) {
this.subscribers.delete(callback)
if (this.subscribers.size === 0) {
this.stopIntercepting()
}
}
private startIntercepting() {
if (this.isIntercepting) return
this.isIntercepting = true
console.log = (...args: unknown[]) => {
this.originalConsole.log(...args)
this.notifySubscribers('log', args)
}
console.warn = (...args: unknown[]) => {
this.originalConsole.warn(...args)
this.notifySubscribers('warn', args)
}
console.error = (...args: unknown[]) => {
this.originalConsole.error(...args)
this.notifySubscribers('error', args)
}
}
private stopIntercepting() {
if (!this.isIntercepting) return
this.isIntercepting = false
console.log = this.originalConsole.log
console.warn = this.originalConsole.warn
console.error = this.originalConsole.error
}
private notifySubscribers(type: string, args: unknown[]) {
this.subscribers.forEach((callback) => {
callback(type, args)
})
}
}
interface JSConsoleProps {
context?: Record<string, unknown>
height?: string
onExecute?: (code: string, result: unknown) => void
placeholder?: string
}
export interface JSConsoleRef {
executeCode: (code: string) => Promise<unknown>
clear: () => void
appendOutput: (content: string) => void
}
interface OutputItem {
type: 'input' | 'output' | 'error' | 'log'
content: string
timestamp: number
}
const JSConsole = forwardRef<JSConsoleRef, JSConsoleProps>(
(
{ context = {}, height = '400px', onExecute, placeholder = 'Enter JavaScript code...' },
ref
) => {
const [input, setInput] = useState('')
const [outputs, setOutputs] = useState<OutputItem[]>([])
const [isExecuting, setIsExecuting] = useState(false)
const inputRef = useRef<HTMLTextAreaElement>(null)
const outputRef = useRef<HTMLDivElement>(null)
// 持久的执行上下文,用于多轮对话共享作用域
const executionContextRef = useRef<Record<string, unknown>>({})
// 格式化结果
const formatResult = (value: unknown): string => {
if (value === null) return 'null'
if (value === undefined) return 'undefined'
if (typeof value === 'string') return `"${value}"`
if (typeof value === 'function') return `[Function: ${value.name || 'anonymous'}]`
if (typeof value === 'object') {
try {
return JSON.stringify(value, null, 2)
} catch {
return value.toString()
}
}
return String(value)
}
// 全局console拦截处理
useEffect(() => {
const interceptor = ConsoleInterceptor.getInstance()
const handleGlobalConsole = (type: string, args: unknown[]) => {
const content = args.map((arg) => formatResult(arg)).join(' ')
const outputItem: OutputItem = {
type: type as any,
content: content,
timestamp: Date.now(),
}
setOutputs((prev) => [...prev, outputItem])
// 自动滚动到底部
setTimeout(() => {
if (outputRef.current) {
outputRef.current.scrollTop = outputRef.current.scrollHeight
}
}, 0)
}
interceptor.subscribe(handleGlobalConsole)
return () => {
interceptor.unsubscribe(handleGlobalConsole)
}
}, [])
// 执行代码
const executeCode = async (code: string): Promise<unknown> => {
if (!code.trim()) return
setIsExecuting(true)
// 添加输入到输出
const inputItem: OutputItem = {
type: 'input',
content: code,
timestamp: Date.now(),
}
setOutputs((prev) => [...prev, inputItem])
try {
// 创建异步函数以支持 await
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
// 合并外部上下文和持久执行上下文
const allContext = { ...context, ...executionContextRef.current }
const contextKeys = Object.keys(allContext)
const contextValues = Object.values(allContext)
// 注入 console.log 重定向
const logs: string[] = []
const mockConsole = {
log: (...args: unknown[]) => {
logs.push(args.map((arg) => formatResult(arg)).join(' '))
},
error: (...args: unknown[]) => {
logs.push('ERROR: ' + args.map((arg) => formatResult(arg)).join(' '))
},
warn: (...args: unknown[]) => {
logs.push('WARN: ' + args.map((arg) => formatResult(arg)).join(' '))
},
}
// 检测代码是否是表达式还是语句
const trimmedCode = code.trim()
const isExpression =
!trimmedCode.includes(';') &&
!trimmedCode.startsWith('const ') &&
!trimmedCode.startsWith('let ') &&
!trimmedCode.startsWith('var ') &&
!trimmedCode.startsWith('function ') &&
!trimmedCode.startsWith('class ') &&
!trimmedCode.startsWith('if ') &&
!trimmedCode.startsWith('for ') &&
!trimmedCode.startsWith('while ') &&
!trimmedCode.startsWith('try ') &&
!trimmedCode.startsWith('{') &&
!trimmedCode.includes('\n')
// 如果是表达式,自动添加 return
const codeToExecute = isExpression ? `return ${code}` : code
const wrappedCode = `
return (async function() {
${codeToExecute}
})();
`
// 执行代码
const func = new AsyncFunction('console', ...contextKeys, wrappedCode)
const result = await func(mockConsole, ...contextValues)
// 添加 console.log 输出
if (logs.length > 0) {
const logItem: OutputItem = {
type: 'log',
content: logs.join('\n'),
timestamp: Date.now(),
}
setOutputs((prev) => [...prev, logItem])
}
// 总是添加执行结果输出(包括 undefined
const outputItem: OutputItem = {
type: 'output',
content: formatResult(result),
timestamp: Date.now(),
}
setOutputs((prev) => [...prev, outputItem])
onExecute?.(code, result)
return result
} catch (error) {
const errorItem: OutputItem = {
type: 'error',
content: error instanceof Error ? error.message : String(error),
timestamp: Date.now(),
}
setOutputs((prev) => [...prev, errorItem])
throw error
} finally {
setIsExecuting(false)
// 滚动到底部
setTimeout(() => {
if (outputRef.current) {
outputRef.current.scrollTop = outputRef.current.scrollHeight
}
}, 0)
}
}
// 清空控制台
const clear = () => {
setOutputs([])
// 同时清空执行上下文
executionContextRef.current = {}
}
// 添加输出
const appendOutput = (content: string) => {
const outputItem: OutputItem = {
type: 'output',
content,
timestamp: Date.now(),
}
setOutputs((prev) => [...prev, outputItem])
}
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
executeCode,
clear,
appendOutput,
}))
// 处理键盘事件
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter') {
if (e.shiftKey) {
// Shift+Enter 换行
return
} else {
// Enter 执行
e.preventDefault()
if (!isExecuting && input.trim()) {
executeCode(input)
setInput('')
setTimeout(() => inputRef.current?.focus(), 0)
}
}
}
}
function getPrompt(type: string) {
let prompt = ' '
if (type === 'input') {
prompt = '>'
} else if (type === 'output') {
prompt = '<'
}
return prompt
}
return (
<div className={styles.console} style={{ height }}>
{/* 历史记录和输入区域 */}
<div className={styles.historyArea} ref={outputRef}>
{outputs.map((item, index) => (
<div key={index} className={`${styles.historyItem} ${styles[item.type]}`}>
<span className={styles.prompt}>{getPrompt(item.type)}</span>
<pre className={styles.content}>
<HighlightSyntax code={item.content} />
</pre>
</div>
))}
{isExecuting && (
<div className={styles.historyItem}>
<span className={styles.prompt}>{'> '}</span>
<span className={styles.executing}>Executing...</span>
</div>
)}
</div>
{/* 当前输入行 */}
<div className={styles.inputArea}>
<span className={styles.prompt}>{'> '}</span>
<textarea
ref={inputRef}
className={styles.input}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={isExecuting}
rows={1}
style={{
height: Math.min(Math.max(20, input.split('\n').length * 20), 120),
}}
/>
</div>
</div>
)
}
)
JSConsole.displayName = 'JSConsole'
export default JSConsole

View File

@@ -0,0 +1,156 @@
import BetaNotice from '@pages/components/BetaNotice'
import CodeEditor from '@pages/components/CodeEditor'
export default function CustomTools() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<BetaNotice />
<p className="text-xl text-foreground/80 mb-8 leading-relaxed">
AI Agent 使 Zod AI
</p>
<div className="space-y-8">
<section>
<h2 className="text-2xl font-bold mb-4"></h2>
<p className="text-foreground/80 mb-4">
namedescriptioninput schema execute
</p>
<CodeEditor
code={`import { z } from 'zod'
import { pageAgent } from 'page-agent'
// 定义输入 schema
const CreateUserSchema = z.object({
name: z.string().min(1, '姓名不能为空'),
email: z.string().email('邮箱格式不正确'),
role: z.enum(['admin', 'user', 'guest']).default('user')
})
// 注册工具
pageAgent.registerTool({
name: 'createUser',
description: '创建新用户账户',
input: CreateUserSchema,
execute: async (params) => {
// 执行业务逻辑
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params)
})
return await response.json()
}
})`}
language="javascript"
/>
</section>
<section>
<h2 className="text-2xl font-bold mb-4"></h2>
<div className="space-y-4">
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-blue-900 dark:text-blue-300 mb-2">
📝 name ()
</h3>
<p className="text-foreground/80 mb-2">AI </p>
<div className="bg-white dark:bg-gray-800 rounded p-3 text-sm">
<code>name: 'searchProducts' // 驼峰命名,语义清晰</code>
</div>
</div>
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-green-900 dark:text-green-300 mb-2">
💬 description ()
</h3>
<p className="text-foreground/80 mb-2"> AI 使</p>
<div className="bg-white dark:bg-gray-800 rounded p-3 text-sm">
<code>description: '根据关键词搜索商品,支持价格区间和分类筛选'</code>
</div>
</div>
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-purple-900 dark:text-purple-300 mb-2">
🔧 input ()
</h3>
<p className="text-foreground/80 mb-2">Zod schema </p>
<div className="bg-white dark:bg-gray-800 rounded p-3 text-sm">
<code>{`input: z.object({
keyword: z.string().min(1),
priceRange: z.object({
min: z.number().optional(),
max: z.number().optional()
}).optional()
})`}</code>
</div>
</div>
<div className="p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-orange-900 dark:text-orange-300 mb-2">
execute ()
</h3>
<p className="text-foreground/80 mb-2"></p>
<div className="bg-white dark:bg-gray-800 rounded p-3 text-sm">
<code>{`execute: async (params) => {
// params 已通过 Zod 验证
const result = await businessLogic(params)
return result // 返回结果给 AI
}`}</code>
</div>
</div>
</div>
</section>
<section>
<h2 className="text-2xl font-bold mb-4"></h2>
<p className="text-foreground/80 mb-4">
<code className="bg-gray-200 dark:bg-gray-700 px-2 py-1 rounded">pageFilter</code>{' '}
</p>
<CodeEditor
code={`pageAgent.registerTool({
name: 'approveOrder',
description: '审批订单',
input: z.object({
orderId: z.string(),
approved: z.boolean()
}),
execute: async (params) => {
// 审批逻辑
},
// 可选:页面过滤器
pageFilter: {
// 只在订单管理页面显示
include: ['/admin/orders', '/admin/orders/*'],
// 排除特定页面
exclude: ['/admin/orders/archived']
}
})`}
language="javascript"
/>
</section>
<section>
<h2 className="text-2xl font-bold mb-4"></h2>
<div className="space-y-4">
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-300 mb-2">
</h3>
<ul className="text-foreground/80 space-y-1 text-sm">
<li> 使 pageFilter </li>
<li> execute </li>
<li> </li>
</ul>
</div>
</div>
</section>
</div>
</div>
)
}

View File

@@ -0,0 +1,44 @@
import BetaNotice from '@pages/components/BetaNotice'
import CodeEditor from '@pages/components/CodeEditor'
export default function DataMasking() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<BetaNotice />
<p className="text-xl text-foreground/80 mb-6 leading-relaxed">
AI
</p>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="space-y-4 mb-6">
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">
🔒
</h3>
<p className="text-foreground/80"></p>
</div>
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300">
</h3>
<p className="text-foreground/80"></p>
</div>
</div>
<CodeEditor
code={`// 数据脱敏配置
// @todo
const rules = [
{ pattern: /\\d{11}/, replacement: '***-****-****' },
{ pattern: /\\d{4}-\\d{4}-\\d{4}-\\d{4}/, replacement: '****-****-****-****' }
]
pageAgent.maskData(rules)`}
/>
</div>
)
}

View File

@@ -0,0 +1,156 @@
import BetaNotice from '@pages/components/BetaNotice'
import CodeEditor from '@pages/components/CodeEditor'
export default function KnowledgeInjection() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<BetaNotice />
<p className="text-xl text-foreground/80 mb-8 leading-relaxed">
AI
</p>
{/* Custom Instruction */}
<section className="mb-12">
<h2 className="text-3xl font-bold mb-6">Instruction - </h2>
<div className="p-6 bg-purple-50 dark:bg-purple-900/20 rounded-lg mb-6">
<h3 className="text-xl font-semibold mb-3 text-purple-900 dark:text-purple-300">
🎯
</h3>
<p className="text-foreground/80 mb-4"> AI </p>
<ul className="list-disc list-inside space-y-2 text-foreground/70">
<li> AI </li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
<CodeEditor
className="mb-6"
code={`// 构造函数中设置系统指令
const pageAgent = new PageAgent({
instruction: \`
# 角色定义
你是专业的电商运营助手。
# 工作风格
- 谨慎:操作前确认
- 准确:确保正确性
- 高效:优化流程
# 错误处理
遇到错误时暂停并报告。
\`
});`}
/>
</section>
{/* App Knowledge */}
<section className="mb-12">
<h2 className="text-3xl font-bold mb-6">App Knowledge - </h2>
<div className="p-6 bg-blue-50 dark:bg-blue-900/20 rounded-lg mb-6">
<h3 className="text-xl font-semibold mb-3 text-blue-900 dark:text-blue-300">
<EFBFBD>
</h3>
<p className="text-foreground/80 mb-4">
AI
</p>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<h4 className="font-semibold text-blue-800 dark:text-blue-200"></h4>
<ul className="list-disc list-inside text-sm text-foreground/70 space-y-1">
<li></li>
<li></li>
<li></li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-blue-800 dark:text-blue-200"></h4>
<ul className="list-disc list-inside text-sm text-foreground/70 space-y-1">
<li></li>
<li></li>
<li></li>
</ul>
</div>
</div>
</div>
<CodeEditor
className="mb-6"
code={`// 应用知识
pageAgent.knowledge.setAppKnowledge(\`
# 产品介绍
电商管理系统:面向中小企业的一站式解决方案。
# 操作流程
## 商品上架
1. 进入商品管理页面 2. 点击新增商品 3. 填写基本信息 4. 设置库存 5. 提交审核
# 术语解释
- SKU库存量单位
- SPU标准产品单位
- 运费模板:物流费用计算规则
# 业务规则
- 库存为0时自动下架
- VIP会员享9.5折
\`);`}
/>
</section>
{/* Page Knowledge */}
<section className="mb-12">
<h2 className="text-3xl font-bold mb-6">Page Knowledge - </h2>
<div className="p-6 bg-green-50 dark:bg-green-900/20 rounded-lg mb-6">
<h3 className="text-xl font-semibold mb-3 text-green-900 dark:text-green-300">
📄
</h3>
<p className="text-foreground/80 mb-4">
AI
</p>
<div className="grid md:grid-cols-3 gap-4">
<div className="space-y-2">
<h4 className="font-semibold text-green-800 dark:text-green-200"></h4>
<p className="text-sm text-foreground/70"></p>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-green-800 dark:text-green-200"></h4>
<p className="text-sm text-foreground/70"></p>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-green-800 dark:text-green-200"></h4>
<p className="text-sm text-foreground/70"></p>
</div>
</div>
</div>
<CodeEditor
className="mb-6"
code={`// 页面知识库
// 添加页面知识
pageAgent.knowledge.addPageKnowledge("/products", \`
商品列表页面,包含搜索、筛选、批量操作功能。
#add-product-btn新增商品按钮
.product-item商品列表项
#search-input搜索框最少2个字符
\`);
pageAgent.knowledge.addPageKnowledge("/orders/*", \`
订单详情页面。
.order-status订单状态标签
#update-status-btn状态更新按钮
\`);
// 移除页面知识
pageAgent.knowledge.removePageKnowledge("/products");`}
/>
</section>
</div>
)
}

View File

@@ -0,0 +1,162 @@
import BetaNotice from '@pages/components/BetaNotice'
import CodeEditor from '@pages/components/CodeEditor'
export default function ModelIntegration() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<BetaNotice />
<p className="text-xl text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">
OpenAI tool call
</p>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg mb-6">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">
🔌 OpenAI
</h3>
<p className="text-foreground/80">
OpenAI API chat/completions OpenAIAzure
使 vLLMOllama
</p>
<p className="text-foreground/80">
tool call json schema tool call
</p>
</div>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="grid md:grid-cols-3 gap-4 mb-6">
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-green-900 dark:text-green-300">
gpt-4.1-mini
</h3>
<p className="text-sm text-foreground/80 mb-2">使 </p>
<ul className="text-sm text-foreground/70 space-y-1">
<li> </li>
<li> </li>
<li> i/o $0.4/$1.6 ( M token)</li>
</ul>
</div>
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300">
🚀 gpt-4.1
</h3>
<p className="text-sm text-foreground/80 mb-2"></p>
<ul className="text-sm text-foreground/70 space-y-1">
<li> </li>
<li> 4.1-mini 5 </li>
<li> </li>
</ul>
</div>
<div className="p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-orange-900 dark:text-orange-300">
🛡 qwen-plus (qwen3)
</h3>
<p className="text-sm text-foreground/80 mb-2"></p>
<ul className="text-sm text-foreground/70 space-y-1">
<li> 便</li>
<li> ToolCall </li>
<li>
<strong></strong>
</li>
</ul>
</div>
</div>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="p-4 bg-emerald-50 dark:bg-emerald-900/20 rounded-lg mb-6">
<h3 className="text-lg font-semibold mb-3 text-emerald-900 dark:text-emerald-300">
</h3>
<div className="flex flex-wrap gap-2">
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
gpt-4.1-mini
</span>
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
gpt-4.1
</span>
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
gpt-5
</span>
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
gpt-5-mini
</span>
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
qwen-plus
</span>
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
deepseek-v3.1
</span>
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
claude-4-sonnet
</span>
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
claude-3.7-sonnet
</span>
</div>
</div>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg mb-6">
<h3 className="text-lg font-semibold mb-2 text-red-900 dark:text-red-300">
🚫
</h3>
<ul className="text-sm text-foreground/80 space-y-1 list-disc pl-5">
<li>reasoning </li>
<li>GPT-5 </li>
<li> agent coder </li>
<li>
json schema openAI tool call
</li>
<li>nano </li>
<li>Gemini OpenAI tool call </li>
</ul>
</div>
<h2 className="text-2xl font-bold mb-3"></h2>
<CodeEditor
code={`
// 百炼等其他兼容服务
const pageAgent = new PageAgent({
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
apiKey: 'your-api-key',
modelName: 'qwen-plus'
});
// 私有部署模型
const pageAgent = new PageAgent({
baseURL: 'http://localhost:11434/v1',
apiKey: 'ollama', // Ollama 通常使用任意值
modelName: 'qwen3:latest'
});`}
/>
<div className="mt-6 p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300">
💡
</h3>
<ul className="text-sm text-foreground/80 space-y-2">
<li>
<strong>baseURL</strong>: API OpenAI
</li>
<li>
<strong>apiKey</strong>: API
</li>
<li>
<strong>modelName</strong>: gpt-4.1-mini
</li>
</ul>
</div>
</div>
)
}

View File

@@ -0,0 +1,75 @@
import BetaNotice from '@pages/components/BetaNotice'
export default function SecurityPermissions() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<BetaNotice />
<p className="text-xl text-foreground/80 mb-8 leading-relaxed">
page-agent AI
</p>
<div className="space-y-6">
<section>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="space-y-3">
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-red-900 dark:text-red-300">
🚫
</h3>
<p className="text-foreground/80"> AI </p>
</div>
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-green-900 dark:text-green-300">
</h3>
<p className="text-foreground/80"> AI </p>
</div>
</div>
</section>
<section>
<h2 className="text-2xl font-bold mb-3">URL </h2>
<div className="space-y-3">
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-red-900 dark:text-red-300">
🚫 URL
</h3>
<p className="text-foreground/80"> AI 访</p>
</div>
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-green-900 dark:text-green-300">
URL
</h3>
<p className="text-foreground/80"> AI 访</p>
</div>
</div>
</section>
<section>
<h2 className="text-2xl font-bold mb-3">Instruction </h2>
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300">
</h3>
<p className="text-foreground/80 mb-3">
AI
</p>
<div className="space-y-2">
<div className="pl-3 border-l-2 border-red-400">
<p className="font-medium text-red-700 dark:text-red-300"></p>
<p className="text-sm text-foreground/70"></p>
</div>
<div className="pl-3 border-l-2 border-orange-400">
<p className="font-medium text-orange-700 dark:text-orange-300"></p>
<p className="text-sm text-foreground/70"></p>
</div>
</div>
</div>
</section>
</div>
</div>
)
}

View File

@@ -0,0 +1,41 @@
import BetaNotice from '@pages/components/BetaNotice'
import CodeEditor from '@pages/components/CodeEditor'
export default function ApiReference() {
return (
<div>
<h1 className="text-4xl font-bold mb-6">API </h1>
<BetaNotice />
<p className="text-xl text-foreground/80 mb-6 leading-relaxed">
API
</p>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="space-y-4 mb-6">
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<h3 className="text-lg font-semibold mb-2 font-mono">pageAgent.init(config)</h3>
<p className="text-foreground/80 mb-3"> page-agent</p>
<CodeEditor code={`const pageAgent = new PageAgent`} />
</div>
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<h3 className="text-lg font-semibold mb-2 font-mono">pageAgent.execute(instruction)</h3>
<p className="text-foreground/80 mb-3"></p>
<CodeEditor
code={`await pageAgent.execute('点击提交按钮');
await pageAgent.execute('填写用户名为张三');`}
/>
</div>
</div>
<h2 className="text-2xl font-bold mb-3"></h2>
<CodeEditor code={`// TODO`} />
</div>
)
}

View File

@@ -0,0 +1,62 @@
import BetaNotice from '@pages/components/BetaNotice'
import CodeEditor from '@pages/components/CodeEditor'
export default function BestPractices() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<BetaNotice />
<p className="text-xl text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">
使 page-agent
</p>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="space-y-4 mb-6">
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-green-900 dark:text-green-300">
API
</h3>
<p className="text-foreground/80 mb-3"> AI </p>
<CodeEditor
code={`// 推荐:合并操作
await pageAgent.execute('填写表单姓名张三邮箱test@example.com然后提交');
// 不推荐:分别操作
await pageAgent.execute('填写姓名张三');
await pageAgent.execute('填写邮箱test@example.com');
await pageAgent.execute('点击提交按钮');`}
/>
</div>
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">
🎯
</h3>
<p className="text-foreground/80">使</p>
</div>
</div>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="space-y-3 mb-6">
<div className="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border-l-4 border-red-500">
<h3 className="font-semibold mb-1 text-red-900 dark:text-red-300"></h3>
<p className="text-foreground/80"></p>
</div>
<div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border-l-4 border-yellow-500">
<h3 className="font-semibold mb-1 text-yellow-900 dark:text-yellow-300"></h3>
<p className="text-foreground/80"></p>
</div>
</div>
<h2 className="text-2xl font-bold mb-3"></h2>
<CodeEditor code={`// TODO`} />
</div>
)
}

View File

@@ -0,0 +1,41 @@
import BetaNotice from '@pages/components/BetaNotice'
import CodeEditor from '@pages/components/CodeEditor'
export default function CdnSetup() {
return (
<div>
<h1 className="text-4xl font-bold mb-6">CDN </h1>
<BetaNotice />
<p className="text-xl text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">
CDN page-agent
</p>
<h2 className="text-2xl font-bold mb-3"></h2>
<CodeEditor
className="mb-8"
code={`<!-- 在 HTML 中引入 -->
// @todo find a cdn
<script src="https://some-cdn.com/page-agent.umd.js"></script>
<script>
const pageAgent = new PageAgent()
pageAgent.panel.show()
</script>`}
/>
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-4 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300">
</h3>
<ul className="text-foreground/80 space-y-1">
<li> 使</li>
<li> HTTPS 使</li>
<li> CSP </li>
</ul>
</div>
</div>
)
}

View File

@@ -0,0 +1,38 @@
import BetaNotice from '@pages/components/BetaNotice'
import CodeEditor from '@pages/components/CodeEditor'
export default function Configuration() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<BetaNotice />
<p className="text-xl text-foreground/80 mb-6 leading-relaxed">
page-agent
</p>
<h2 className="text-2xl font-bold mb-3"></h2>
<CodeEditor className="mb-8" code={`// TODO`} />
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="space-y-4">
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">
🎯
</h3>
<p className="text-foreground/80"> AI </p>
</div>
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-green-900 dark:text-green-300">
</h3>
<p className="text-foreground/80"></p>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,113 @@
import CodeEditor from '@pages/components/CodeEditor'
export default function ThirdPartyAgentPage() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"> Agent</h1>
<p className="mb-6 leading-relaxed">
pageAgent Agent Agent
</p>
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 mb-6">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">💡 </h3>
<p className="text-blue-800 dark:text-blue-200">
Agent "嘴巴""眼睛""手"
</p>
</div>
<h2 className="text-2xl font-bold mb-4"></h2>
<div className="space-y-4 mb-6">
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-green-900 dark:text-green-300">
1. Function Calling
</h3>
<CodeEditor
code={`// 定义工具
const pageAgentTool = {
name: "page_agent",
description: "执行网页操作",
parameters: {
type: "object",
properties: {
instruction: { type: "string", description: "操作指令" }
},
required: ["instruction"]
},
execute: async (params) => {
const result = await pageAgent.execute(params.instruction)
return { success: result.success, message: result.message }
}
}
// 注册到你的 agent 中`}
language="javascript"
/>
</div>
</div>
<h2 className="text-2xl font-bold mb-4"></h2>
<div className="grid md:grid-cols-2 gap-4 mb-6">
<div className="bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
<h4 className="font-semibold mb-2">🤖 </h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
"帮我提交工单"
</p>
</div>
<div className="bg-gradient-to-br from-green-50 to-blue-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
<h4 className="font-semibold mb-2">📋 </h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
"完成客户入职"
</p>
</div>
<div className="bg-gradient-to-br from-purple-50 to-pink-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
<h4 className="font-semibold mb-2">🎯 </h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
"预订会议室"
</p>
</div>
<div className="bg-gradient-to-br from-orange-50 to-red-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
<h4 className="font-semibold mb-2">🔧 </h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
"重启服务器"
</p>
</div>
</div>
<h2 className="text-2xl font-bold mb-4"></h2>
<div className="space-y-4 mb-6">
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<h3 className="text-lg font-semibold mb-2"></h3>
<CodeEditor code={`// @TODO`} language="javascript" />
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<h3 className="text-lg font-semibold mb-2"></h3>
<CodeEditor code={`// @TODO`} language="javascript" />
</div>
</div>
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4 mb-6">
<h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-100 mb-2">
</h3>
<ul className="text-yellow-800 dark:text-yellow-200 space-y-1 text-sm">
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
<div className="bg-gradient-to-r from-green-50 to-blue-50 dark:from-green-900/20 dark:to-blue-900/20 p-4 rounded-lg">
<h3 className="text-lg font-semibold mb-2">🎉 </h3>
<p className="mb-3"> Agent </p>
<a
href="/docs/integration/configuration"
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200"
>
</a>
</div>
</div>
)
}

View File

@@ -0,0 +1,156 @@
export default function LimitationsPage() {
return (
<div className="max-w-4xl mx-auto">
<div className="mb-8">
<h1 className="text-4xl font-bold mb-4 text-gray-900 dark:text-white">使</h1>
<p className="text-xl text-gray-600 dark:text-gray-300">
page-agent
</p>
</div>
<div className="prose prose-lg dark:prose-invert max-w-none">
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-400 p-4 mb-6">
<h3 className="font-semibold text-blue-800 dark:text-blue-200 mb-2"></h3>
<ul className="text-blue-700 dark:text-blue-300 space-y-2">
<li>
<strong>SPA</strong>
</li>
<li>
<strong></strong>
</li>
<li>
<strong></strong> page-agent
</li>
</ul>
</div>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-6">
<h3 className="font-semibold mb-4"></h3>
<div className="grid md:grid-cols-2 gap-4 mb-6">
<div className="space-y-2">
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span></span>
</div>
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span></span>
</div>
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span></span>
</div>
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span></span>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span></span>
</div>
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span></span>
</div>
</div>
</div>
<h3 className="font-semibold mb-4"></h3>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span>
<span>hover</span>
</div>
<div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span>
<span></span>
</div>
<div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span>
<span></span>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span>
<span></span>
</div>
<div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span>
<span></span>
</div>
<div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span>
<span></span>
</div>
</div>
</div>
</div>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="bg-red-50 dark:bg-red-900/20 border-l-4 border-red-400 p-4 mb-6">
<h3 className="font-semibold text-red-800 dark:text-red-200 mb-2"></h3>
<p className="text-red-700 dark:text-red-300 mb-3">
page-agent DOM <strong></strong>
</p>
<ul className="text-red-700 dark:text-red-300 space-y-1">
<li>
<strong></strong>
</li>
<li>
<strong>Canvas </strong> Canvas
</li>
<li>
<strong>WebGL 3D </strong> 3D
</li>
<li>
<strong>SVG </strong> SVG
</li>
</ul>
</div>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-6">
<div className="space-y-4">
<div>
<h3 className="font-semibold mb-2"></h3>
<p className="text-gray-600 dark:text-gray-300">
DOM
accessibility AI
</p>
</div>
<div>
<h3 className="font-semibold mb-2">UI/UX</h3>
<p className="text-gray-600 dark:text-gray-300">
AI
</p>
</div>
<div>
<h3 className="font-semibold mb-2"></h3>
<p className="text-gray-600 dark:text-gray-300">modern browser</p>
</div>
</div>
</div>
<h2></h2>
<div className="bg-green-50 dark:bg-green-900/20 border-l-4 border-green-400 p-4">
<h3 className="font-semibold text-green-800 dark:text-green-200 mb-2"></h3>
<ul className="text-green-700 dark:text-green-300 space-y-1">
<li> </li>
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,188 @@
export default function Overview() {
return (
<article>
{/* 头图 */}
<figure className="mb-8 rounded-xl overflow-hidden">
<img
src="https://img.alicdn.com/imgextra/i4/O1CN01eppTGh27iefUEegnN_!!6000000007831-0-tps-3015-1024.jpg"
alt="page-agent 概览图示"
className="w-full h-64 object-cover"
/>
</figure>
<h1 className="text-4xl font-bold mb-6">Overview</h1>
<p className="text-xl text-foreground/80 mb-8 leading-relaxed">
page-agent Web技术的 UI Agent AI CDN
Web
</p>
<section>
<h2 className="text-2xl font-bold mb-4"> page-agent</h2>
<p className="text-foreground/80 mb-8 leading-relaxed ">
page-agent <strong> UI Agent</strong>
page-agent <strong></strong>
Agent开发者 Agent
</p>
</section>
<section>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="grid md:grid-cols-2 gap-4 mb-8" role="list">
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">
🧠 DOM
</h3>
<p className="">
token DOM
</p>
</div>
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300">
🔒
</h3>
<p className="">
AI
</p>
</div>
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-green-900 dark:text-green-300">
</h3>
<p className="">CDN LLM OpenAI qwen3</p>
</div>
<div className="p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-orange-900 dark:text-orange-300">
</h3>
<p className=""> B端系统</p>
</div>
</div>
<h2 className="text-2xl font-bold mb-4"> browser-use </h2>
<div className="overflow-x-auto mb-8">
<table className="w-full border-collapse border border-gray-300 dark:border-gray-600">
<thead>
<tr className="bg-gray-50 dark:bg-gray-800">
<th className="border border-gray-300 dark:border-gray-600 px-4 py-3 text-left">
</th>
<th className="border border-gray-300 dark:border-gray-600 px-4 py-3 text-left">
page-agent
</th>
<th className="border border-gray-300 dark:border-gray-600 px-4 py-3 text-left">
browser-use
</th>
</tr>
</thead>
<tbody>
<tr>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3 font-medium">
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3"></td>
</tr>
<tr>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3 font-medium">
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3"></td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
</td>
</tr>
<tr>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3 font-medium">
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
/Agent
</td>
</tr>
<tr>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3 font-medium">
使
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
</td>
</tr>
</tbody>
</table>
</div>
<h2 className="text-2xl font-bold mb-4"></h2>
<ul className="space-y-4 mb-8">
<li className="flex items-start space-x-3">
<span className="w-6 h-6 bg-blue-500 text-white rounded-full flex items-center justify-center font-bold mt-0.5">
1
</span>
<div className="">
<strong></strong>
Agent"请先点击设置按钮然后点击..."
</div>
</li>
<li className="flex items-start space-x-3">
<span className="w-6 h-6 bg-green-500 text-white rounded-full flex items-center justify-center font-bold mt-0.5">
2
</span>
<div className="">
<strong>/</strong>
Agent B
</div>
</li>
<li className="flex items-start space-x-3">
<span className="w-6 h-6 bg-purple-500 text-white rounded-full flex items-center justify-center font-bold mt-0.5">
3
</span>
<div className="">
<strong></strong>
AI演示"如何提交报销申请"
</div>
</li>
<li className="flex items-start space-x-3">
<span className="w-6 h-6 bg-orange-500 text-white rounded-full flex items-center justify-center font-bold mt-0.5">
4
</span>
<div className="">
<strong></strong>
</div>
</li>
</ul>
<div className="bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 p-4 rounded-lg">
<h3 className="text-lg font-semibold mb-2">🚀 使</h3>
<p className="mb-3 ">
AI
</p>
<a
href="/docs/introduction/quick-start"
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200"
role="button"
>
</a>
</div>
</section>
</article>
)
}

View File

@@ -0,0 +1,81 @@
import BetaNotice from '@pages/components/BetaNotice'
import CodeEditor from '@pages/components/CodeEditor'
export default function QuickStart() {
return (
<div>
<h1 className="text-4xl font-bold mb-6">Quick Start</h1>
<BetaNotice />
<p className=" mb-6 leading-relaxed">
page-agent AI
</p>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="space-y-4 mb-6">
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">
1.
</h3>
<div className="space-y-3">
<div>
<p className="text-sm font-medium mb-2">CDN </p>
<CodeEditor
code={`// 仅供测试使用,生产环境联系 @西萌
<script src="https://dev.g.alicdn.com/dt/page-use.js/0.0.1/lib/page-agent.umd.cjs"></script>`}
language="html"
/>
</div>
<div>
<p className="text-sm font-medium mb-2">NPM </p>
<CodeEditor
code={`// npm install page-agent
import PageAgent from 'page-agent'`}
language="bash"
/>
</div>
</div>
</div>
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-green-900 dark:text-green-300">
2.
</h3>
<CodeEditor
code={`// 仅供测试使用,生产环境需要配置 LLM 接入点,本工具不提供 LLM 服务
const pageAgent = new PageAgent()`}
language="javascript"
/>
</div>
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300">
3. 使
</h3>
<CodeEditor
code={`// 程序化执行自然语言指令
await pageAgent.execute('点击提交按钮,然后填写用户名为张三');
// 或者显示对话框让用户输入指令
pageAgent.panel.show()
`}
language="javascript"
/>
</div>
</div>
<div className="bg-gradient-to-r from-green-50 to-blue-50 dark:from-green-900/20 dark:to-blue-900/20 p-4 rounded-lg">
<h3 className="text-lg font-semibold mb-2">🎉 </h3>
<p className="mb-3 "> AI </p>
<a
href="/docs/features/security-permissions"
className="inline-flex items-center px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors duration-200"
>
</a>
</div>
</div>
)
}

33
pages/index.css Normal file
View File

@@ -0,0 +1,33 @@
@import 'tailwindcss';
:root {
--background: #ffffff;
--foreground: #171717;
/* 主题色渐变 */
--theme-color-1: rgb(88, 192, 252);
--theme-color-2: rgb(189, 69, 251);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
/* 添加 Tailwind 自定义颜色 */
@theme {
--color-background: var(--background);
--color-foreground: var(--foreground);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

30
pages/main.tsx Normal file
View File

@@ -0,0 +1,30 @@
// import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { Route, Router, Switch } from 'wouter'
import { useHashLocation } from 'wouter/use-hash-location'
import { default as PagesRouter } from './router.tsx'
import { default as TestPagesRouter } from './test-pages/router.tsx'
import './index.css'
let baseURL: string
// 如果是 localhost就用 /
// 如果是其他环境,阶段到 index.html
if (window.location.hostname === 'localhost') {
baseURL = '/'
} else {
baseURL = window.location.pathname.split('index.html')[0] + 'index.html'
}
createRoot(document.getElementById('root')!).render(
// <StrictMode>
<Router hook={useHashLocation}>
<Switch>
<Route path="/test-pages" component={TestPagesRouter} nest />
<Route path="/" component={PagesRouter} nest />
</Switch>
</Router>
// </StrictMode>
)

460
pages/page.tsx Normal file
View File

@@ -0,0 +1,460 @@
/* eslint-disable react-dom/no-dangerously-set-innerhtml */
import { useState } from 'react'
import { Link, useSearchParams } from 'wouter'
import { PageAgent } from '@/PageAgent.js'
import Footer from './components/Footer'
import Header from './components/Header'
const injection = encodeURI(
"javascript:(function(){var s=document.createElement('script');s.src=`https://dev.g.alicdn.com/dt/page-use.js/0.0.1/lib/page-agent.umd.cjs?t=${Math.random()}`;s.setAttribute('crossorigin', true);s.onload=()=>console.log('PageAgent ready!');document.head.appendChild(s);})();"
)
const injectionQwen = encodeURI(
"javascript:(function(){var s=document.createElement('script');s.src=`https://dev.g.alicdn.com/dt/page-use.js/0.0.1/lib/page-agent.umd.cjs?t=${Math.random()}&model=qwen-plus-latest`;s.setAttribute('crossorigin', true);s.onload=()=>console.log('PageAgent ready!');document.head.appendChild(s);})();"
)
const injectionA = `
<a
href=${injection}
class="inline-flex items-center text-xs px-3 py-2 bg-blue-500 text-white font-medium rounded-lg hover:shadow-md transform hover:scale-105 transition-all duration-200 cursor-move border-2 border-dashed border-green-300"
draggable="true"
title="拖拽我到收藏夹栏"
>
✨PageAgent
</a>
<a
href=${injectionQwen}
class="inline-flex items-center text-xs px-3 py-2 bg-purple-500 text-white font-medium rounded-lg hover:shadow-md transform hover:scale-105 transition-all duration-200 cursor-move border-2 border-dashed border-green-300"
draggable="true"
title="拖拽我到收藏夹栏"
>
✨PageAgent (Qwen)
</a>
`
export default function HomePage() {
const [task, setTask] = useState('进入文档页,打开数据脱敏相关的文档,帮我总结成 markdown')
const [params, setParams] = useSearchParams()
const isOther = params.has('try_other')
const [activeTab, setActiveTab] = useState<'try' | 'other'>(isOther ? 'other' : 'try')
const handleExecute = async () => {
if (!task.trim()) return
let pageAgent: PageAgent
if (window.pageAgent && !window.pageAgent.disposed) {
pageAgent = window.pageAgent
} else {
pageAgent = new PageAgent({
// 把 react 根元素排除掉,挂了很多冒泡时间导致假阳
interactiveBlacklist: [document.getElementById('root')!],
language: 'zh-CN',
})
window.pageAgent = pageAgent
}
const result = await pageAgent.execute(task)
console.log(result)
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-900 dark:to-gray-800">
<Header />
{/* Hero Section */}
<main id="main-content">
<section className="relative px-6 py-22 lg:py-28" aria-labelledby="hero-heading">
<div className="max-w-7xl mx-auto text-center">
{/* Background Pattern */}
<div className="absolute inset-0 opacity-30" aria-hidden="true">
<div className="absolute inset-0 bg-gradient-to-r from-blue-400/20 to-purple-400/20 rounded-3xl transform rotate-1"></div>
<div className="absolute inset-0 bg-gradient-to-l from-purple-400/20 to-blue-400/20 rounded-3xl transform -rotate-1"></div>
</div>
<div className="relative z-10">
<div className="inline-flex items-center px-4 py-2 mb-8 text-sm font-medium text-blue-700 bg-blue-100 rounded-full dark:text-blue-300 dark:bg-blue-900/30">
<span
className="w-2 h-2 bg-blue-500 rounded-full mr-2 animate-pulse"
aria-hidden="true"
></span>
UI Agent in your webpage
</div>
<h1
id="hero-heading"
className="text-5xl lg:text-7xl font-bold mb-8 bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"
>
Web
<br />
AI
</h1>
<p className="text-xl lg:text-2xl text-gray-600 dark:text-gray-300 mb-12 max-w-4xl mx-auto leading-relaxed">
<span className="bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent font-bold">
🪄 CDN
</span>
UI Agent
<br />
/AI
</p>
{/* Try It Now Section - Tab Card */}
<div className="mt-8 mb-6">
<div className="max-w-3xl mx-auto">
<div className="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
{/* Tab Headers */}
<div className="flex border-b border-gray-200 dark:border-gray-700">
<button
onClick={() => setActiveTab('try')}
className={`flex-1 px-4 py-4 text-lg font-medium transition-colors duration-200 ${
activeTab === 'try'
? 'bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/30 dark:to-purple-900/30 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
🚀
</button>
<button
onClick={() => setActiveTab('other')}
className={`flex-1 px-4 py-4 text-lg font-medium transition-colors duration-200 ${
activeTab === 'other'
? 'bg-gradient-to-r from-green-50 to-blue-50 dark:from-green-900/30 dark:to-blue-900/30 text-green-700 dark:text-green-300 border-b-2 border-green-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
🌐
</button>
</div>
{/* Tab Content */}
<div className="p-4">
{activeTab === 'try' && (
<div className="space-y-4">
<div className="relative">
<input
value={task}
onChange={(e) => setTask(e.target.value)}
placeholder="输入您想要 AI 执行的任务..."
className="w-full px-4 py-3 pr-20 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none text-sm mb-0"
data-page-agent-not-interactive
/>
<button
onClick={handleExecute}
disabled={!task.trim()}
className="absolute right-2 top-2 px-5 py-1.5 bg-gradient-to-r from-blue-600 to-purple-600 text-white font-medium rounded-md hover:shadow-md transform hover:scale-105 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none text-sm"
data-page-agent-not-interactive
>
</button>
</div>
</div>
)}
{activeTab === 'other' && (
<div className="grid md:grid-cols-2 gap-6">
{/* 左侧:操作步骤 */}
<div className="space-y-4">
{/* Keyboard Shortcut Hint */}
<div className="bg-blue-50 dark:bg-gray-700 p-4 rounded-lg">
<p className="text-gray-700 dark:text-gray-300 text-sm mb-3">
<span className="font-semibold"> 1:</span>
</p>
<div className="flex items-center justify-center gap-2">
<kbd className="px-2 py-1 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded text-xs font-mono">
Ctrl + Shift + B
</kbd>
<span className="text-gray-500 dark:text-gray-400"></span>
<kbd className="px-2 py-1 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded text-xs font-mono">
+ Shift + B
</kbd>
</div>
</div>
{/* Draggable Bookmarklet */}
<div className="bg-green-50 dark:bg-gray-700 p-4 rounded-lg">
<p className="text-gray-700 dark:text-gray-300 text-sm mb-3">
<span className="font-semibold"> 2:</span>{' '}
</p>
<div
className="flex items-center justify-center gap-2 text-gray-500 dark:text-gray-400"
dangerouslySetInnerHTML={{ __html: injectionA }}
></div>
</div>
{/* Usage Instructions */}
<div className="bg-purple-50 dark:bg-gray-700 p-4 rounded-lg">
<p className="text-gray-700 dark:text-gray-300 text-sm">
<span className="font-semibold"> 3:</span>{' '}
使
</p>
</div>
</div>
{/* 右侧:注意事项 */}
<div className="bg-yellow-50 dark:bg-gray-700 p-4 rounded-lg">
<h4 className="font-semibold text-gray-900 dark:text-white mb-3 text-sm">
</h4>
<ul className="space-y-2 text-sm text-gray-700 dark:text-gray-300">
<li className="flex items-start text-left">
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 flex-shrink-0 "></span>
</li>
<li className="flex items-start text-left">
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 flex-shrink-0 "></span>
</li>
<li className="flex items-start text-left">
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 flex-shrink-0 "></span>
</li>
<li className="flex items-start text-left">
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 flex-shrink-0 "></span>
使{' '}
<Link
href="/docs/introduction/limitations"
className="text-blue-600 dark:text-blue-400 hover:underline"
>
</Link>
</li>
<li className="flex items-start text-left">
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 flex-shrink-0 "></span>
CSP
</li>
<li className="flex items-start text-left">
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 flex-shrink-0 "></span>
使 gpt-4.1-mini qwen
</li>
</ul>
</div>
</div>
)}
</div>
</div>
</div>
</div>
<ul
className="flex flex-wrap justify-center gap-6 text-sm text-gray-500 dark:text-gray-400"
role="list"
>
<li className="flex items-center">
<span
className="w-2 h-2 bg-green-500 rounded-full mr-2"
aria-hidden="true"
></span>
</li>
<li className="flex items-center">
<span
className="w-2 h-2 bg-green-500 rounded-full mr-2"
aria-hidden="true"
></span>
</li>
<li className="flex items-center">
<span
className="w-2 h-2 bg-green-500 rounded-full mr-2"
aria-hidden="true"
></span>
</li>
<li className="flex items-center">
<span
className="w-2 h-2 bg-green-500 rounded-full mr-2"
aria-hidden="true"
></span>
DOM
</li>
</ul>
</div>
</div>
</section>
{/* Features Section */}
<section
className="px-6 py-20 bg-white/50 dark:bg-gray-800/50 backdrop-blur-sm"
aria-labelledby="features-heading"
>
<div className="max-w-7xl mx-auto">
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8" role="list">
{/* Feature 1 */}
<article
className="group p-8 bg-gradient-to-br from-blue-100 to-purple-100 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700"
role="listitem"
>
<div
className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300"
aria-hidden="true"
>
<span className="text-white text-xl">🧠</span>
</div>
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
DOM
</h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
DOM
</p>
</article>
{/* Feature 2 */}
<article
className="group p-8 bg-gradient-to-br from-green-100 to-blue-100 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700"
role="listitem"
>
<div
className="w-12 h-12 bg-gradient-to-br from-purple-500 to-purple-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300"
aria-hidden="true"
>
<span className="text-white text-xl">🔒</span>
</div>
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
</h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
AI
</p>
</article>
{/* Feature 3 */}
<article
className="group p-8 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700"
role="listitem"
>
<div
className="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300"
aria-hidden="true"
>
<span className="text-white text-xl"></span>
</div>
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white"></h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
CDN LLM OpenAI qwen3
</p>
</article>
{/* Feature 4 */}
<article
className="group p-8 bg-gradient-to-br from-orange-100 to-red-100 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700"
role="listitem"
>
<div
className="w-12 h-12 bg-gradient-to-br from-orange-500 to-orange-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300"
aria-hidden="true"
>
<span className="text-white text-xl"></span>
</div>
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
</h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
B端系统
</p>
</article>
</div>
</div>
</section>
{/* Use Cases Section */}
<section className="px-6 py-20" aria-labelledby="use-cases-heading">
<div className="max-w-7xl mx-auto">
<div className="text-center mb-16">
<h2
id="use-cases-heading"
className="text-4xl lg:text-5xl mb-6 text-gray-900 dark:text-white"
>
</h2>
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
AI
</p>
</div>
<div className="grid lg:grid-cols-2 gap-12" role="list">
{/* Use Case 1 */}
<div className="bg-gradient-to-br from-blue-100 to-purple-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl">
<div className="flex items-start space-x-4 h-20">
<div className="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0">
<span className="text-white font-bold">1</span>
</div>
<div>
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
</h3>
<p className="text-gray-600 dark:text-gray-300">
Agent"请先点击设置按钮然后点击..."
</p>
</div>
</div>
</div>
{/* Use Case 2 */}
<div className="bg-gradient-to-br from-green-100 to-blue-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl">
<div className="flex items-start space-x-4 h-20">
<div className="w-10 h-10 bg-green-500 rounded-lg flex items-center justify-center flex-shrink-0">
<span className="text-white font-bold">2</span>
</div>
<div>
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
/
</h3>
<p className="text-gray-600 dark:text-gray-300">
Agent B
</p>
</div>
</div>
</div>
{/* Use Case 3 */}
<div className="bg-gradient-to-br from-purple-100 to-pink-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl">
<div className="flex items-start space-x-4 h-20">
<div className="w-10 h-10 bg-purple-500 rounded-lg flex items-center justify-center flex-shrink-0">
<span className="text-white font-bold">3</span>
</div>
<div>
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
</h3>
<p className="text-gray-600 dark:text-gray-300">
AI演示"如何提交报销申请"
</p>
</div>
</div>
</div>
{/* Use Case 4 */}
<div className="bg-gradient-to-br from-orange-100 to-red-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl">
<div className="flex items-start space-x-4 h-20">
<div className="w-10 h-10 bg-orange-500 rounded-lg flex items-center justify-center flex-shrink-0">
<span className="text-white font-bold">4</span>
</div>
<div>
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
</h3>
<p className="text-gray-600 dark:text-gray-300">
</p>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<Footer />
</div>
)
}

158
pages/router.tsx Normal file
View File

@@ -0,0 +1,158 @@
import { Route, Switch } from 'wouter'
import DocsLayout from './components/DocsLayout'
import Header from './components/Header'
// Features pages
import CustomTools from './docs/features/custom-tools/page'
import DataMasking from './docs/features/data-masking/page'
import KnowledgeInjection from './docs/features/knowledge-injection/page'
import ModelIntegration from './docs/features/model-integration/page'
import SecurityPermissions from './docs/features/security-permissions/page'
import ApiReference from './docs/integration/api-reference/page'
import BestPractices from './docs/integration/best-practices/page'
// Integration pages
import CdnSetup from './docs/integration/cdn-setup/page'
import Configuration from './docs/integration/configuration/page'
import ThirdPartyAgent from './docs/integration/third-party-agent/page'
import Limitations from './docs/introduction/limitations/page'
// Introduction pages
import Overview from './docs/introduction/overview/page'
import QuickStart from './docs/introduction/quick-start/page'
import HomePage from './page'
export default function Router() {
return (
<Switch>
{/* Home page */}
<Route path="/" component={HomePage} />
{/* Documentation pages with layout */}
<Route path="/docs/introduction/overview">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<Overview />
</DocsLayout>
</div>
</Route>
<Route path="/docs/introduction/quick-start">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<QuickStart />
</DocsLayout>
</div>
</Route>
<Route path="/docs/introduction/limitations">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<Limitations />
</DocsLayout>
</div>
</Route>
<Route path="/docs/features/security-permissions">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<SecurityPermissions />
</DocsLayout>
</div>
</Route>
<Route path="/docs/features/custom-tools">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<CustomTools />
</DocsLayout>
</div>
</Route>
<Route path="/docs/features/data-masking">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<DataMasking />
</DocsLayout>
</div>
</Route>
<Route path="/docs/features/knowledge-injection">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<KnowledgeInjection />
</DocsLayout>
</div>
</Route>
<Route path="/docs/features/model-integration">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<ModelIntegration />
</DocsLayout>
</div>
</Route>
<Route path="/docs/integration/cdn-setup">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<CdnSetup />
</DocsLayout>
</div>
</Route>
<Route path="/docs/integration/configuration">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<Configuration />
</DocsLayout>
</div>
</Route>
<Route path="/docs/integration/api-reference">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<ApiReference />
</DocsLayout>
</div>
</Route>
<Route path="/docs/integration/best-practices">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<BestPractices />
</DocsLayout>
</div>
</Route>
<Route path="/docs/integration/third-party-agent">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<ThirdPartyAgent />
</DocsLayout>
</div>
</Route>
{/* 404 page */}
<Route>
<div className="min-h-screen bg-white dark:bg-gray-900 flex items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4 text-gray-900 dark:text-white">404</h1>
<p className="text-xl text-gray-600 dark:text-gray-300"></p>
</div>
</div>
</Route>
</Switch>
)
}

209
pages/test-pages/README.md Normal file
View File

@@ -0,0 +1,209 @@
# Page Use Agent 测试页面
这个目录包含了一系列专门设计的测试页面,用于验证 Page Use Agent 的各种能力。每个页面都模拟了真实 Web 应用中的常见交互模式和边界情况。
## 测试页面列表
### 1. 表单测试页面 (`/test-pages/form`)
**测试目标:** 表单填写、验证和提交能力
**包含功能:**
- 各种输入类型文本、邮箱、密码、数字、日期、电话、URL
- 下拉选择框和复选框
- 实时表单验证
- 异步提交和错误处理
- 重置表单功能
**测试任务示例:**
- 填写完整的用户注册表单并提交
- 故意输入错误信息触发验证错误
- 测试密码确认功能
- 尝试提交空表单查看错误提示
### 2. 导航测试页面 (`/test-pages/navigation`)
**测试目标:** 复杂导航和交互元素处理
**包含功能:**
- 顶部导航栏和下拉菜单
- 面包屑导航
- 标签页切换
- 模态框弹窗
- 通知系统
- 用户菜单
**测试任务示例:**
- 点击产品下拉菜单选择不同选项
- 切换不同的标签页查看内容
- 打开和关闭模态框
- 点击面包屑导航
- 添加新通知并标记为已读
### 3. 列表测试页面 (`/test-pages/list`)
**测试目标:** 列表操作、搜索、过滤和分页
**包含功能:**
- 产品列表展示(网格和列表视图)
- 搜索功能
- 类别过滤
- 排序功能
- 分页导航
- 加载状态和骨架屏
**测试任务示例:**
- 搜索特定产品名称
- 按价格排序产品列表
- 切换网格和列表视图
- 使用分页浏览不同页面
- 按类别过滤产品
### 4. 复杂交互测试页面 (`/test-pages/complex`)
**测试目标:** 多步骤流程和状态管理
**包含功能:**
- 购物车管理(添加、删除、修改数量)
- 多步骤向导流程
- 步骤验证和导航
- 订单确认流程
- 异步提交处理
**测试任务示例:**
- 完成完整的购买流程
- 在向导中前进和后退
- 修改购物车中的商品数量
- 添加新商品到购物车
- 提交订单并处理可能的错误
### 5. 错误处理测试页面 (`/test-pages/errors`)
**测试目标:** 错误识别和重试机制
**包含功能:**
- 网络连接错误模拟
- 表单验证错误
- 权限不足错误
- 请求超时错误
- 服务器内部错误
- 文件上传错误处理
**测试任务示例:**
- 触发网络错误并重试
- 提交不完整表单查看验证错误
- 测试权限验证(用户名需为"admin"
- 上传超大文件触发错误
- 处理各种错误场景的重试逻辑
### 6. 异步操作测试页面 (`/test-pages/async`)
**测试目标:** 等待和异步操作处理
**包含功能:**
- 文件上传进度条
- 实时数据更新
- 数据加载骨架屏
- 长时间运行任务
- 进度跟踪和日志显示
**测试任务示例:**
- 启动文件上传并等待完成
- 开启实时数据更新功能
- 加载数据并等待所有项目完成
- 执行长时间任务并监控进度
- 处理上传失败的重试
## 测试任务集合
### 基础操作测试
1. **导航测试**
- 前往表单测试页面
- 返回测试页面首页
- 前往导航测试页面
2. **表单填写测试**
- 填写用户注册表单的所有必填字段
- 提交表单并等待结果
- 重置表单并重新填写
3. **搜索和过滤测试**
- 在列表页面搜索"Apple"
- 按价格降序排列产品
- 过滤显示"手机"类别的产品
### 中级交互测试
4. **购物流程测试**
- 前往复杂交互页面
- 添加商品到购物车
- 完成多步骤购买流程
- 填写个人信息、地址和支付信息
- 提交订单
5. **导航和菜单测试**
- 点击产品下拉菜单选择"手机"
- 切换到"订单管理"标签页
- 打开模态框并关闭
- 添加新的面包屑导航
6. **异步操作测试**
- 启动文件上传
- 开启实时数据更新
- 执行长时间任务并等待完成
### 高级错误处理测试
7. **错误恢复测试**
- 触发网络连接错误
- 重试失败的操作
- 处理表单验证错误
- 测试权限验证(用户名输入"admin"
8. **边界情况测试**
- 提交空表单查看错误
- 上传不支持的文件类型
- 在向导中跳过必填步骤
- 处理超时错误
### 综合场景测试
9. **完整用户流程**
- 浏览产品列表
- 搜索并过滤产品
- 添加产品到购物车
- 完成购买流程
- 处理可能出现的错误
10. **压力和边界测试**
- 快速连续点击按钮
- 在加载过程中尝试其他操作
- 测试各种错误恢复场景
- 验证所有异步操作的完成
## 使用说明
### 对于 Agent 开发者
- 每个页面都包含了详细的状态指示器和反馈信息
- 错误信息清晰明确,便于 Agent 理解和处理
- 异步操作都有明确的完成标志
- 所有交互元素都有适当的可访问性标记
### 对于测试人员
- 可以按照测试任务逐一验证 Agent 的能力
- 每个页面都是独立的,可以单独测试
- 包含了各种真实场景的模拟
- 错误场景是随机的,确保测试的真实性
### 技术特性
- 使用 React + TypeScript 构建
- 响应式设计,支持不同屏幕尺寸
- 深色模式支持
- 无需外部依赖,完全自包含
- 模拟真实的网络延迟和错误
## 扩展建议
如需添加新的测试场景,建议考虑以下方面:
- 特定行业的业务流程
- 更复杂的数据可视化交互
- 多媒体内容处理
- 实时协作功能
- 移动端特有的交互模式
每个新页面都应该:
- 有明确的测试目标
- 包含多种难度级别的任务
- 提供清晰的状态反馈
- 模拟真实的用户场景

View File

@@ -0,0 +1,520 @@
import { useState, useEffect } from 'react'
import { Link } from 'wouter'
interface UploadProgress {
id: string
name: string
progress: number
status: 'uploading' | 'completed' | 'error'
speed: string
timeRemaining: string
}
interface DataItem {
id: number
title: string
content: string
timestamp: string
status: 'loading' | 'loaded' | 'error'
}
export default function AsyncTestPage() {
const [uploads, setUploads] = useState<UploadProgress[]>([])
const [dataItems, setDataItems] = useState<DataItem[]>([])
const [isLoadingData, setIsLoadingData] = useState(false)
const [realTimeData, setRealTimeData] = useState<string[]>([])
const [isRealTimeActive, setIsRealTimeActive] = useState(false)
const [longRunningTask, setLongRunningTask] = useState<{
isRunning: boolean
progress: number
currentStep: string
logs: string[]
}>({
isRunning: false,
progress: 0,
currentStep: '',
logs: []
})
// 模拟实时数据更新
useEffect(() => {
let interval: NodeJS.Timeout
if (isRealTimeActive) {
interval = setInterval(() => {
const newData = `数据更新 ${new Date().toLocaleTimeString()}: ${Math.floor(Math.random() * 1000)}`
setRealTimeData(prev => [newData, ...prev.slice(0, 9)]) // 保持最新10条
}, 2000)
}
return () => {
if (interval) clearInterval(interval)
}
}, [isRealTimeActive])
// 模拟文件上传
const simulateFileUpload = (fileName: string) => {
const uploadId = Date.now().toString()
const newUpload: UploadProgress = {
id: uploadId,
name: fileName,
progress: 0,
status: 'uploading',
speed: '0 KB/s',
timeRemaining: '计算中...'
}
setUploads(prev => [...prev, newUpload])
// 模拟上传进度
const interval = setInterval(() => {
setUploads(prev => prev.map(upload => {
if (upload.id === uploadId) {
const newProgress = Math.min(upload.progress + Math.random() * 15, 100)
const speed = `${(Math.random() * 500 + 100).toFixed(0)} KB/s`
const timeRemaining = newProgress >= 100 ? '完成' : `${Math.ceil((100 - newProgress) / 10)}`
// 模拟随机失败
if (newProgress > 50 && Math.random() < 0.1) {
clearInterval(interval)
return {
...upload,
status: 'error' as const,
speed: '0 KB/s',
timeRemaining: '失败'
}
}
if (newProgress >= 100) {
clearInterval(interval)
return {
...upload,
progress: 100,
status: 'completed' as const,
speed,
timeRemaining
}
}
return {
...upload,
progress: newProgress,
speed,
timeRemaining
}
}
return upload
}))
}, 500)
}
// 模拟数据加载
const loadData = async () => {
setIsLoadingData(true)
setDataItems([])
// 创建骨架屏数据
const skeletonItems: DataItem[] = Array.from({ length: 6 }, (_, i) => ({
id: i,
title: '',
content: '',
timestamp: '',
status: 'loading'
}))
setDataItems(skeletonItems)
// 逐个加载数据项
for (let i = 0; i < 6; i++) {
await new Promise(resolve => setTimeout(resolve, 800 + Math.random() * 1000))
setDataItems(prev => prev.map(item => {
if (item.id === i) {
// 模拟随机加载失败
if (Math.random() < 0.15) {
return {
...item,
status: 'error',
title: '加载失败',
content: '数据加载失败,请重试'
}
}
return {
...item,
status: 'loaded',
title: `数据项 ${i + 1}`,
content: `这是第 ${i + 1} 个数据项的内容,包含了一些示例文本用于展示加载效果。`,
timestamp: new Date().toLocaleString()
}
}
return item
}))
}
setIsLoadingData(false)
}
// 模拟长时间运行的任务
const startLongRunningTask = async () => {
setLongRunningTask({
isRunning: true,
progress: 0,
currentStep: '初始化任务...',
logs: ['任务开始']
})
const steps = [
{ name: '初始化任务...', duration: 2000 },
{ name: '连接服务器...', duration: 1500 },
{ name: '验证权限...', duration: 1000 },
{ name: '下载数据...', duration: 3000 },
{ name: '处理数据...', duration: 2500 },
{ name: '生成报告...', duration: 2000 },
{ name: '保存结果...', duration: 1000 },
{ name: '清理资源...', duration: 500 }
]
for (let i = 0; i < steps.length; i++) {
const step = steps[i]
setLongRunningTask(prev => ({
...prev,
currentStep: step.name,
logs: [...prev.logs, `开始: ${step.name}`]
}))
// 模拟步骤执行时间
const startTime = Date.now()
while (Date.now() - startTime < step.duration) {
await new Promise(resolve => setTimeout(resolve, 100))
const elapsed = Date.now() - startTime
const stepProgress = Math.min((elapsed / step.duration) * 100, 100)
const totalProgress = ((i + stepProgress / 100) / steps.length) * 100
setLongRunningTask(prev => ({
...prev,
progress: totalProgress
}))
}
setLongRunningTask(prev => ({
...prev,
logs: [...prev.logs, `完成: ${step.name}`]
}))
// 模拟随机失败
if (i === 3 && Math.random() < 0.2) {
setLongRunningTask(prev => ({
...prev,
isRunning: false,
currentStep: '任务失败',
logs: [...prev.logs, '错误: 数据下载失败,请重试']
}))
return
}
}
setLongRunningTask(prev => ({
...prev,
isRunning: false,
progress: 100,
currentStep: '任务完成',
logs: [...prev.logs, '任务成功完成!']
}))
}
const clearUploads = () => {
setUploads([])
}
const retryFailedUpload = (uploadId: string) => {
const failedUpload = uploads.find(u => u.id === uploadId)
if (failedUpload) {
setUploads(prev => prev.filter(u => u.id !== uploadId))
simulateFileUpload(failedUpload.name)
}
}
const retryDataLoad = (itemId: number) => {
setDataItems(prev => prev.map(item => {
if (item.id === itemId) {
return { ...item, status: 'loading', title: '', content: '', timestamp: '' }
}
return item
}))
setTimeout(() => {
setDataItems(prev => prev.map(item => {
if (item.id === itemId) {
return {
...item,
status: 'loaded',
title: `数据项 ${itemId + 1}`,
content: `这是重新加载的第 ${itemId + 1} 个数据项的内容。`,
timestamp: new Date().toLocaleString()
}
}
return item
}))
}, 1000)
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-6xl mx-auto px-4">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
</h1>
<p className="text-gray-600 dark:text-gray-300">
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* 文件上传进度 */}
<div className="space-y-6">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
</h2>
<div className="space-x-2">
<button
type="button"
onClick={() => simulateFileUpload(`文件_${Date.now()}.pdf`)}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors text-sm"
>
</button>
<button
type="button"
onClick={clearUploads}
className="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-md transition-colors text-sm"
>
</button>
</div>
</div>
<div className="space-y-4">
{uploads.length === 0 ? (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
"开始上传"
</div>
) : (
uploads.map(upload => (
<div key={upload.id} className="border border-gray-200 dark:border-gray-600 rounded-lg p-4">
<div className="flex justify-between items-center mb-2">
<span className="font-medium text-gray-900 dark:text-white">
{upload.name}
</span>
<span className={`text-sm ${
upload.status === 'completed' ? 'text-green-600 dark:text-green-400' :
upload.status === 'error' ? 'text-red-600 dark:text-red-400' :
'text-blue-600 dark:text-blue-400'
}`}>
{upload.status === 'completed' ? '✓ 完成' :
upload.status === 'error' ? '✗ 失败' :
'上传中...'}
</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
upload.status === 'completed' ? 'bg-green-500' :
upload.status === 'error' ? 'bg-red-500' :
'bg-blue-500'
}`}
style={{ width: `${upload.progress}%` }}
/>
</div>
<div className="flex justify-between text-sm text-gray-600 dark:text-gray-300">
<span>{upload.progress.toFixed(1)}%</span>
<span>{upload.speed}</span>
<span>{upload.timeRemaining}</span>
</div>
{upload.status === 'error' && (
<button
type="button"
onClick={() => retryFailedUpload(upload.id)}
className="mt-2 px-3 py-1 bg-red-600 hover:bg-red-700 text-white rounded text-sm transition-colors"
>
</button>
)}
</div>
))
)}
</div>
</div>
{/* 实时数据更新 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
</h2>
<button
type="button"
onClick={() => setIsRealTimeActive(!isRealTimeActive)}
className={`px-4 py-2 rounded-md transition-colors text-sm ${
isRealTimeActive
? 'bg-red-600 hover:bg-red-700 text-white'
: 'bg-green-600 hover:bg-green-700 text-white'
}`}
>
{isRealTimeActive ? '停止更新' : '开始更新'}
</button>
</div>
<div className="space-y-2 max-h-64 overflow-y-auto">
{realTimeData.length === 0 ? (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
"开始更新"
</div>
) : (
realTimeData.map((data, index) => (
<div
key={index}
className={`p-3 rounded-lg border transition-all duration-300 ${
index === 0
? 'bg-blue-50 dark:bg-blue-900 border-blue-200 dark:border-blue-700'
: 'bg-gray-50 dark:bg-gray-700 border-gray-200 dark:border-gray-600'
}`}
>
<span className="text-sm text-gray-900 dark:text-white">
{data}
</span>
</div>
))
)}
</div>
</div>
</div>
{/* 数据加载和长时间任务 */}
<div className="space-y-6">
{/* 数据加载骨架屏 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
</h2>
<button
type="button"
onClick={loadData}
disabled={isLoadingData}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-md transition-colors text-sm"
>
{isLoadingData ? '加载中...' : '加载数据'}
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{dataItems.map(item => (
<div key={item.id} className="border border-gray-200 dark:border-gray-600 rounded-lg p-4">
{item.status === 'loading' ? (
<div className="animate-pulse">
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-3/4 mb-2"></div>
<div className="h-3 bg-gray-300 dark:bg-gray-600 rounded w-full mb-1"></div>
<div className="h-3 bg-gray-300 dark:bg-gray-600 rounded w-2/3"></div>
</div>
) : item.status === 'error' ? (
<div>
<h3 className="font-medium text-red-600 dark:text-red-400 mb-2">
{item.title}
</h3>
<p className="text-sm text-red-500 dark:text-red-400 mb-2">
{item.content}
</p>
<button
type="button"
onClick={() => retryDataLoad(item.id)}
className="px-3 py-1 bg-red-600 hover:bg-red-700 text-white rounded text-sm transition-colors"
>
</button>
</div>
) : (
<div>
<h3 className="font-medium text-gray-900 dark:text-white mb-2">
{item.title}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
{item.content}
</p>
<span className="text-xs text-gray-500 dark:text-gray-400">
{item.timestamp}
</span>
</div>
)}
</div>
))}
</div>
</div>
{/* 长时间运行任务 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
</h2>
<button
type="button"
onClick={startLongRunningTask}
disabled={longRunningTask.isRunning}
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-purple-400 text-white rounded-md transition-colors text-sm"
>
{longRunningTask.isRunning ? '执行中...' : '开始任务'}
</button>
</div>
{longRunningTask.progress > 0 && (
<div className="mb-4">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-gray-900 dark:text-white">
{longRunningTask.currentStep}
</span>
<span className="text-sm text-gray-600 dark:text-gray-300">
{longRunningTask.progress.toFixed(1)}%
</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
className="bg-purple-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${longRunningTask.progress}%` }}
/>
</div>
</div>
)}
{longRunningTask.logs.length > 0 && (
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 max-h-48 overflow-y-auto">
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-2">
:
</h4>
<div className="space-y-1">
{longRunningTask.logs.map((log, index) => (
<div key={index} className="text-sm text-gray-600 dark:text-gray-300 font-mono">
{log}
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
{/* 返回链接 */}
<div className="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
<Link href="/" className="text-blue-600 hover:text-blue-500 dark:text-blue-400">
</Link>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,532 @@
import { useState } from 'react'
import { Link } from 'wouter'
interface CartItem {
id: number
name: string
price: number
quantity: number
image: string
}
interface WizardStep {
id: number
title: string
description: string
completed: boolean
}
export default function ComplexTestPage() {
const [currentStep, setCurrentStep] = useState(1)
const [cartItems, setCartItems] = useState<CartItem[]>([
{ id: 1, name: 'iPhone 15 Pro', price: 7999, quantity: 1, image: 'https://picsum.photos/100/100?random=1' },
{ id: 2, name: 'MacBook Air', price: 8999, quantity: 1, image: 'https://picsum.photos/100/100?random=2' }
])
const [wizardData, setWizardData] = useState({
personalInfo: { name: '', email: '', phone: '' },
address: { street: '', city: '', zipCode: '' },
payment: { cardNumber: '', expiryDate: '', cvv: '' }
})
const [wizardSteps, setWizardSteps] = useState<WizardStep[]>([
{ id: 1, title: '个人信息', description: '填写基本信息', completed: false },
{ id: 2, title: '收货地址', description: '填写收货地址', completed: false },
{ id: 3, title: '支付方式', description: '选择支付方式', completed: false },
{ id: 4, title: '确认订单', description: '确认订单信息', completed: false }
])
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
const [isProcessing, setIsProcessing] = useState(false)
const [orderComplete, setOrderComplete] = useState(false)
// 购物车操作
const updateQuantity = (id: number, newQuantity: number) => {
if (newQuantity <= 0) {
removeItem(id)
return
}
setCartItems(prev =>
prev.map(item =>
item.id === id ? { ...item, quantity: newQuantity } : item
)
)
}
const removeItem = (id: number) => {
setCartItems(prev => prev.filter(item => item.id !== id))
}
const addItem = () => {
const newItem: CartItem = {
id: Date.now(),
name: `新产品 ${cartItems.length + 1}`,
price: Math.floor(Math.random() * 5000) + 1000,
quantity: 1,
image: `https://picsum.photos/100/100?random=${Date.now()}`
}
setCartItems(prev => [...prev, newItem])
}
const getTotalPrice = () => {
return cartItems.reduce((total, item) => total + item.price * item.quantity, 0)
}
// 向导步骤验证
const validateStep = (step: number): boolean => {
switch (step) {
case 1:
return !!(wizardData.personalInfo.name && wizardData.personalInfo.email && wizardData.personalInfo.phone)
case 2:
return !!(wizardData.address.street && wizardData.address.city && wizardData.address.zipCode)
case 3:
return !!(wizardData.payment.cardNumber && wizardData.payment.expiryDate && wizardData.payment.cvv)
default:
return true
}
}
const goToStep = (step: number) => {
// 验证当前步骤
if (step > currentStep && !validateStep(currentStep)) {
alert('请完成当前步骤的必填信息')
return
}
// 更新步骤完成状态
if (step > currentStep) {
setWizardSteps(prev =>
prev.map(s =>
s.id === currentStep ? { ...s, completed: true } : s
)
)
}
setCurrentStep(step)
}
const handleInputChange = (section: string, field: string, value: string) => {
setWizardData(prev => ({
...prev,
[section]: {
...prev[section as keyof typeof prev],
[field]: value
}
}))
}
const handleSubmitOrder = async () => {
setIsProcessing(true)
// 模拟处理时间
await new Promise(resolve => setTimeout(resolve, 3000))
// 模拟随机失败
if (Math.random() < 0.2) {
setIsProcessing(false)
alert('订单提交失败,请重试')
return
}
setIsProcessing(false)
setOrderComplete(true)
setShowConfirmDialog(false)
}
const resetWizard = () => {
setCurrentStep(1)
setWizardData({
personalInfo: { name: '', email: '', phone: '' },
address: { street: '', city: '', zipCode: '' },
payment: { cardNumber: '', expiryDate: '', cvv: '' }
})
setWizardSteps(prev => prev.map(s => ({ ...s, completed: false })))
setOrderComplete(false)
setShowConfirmDialog(false)
}
if (orderComplete) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
<div className="max-w-md mx-auto text-center">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8">
<div className="text-6xl mb-4">🎉</div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
</h2>
<p className="text-gray-600 dark:text-gray-300 mb-6">
</p>
<div className="space-y-3">
<button
type="button"
onClick={resetWizard}
className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition-colors"
>
</button>
<Link href="/test-pages" className="block w-full bg-gray-600 hover:bg-gray-700 text-white py-2 px-4 rounded-md transition-colors text-center">
</Link>
</div>
</div>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-6xl mx-auto px-4">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
</h1>
<p className="text-gray-600 dark:text-gray-300">
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* 购物车区域 */}
<div className="lg:col-span-1">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 sticky top-8">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
({cartItems.length})
</h3>
<div className="space-y-4 mb-6">
{cartItems.map(item => (
<div key={item.id} className="flex items-center space-x-3 p-3 border border-gray-200 dark:border-gray-600 rounded-lg">
<img
src={item.image}
alt={item.name}
className="w-12 h-12 object-cover rounded"
/>
<div className="flex-1 min-w-0">
<h4 className="text-sm font-medium text-gray-900 dark:text-white truncate">
{item.name}
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
¥{item.price.toLocaleString()}
</p>
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => updateQuantity(item.id, item.quantity - 1)}
className="w-6 h-6 flex items-center justify-center bg-gray-200 dark:bg-gray-600 rounded text-sm hover:bg-gray-300 dark:hover:bg-gray-500"
>
-
</button>
<span className="text-sm font-medium w-8 text-center">
{item.quantity}
</span>
<button
onClick={() => updateQuantity(item.id, item.quantity + 1)}
className="w-6 h-6 flex items-center justify-center bg-gray-200 dark:bg-gray-600 rounded text-sm hover:bg-gray-300 dark:hover:bg-gray-500"
>
+
</button>
<button
onClick={() => removeItem(item.id)}
className="w-6 h-6 flex items-center justify-center bg-red-500 text-white rounded text-sm hover:bg-red-600"
>
×
</button>
</div>
</div>
))}
</div>
<button
onClick={addItem}
className="w-full mb-4 py-2 px-4 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg text-gray-500 dark:text-gray-400 hover:border-blue-500 hover:text-blue-500 transition-colors"
>
+
</button>
<div className="border-t border-gray-200 dark:border-gray-600 pt-4">
<div className="flex justify-between items-center text-lg font-semibold text-gray-900 dark:text-white">
<span>:</span>
<span>¥{getTotalPrice().toLocaleString()}</span>
</div>
</div>
</div>
</div>
{/* 向导区域 */}
<div className="lg:col-span-2">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
{/* 步骤指示器 */}
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-600">
<div className="flex items-center justify-between">
{wizardSteps.map((step, index) => (
<div key={step.id} className="flex items-center">
<button
onClick={() => goToStep(step.id)}
className={`w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium transition-colors ${
step.completed
? 'bg-green-500 text-white'
: step.id === currentStep
? 'bg-blue-500 text-white'
: 'bg-gray-200 dark:bg-gray-600 text-gray-500 dark:text-gray-400'
}`}
>
{step.completed ? '✓' : step.id}
</button>
{index < wizardSteps.length - 1 && (
<div className={`w-16 h-1 mx-2 ${
step.completed ? 'bg-green-500' : 'bg-gray-200 dark:bg-gray-600'
}`} />
)}
</div>
))}
</div>
<div className="mt-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{wizardSteps[currentStep - 1].title}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{wizardSteps[currentStep - 1].description}
</p>
</div>
</div>
{/* 步骤内容 */}
<div className="p-6">
{currentStep === 1 && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="text"
value={wizardData.personalInfo.name}
onChange={(e) => handleInputChange('personalInfo', 'name', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入您的姓名"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="email"
value={wizardData.personalInfo.email}
onChange={(e) => handleInputChange('personalInfo', 'email', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入您的邮箱"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="tel"
value={wizardData.personalInfo.phone}
onChange={(e) => handleInputChange('personalInfo', 'phone', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入您的手机号"
/>
</div>
</div>
)}
{currentStep === 2 && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="text"
value={wizardData.address.street}
onChange={(e) => handleInputChange('address', 'street', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入详细地址"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="text"
value={wizardData.address.city}
onChange={(e) => handleInputChange('address', 'city', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入城市"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="text"
value={wizardData.address.zipCode}
onChange={(e) => handleInputChange('address', 'zipCode', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入邮政编码"
/>
</div>
</div>
)}
{currentStep === 3 && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="text"
value={wizardData.payment.cardNumber}
onChange={(e) => handleInputChange('payment', 'cardNumber', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入银行卡号"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="text"
value={wizardData.payment.expiryDate}
onChange={(e) => handleInputChange('payment', 'expiryDate', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="MM/YY"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
CVV *
</label>
<input
type="text"
value={wizardData.payment.cvv}
onChange={(e) => handleInputChange('payment', 'cvv', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="CVV"
/>
</div>
</div>
</div>
)}
{currentStep === 4 && (
<div className="space-y-6">
<div>
<h4 className="text-lg font-medium text-gray-900 dark:text-white mb-4"></h4>
<div className="space-y-4">
<div className="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<h5 className="font-medium text-gray-900 dark:text-white mb-2"></h5>
<p className="text-sm text-gray-600 dark:text-gray-300">
{wizardData.personalInfo.name} | {wizardData.personalInfo.email} | {wizardData.personalInfo.phone}
</p>
</div>
<div className="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<h5 className="font-medium text-gray-900 dark:text-white mb-2"></h5>
<p className="text-sm text-gray-600 dark:text-gray-300">
{wizardData.address.street}, {wizardData.address.city} {wizardData.address.zipCode}
</p>
</div>
<div className="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<h5 className="font-medium text-gray-900 dark:text-white mb-2"></h5>
<p className="text-sm text-gray-600 dark:text-gray-300">
**** **** **** {wizardData.payment.cardNumber.slice(-4)}
</p>
</div>
</div>
</div>
</div>
)}
</div>
{/* 导航按钮 */}
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-600 flex justify-between">
<button
onClick={() => goToStep(currentStep - 1)}
disabled={currentStep === 1}
className="px-4 py-2 text-gray-600 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
</button>
{currentStep < 4 ? (
<button
onClick={() => goToStep(currentStep + 1)}
disabled={!validateStep(currentStep)}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-md transition-colors"
>
</button>
) : (
<button
onClick={() => setShowConfirmDialog(true)}
disabled={cartItems.length === 0}
className="px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-green-400 text-white rounded-md transition-colors"
>
</button>
)}
</div>
</div>
</div>
</div>
{/* 确认对话框 */}
{showConfirmDialog && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-6">
¥{getTotalPrice().toLocaleString()}
</p>
<div className="flex justify-end space-x-3">
<button
onClick={() => setShowConfirmDialog(false)}
disabled={isProcessing}
className="px-4 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 transition-colors"
>
</button>
<button
onClick={handleSubmitOrder}
disabled={isProcessing}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-md transition-colors flex items-center"
>
{isProcessing ? (
<>
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
...
</>
) : (
'确认提交'
)}
</button>
</div>
</div>
</div>
)}
{/* 返回链接 */}
<div className="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
<Link href="/" className="text-blue-600 hover:text-blue-500 dark:text-blue-400">
</Link>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,433 @@
import { useState } from 'react'
import { Link } from 'wouter'
interface ErrorScenario {
id: string
title: string
description: string
type: 'network' | 'validation' | 'permission' | 'timeout' | 'server'
}
export default function ErrorTestPage() {
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
const [retryCount, setRetryCount] = useState(0)
const [formData, setFormData] = useState({
username: '',
password: '',
email: '',
file: null as File | null
})
const errorScenarios: ErrorScenario[] = [
{
id: 'network-error',
title: '网络连接错误',
description: '模拟网络连接失败,测试重试机制',
type: 'network'
},
{
id: 'validation-error',
title: '表单验证错误',
description: '模拟表单验证失败,测试错误提示',
type: 'validation'
},
{
id: 'permission-error',
title: '权限不足错误',
description: '模拟权限验证失败,测试权限处理',
type: 'permission'
},
{
id: 'timeout-error',
title: '请求超时错误',
description: '模拟请求超时,测试超时处理',
type: 'timeout'
},
{
id: 'server-error',
title: '服务器内部错误',
description: '模拟服务器500错误测试错误恢复',
type: 'server'
}
]
const simulateError = async (scenario: ErrorScenario): Promise<void> => {
setIsLoading(true)
setError(null)
setSuccess(null)
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000))
switch (scenario.type) {
case 'network':
// 70% 概率失败
if (Math.random() < 0.7) {
throw new Error('网络连接失败:无法连接到服务器,请检查您的网络连接')
}
break
case 'validation':
// 检查表单数据
if (!formData.username || formData.username.length < 3) {
throw new Error('用户名验证失败用户名至少需要3个字符')
}
if (!formData.password || formData.password.length < 6) {
throw new Error('密码验证失败密码至少需要6个字符')
}
if (!formData.email?.includes('@')) {
throw new Error('邮箱验证失败:请输入有效的邮箱地址')
}
break
case 'permission':
// 模拟权限检查
if (formData.username !== 'admin') {
throw new Error('权限不足:您没有执行此操作的权限,请联系管理员')
}
break
case 'timeout':
// 模拟超时
await new Promise(resolve => setTimeout(resolve, 8000))
throw new Error('请求超时:服务器响应时间过长,请稍后重试')
case 'server':
// 50% 概率服务器错误
if (Math.random() < 0.5) {
throw new Error('服务器内部错误:服务器遇到了一个错误,请稍后重试')
}
break
default:
throw new Error('未知错误:发生了未预期的错误')
}
// 成功情况
return Promise.resolve()
}
const handleScenarioTest = async (scenario: ErrorScenario) => {
try {
await simulateError(scenario)
setSuccess(`${scenario.title} 测试成功完成!`)
setRetryCount(0)
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '未知错误'
setError(errorMessage)
setRetryCount(prev => prev + 1)
} finally {
setIsLoading(false)
}
}
const handleRetry = async (scenario: ErrorScenario) => {
if (retryCount >= 3) {
setError('重试次数已达上限,请稍后再试或联系技术支持')
return
}
await handleScenarioTest(scenario)
}
const handleFileUpload = async () => {
if (!formData.file) {
setError('请选择要上传的文件')
return
}
setIsLoading(true)
setError(null)
setSuccess(null)
try {
// 模拟文件大小检查
if (formData.file.size > 5 * 1024 * 1024) {
throw new Error('文件上传失败文件大小不能超过5MB')
}
// 模拟文件类型检查
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf']
if (!allowedTypes.includes(formData.file.type)) {
throw new Error('文件上传失败不支持的文件类型请上传图片或PDF文件')
}
// 模拟上传过程
await new Promise(resolve => setTimeout(resolve, 2000))
// 模拟随机失败
if (Math.random() < 0.3) {
throw new Error('文件上传失败:上传过程中发生错误,请重试')
}
setSuccess('文件上传成功!')
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '文件上传失败'
setError(errorMessage)
} finally {
setIsLoading(false)
}
}
const clearMessages = () => {
setError(null)
setSuccess(null)
setRetryCount(0)
}
const getErrorIcon = (type: string) => {
switch (type) {
case 'network': return '🌐'
case 'validation': return '⚠️'
case 'permission': return '🔒'
case 'timeout': return '⏰'
case 'server': return '🔧'
default: return '❌'
}
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-4xl mx-auto px-4">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
</h1>
<p className="text-gray-600 dark:text-gray-300">
Agent
</p>
</div>
{/* 全局消息显示 */}
{(error || success) && (
<div className="mb-8">
{error && (
<div className="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded-lg p-4 mb-4">
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3 flex-1">
<h3 className="text-sm font-medium text-red-800 dark:text-red-200">
</h3>
<p className="mt-1 text-sm text-red-700 dark:text-red-300">
{error}
</p>
{retryCount > 0 && (
<p className="mt-2 text-xs text-red-600 dark:text-red-400">
{retryCount} {retryCount >= 3 && '(已达最大重试次数)'}
</p>
)}
</div>
<button
onClick={clearMessages}
className="ml-3 text-red-400 hover:text-red-600 dark:hover:text-red-300"
>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
)}
{success && (
<div className="bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-lg p-4 mb-4">
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3 flex-1">
<h3 className="text-sm font-medium text-green-800 dark:text-green-200">
</h3>
<p className="mt-1 text-sm text-green-700 dark:text-green-300">
{success}
</p>
</div>
<button
onClick={clearMessages}
className="ml-3 text-green-400 hover:text-green-600 dark:hover:text-green-300"
>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
)}
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* 错误场景测试 */}
<div className="space-y-6">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
</h2>
{errorScenarios.map((scenario) => (
<div key={scenario.id} className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex items-start space-x-4">
<div className="text-3xl">{getErrorIcon(scenario.type)}</div>
<div className="flex-1">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
{scenario.title}
</h3>
<p className="text-gray-600 dark:text-gray-300 text-sm mb-4">
{scenario.description}
</p>
<div className="flex space-x-3">
<button
onClick={() => handleScenarioTest(scenario)}
disabled={isLoading}
className="px-4 py-2 bg-red-600 hover:bg-red-700 disabled:bg-red-400 text-white rounded-md transition-colors text-sm"
>
{isLoading ? '测试中...' : '触发错误'}
</button>
{error && retryCount > 0 && retryCount < 3 && (
<button
onClick={() => handleRetry(scenario)}
disabled={isLoading}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-md transition-colors text-sm"
>
({retryCount}/3)
</button>
)}
</div>
</div>
</div>
</div>
))}
</div>
{/* 表单验证测试 */}
<div className="space-y-6">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
</h2>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
(3)
</label>
<input
type="text"
value={formData.username}
onChange={(e) => setFormData(prev => ({ ...prev, username: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入用户名"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
(6)
</label>
<input
type="password"
value={formData.password}
onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入密码"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入邮箱地址"
/>
</div>
<button
onClick={() => handleScenarioTest(errorScenarios.find(s => s.type === 'validation')!)}
disabled={isLoading}
className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-md transition-colors"
>
{isLoading ? '验证中...' : '提交表单'}
</button>
</div>
</div>
{/* 文件上传测试 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
(5MBPDF)
</label>
<input
type="file"
onChange={(e) => setFormData(prev => ({ ...prev, file: e.target.files?.[0] || null }))}
accept="image/*,.pdf"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
/>
</div>
{formData.file && (
<div className="text-sm text-gray-600 dark:text-gray-300">
: {formData.file.name} ({(formData.file.size / 1024 / 1024).toFixed(2)} MB)
</div>
)}
<button
onClick={handleFileUpload}
disabled={isLoading || !formData.file}
className="w-full px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-green-400 text-white rounded-md transition-colors"
>
{isLoading ? '上传中...' : '上传文件'}
</button>
</div>
</div>
{/* 权限测试说明 */}
<div className="bg-yellow-50 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-700 rounded-lg p-4">
<h4 className="text-sm font-medium text-yellow-800 dark:text-yellow-200 mb-2">
💡
</h4>
<p className="text-sm text-yellow-700 dark:text-yellow-300">
"admin""触发错误"
</p>
</div>
</div>
</div>
{/* 加载状态指示器 */}
{isLoading && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 flex items-center space-x-4">
<svg className="animate-spin h-8 w-8 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span className="text-gray-900 dark:text-white">...</span>
</div>
</div>
)}
{/* 返回链接 */}
<div className="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
<Link href="/" className="text-blue-600 hover:text-blue-500 dark:text-blue-400">
</Link>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,451 @@
import { useState } from 'react'
import { Link } from 'wouter'
interface FormData {
username: string
email: string
password: string
confirmPassword: string
age: string
birthDate: string
phone: string
website: string
bio: string
country: string
newsletter: boolean
terms: boolean
}
type FormErrors = Record<string, string>;
export default function FormTestPage() {
const [formData, setFormData] = useState<FormData>({
username: '',
email: '',
password: '',
confirmPassword: '',
age: '',
birthDate: '',
phone: '',
website: '',
bio: '',
country: '',
newsletter: false,
terms: false
})
const [errors, setErrors] = useState<FormErrors>({})
const [isSubmitting, setIsSubmitting] = useState(false)
const [submitResult, setSubmitResult] = useState<'success' | 'error' | null>(null)
const [submitMessage, setSubmitMessage] = useState('')
const validateField = (name: string, value: string | boolean): string => {
switch (name) {
case 'username':
if (!value) return '用户名不能为空'
if (typeof value === 'string' && value.length < 3) return '用户名至少需要3个字符'
if (typeof value === 'string' && !/^[a-zA-Z0-9_]+$/.test(value)) return '用户名只能包含字母、数字和下划线'
return ''
case 'email':
if (!value) return '邮箱不能为空'
if (typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return '请输入有效的邮箱地址'
return ''
case 'password':
if (!value) return '密码不能为空'
if (typeof value === 'string' && value.length < 6) return '密码至少需要6个字符'
if (typeof value === 'string' && !/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) return '密码必须包含大小写字母和数字'
return ''
case 'confirmPassword':
if (!value) return '请确认密码'
if (value !== formData.password) return '两次输入的密码不一致'
return ''
case 'age': {
if (!value) return '年龄不能为空'
const age = parseInt(value as string)
if (isNaN(age) || age < 18 || age > 120) return '年龄必须在18-120之间'
return ''
}
case 'phone':
if (!value) return '手机号不能为空'
if (typeof value === 'string' && !/^1[3-9]\d{9}$/.test(value)) return '请输入有效的手机号'
return ''
case 'terms':
if (!value) return '请同意服务条款'
return ''
default:
return ''
}
}
const handleInputChange = (name: string, value: string | boolean) => {
console.log(`Input changed: ${name} = ${value}`)
setFormData(prev => ({ ...prev, [name]: value }))
// 实时验证
const error = validateField(name, value)
setErrors(prev => ({ ...prev, [name]: error }))
}
const validateForm = (): boolean => {
const newErrors: FormErrors = {}
let isValid = true
Object.keys(formData).forEach(key => {
const error = validateField(key, formData[key as keyof FormData])
if (error) {
newErrors[key] = error
isValid = false
}
})
setErrors(newErrors)
return isValid
}
const simulateSubmit = async (): Promise<{ success: boolean; message: string }> => {
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 2000 + Math.random() * 2000))
// 模拟随机失败
if (Math.random() < 0.3) {
throw new Error('网络错误:服务器暂时不可用,请稍后重试')
}
// 模拟服务器验证错误
if (formData.username.toLowerCase() === 'admin') {
throw new Error('用户名 "admin" 已被占用,请选择其他用户名')
}
return {
success: true,
message: '注册成功!欢迎加入我们的平台。'
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!validateForm()) {
setSubmitResult('error')
setSubmitMessage('请修正表单中的错误')
return
}
setIsSubmitting(true)
setSubmitResult(null)
setSubmitMessage('')
try {
const result = await simulateSubmit()
setSubmitResult('success')
setSubmitMessage(result.message)
} catch (error) {
setSubmitResult('error')
setSubmitMessage(error instanceof Error ? error.message : '提交失败,请重试')
} finally {
setIsSubmitting(false)
}
}
const resetForm = () => {
setFormData({
username: '',
email: '',
password: '',
confirmPassword: '',
age: '',
birthDate: '',
phone: '',
website: '',
bio: '',
country: '',
newsletter: false,
terms: false
})
setErrors({})
setSubmitResult(null)
setSubmitMessage('')
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-2xl mx-auto px-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
</h1>
<p className="text-gray-600 dark:text-gray-300">
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* 用户名 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="text"
value={formData.username}
onChange={(e) => handleInputChange('username', e.target.value)}
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white ${
errors.username ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
}`}
placeholder="请输入用户名"
/>
{errors.username && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.username}</p>
)}
</div>
{/* 邮箱 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white ${
errors.email ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
}`}
placeholder="请输入邮箱地址"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.email}</p>
)}
</div>
{/* 密码 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="password"
value={formData.password}
onChange={(e) => handleInputChange('password', e.target.value)}
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white ${
errors.password ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
}`}
placeholder="请输入密码"
/>
{errors.password && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.password}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="password"
value={formData.confirmPassword}
onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white ${
errors.confirmPassword ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
}`}
placeholder="请再次输入密码"
/>
{errors.confirmPassword && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.confirmPassword}</p>
)}
</div>
</div>
{/* 年龄和生日 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="number"
value={formData.age}
onChange={(e) => handleInputChange('age', e.target.value)}
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white ${
errors.age ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
}`}
placeholder="请输入年龄"
min="18"
max="120"
/>
{errors.age && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.age}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<input
type="date"
value={formData.birthDate}
onChange={(e) => handleInputChange('birthDate', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
/>
</div>
</div>
{/* 手机和网站 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="tel"
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white ${
errors.phone ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
}`}
placeholder="请输入手机号"
/>
{errors.phone && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.phone}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<input
type="url"
value={formData.website}
onChange={(e) => handleInputChange('website', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="https://example.com"
/>
</div>
</div>
{/* 国家选择 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
/
</label>
<select
value={formData.country}
onChange={(e) => handleInputChange('country', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value="">/</option>
<option value="CN"></option>
<option value="US"></option>
<option value="JP"></option>
<option value="KR"></option>
<option value="GB"></option>
<option value="DE"></option>
<option value="FR"></option>
<option value="CA"></option>
<option value="AU"></option>
</select>
</div>
{/* 个人简介 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<textarea
value={formData.bio}
onChange={(e) => handleInputChange('bio', e.target.value)}
rows={4}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请简单介绍一下自己..."
/>
</div>
{/* 复选框 */}
<div className="space-y-3">
<div className="flex items-center">
<input
type="checkbox"
id="newsletter"
checked={formData.newsletter}
onChange={(e) => handleInputChange('newsletter', e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="newsletter" className="ml-2 block text-sm text-gray-700 dark:text-gray-300">
</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="terms"
checked={formData.terms}
onChange={(e) => handleInputChange('terms', e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="terms" className="ml-2 block text-sm text-gray-700 dark:text-gray-300">
<a href="#" className="text-blue-600 hover:text-blue-500"></a> <a href="#" className="text-blue-600 hover:text-blue-500"></a> *
</label>
</div>
{errors.terms && (
<p className="text-sm text-red-600 dark:text-red-400">{errors.terms}</p>
)}
</div>
{/* 提交结果 */}
{submitResult && (
<div className={`p-4 rounded-md ${
submitResult === 'success'
? 'bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700'
: 'bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700'
}`}>
<p className={`text-sm ${
submitResult === 'success'
? 'text-green-800 dark:text-green-200'
: 'text-red-800 dark:text-red-200'
}`}>
{submitMessage}
</p>
</div>
)}
{/* 按钮组 */}
<div className="flex flex-col sm:flex-row gap-4">
<button
type="submit"
disabled={isSubmitting}
className="flex-1 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium py-2 px-4 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
{isSubmitting ? (
<span className="flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
...
</span>
) : (
'注册账户'
)}
</button>
<button
type="button"
onClick={resetForm}
className="flex-1 bg-gray-600 hover:bg-gray-700 text-white font-medium py-2 px-4 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2"
>
</button>
</div>
</form>
<div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
<Link href="/" className="text-blue-600 hover:text-blue-500 dark:text-blue-400">
</Link>
</div>
</div>
</div>
</div>
)
}

107
pages/test-pages/index.tsx Normal file
View File

@@ -0,0 +1,107 @@
import { Link } from 'wouter'
export default function IndexPage() {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-900 dark:to-gray-800 p-8">
<div className="max-w-4xl mx-auto">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-4">
Page Use Agent
</h1>
<p className="text-lg text-gray-600 dark:text-gray-300">
AI Agent
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<TestPageCard
title="表单测试"
description="测试输入、验证、提交等表单操作"
path="/form"
icon="📝"
difficulty="简单"
/>
<TestPageCard
title="导航测试"
description="测试菜单、下拉框、弹窗等交互"
path="/navigation"
icon="🧭"
difficulty="中等"
/>
<TestPageCard
title="列表测试"
description="测试滚动、分页、搜索、排序"
path="/list"
icon="📋"
difficulty="中等"
/>
<TestPageCard
title="复杂交互"
description="测试多步骤操作和状态管理"
path="/complex"
icon="⚙️"
difficulty="困难"
/>
<TestPageCard
title="错误处理"
description="测试错误识别和重试机制"
path="/errors"
icon="⚠️"
difficulty="困难"
/>
<TestPageCard
title="异步操作"
description="测试等待、加载状态识别"
path="/async"
icon="⏳"
difficulty="中等"
/>
</div>
<div className="text-center">
<Link href="/" className="inline-flex items-center px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
Page Use
</Link>
</div>
</div>
</div>
)
}
interface TestPageCardProps {
title: string
description: string
path: string
icon: string
difficulty: string
}
function TestPageCard({ title, description, path, icon, difficulty }: TestPageCardProps) {
const difficultyColors = {
'简单': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
'中等': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
'困难': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
}
return (
<Link href={path}>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105 cursor-pointer border border-gray-200 dark:border-gray-700">
<div className="text-4xl mb-4">{icon}</div>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
{title}
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4 text-sm">
{description}
</p>
<div className="flex justify-between items-center">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${difficultyColors[difficulty as keyof typeof difficultyColors]}`}>
{difficulty}
</span>
<span className="text-blue-600 dark:text-blue-400 text-sm font-medium">
</span>
</div>
</div>
</Link>
)
}

View File

@@ -0,0 +1,448 @@
import { useState, useEffect } from 'react'
import { Link } from 'wouter'
interface Product {
id: number
name: string
category: string
price: number
stock: number
rating: number
image: string
description: string
tags: string[]
}
const generateProducts = (count: number): Product[] => {
const categories = ['手机', '电脑', '平板', '耳机', '手表', '相机']
const brands = ['Apple', 'Samsung', 'Huawei', 'Xiaomi', 'Sony', 'Dell']
const adjectives = ['Pro', 'Max', 'Ultra', 'Plus', 'Air', 'Mini']
return Array.from({ length: count }, (_, i) => ({
id: i + 1,
name: `${brands[i % brands.length]} ${categories[i % categories.length]} ${adjectives[i % adjectives.length]}`,
category: categories[i % categories.length],
price: Math.floor(Math.random() * 10000) + 500,
stock: Math.floor(Math.random() * 100),
rating: Math.round((Math.random() * 2 + 3) * 10) / 10,
image: `https://picsum.photos/200/200?random=${i}`,
description: `这是一款优秀的${categories[i % categories.length]}产品,具有出色的性能和设计。`,
tags: ['热销', '新品', '推荐'].slice(0, Math.floor(Math.random() * 3) + 1)
}))
}
export default function ListTestPage() {
const [products, setProducts] = useState<Product[]>([])
const [filteredProducts, setFilteredProducts] = useState<Product[]>([])
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState('')
const [selectedCategory, setSelectedCategory] = useState('全部')
const [sortBy, setSortBy] = useState('name')
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc')
const [currentPage, setCurrentPage] = useState(1)
const [itemsPerPage, setItemsPerPage] = useState(12)
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
const categories = ['全部', '手机', '电脑', '平板', '耳机', '手表', '相机']
// 模拟数据加载
useEffect(() => {
const loadData = async () => {
setLoading(true)
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1500))
const data = generateProducts(150)
setProducts(data)
setFilteredProducts(data)
setLoading(false)
}
loadData()
}, [])
// 搜索和过滤
useEffect(() => {
let filtered = products
// 按类别过滤
if (selectedCategory !== '全部') {
filtered = filtered.filter(product => product.category === selectedCategory)
}
// 按搜索词过滤
if (searchTerm) {
filtered = filtered.filter(product =>
product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
product.description.toLowerCase().includes(searchTerm.toLowerCase())
)
}
// 排序
filtered.sort((a, b) => {
let aValue: any = a[sortBy as keyof Product]
let bValue: any = b[sortBy as keyof Product]
if (typeof aValue === 'string') {
aValue = aValue.toLowerCase()
bValue = bValue.toLowerCase()
}
if (sortOrder === 'asc') {
return aValue > bValue ? 1 : -1
} else {
return aValue < bValue ? 1 : -1
}
})
setFilteredProducts(filtered)
setCurrentPage(1) // 重置到第一页
}, [products, searchTerm, selectedCategory, sortBy, sortOrder])
// 分页计算
const totalPages = Math.ceil(filteredProducts.length / itemsPerPage)
const startIndex = (currentPage - 1) * itemsPerPage
const endIndex = startIndex + itemsPerPage
const currentProducts = filteredProducts.slice(startIndex, endIndex)
const handlePageChange = (page: number) => {
setCurrentPage(page)
// 滚动到顶部
window.scrollTo({ top: 0, behavior: 'smooth' })
}
const LoadingSkeleton = () => (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{Array.from({ length: 12 }).map((_, i) => (
<div key={i} className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 animate-pulse">
<div className="bg-gray-300 dark:bg-gray-600 h-48 rounded-lg mb-4"></div>
<div className="space-y-2">
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-3/4"></div>
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-1/2"></div>
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-1/4"></div>
</div>
</div>
))}
</div>
)
const ProductCard = ({ product }: { product: Product }) => (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-4">
<div className="relative mb-4">
<img
src={product.image}
alt={product.name}
className="w-full h-48 object-cover rounded-lg"
loading="lazy"
/>
<div className="absolute top-2 right-2 flex flex-wrap gap-1">
{product.tags.map((tag, index) => (
<span
key={index}
className="bg-red-500 text-white text-xs px-2 py-1 rounded-full"
>
{tag}
</span>
))}
</div>
</div>
<h3 className="font-semibold text-gray-900 dark:text-white mb-2 line-clamp-2">
{product.name}
</h3>
<p className="text-gray-600 dark:text-gray-300 text-sm mb-3 line-clamp-2">
{product.description}
</p>
<div className="flex items-center justify-between mb-3">
<span className="text-2xl font-bold text-blue-600 dark:text-blue-400">
¥{product.price.toLocaleString()}
</span>
<div className="flex items-center">
<span className="text-yellow-400"></span>
<span className="text-sm text-gray-600 dark:text-gray-300 ml-1">
{product.rating}
</span>
</div>
</div>
<div className="flex items-center justify-between mb-4">
<span className="text-sm text-gray-500 dark:text-gray-400">
: {product.stock}
</span>
<span className="text-sm text-gray-500 dark:text-gray-400">
{product.category}
</span>
</div>
<button className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition-colors">
</button>
</div>
)
const ProductListItem = ({ product }: { product: Product }) => (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 flex items-center space-x-4">
<img
src={product.image}
alt={product.name}
className="w-20 h-20 object-cover rounded-lg flex-shrink-0"
loading="lazy"
/>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-900 dark:text-white mb-1">
{product.name}
</h3>
<p className="text-gray-600 dark:text-gray-300 text-sm mb-2 line-clamp-1">
{product.description}
</p>
<div className="flex items-center space-x-4 text-sm text-gray-500 dark:text-gray-400">
<span>{product.category}</span>
<span>: {product.stock}</span>
<div className="flex items-center">
<span className="text-yellow-400"></span>
<span className="ml-1">{product.rating}</span>
</div>
</div>
</div>
<div className="flex items-center space-x-4">
<span className="text-xl font-bold text-blue-600 dark:text-blue-400">
¥{product.price.toLocaleString()}
</span>
<button className="bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition-colors">
</button>
</div>
</div>
)
const Pagination = () => {
const getPageNumbers = () => {
const pages = []
const maxVisible = 5
let start = Math.max(1, currentPage - Math.floor(maxVisible / 2))
const end = Math.min(totalPages, start + maxVisible - 1)
if (end - start + 1 < maxVisible) {
start = Math.max(1, end - maxVisible + 1)
}
for (let i = start; i <= end; i++) {
pages.push(i)
}
return pages
}
return (
<div className="flex items-center justify-between mt-8">
<div className="text-sm text-gray-700 dark:text-gray-300">
{startIndex + 1}-{Math.min(endIndex, filteredProducts.length)}
{filteredProducts.length}
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
>
</button>
{getPageNumbers().map(page => (
<button
key={page}
onClick={() => handlePageChange(page)}
className={`px-3 py-2 text-sm font-medium rounded-md ${
page === currentPage
? 'bg-blue-600 text-white'
: 'text-gray-500 bg-white border border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700'
}`}
>
{page}
</button>
))}
<button
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
>
</button>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-7xl mx-auto px-4">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
</h1>
<p className="text-gray-600 dark:text-gray-300">
</p>
</div>
{/* 搜索和过滤栏 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
{/* 搜索框 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="输入产品名称或描述..."
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
/>
</div>
{/* 类别过滤 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
>
{categories.map(category => (
<option key={category} value={category}>{category}</option>
))}
</select>
</div>
{/* 排序方式 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value="name"></option>
<option value="price"></option>
<option value="rating"></option>
<option value="stock"></option>
</select>
</div>
{/* 排序顺序 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<select
value={sortOrder}
onChange={(e) => setSortOrder(e.target.value as 'asc' | 'desc')}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value="asc"></option>
<option value="desc"></option>
</select>
</div>
</div>
{/* 视图控制 */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
:
</span>
<select
value={itemsPerPage}
onChange={(e) => setItemsPerPage(Number(e.target.value))}
className="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value={12}>12</option>
<option value={24}>24</option>
<option value={48}>48</option>
</select>
</div>
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
:
</span>
<button
onClick={() => setViewMode('grid')}
className={`p-2 rounded-md ${
viewMode === 'grid'
? 'bg-blue-600 text-white'
: 'bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300'
}`}
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path d="M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
</svg>
</button>
<button
onClick={() => setViewMode('list')}
className={`p-2 rounded-md ${
viewMode === 'list'
? 'bg-blue-600 text-white'
: 'bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300'
}`}
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 8a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 12a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 16a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" />
</svg>
</button>
</div>
</div>
</div>
{/* 产品列表 */}
{loading ? (
<LoadingSkeleton />
) : filteredProducts.length === 0 ? (
<div className="text-center py-12">
<div className="text-6xl mb-4">🔍</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
</h3>
<p className="text-gray-600 dark:text-gray-300">
</p>
</div>
) : (
<>
{viewMode === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{currentProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
) : (
<div className="space-y-4">
{currentProducts.map(product => (
<ProductListItem key={product.id} product={product} />
))}
</div>
)}
<Pagination />
</>
)}
{/* 返回顶部按钮 */}
<button
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
className="fixed bottom-8 right-8 bg-blue-600 hover:bg-blue-700 text-white p-3 rounded-full shadow-lg transition-colors"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" />
</svg>
</button>
{/* 返回链接 */}
<div className="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
<Link href="/" className="text-blue-600 hover:text-blue-500 dark:text-blue-400">
</Link>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,453 @@
import { useState } from 'react'
import { Link } from 'wouter'
export default function NavigationTestPage() {
const [activeTab, setActiveTab] = useState('home')
const [isModalOpen, setIsModalOpen] = useState(false)
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false)
const [breadcrumbs, setBreadcrumbs] = useState(['首页', '产品', '手机'])
const [notifications, setNotifications] = useState([
{ id: 1, title: '新消息', content: '您有一条新的私信', time: '2分钟前', unread: true },
{ id: 2, title: '系统通知', content: '系统将于今晚维护', time: '1小时前', unread: true },
{ id: 3, title: '订单更新', content: '您的订单已发货', time: '3小时前', unread: false }
])
const handleBreadcrumbClick = (index: number) => {
const newBreadcrumbs = breadcrumbs.slice(0, index + 1)
setBreadcrumbs(newBreadcrumbs)
}
const markNotificationAsRead = (id: number) => {
setNotifications(prev =>
prev.map(notif =>
notif.id === id ? { ...notif, unread: false } : notif
)
)
}
const unreadCount = notifications.filter(n => n.unread).length
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
{/* 顶部导航栏 */}
<nav className="bg-white dark:bg-gray-800 shadow-lg border-b border-gray-200 dark:border-gray-700">
<div className="max-w-7xl mx-auto px-4">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<div className="flex items-center">
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">
TestNav
</div>
</div>
{/* 主导航菜单 */}
<div className="hidden md:flex space-x-8">
<a href="#" className="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 px-3 py-2 rounded-md text-sm font-medium transition-colors">
</a>
{/* 产品下拉菜单 */}
<div className="relative">
<button
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 px-3 py-2 rounded-md text-sm font-medium transition-colors flex items-center"
>
<svg className="ml-1 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{isDropdownOpen && (
<div className="absolute top-full left-0 mt-1 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-50">
<div className="py-1">
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
</a>
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
</a>
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
</a>
<div className="border-t border-gray-200 dark:border-gray-600 my-1"></div>
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
</a>
</div>
</div>
)}
</div>
<a href="#" className="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 px-3 py-2 rounded-md text-sm font-medium transition-colors">
</a>
<a href="#" className="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 px-3 py-2 rounded-md text-sm font-medium transition-colors">
</a>
</div>
{/* 右侧菜单 */}
<div className="flex items-center space-x-4">
{/* 通知铃铛 */}
<div className="relative">
<button className="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 p-2 rounded-full transition-colors">
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-5 5v-5zM10.5 3.75a6 6 0 0 1 6 6v2.25l2.25 2.25v2.25H2.25V14.25L4.5 12V9.75a6 6 0 0 1 6-6z" />
</svg>
{unreadCount > 0 && (
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
{unreadCount}
</span>
)}
</button>
</div>
{/* 用户菜单 */}
<div className="relative">
<button
onClick={() => setIsUserMenuOpen(!isUserMenuOpen)}
className="flex items-center text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 p-2 rounded-full transition-colors"
>
<div className="h-8 w-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm font-medium">
U
</div>
</button>
{isUserMenuOpen && (
<div className="absolute top-full right-0 mt-1 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-50">
<div className="py-1">
<div className="px-4 py-2 text-sm text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-600">
user@example.com
</div>
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
</a>
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
</a>
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
</a>
<div className="border-t border-gray-200 dark:border-gray-600 my-1"></div>
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
退
</a>
</div>
</div>
)}
</div>
</div>
</div>
</div>
</nav>
{/* 面包屑导航 */}
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div className="max-w-7xl mx-auto px-4 py-3">
<nav className="flex" aria-label="Breadcrumb">
<ol className="flex items-center space-x-2">
{breadcrumbs.map((crumb, index) => (
<li key={index} className="flex items-center">
{index > 0 && (
<svg className="h-4 w-4 text-gray-400 mx-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
)}
<button
onClick={() => handleBreadcrumbClick(index)}
className={`text-sm font-medium transition-colors ${
index === breadcrumbs.length - 1
? 'text-gray-500 dark:text-gray-400 cursor-default'
: 'text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300'
}`}
>
{crumb}
</button>
</li>
))}
</ol>
</nav>
</div>
</div>
{/* 主要内容区域 */}
<div className="max-w-7xl mx-auto px-4 py-8">
{/* 标签页导航 */}
<div className="mb-8">
<div className="border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-8">
{[
{ id: 'home', label: '概览', icon: '🏠' },
{ id: 'products', label: '产品列表', icon: '📱' },
{ id: 'orders', label: '订单管理', icon: '📦' },
{ id: 'analytics', label: '数据分析', icon: '📊' },
{ id: 'settings', label: '设置', icon: '⚙️' }
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`py-2 px-1 border-b-2 font-medium text-sm transition-colors ${
activeTab === tab.id
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
<span className="mr-2">{tab.icon}</span>
{tab.label}
</button>
))}
</nav>
</div>
</div>
{/* 标签页内容 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
{activeTab === 'home' && (
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"></h2>
<p className="text-gray-600 dark:text-gray-300 mb-6">
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-blue-50 dark:bg-blue-900 p-4 rounded-lg">
<h3 className="font-semibold text-blue-900 dark:text-blue-100"></h3>
<p className="text-2xl font-bold text-blue-600 dark:text-blue-400">¥123,456</p>
</div>
<div className="bg-green-50 dark:bg-green-900 p-4 rounded-lg">
<h3 className="font-semibold text-green-900 dark:text-green-100"></h3>
<p className="text-2xl font-bold text-green-600 dark:text-green-400">1,234</p>
</div>
<div className="bg-purple-50 dark:bg-purple-900 p-4 rounded-lg">
<h3 className="font-semibold text-purple-900 dark:text-purple-100"></h3>
<p className="text-2xl font-bold text-purple-600 dark:text-purple-400">5,678</p>
</div>
</div>
</div>
)}
{activeTab === 'products' && (
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"></h2>
<div className="space-y-4">
{['iPhone 15 Pro', 'MacBook Air', 'iPad Pro', 'Apple Watch'].map((product, index) => (
<div key={index} className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-600 rounded-lg">
<div>
<h3 className="font-medium text-gray-900 dark:text-white">{product}</h3>
<p className="text-gray-500 dark:text-gray-400">...</p>
</div>
<button className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md transition-colors">
</button>
</div>
))}
</div>
</div>
)}
{activeTab === 'orders' && (
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"></h2>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"></th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"></th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"></th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"></th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-600">
{[
{ id: '#001', customer: '张三', status: '已发货', amount: '¥1,299' },
{ id: '#002', customer: '李四', status: '处理中', amount: '¥2,599' },
{ id: '#003', customer: '王五', status: '已完成', amount: '¥899' }
].map((order, index) => (
<tr key={index}>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{order.id}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{order.customer}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 text-xs font-medium rounded-full ${
order.status === '已完成' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' :
order.status === '已发货' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' :
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
}`}>
{order.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{order.amount}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{activeTab === 'analytics' && (
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"></h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-gray-50 dark:bg-gray-700 p-6 rounded-lg">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4"></h3>
<div className="h-32 bg-gradient-to-r from-blue-400 to-purple-500 rounded-lg flex items-center justify-center text-white">
📈
</div>
</div>
<div className="bg-gray-50 dark:bg-gray-700 p-6 rounded-lg">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4"></h3>
<div className="h-32 bg-gradient-to-r from-green-400 to-blue-500 rounded-lg flex items-center justify-center text-white">
🗺
</div>
</div>
</div>
</div>
)}
{activeTab === 'settings' && (
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"></h2>
<div className="space-y-6">
<div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2"></h3>
<div className="space-y-2">
<label className="flex items-center">
<input type="checkbox" className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" defaultChecked />
<span className="ml-2 text-gray-700 dark:text-gray-300"></span>
</label>
<label className="flex items-center">
<input type="checkbox" className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
<span className="ml-2 text-gray-700 dark:text-gray-300"></span>
</label>
</div>
</div>
<div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2"></h3>
<div className="space-y-2">
<label className="flex items-center">
<input type="checkbox" className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" defaultChecked />
<span className="ml-2 text-gray-700 dark:text-gray-300"></span>
</label>
<label className="flex items-center">
<input type="checkbox" className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" defaultChecked />
<span className="ml-2 text-gray-700 dark:text-gray-300"></span>
</label>
</div>
</div>
</div>
</div>
)}
</div>
{/* 操作按钮 */}
<div className="mt-8 flex flex-wrap gap-4">
<button
onClick={() => setIsModalOpen(true)}
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-md transition-colors"
>
</button>
<button
onClick={() => setBreadcrumbs([...breadcrumbs, `新页面${breadcrumbs.length}`])}
className="bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded-md transition-colors"
>
</button>
<button
onClick={() => {
const newNotif = {
id: Date.now(),
title: '新通知',
content: `这是第 ${notifications.length + 1} 条通知`,
time: '刚刚',
unread: true
}
setNotifications(prev => [newNotif, ...prev])
}}
className="bg-purple-600 hover:bg-purple-700 text-white px-6 py-2 rounded-md transition-colors"
>
</button>
</div>
{/* 通知列表 */}
{notifications.length > 0 && (
<div className="mt-8">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4"></h3>
<div className="space-y-2">
{notifications.map((notification) => (
<div
key={notification.id}
className={`p-4 rounded-lg border cursor-pointer transition-colors ${
notification.unread
? 'bg-blue-50 dark:bg-blue-900 border-blue-200 dark:border-blue-700'
: 'bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-600'
}`}
onClick={() => markNotificationAsRead(notification.id)}
>
<div className="flex justify-between items-start">
<div className="flex-1">
<h4 className="font-medium text-gray-900 dark:text-white">{notification.title}</h4>
<p className="text-gray-600 dark:text-gray-300 text-sm">{notification.content}</p>
</div>
<div className="flex items-center space-x-2">
<span className="text-xs text-gray-500 dark:text-gray-400">{notification.time}</span>
{notification.unread && (
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
)}
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
{/* 模态框 */}
{isModalOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white"></h3>
<button
onClick={() => setIsModalOpen(false)}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<p className="text-gray-600 dark:text-gray-300 mb-6">
Agent
</p>
<div className="flex justify-end space-x-3">
<button
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
</button>
<button
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors"
>
</button>
</div>
</div>
</div>
)}
{/* 返回链接 */}
<div className="max-w-7xl mx-auto px-4 py-8">
<Link href="/" className="text-blue-600 hover:text-blue-500 dark:text-blue-400">
</Link>
</div>
</div>
)
}

View File

@@ -0,0 +1,24 @@
import { Route, Switch } from 'wouter'
import FormTestPage from './form-test'
import NavigationTestPage from './navigation-test'
import ListTestPage from './list-test'
import ComplexTestPage from './complex-test'
import ErrorTestPage from './error-test'
import AsyncTestPage from './async-test'
import IndexPage from './index'
export default function Router() {
return (
<>
<Switch>
<Route path="/form" component={FormTestPage} />
<Route path="/navigation" component={NavigationTestPage} />
<Route path="/list" component={ListTestPage} />
<Route path="/complex" component={ComplexTestPage} />
<Route path="/errors" component={ErrorTestPage} />
<Route path="/async" component={AsyncTestPage} />
<Route path="" component={IndexPage} />
</Switch>
</>
)
}