chore: rm test-pages
This commit is contained in:
@@ -8,12 +8,7 @@ import globals from 'globals'
|
|||||||
import tseslint from 'typescript-eslint'
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
globalIgnores([
|
globalIgnores(['**/dist', '**/node_modules', 'packages/website/src/components/ui']),
|
||||||
'**/dist',
|
|
||||||
'**/test-pages',
|
|
||||||
'**/node_modules',
|
|
||||||
'packages/website/src/components/ui',
|
|
||||||
]),
|
|
||||||
{
|
{
|
||||||
plugins: {
|
plugins: {
|
||||||
'react-hooks': reactHooks,
|
'react-hooks': reactHooks,
|
||||||
|
|||||||
@@ -37,14 +37,17 @@ Available Magic UI components: https://magicui.design/docs/components
|
|||||||
Located in `src/components/ui/`:
|
Located in `src/components/ui/`:
|
||||||
|
|
||||||
**From shadcn/ui:**
|
**From shadcn/ui:**
|
||||||
|
|
||||||
- `alert`, `badge`, `button`, `separator`, `sonner`, `switch`, `tooltip`
|
- `alert`, `badge`, `button`, `separator`, `sonner`, `switch`, `tooltip`
|
||||||
|
|
||||||
**From Magic UI:**
|
**From Magic UI:**
|
||||||
|
|
||||||
- `animated-gradient-text`, `animated-shiny-text`, `aurora-text`
|
- `animated-gradient-text`, `animated-shiny-text`, `aurora-text`
|
||||||
- `hyper-text`, `magic-card`, `neon-gradient-card`, `particles`
|
- `hyper-text`, `magic-card`, `neon-gradient-card`, `particles`
|
||||||
- `sparkles-text`, `text-animate`, `typing-animation`
|
- `sparkles-text`, `text-animate`, `typing-animation`
|
||||||
|
|
||||||
**Custom:**
|
**Custom:**
|
||||||
|
|
||||||
- `highlighter`, `kbd`, `spinner`
|
- `highlighter`, `kbd`, `spinner`
|
||||||
|
|
||||||
### Styling Rules
|
### Styling Rules
|
||||||
@@ -65,7 +68,6 @@ src/
|
|||||||
│ └── DocsLayout.tsx # Documentation sidebar
|
│ └── DocsLayout.tsx # Documentation sidebar
|
||||||
├── docs/ # Documentation pages
|
├── docs/ # Documentation pages
|
||||||
│ └── [section]/[topic]/page.tsx
|
│ └── [section]/[topic]/page.tsx
|
||||||
├── test-pages/ # Library integration tests
|
|
||||||
├── i18n/ # Internationalization
|
├── i18n/ # Internationalization
|
||||||
├── router.tsx # Central routing
|
├── router.tsx # Central routing
|
||||||
├── page.tsx # Homepage
|
├── page.tsx # Homepage
|
||||||
@@ -80,11 +82,6 @@ src/
|
|||||||
2. Add route to `src/router.tsx` with `<Header /> + <DocsLayout>` wrapper
|
2. Add route to `src/router.tsx` with `<Header /> + <DocsLayout>` wrapper
|
||||||
3. Add navigation item to `DocsLayout.tsx`
|
3. Add navigation item to `DocsLayout.tsx`
|
||||||
|
|
||||||
### Test Page
|
|
||||||
|
|
||||||
1. Create `src/test-pages/<name>.tsx`
|
|
||||||
2. Add route to `src/test-pages/router.tsx`
|
|
||||||
|
|
||||||
## Routing
|
## Routing
|
||||||
|
|
||||||
Uses hash-based routing for static hosting:
|
Uses hash-based routing for static hosting:
|
||||||
@@ -93,18 +90,16 @@ Uses hash-based routing for static hosting:
|
|||||||
import { Router } from 'wouter'
|
import { Router } from 'wouter'
|
||||||
import { useHashLocation } from 'wouter/use-hash-location'
|
import { useHashLocation } from 'wouter/use-hash-location'
|
||||||
|
|
||||||
<Router hook={useHashLocation}>
|
;<Router hook={useHashLocation}>{/* routes */}</Router>
|
||||||
{/* routes */}
|
|
||||||
</Router>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration Files
|
## Configuration Files
|
||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
| ----------------- | ----------------------- |
|
||||||
| `components.json` | shadcn/ui configuration |
|
| `components.json` | shadcn/ui configuration |
|
||||||
| `vite.config.js` | Vite build settings |
|
| `vite.config.js` | Vite build settings |
|
||||||
| `tsconfig.json` | TypeScript config |
|
| `tsconfig.json` | TypeScript config |
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -112,4 +107,3 @@ import { useHashLocation } from 'wouter/use-hash-location'
|
|||||||
npm start # Dev server (from root)
|
npm start # Dev server (from root)
|
||||||
npm run build:website # Build website (from root)
|
npm run build:website # Build website (from root)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import { Route, Router, Switch } from 'wouter'
|
import { Router } from 'wouter'
|
||||||
import { useHashLocation } from 'wouter/use-hash-location'
|
import { useHashLocation } from 'wouter/use-hash-location'
|
||||||
|
|
||||||
import './i18n/config'
|
import './i18n/config'
|
||||||
import './i18n/types'
|
import './i18n/types'
|
||||||
import { default as PagesRouter } from './router'
|
import { default as PagesRouter } from './router'
|
||||||
import { default as TestPagesRouter } from './test-pages/router'
|
|
||||||
|
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<Router hook={useHashLocation}>
|
<Router hook={useHashLocation}>
|
||||||
<Switch>
|
<PagesRouter />
|
||||||
<Route path="/test-pages" component={TestPagesRouter} nest />
|
|
||||||
<Route path="/" component={PagesRouter} nest />
|
|
||||||
</Switch>
|
|
||||||
</Router>
|
</Router>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,209 +0,0 @@
|
|||||||
# 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 构建
|
|
||||||
- 响应式设计,支持不同屏幕尺寸
|
|
||||||
- 深色模式支持
|
|
||||||
- 无需外部依赖,完全自包含
|
|
||||||
- 模拟真实的网络延迟和错误
|
|
||||||
|
|
||||||
## 扩展建议
|
|
||||||
|
|
||||||
如需添加新的测试场景,建议考虑以下方面:
|
|
||||||
- 特定行业的业务流程
|
|
||||||
- 更复杂的数据可视化交互
|
|
||||||
- 多媒体内容处理
|
|
||||||
- 实时协作功能
|
|
||||||
- 移动端特有的交互模式
|
|
||||||
|
|
||||||
每个新页面都应该:
|
|
||||||
- 有明确的测试目标
|
|
||||||
- 包含多种难度级别的任务
|
|
||||||
- 提供清晰的状态反馈
|
|
||||||
- 模拟真实的用户场景
|
|
||||||
@@ -1,543 +0,0 @@
|
|||||||
import { useEffect, useState } 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) => (
|
|
||||||
<div
|
|
||||||
key={data}
|
|
||||||
className={`p-3 rounded-lg border transition-all duration-300 ${
|
|
||||||
data === realTimeData[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, logIdx) => {
|
|
||||||
const logKey = `${logIdx + 1}-${log.substring(0, 30)}`
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={logKey}
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,582 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,464 +0,0 @@
|
|||||||
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="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="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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,488 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import { Link } from 'wouter'
|
|
||||||
|
|
||||||
export default function IndexPage() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-linear-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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,481 +0,0 @@
|
|||||||
import { useEffect, useMemo, useState } 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),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loading skeleton component
|
|
||||||
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 }, (_, i) => `skeleton-item-${i}`).map((id) => (
|
|
||||||
<div key={id} 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>
|
|
||||||
)
|
|
||||||
|
|
||||||
// Product card component
|
|
||||||
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) => (
|
|
||||||
<span key={tag} 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>
|
|
||||||
)
|
|
||||||
|
|
||||||
// Product list item component
|
|
||||||
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 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>
|
|
||||||
)
|
|
||||||
|
|
||||||
// Pagination component
|
|
||||||
interface PaginationProps {
|
|
||||||
currentPage: number
|
|
||||||
totalPages: number
|
|
||||||
startIndex: number
|
|
||||||
endIndex: number
|
|
||||||
totalItems: number
|
|
||||||
onPageChange: (page: number) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const Pagination = ({
|
|
||||||
currentPage,
|
|
||||||
totalPages,
|
|
||||||
startIndex,
|
|
||||||
endIndex,
|
|
||||||
totalItems,
|
|
||||||
onPageChange,
|
|
||||||
}: PaginationProps) => {
|
|
||||||
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, totalItems)} 条, 共 {totalItems} 条结果
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={() => onPageChange(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={() => onPageChange(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={() => onPageChange(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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ListTestPage() {
|
|
||||||
const [products, setProducts] = 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 = ['全部', '手机', '电脑', '平板', '耳机', '手表', '相机']
|
|
||||||
|
|
||||||
// Helper to set filters and reset page
|
|
||||||
const handleSearchChange = (term: string) => {
|
|
||||||
setSearchTerm(term)
|
|
||||||
setCurrentPage(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCategoryChange = (category: string) => {
|
|
||||||
setSelectedCategory(category)
|
|
||||||
setCurrentPage(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSortChange = (sort: string) => {
|
|
||||||
setSortBy(sort)
|
|
||||||
setCurrentPage(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSortOrderChange = (order: 'asc' | 'desc') => {
|
|
||||||
setSortOrder(order)
|
|
||||||
setCurrentPage(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模拟数据加载
|
|
||||||
useEffect(() => {
|
|
||||||
const loadData = async () => {
|
|
||||||
setLoading(true)
|
|
||||||
// 模拟网络延迟
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
|
||||||
const data = generateProducts(150)
|
|
||||||
setProducts(data)
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
loadData()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// 搜索和过滤 - Use useMemo to compute filtered products
|
|
||||||
const filteredProducts = useMemo(() => {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return filtered
|
|
||||||
}, [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' })
|
|
||||||
}
|
|
||||||
|
|
||||||
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) => handleSearchChange(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) => handleCategoryChange(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) => handleSortChange(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) => handleSortOrderChange(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: Product) => (
|
|
||||||
<ProductCard key={product.id} product={product} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{currentProducts.map((product: Product) => (
|
|
||||||
<ProductListItem key={product.id} product={product} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Pagination
|
|
||||||
currentPage={currentPage}
|
|
||||||
totalPages={totalPages}
|
|
||||||
startIndex={startIndex}
|
|
||||||
endIndex={endIndex}
|
|
||||||
totalItems={filteredProducts.length}
|
|
||||||
onPageChange={handlePageChange}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 返回顶部按钮 */}
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,566 +0,0 @@
|
|||||||
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, crumbIdx) => {
|
|
||||||
const isLast = crumbIdx === breadcrumbs.length - 1
|
|
||||||
const showSeparator = crumbIdx > 0
|
|
||||||
return (
|
|
||||||
<li key={`${crumb}-${crumbIdx + 1}`} className="flex items-center">
|
|
||||||
{showSeparator && (
|
|
||||||
<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(crumbIdx)}
|
|
||||||
className={`text-sm font-medium transition-colors ${
|
|
||||||
isLast
|
|
||||||
? '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) => (
|
|
||||||
<div
|
|
||||||
key={product}
|
|
||||||
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) => (
|
|
||||||
<tr key={order.id}>
|
|
||||||
<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-linear-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-linear-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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Route, Switch } from 'wouter'
|
|
||||||
|
|
||||||
import AsyncTestPage from './async-test'
|
|
||||||
import ComplexTestPage from './complex-test'
|
|
||||||
import ErrorTestPage from './error-test'
|
|
||||||
import FormTestPage from './form-test'
|
|
||||||
import IndexPage from './index'
|
|
||||||
import ListTestPage from './list-test'
|
|
||||||
import NavigationTestPage from './navigation-test'
|
|
||||||
|
|
||||||
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