feat: init
This commit is contained in:
1
pages/README.md
Normal file
1
pages/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# Landing Page & Docs
|
||||
19
pages/components/BetaNotice.tsx
Normal file
19
pages/components/BetaNotice.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
126
pages/components/CodeEditor.tsx
Normal file
126
pages/components/CodeEditor.tsx
Normal 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
|
||||
95
pages/components/DocsLayout.tsx
Normal file
95
pages/components/DocsLayout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
91
pages/components/Footer.tsx
Normal file
91
pages/components/Footer.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
74
pages/components/Header.tsx
Normal file
74
pages/components/Header.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
21
pages/components/HighlightSyntax.module.css
Normal file
21
pages/components/HighlightSyntax.module.css
Normal 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;
|
||||
}
|
||||
77
pages/components/HighlightSyntax.tsx
Normal file
77
pages/components/HighlightSyntax.tsx
Normal 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, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
|
||||
// 单行字符串,所有反斜杠双重转义,保证正则安全
|
||||
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
|
||||
164
pages/components/JSConsole.module.css
Normal file
164
pages/components/JSConsole.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
369
pages/components/JSConsole.tsx
Normal file
369
pages/components/JSConsole.tsx
Normal 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
|
||||
156
pages/docs/features/custom-tools/page.tsx
Normal file
156
pages/docs/features/custom-tools/page.tsx
Normal 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">
|
||||
每个自定义工具需要定义四个核心属性:name、description、input 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>
|
||||
)
|
||||
}
|
||||
44
pages/docs/features/data-masking/page.tsx
Normal file
44
pages/docs/features/data-masking/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
156
pages/docs/features/knowledge-injection/page.tsx
Normal file
156
pages/docs/features/knowledge-injection/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
162
pages/docs/features/model-integration/page.tsx
Normal file
162
pages/docs/features/model-integration/page.tsx
Normal 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 接口规范的服务,包括但不限于 OpenAI、Azure
|
||||
阿里云等各大云厂商的模型服务,以及使用 vLLM、Ollama 等框架部署的私有模型。
|
||||
</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>
|
||||
)
|
||||
}
|
||||
75
pages/docs/features/security-permissions/page.tsx
Normal file
75
pages/docs/features/security-permissions/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
41
pages/docs/integration/api-reference/page.tsx
Normal file
41
pages/docs/integration/api-reference/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
62
pages/docs/integration/best-practices/page.tsx
Normal file
62
pages/docs/integration/best-practices/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
41
pages/docs/integration/cdn-setup/page.tsx
Normal file
41
pages/docs/integration/cdn-setup/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
38
pages/docs/integration/configuration/page.tsx
Normal file
38
pages/docs/integration/configuration/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
113
pages/docs/integration/third-party-agent/page.tsx
Normal file
113
pages/docs/integration/third-party-agent/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
156
pages/docs/introduction/limitations/page.tsx
Normal file
156
pages/docs/introduction/limitations/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
188
pages/docs/introduction/overview/page.tsx
Normal file
188
pages/docs/introduction/overview/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
81
pages/docs/introduction/quick-start/page.tsx
Normal file
81
pages/docs/introduction/quick-start/page.tsx
Normal 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
33
pages/index.css
Normal 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
30
pages/main.tsx
Normal 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
460
pages/page.tsx
Normal 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
158
pages/router.tsx
Normal 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
209
pages/test-pages/README.md
Normal 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 构建
|
||||
- 响应式设计,支持不同屏幕尺寸
|
||||
- 深色模式支持
|
||||
- 无需外部依赖,完全自包含
|
||||
- 模拟真实的网络延迟和错误
|
||||
|
||||
## 扩展建议
|
||||
|
||||
如需添加新的测试场景,建议考虑以下方面:
|
||||
- 特定行业的业务流程
|
||||
- 更复杂的数据可视化交互
|
||||
- 多媒体内容处理
|
||||
- 实时协作功能
|
||||
- 移动端特有的交互模式
|
||||
|
||||
每个新页面都应该:
|
||||
- 有明确的测试目标
|
||||
- 包含多种难度级别的任务
|
||||
- 提供清晰的状态反馈
|
||||
- 模拟真实的用户场景
|
||||
520
pages/test-pages/async-test.tsx
Normal file
520
pages/test-pages/async-test.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
532
pages/test-pages/complex-test.tsx
Normal file
532
pages/test-pages/complex-test.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
433
pages/test-pages/error-test.tsx
Normal file
433
pages/test-pages/error-test.tsx
Normal 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">
|
||||
选择文件 (最大5MB,支持图片和PDF)
|
||||
</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>
|
||||
)
|
||||
}
|
||||
451
pages/test-pages/form-test.tsx
Normal file
451
pages/test-pages/form-test.tsx
Normal 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
107
pages/test-pages/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
448
pages/test-pages/list-test.tsx
Normal file
448
pages/test-pages/list-test.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
453
pages/test-pages/navigation-test.tsx
Normal file
453
pages/test-pages/navigation-test.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
24
pages/test-pages/router.tsx
Normal file
24
pages/test-pages/router.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user