chore: clean up lint warnings
This commit is contained in:
@@ -74,11 +74,14 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||
{showLineNumbers && (
|
||||
<div className={`flex-shrink-0 px-4 py-4 ${lineNumbersClasses} border-r select-none`}>
|
||||
<div className="text-xs font-mono leading-6">
|
||||
{lines.map((_, index) => (
|
||||
<div key={index} className="text-right">
|
||||
{index + 1}
|
||||
</div>
|
||||
))}
|
||||
{lines.map((line, lineIdx) => {
|
||||
const lineNum = lineIdx + 1
|
||||
return (
|
||||
<div key={`${lineNum}-${line.substring(0, 20)}`} className="text-right">
|
||||
{lineNum}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/**
|
||||
* JS 调试台,适合在文档中直接让用户运行代码,并且实时查看运行结果
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-base-to-string */
|
||||
import { KeyboardEvent, forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
|
||||
import { KeyboardEvent, useEffect, useImperativeHandle, useRef, useState } from 'react'
|
||||
|
||||
import HighlightSyntax from './HighlightSyntax'
|
||||
|
||||
@@ -29,7 +28,6 @@ class ConsoleInterceptor {
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!ConsoleInterceptor.instance) {
|
||||
ConsoleInterceptor.instance = new ConsoleInterceptor()
|
||||
}
|
||||
@@ -90,6 +88,7 @@ interface JSConsoleProps {
|
||||
height?: string
|
||||
onExecute?: (code: string, result: unknown) => void
|
||||
placeholder?: string
|
||||
ref?: React.Ref<JSConsoleRef>
|
||||
}
|
||||
|
||||
export interface JSConsoleRef {
|
||||
@@ -104,266 +103,267 @@ interface OutputItem {
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
const JSConsole = forwardRef<JSConsoleRef, JSConsoleProps>(
|
||||
(
|
||||
{ context = {}, height = '400px', onExecute, placeholder = 'Enter JavaScript code...' },
|
||||
ref
|
||||
) => {
|
||||
const [input, setInput] = useState('')
|
||||
const [outputs, setOutputs] = useState<OutputItem[]>([])
|
||||
const [isExecuting, setIsExecuting] = useState(false)
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||
const outputRef = useRef<HTMLDivElement>(null)
|
||||
const DEFAULT_CONTEXT = {}
|
||||
|
||||
// 持久的执行上下文,用于多轮对话共享作用域
|
||||
const executionContextRef = useRef<Record<string, unknown>>({})
|
||||
function JSConsole({
|
||||
context = DEFAULT_CONTEXT,
|
||||
height = '400px',
|
||||
onExecute,
|
||||
placeholder = 'Enter JavaScript code...',
|
||||
ref,
|
||||
}: JSConsoleProps) {
|
||||
const [input, setInput] = useState('')
|
||||
const [outputs, setOutputs] = useState<OutputItem[]>([])
|
||||
const [isExecuting, setIsExecuting] = useState(false)
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||
const outputRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 格式化结果
|
||||
const formatResult = (value: unknown): string => {
|
||||
if (value === null) return 'null'
|
||||
if (value === undefined) return 'undefined'
|
||||
if (typeof value === 'string') return `"${value}"`
|
||||
if (typeof value === 'function') return `[Function: ${value.name || 'anonymous'}]`
|
||||
if (typeof value === 'object') {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2)
|
||||
} catch {
|
||||
return value.toString()
|
||||
}
|
||||
// 持久的执行上下文,用于多轮对话共享作用域
|
||||
const executionContextRef = useRef<Record<string, unknown>>({})
|
||||
|
||||
// 格式化结果
|
||||
const formatResult = (value: unknown): string => {
|
||||
if (value === null) return 'null'
|
||||
if (value === undefined) return 'undefined'
|
||||
if (typeof value === 'string') return `"${value}"`
|
||||
if (typeof value === 'function') return `[Function: ${value.name || 'anonymous'}]`
|
||||
if (typeof value === 'object') {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2)
|
||||
} catch {
|
||||
return value.toString()
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
// 全局console拦截处理
|
||||
useEffect(() => {
|
||||
const interceptor = ConsoleInterceptor.getInstance()
|
||||
// 全局console拦截处理
|
||||
useEffect(() => {
|
||||
const interceptor = ConsoleInterceptor.getInstance()
|
||||
|
||||
const handleGlobalConsole = (type: string, args: unknown[]) => {
|
||||
const content = args.map((arg) => formatResult(arg)).join(' ')
|
||||
const handleGlobalConsole = (type: string, args: unknown[]) => {
|
||||
const content = args.map((arg) => formatResult(arg)).join(' ')
|
||||
|
||||
const outputItem: OutputItem = {
|
||||
type: type as any,
|
||||
content: content,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
setOutputs((prev) => [...prev, outputItem])
|
||||
|
||||
// 自动滚动到底部
|
||||
setTimeout(() => {
|
||||
if (outputRef.current) {
|
||||
outputRef.current.scrollTop = outputRef.current.scrollHeight
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
interceptor.subscribe(handleGlobalConsole)
|
||||
|
||||
return () => {
|
||||
interceptor.unsubscribe(handleGlobalConsole)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 执行代码
|
||||
const executeCode = async (code: string): Promise<unknown> => {
|
||||
if (!code.trim()) return
|
||||
|
||||
setIsExecuting(true)
|
||||
|
||||
// 添加输入到输出
|
||||
const inputItem: OutputItem = {
|
||||
type: 'input',
|
||||
content: code,
|
||||
const outputItem: OutputItem = {
|
||||
type: type as any,
|
||||
content: content,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
setOutputs((prev) => [...prev, inputItem])
|
||||
setOutputs((prev) => [...prev, outputItem])
|
||||
|
||||
try {
|
||||
// 创建异步函数以支持 await
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
|
||||
|
||||
// 合并外部上下文和持久执行上下文
|
||||
const allContext = { ...context, ...executionContextRef.current }
|
||||
const contextKeys = Object.keys(allContext)
|
||||
const contextValues = Object.values(allContext)
|
||||
|
||||
// 注入 console.log 重定向
|
||||
const logs: string[] = []
|
||||
const mockConsole = {
|
||||
log: (...args: unknown[]) => {
|
||||
logs.push(args.map((arg) => formatResult(arg)).join(' '))
|
||||
},
|
||||
error: (...args: unknown[]) => {
|
||||
logs.push('ERROR: ' + args.map((arg) => formatResult(arg)).join(' '))
|
||||
},
|
||||
warn: (...args: unknown[]) => {
|
||||
logs.push('WARN: ' + args.map((arg) => formatResult(arg)).join(' '))
|
||||
},
|
||||
// 自动滚动到底部
|
||||
setTimeout(() => {
|
||||
if (outputRef.current) {
|
||||
outputRef.current.scrollTop = outputRef.current.scrollHeight
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// 检测代码是否是表达式还是语句
|
||||
const trimmedCode = code.trim()
|
||||
const isExpression =
|
||||
!trimmedCode.includes(';') &&
|
||||
!trimmedCode.startsWith('const ') &&
|
||||
!trimmedCode.startsWith('let ') &&
|
||||
!trimmedCode.startsWith('var ') &&
|
||||
!trimmedCode.startsWith('function ') &&
|
||||
!trimmedCode.startsWith('class ') &&
|
||||
!trimmedCode.startsWith('if ') &&
|
||||
!trimmedCode.startsWith('for ') &&
|
||||
!trimmedCode.startsWith('while ') &&
|
||||
!trimmedCode.startsWith('try ') &&
|
||||
!trimmedCode.startsWith('{') &&
|
||||
!trimmedCode.includes('\n')
|
||||
interceptor.subscribe(handleGlobalConsole)
|
||||
|
||||
// 如果是表达式,自动添加 return
|
||||
const codeToExecute = isExpression ? `return ${code}` : code
|
||||
return () => {
|
||||
interceptor.unsubscribe(handleGlobalConsole)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const wrappedCode = `
|
||||
// 执行代码
|
||||
const executeCode = async (code: string): Promise<unknown> => {
|
||||
if (!code.trim()) return
|
||||
|
||||
setIsExecuting(true)
|
||||
|
||||
// 添加输入到输出
|
||||
const inputItem: OutputItem = {
|
||||
type: 'input',
|
||||
content: code,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
setOutputs((prev) => [...prev, inputItem])
|
||||
|
||||
try {
|
||||
// 创建异步函数以支持 await
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
|
||||
|
||||
// 合并外部上下文和持久执行上下文
|
||||
const allContext = { ...context, ...executionContextRef.current }
|
||||
const contextKeys = Object.keys(allContext)
|
||||
const contextValues = Object.values(allContext)
|
||||
|
||||
// 注入 console.log 重定向
|
||||
const logs: string[] = []
|
||||
const mockConsole = {
|
||||
log: (...args: unknown[]) => {
|
||||
logs.push(args.map((arg) => formatResult(arg)).join(' '))
|
||||
},
|
||||
error: (...args: unknown[]) => {
|
||||
logs.push('ERROR: ' + args.map((arg) => formatResult(arg)).join(' '))
|
||||
},
|
||||
warn: (...args: unknown[]) => {
|
||||
logs.push('WARN: ' + args.map((arg) => formatResult(arg)).join(' '))
|
||||
},
|
||||
}
|
||||
|
||||
// 检测代码是否是表达式还是语句
|
||||
const trimmedCode = code.trim()
|
||||
const isExpression =
|
||||
!trimmedCode.includes(';') &&
|
||||
!trimmedCode.startsWith('const ') &&
|
||||
!trimmedCode.startsWith('let ') &&
|
||||
!trimmedCode.startsWith('var ') &&
|
||||
!trimmedCode.startsWith('function ') &&
|
||||
!trimmedCode.startsWith('class ') &&
|
||||
!trimmedCode.startsWith('if ') &&
|
||||
!trimmedCode.startsWith('for ') &&
|
||||
!trimmedCode.startsWith('while ') &&
|
||||
!trimmedCode.startsWith('try ') &&
|
||||
!trimmedCode.startsWith('{') &&
|
||||
!trimmedCode.includes('\n')
|
||||
|
||||
// 如果是表达式,自动添加 return
|
||||
const codeToExecute = isExpression ? `return ${code}` : code
|
||||
|
||||
const wrappedCode = `
|
||||
return (async function() {
|
||||
${codeToExecute}
|
||||
})();
|
||||
`
|
||||
|
||||
// 执行代码
|
||||
const func = new AsyncFunction('console', ...contextKeys, wrappedCode)
|
||||
const result = await func(mockConsole, ...contextValues)
|
||||
// 执行代码
|
||||
const func = new AsyncFunction('console', ...contextKeys, wrappedCode)
|
||||
const result = await func(mockConsole, ...contextValues)
|
||||
|
||||
// 添加 console.log 输出
|
||||
if (logs.length > 0) {
|
||||
const logItem: OutputItem = {
|
||||
type: 'log',
|
||||
content: logs.join('\n'),
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
setOutputs((prev) => [...prev, logItem])
|
||||
}
|
||||
|
||||
// 总是添加执行结果输出(包括 undefined)
|
||||
const outputItem: OutputItem = {
|
||||
type: 'output',
|
||||
content: formatResult(result),
|
||||
// 添加 console.log 输出
|
||||
if (logs.length > 0) {
|
||||
const logItem: OutputItem = {
|
||||
type: 'log',
|
||||
content: logs.join('\n'),
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
setOutputs((prev) => [...prev, outputItem])
|
||||
|
||||
onExecute?.(code, result)
|
||||
return result
|
||||
} catch (error) {
|
||||
const errorItem: OutputItem = {
|
||||
type: 'error',
|
||||
content: error instanceof Error ? error.message : String(error),
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
setOutputs((prev) => [...prev, errorItem])
|
||||
throw error
|
||||
} finally {
|
||||
setIsExecuting(false)
|
||||
// 滚动到底部
|
||||
setTimeout(() => {
|
||||
if (outputRef.current) {
|
||||
outputRef.current.scrollTop = outputRef.current.scrollHeight
|
||||
}
|
||||
}, 0)
|
||||
setOutputs((prev) => [...prev, logItem])
|
||||
}
|
||||
}
|
||||
|
||||
// 清空控制台
|
||||
const clear = () => {
|
||||
setOutputs([])
|
||||
// 同时清空执行上下文
|
||||
executionContextRef.current = {}
|
||||
}
|
||||
|
||||
// 添加输出
|
||||
const appendOutput = (content: string) => {
|
||||
// 总是添加执行结果输出(包括 undefined)
|
||||
const outputItem: OutputItem = {
|
||||
type: 'output',
|
||||
content,
|
||||
content: formatResult(result),
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
setOutputs((prev) => [...prev, outputItem])
|
||||
|
||||
onExecute?.(code, result)
|
||||
return result
|
||||
} catch (error) {
|
||||
const errorItem: OutputItem = {
|
||||
type: 'error',
|
||||
content: error instanceof Error ? error.message : String(error),
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
setOutputs((prev) => [...prev, errorItem])
|
||||
throw error
|
||||
} finally {
|
||||
setIsExecuting(false)
|
||||
// 滚动到底部
|
||||
setTimeout(() => {
|
||||
if (outputRef.current) {
|
||||
outputRef.current.scrollTop = outputRef.current.scrollHeight
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
executeCode,
|
||||
clear,
|
||||
appendOutput,
|
||||
}))
|
||||
// 清空控制台
|
||||
const clear = () => {
|
||||
setOutputs([])
|
||||
// 同时清空执行上下文
|
||||
executionContextRef.current = {}
|
||||
}
|
||||
|
||||
// 处理键盘事件
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (e.shiftKey) {
|
||||
// Shift+Enter 换行
|
||||
return
|
||||
} else {
|
||||
// Enter 执行
|
||||
e.preventDefault()
|
||||
if (!isExecuting && input.trim()) {
|
||||
executeCode(input)
|
||||
setInput('')
|
||||
setTimeout(() => inputRef.current?.focus(), 0)
|
||||
}
|
||||
// 添加输出
|
||||
const appendOutput = (content: string) => {
|
||||
const outputItem: OutputItem = {
|
||||
type: 'output',
|
||||
content,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
setOutputs((prev) => [...prev, outputItem])
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
executeCode,
|
||||
clear,
|
||||
appendOutput,
|
||||
}))
|
||||
|
||||
// 处理键盘事件
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (e.shiftKey) {
|
||||
// Shift+Enter 换行
|
||||
return
|
||||
} else {
|
||||
// Enter 执行
|
||||
e.preventDefault()
|
||||
if (!isExecuting && input.trim()) {
|
||||
executeCode(input)
|
||||
setInput('')
|
||||
setTimeout(() => inputRef.current?.focus(), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getPrompt(type: string) {
|
||||
let prompt = ' '
|
||||
if (type === 'input') {
|
||||
prompt = '>'
|
||||
} else if (type === 'output') {
|
||||
prompt = '<'
|
||||
}
|
||||
return prompt
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.console} style={{ height }}>
|
||||
{/* 历史记录和输入区域 */}
|
||||
<div className={styles.historyArea} ref={outputRef}>
|
||||
{outputs.map((item, index) => (
|
||||
<div key={index} className={`${styles.historyItem} ${styles[item.type]}`}>
|
||||
<span className={styles.prompt}>{getPrompt(item.type)}</span>
|
||||
<pre className={styles.content}>
|
||||
<HighlightSyntax code={item.content} />
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
{isExecuting && (
|
||||
<div className={styles.historyItem}>
|
||||
<span className={styles.prompt}>{'> '}</span>
|
||||
<span className={styles.executing}>Executing...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 当前输入行 */}
|
||||
<div className={styles.inputArea}>
|
||||
<span className={styles.prompt}>{'> '}</span>
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
className={styles.input}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
disabled={isExecuting}
|
||||
rows={1}
|
||||
style={{
|
||||
height: Math.min(Math.max(20, input.split('\n').length * 20), 120),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
JSConsole.displayName = 'JSConsole'
|
||||
function getPrompt(type: string) {
|
||||
let prompt = ' '
|
||||
if (type === 'input') {
|
||||
prompt = '>'
|
||||
} else if (type === 'output') {
|
||||
prompt = '<'
|
||||
}
|
||||
return prompt
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.console} style={{ height }}>
|
||||
{/* 历史记录和输入区域 */}
|
||||
<div className={styles.historyArea} ref={outputRef}>
|
||||
{outputs.map((item) => (
|
||||
<div key={item.timestamp} className={`${styles.historyItem} ${styles[item.type]}`}>
|
||||
<span className={styles.prompt}>{getPrompt(item.type)}</span>
|
||||
<pre className={styles.content}>
|
||||
<HighlightSyntax code={item.content} />
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
{isExecuting && (
|
||||
<div className={styles.historyItem}>
|
||||
<span className={styles.prompt}>{'> '}</span>
|
||||
<span className={styles.executing}>Executing...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 当前输入行 */}
|
||||
<div className={styles.inputArea}>
|
||||
<span className={styles.prompt}>{'> '}</span>
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
className={styles.input}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
disabled={isExecuting}
|
||||
rows={1}
|
||||
style={{
|
||||
height: Math.min(Math.max(20, input.split('\n').length * 20), 120),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default JSConsole
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { Route, Router, Switch } from 'wouter'
|
||||
import { useHashLocation } from 'wouter/use-hash-location'
|
||||
@@ -8,23 +7,11 @@ import { default as TestPagesRouter } from './test-pages/router.tsx'
|
||||
|
||||
import './index.css'
|
||||
|
||||
let baseURL: string
|
||||
|
||||
// 如果是 localhost,就用 /
|
||||
// 如果是其他环境,阶段到 index.html
|
||||
if (window.location.hostname === 'localhost') {
|
||||
baseURL = '/'
|
||||
} else {
|
||||
baseURL = window.location.pathname.split('index.html')[0] + 'index.html'
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
// <StrictMode>
|
||||
<Router hook={useHashLocation}>
|
||||
<Switch>
|
||||
<Route path="/test-pages" component={TestPagesRouter} nest />
|
||||
<Route path="/" component={PagesRouter} nest />
|
||||
</Switch>
|
||||
</Router>
|
||||
// </StrictMode>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link } from 'wouter'
|
||||
|
||||
interface UploadProgress {
|
||||
@@ -33,7 +33,7 @@ export default function AsyncTestPage() {
|
||||
isRunning: false,
|
||||
progress: 0,
|
||||
currentStep: '',
|
||||
logs: []
|
||||
logs: [],
|
||||
})
|
||||
|
||||
// 模拟实时数据更新
|
||||
@@ -42,7 +42,7 @@ export default function AsyncTestPage() {
|
||||
if (isRealTimeActive) {
|
||||
interval = setInterval(() => {
|
||||
const newData = `数据更新 ${new Date().toLocaleTimeString()}: ${Math.floor(Math.random() * 1000)}`
|
||||
setRealTimeData(prev => [newData, ...prev.slice(0, 9)]) // 保持最新10条
|
||||
setRealTimeData((prev) => [newData, ...prev.slice(0, 9)]) // 保持最新10条
|
||||
}, 2000)
|
||||
}
|
||||
return () => {
|
||||
@@ -59,50 +59,53 @@ export default function AsyncTestPage() {
|
||||
progress: 0,
|
||||
status: 'uploading',
|
||||
speed: '0 KB/s',
|
||||
timeRemaining: '计算中...'
|
||||
timeRemaining: '计算中...',
|
||||
}
|
||||
|
||||
setUploads(prev => [...prev, newUpload])
|
||||
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)}秒`
|
||||
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 > 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newProgress >= 100) {
|
||||
clearInterval(interval)
|
||||
return {
|
||||
...upload,
|
||||
progress: 100,
|
||||
status: 'completed' as const,
|
||||
progress: newProgress,
|
||||
speed,
|
||||
timeRemaining
|
||||
timeRemaining,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...upload,
|
||||
progress: newProgress,
|
||||
speed,
|
||||
timeRemaining
|
||||
}
|
||||
}
|
||||
return upload
|
||||
}))
|
||||
return upload
|
||||
})
|
||||
)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
@@ -117,36 +120,38 @@ export default function AsyncTestPage() {
|
||||
title: '',
|
||||
content: '',
|
||||
timestamp: '',
|
||||
status: 'loading'
|
||||
status: 'loading',
|
||||
}))
|
||||
setDataItems(skeletonItems)
|
||||
|
||||
// 逐个加载数据项
|
||||
for (let i = 0; i < 6; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 800 + Math.random() * 1000))
|
||||
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: '数据加载失败,请重试',
|
||||
}
|
||||
}
|
||||
|
||||
setDataItems(prev => prev.map(item => {
|
||||
if (item.id === i) {
|
||||
// 模拟随机加载失败
|
||||
if (Math.random() < 0.15) {
|
||||
return {
|
||||
...item,
|
||||
status: 'error',
|
||||
title: '加载失败',
|
||||
content: '数据加载失败,请重试'
|
||||
status: 'loaded',
|
||||
title: `数据项 ${i + 1}`,
|
||||
content: `这是第 ${i + 1} 个数据项的内容,包含了一些示例文本用于展示加载效果。`,
|
||||
timestamp: new Date().toLocaleString(),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
status: 'loaded',
|
||||
title: `数据项 ${i + 1}`,
|
||||
content: `这是第 ${i + 1} 个数据项的内容,包含了一些示例文本用于展示加载效果。`,
|
||||
timestamp: new Date().toLocaleString()
|
||||
}
|
||||
}
|
||||
return item
|
||||
}))
|
||||
return item
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
setIsLoadingData(false)
|
||||
@@ -158,7 +163,7 @@ export default function AsyncTestPage() {
|
||||
isRunning: true,
|
||||
progress: 0,
|
||||
currentStep: '初始化任务...',
|
||||
logs: ['任务开始']
|
||||
logs: ['任务开始'],
|
||||
})
|
||||
|
||||
const steps = [
|
||||
@@ -169,55 +174,55 @@ export default function AsyncTestPage() {
|
||||
{ name: '处理数据...', duration: 2500 },
|
||||
{ name: '生成报告...', duration: 2000 },
|
||||
{ name: '保存结果...', duration: 1000 },
|
||||
{ name: '清理资源...', duration: 500 }
|
||||
{ name: '清理资源...', duration: 500 },
|
||||
]
|
||||
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
const step = steps[i]
|
||||
|
||||
setLongRunningTask(prev => ({
|
||||
setLongRunningTask((prev) => ({
|
||||
...prev,
|
||||
currentStep: step.name,
|
||||
logs: [...prev.logs, `开始: ${step.name}`]
|
||||
logs: [...prev.logs, `开始: ${step.name}`],
|
||||
}))
|
||||
|
||||
// 模拟步骤执行时间
|
||||
const startTime = Date.now()
|
||||
while (Date.now() - startTime < step.duration) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
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 => ({
|
||||
setLongRunningTask((prev) => ({
|
||||
...prev,
|
||||
progress: totalProgress
|
||||
progress: totalProgress,
|
||||
}))
|
||||
}
|
||||
|
||||
setLongRunningTask(prev => ({
|
||||
setLongRunningTask((prev) => ({
|
||||
...prev,
|
||||
logs: [...prev.logs, `完成: ${step.name}`]
|
||||
logs: [...prev.logs, `完成: ${step.name}`],
|
||||
}))
|
||||
|
||||
// 模拟随机失败
|
||||
if (i === 3 && Math.random() < 0.2) {
|
||||
setLongRunningTask(prev => ({
|
||||
setLongRunningTask((prev) => ({
|
||||
...prev,
|
||||
isRunning: false,
|
||||
currentStep: '任务失败',
|
||||
logs: [...prev.logs, '错误: 数据下载失败,请重试']
|
||||
logs: [...prev.logs, '错误: 数据下载失败,请重试'],
|
||||
}))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setLongRunningTask(prev => ({
|
||||
setLongRunningTask((prev) => ({
|
||||
...prev,
|
||||
isRunning: false,
|
||||
progress: 100,
|
||||
currentStep: '任务完成',
|
||||
logs: [...prev.logs, '任务成功完成!']
|
||||
logs: [...prev.logs, '任务成功完成!'],
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -226,34 +231,38 @@ export default function AsyncTestPage() {
|
||||
}
|
||||
|
||||
const retryFailedUpload = (uploadId: string) => {
|
||||
const failedUpload = uploads.find(u => u.id === uploadId)
|
||||
const failedUpload = uploads.find((u) => u.id === uploadId)
|
||||
if (failedUpload) {
|
||||
setUploads(prev => prev.filter(u => u.id !== uploadId))
|
||||
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 => {
|
||||
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, 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)
|
||||
}
|
||||
|
||||
@@ -261,9 +270,7 @@ export default function AsyncTestPage() {
|
||||
<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>
|
||||
<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>
|
||||
@@ -301,29 +308,40 @@ export default function AsyncTestPage() {
|
||||
点击"开始上传"来模拟文件上传
|
||||
</div>
|
||||
) : (
|
||||
uploads.map(upload => (
|
||||
<div key={upload.id} className="border border-gray-200 dark:border-gray-600 rounded-lg p-4">
|
||||
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
|
||||
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'
|
||||
upload.status === 'completed'
|
||||
? 'bg-green-500'
|
||||
: upload.status === 'error'
|
||||
? 'bg-red-500'
|
||||
: 'bg-blue-500'
|
||||
}`}
|
||||
style={{ width: `${upload.progress}%` }}
|
||||
/>
|
||||
@@ -375,18 +393,16 @@ export default function AsyncTestPage() {
|
||||
点击"开始更新"来查看实时数据
|
||||
</div>
|
||||
) : (
|
||||
realTimeData.map((data, index) => (
|
||||
realTimeData.map((data) => (
|
||||
<div
|
||||
key={index}
|
||||
key={data}
|
||||
className={`p-3 rounded-lg border transition-all duration-300 ${
|
||||
index === 0
|
||||
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>
|
||||
<span className="text-sm text-gray-900 dark:text-white">{data}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
@@ -413,8 +429,11 @@ export default function AsyncTestPage() {
|
||||
</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">
|
||||
{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>
|
||||
@@ -458,9 +477,7 @@ export default function AsyncTestPage() {
|
||||
{/* 长时间运行任务 */}
|
||||
<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>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">长时间任务</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={startLongRunningTask}
|
||||
@@ -496,11 +513,17 @@ export default function AsyncTestPage() {
|
||||
执行日志:
|
||||
</h4>
|
||||
<div className="space-y-1">
|
||||
{longRunningTask.logs.map((log, index) => (
|
||||
<div key={index} className="text-sm text-gray-600 dark:text-gray-300 font-mono">
|
||||
{log}
|
||||
</div>
|
||||
))}
|
||||
{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>
|
||||
)}
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function NavigationTestPage() {
|
||||
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 }
|
||||
{ id: 3, title: '订单更新', content: '您的订单已发货', time: '3小时前', unread: false },
|
||||
])
|
||||
|
||||
const handleBreadcrumbClick = (index: number) => {
|
||||
@@ -19,14 +19,12 @@ export default function NavigationTestPage() {
|
||||
}
|
||||
|
||||
const markNotificationAsRead = (id: number) => {
|
||||
setNotifications(prev =>
|
||||
prev.map(notif =>
|
||||
notif.id === id ? { ...notif, unread: false } : notif
|
||||
)
|
||||
setNotifications((prev) =>
|
||||
prev.map((notif) => (notif.id === id ? { ...notif, unread: false } : notif))
|
||||
)
|
||||
}
|
||||
|
||||
const unreadCount = notifications.filter(n => n.unread).length
|
||||
const unreadCount = notifications.filter((n) => n.unread).length
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
@@ -36,14 +34,15 @@ export default function NavigationTestPage() {
|
||||
<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 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
|
||||
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>
|
||||
|
||||
@@ -54,25 +53,47 @@ export default function NavigationTestPage() {
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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>
|
||||
@@ -80,10 +101,16 @@ export default function NavigationTestPage() {
|
||||
)}
|
||||
</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
|
||||
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
|
||||
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>
|
||||
@@ -94,7 +121,12 @@ export default function NavigationTestPage() {
|
||||
<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" />
|
||||
<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">
|
||||
@@ -121,17 +153,29 @@ export default function NavigationTestPage() {
|
||||
<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
|
||||
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
|
||||
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
|
||||
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
|
||||
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>
|
||||
@@ -148,25 +192,39 @@ export default function NavigationTestPage() {
|
||||
<div className="max-w-7xl mx-auto px-4 py-3">
|
||||
<nav className="flex" aria-label="Breadcrumb">
|
||||
<ol className="flex items-center space-x-2">
|
||||
{breadcrumbs.map((crumb, index) => (
|
||||
<li key={index} className="flex items-center">
|
||||
{index > 0 && (
|
||||
<svg className="h-4 w-4 text-gray-400 mx-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleBreadcrumbClick(index)}
|
||||
className={`text-sm font-medium transition-colors ${
|
||||
index === breadcrumbs.length - 1
|
||||
? 'text-gray-500 dark:text-gray-400 cursor-default'
|
||||
: 'text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300'
|
||||
}`}
|
||||
>
|
||||
{crumb}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{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>
|
||||
@@ -183,7 +241,7 @@ export default function NavigationTestPage() {
|
||||
{ id: 'products', label: '产品列表', icon: '📱' },
|
||||
{ id: 'orders', label: '订单管理', icon: '📦' },
|
||||
{ id: 'analytics', label: '数据分析', icon: '📊' },
|
||||
{ id: 'settings', label: '设置', icon: '⚙️' }
|
||||
{ id: 'settings', label: '设置', icon: '⚙️' },
|
||||
].map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
@@ -231,8 +289,11 @@ export default function NavigationTestPage() {
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">产品列表</h2>
|
||||
<div className="space-y-4">
|
||||
{['iPhone 15 Pro', 'MacBook Air', 'iPad Pro', 'Apple Watch'].map((product, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-600 rounded-lg">
|
||||
{['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>
|
||||
@@ -253,31 +314,49 @@ export default function NavigationTestPage() {
|
||||
<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>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
订单号
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
客户
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
状态
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
金额
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-600">
|
||||
{[
|
||||
{ id: '#001', customer: '张三', status: '已发货', amount: '¥1,299' },
|
||||
{ id: '#002', customer: '李四', status: '处理中', amount: '¥2,599' },
|
||||
{ id: '#003', customer: '王五', status: '已完成', amount: '¥899' }
|
||||
].map((order, index) => (
|
||||
<tr key={index}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{order.id}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{order.customer}</td>
|
||||
{ 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'
|
||||
}`}>
|
||||
<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>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
||||
{order.amount}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -291,13 +370,17 @@ export default function NavigationTestPage() {
|
||||
<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>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
销售趋势
|
||||
</h3>
|
||||
<div className="h-32 bg-gradient-to-r from-blue-400 to-purple-500 rounded-lg flex items-center justify-center text-white">
|
||||
📈 图表占位符
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-700 p-6 rounded-lg">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">用户分布</h3>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
用户分布
|
||||
</h3>
|
||||
<div className="h-32 bg-gradient-to-r from-green-400 to-blue-500 rounded-lg flex items-center justify-center text-white">
|
||||
🗺️ 地图占位符
|
||||
</div>
|
||||
@@ -311,27 +394,46 @@ export default function NavigationTestPage() {
|
||||
<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>
|
||||
<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 />
|
||||
<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" />
|
||||
<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>
|
||||
<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 />
|
||||
<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 />
|
||||
<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>
|
||||
@@ -362,9 +464,9 @@ export default function NavigationTestPage() {
|
||||
title: '新通知',
|
||||
content: `这是第 ${notifications.length + 1} 条通知`,
|
||||
time: '刚刚',
|
||||
unread: true
|
||||
unread: true,
|
||||
}
|
||||
setNotifications(prev => [newNotif, ...prev])
|
||||
setNotifications((prev) => [newNotif, ...prev])
|
||||
}}
|
||||
className="bg-purple-600 hover:bg-purple-700 text-white px-6 py-2 rounded-md transition-colors"
|
||||
>
|
||||
@@ -389,11 +491,17 @@ export default function NavigationTestPage() {
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
)}
|
||||
@@ -417,7 +525,12 @@ export default function NavigationTestPage() {
|
||||
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" />
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -84,12 +84,11 @@ export class LLM {
|
||||
usage: LanguageModelUsage
|
||||
}> {
|
||||
const isClaude = this.config.modelName.slice(0, 8).includes('claude')
|
||||
const isQwen = this.config.modelName.slice(0, 6).includes('qwen')
|
||||
const isGPT = this.config.modelName.slice(0, 5).includes('gpt')
|
||||
// const isQwen = this.config.modelName.slice(0, 6).includes('qwen')
|
||||
// const isGPT = this.config.modelName.slice(0, 5).includes('gpt')
|
||||
|
||||
return await withRetry(
|
||||
async () => {
|
||||
// try {
|
||||
const result = await generateText({
|
||||
model: this.#model,
|
||||
messages,
|
||||
@@ -165,16 +164,6 @@ export class LLM {
|
||||
toolResult,
|
||||
usage,
|
||||
}
|
||||
|
||||
// } catch (error) {
|
||||
// // handle ai-sdk internal error here
|
||||
// // currently useless since we bypassed most of ai-sdk logic
|
||||
// console.log('generateText error', error)
|
||||
// console.log('APICallError', APICallError.isInstance(error))
|
||||
// console.log('isNoSuchModelError', NoSuchModelError.isInstance(error))
|
||||
|
||||
// throw error
|
||||
// }
|
||||
},
|
||||
// retry settings
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user