chore: clean up lint warnings
This commit is contained in:
@@ -74,11 +74,14 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
|
|||||||
{showLineNumbers && (
|
{showLineNumbers && (
|
||||||
<div className={`flex-shrink-0 px-4 py-4 ${lineNumbersClasses} border-r select-none`}>
|
<div className={`flex-shrink-0 px-4 py-4 ${lineNumbersClasses} border-r select-none`}>
|
||||||
<div className="text-xs font-mono leading-6">
|
<div className="text-xs font-mono leading-6">
|
||||||
{lines.map((_, index) => (
|
{lines.map((line, lineIdx) => {
|
||||||
<div key={index} className="text-right">
|
const lineNum = lineIdx + 1
|
||||||
{index + 1}
|
return (
|
||||||
</div>
|
<div key={`${lineNum}-${line.substring(0, 20)}`} className="text-right">
|
||||||
))}
|
{lineNum}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* JS 调试台,适合在文档中直接让用户运行代码,并且实时查看运行结果
|
* JS 调试台,适合在文档中直接让用户运行代码,并且实时查看运行结果
|
||||||
*/
|
*/
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
||||||
/* eslint-disable @typescript-eslint/no-base-to-string */
|
/* 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'
|
import HighlightSyntax from './HighlightSyntax'
|
||||||
|
|
||||||
@@ -29,7 +28,6 @@ class ConsoleInterceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static getInstance() {
|
static getInstance() {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
||||||
if (!ConsoleInterceptor.instance) {
|
if (!ConsoleInterceptor.instance) {
|
||||||
ConsoleInterceptor.instance = new ConsoleInterceptor()
|
ConsoleInterceptor.instance = new ConsoleInterceptor()
|
||||||
}
|
}
|
||||||
@@ -90,6 +88,7 @@ interface JSConsoleProps {
|
|||||||
height?: string
|
height?: string
|
||||||
onExecute?: (code: string, result: unknown) => void
|
onExecute?: (code: string, result: unknown) => void
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
|
ref?: React.Ref<JSConsoleRef>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JSConsoleRef {
|
export interface JSConsoleRef {
|
||||||
@@ -104,266 +103,267 @@ interface OutputItem {
|
|||||||
timestamp: number
|
timestamp: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const JSConsole = forwardRef<JSConsoleRef, JSConsoleProps>(
|
const DEFAULT_CONTEXT = {}
|
||||||
(
|
|
||||||
{ 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)
|
|
||||||
|
|
||||||
// 持久的执行上下文,用于多轮对话共享作用域
|
function JSConsole({
|
||||||
const executionContextRef = useRef<Record<string, unknown>>({})
|
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 => {
|
const executionContextRef = useRef<Record<string, unknown>>({})
|
||||||
if (value === null) return 'null'
|
|
||||||
if (value === undefined) return 'undefined'
|
// 格式化结果
|
||||||
if (typeof value === 'string') return `"${value}"`
|
const formatResult = (value: unknown): string => {
|
||||||
if (typeof value === 'function') return `[Function: ${value.name || 'anonymous'}]`
|
if (value === null) return 'null'
|
||||||
if (typeof value === 'object') {
|
if (value === undefined) return 'undefined'
|
||||||
try {
|
if (typeof value === 'string') return `"${value}"`
|
||||||
return JSON.stringify(value, null, 2)
|
if (typeof value === 'function') return `[Function: ${value.name || 'anonymous'}]`
|
||||||
} catch {
|
if (typeof value === 'object') {
|
||||||
return value.toString()
|
try {
|
||||||
}
|
return JSON.stringify(value, null, 2)
|
||||||
|
} catch {
|
||||||
|
return value.toString()
|
||||||
}
|
}
|
||||||
return String(value)
|
|
||||||
}
|
}
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
|
||||||
// 全局console拦截处理
|
// 全局console拦截处理
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interceptor = ConsoleInterceptor.getInstance()
|
const interceptor = ConsoleInterceptor.getInstance()
|
||||||
|
|
||||||
const handleGlobalConsole = (type: string, args: unknown[]) => {
|
const handleGlobalConsole = (type: string, args: unknown[]) => {
|
||||||
const content = args.map((arg) => formatResult(arg)).join(' ')
|
const content = args.map((arg) => formatResult(arg)).join(' ')
|
||||||
|
|
||||||
const outputItem: OutputItem = {
|
const outputItem: OutputItem = {
|
||||||
type: type as any,
|
type: type as any,
|
||||||
content: content,
|
content: content,
|
||||||
timestamp: Date.now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
setOutputs((prev) => [...prev, outputItem])
|
|
||||||
|
|
||||||
// 自动滚动到底部
|
|
||||||
setTimeout(() => {
|
|
||||||
if (outputRef.current) {
|
|
||||||
outputRef.current.scrollTop = outputRef.current.scrollHeight
|
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
interceptor.subscribe(handleGlobalConsole)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
interceptor.unsubscribe(handleGlobalConsole)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// 执行代码
|
|
||||||
const executeCode = async (code: string): Promise<unknown> => {
|
|
||||||
if (!code.trim()) return
|
|
||||||
|
|
||||||
setIsExecuting(true)
|
|
||||||
|
|
||||||
// 添加输入到输出
|
|
||||||
const inputItem: OutputItem = {
|
|
||||||
type: 'input',
|
|
||||||
content: code,
|
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
setOutputs((prev) => [...prev, inputItem])
|
setOutputs((prev) => [...prev, outputItem])
|
||||||
|
|
||||||
try {
|
// 自动滚动到底部
|
||||||
// 创建异步函数以支持 await
|
setTimeout(() => {
|
||||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
|
if (outputRef.current) {
|
||||||
|
outputRef.current.scrollTop = outputRef.current.scrollHeight
|
||||||
// 合并外部上下文和持久执行上下文
|
|
||||||
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(' '))
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
// 检测代码是否是表达式还是语句
|
interceptor.subscribe(handleGlobalConsole)
|
||||||
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
|
return () => {
|
||||||
const codeToExecute = isExpression ? `return ${code}` : code
|
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() {
|
return (async function() {
|
||||||
${codeToExecute}
|
${codeToExecute}
|
||||||
})();
|
})();
|
||||||
`
|
`
|
||||||
|
|
||||||
// 执行代码
|
// 执行代码
|
||||||
const func = new AsyncFunction('console', ...contextKeys, wrappedCode)
|
const func = new AsyncFunction('console', ...contextKeys, wrappedCode)
|
||||||
const result = await func(mockConsole, ...contextValues)
|
const result = await func(mockConsole, ...contextValues)
|
||||||
|
|
||||||
// 添加 console.log 输出
|
// 添加 console.log 输出
|
||||||
if (logs.length > 0) {
|
if (logs.length > 0) {
|
||||||
const logItem: OutputItem = {
|
const logItem: OutputItem = {
|
||||||
type: 'log',
|
type: 'log',
|
||||||
content: logs.join('\n'),
|
content: logs.join('\n'),
|
||||||
timestamp: Date.now(),
|
|
||||||
}
|
|
||||||
setOutputs((prev) => [...prev, logItem])
|
|
||||||
}
|
|
||||||
|
|
||||||
// 总是添加执行结果输出(包括 undefined)
|
|
||||||
const outputItem: OutputItem = {
|
|
||||||
type: 'output',
|
|
||||||
content: formatResult(result),
|
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
}
|
}
|
||||||
setOutputs((prev) => [...prev, outputItem])
|
setOutputs((prev) => [...prev, logItem])
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 清空控制台
|
// 总是添加执行结果输出(包括 undefined)
|
||||||
const clear = () => {
|
|
||||||
setOutputs([])
|
|
||||||
// 同时清空执行上下文
|
|
||||||
executionContextRef.current = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加输出
|
|
||||||
const appendOutput = (content: string) => {
|
|
||||||
const outputItem: OutputItem = {
|
const outputItem: OutputItem = {
|
||||||
type: 'output',
|
type: 'output',
|
||||||
content,
|
content: formatResult(result),
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
}
|
}
|
||||||
setOutputs((prev) => [...prev, outputItem])
|
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, () => ({
|
const clear = () => {
|
||||||
executeCode,
|
setOutputs([])
|
||||||
clear,
|
// 同时清空执行上下文
|
||||||
appendOutput,
|
executionContextRef.current = {}
|
||||||
}))
|
}
|
||||||
|
|
||||||
// 处理键盘事件
|
// 添加输出
|
||||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
const appendOutput = (content: string) => {
|
||||||
if (e.key === 'Enter') {
|
const outputItem: OutputItem = {
|
||||||
if (e.shiftKey) {
|
type: 'output',
|
||||||
// Shift+Enter 换行
|
content,
|
||||||
return
|
timestamp: Date.now(),
|
||||||
} else {
|
}
|
||||||
// Enter 执行
|
setOutputs((prev) => [...prev, outputItem])
|
||||||
e.preventDefault()
|
}
|
||||||
if (!isExecuting && input.trim()) {
|
|
||||||
executeCode(input)
|
// 暴露方法给父组件
|
||||||
setInput('')
|
useImperativeHandle(ref, () => ({
|
||||||
setTimeout(() => inputRef.current?.focus(), 0)
|
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
|
export default JSConsole
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// import { StrictMode } from 'react'
|
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import { Route, Router, Switch } from 'wouter'
|
import { Route, Router, Switch } from 'wouter'
|
||||||
import { useHashLocation } from 'wouter/use-hash-location'
|
import { useHashLocation } from 'wouter/use-hash-location'
|
||||||
@@ -8,23 +7,11 @@ import { default as TestPagesRouter } from './test-pages/router.tsx'
|
|||||||
|
|
||||||
import './index.css'
|
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(
|
createRoot(document.getElementById('root')!).render(
|
||||||
// <StrictMode>
|
|
||||||
<Router hook={useHashLocation}>
|
<Router hook={useHashLocation}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/test-pages" component={TestPagesRouter} nest />
|
<Route path="/test-pages" component={TestPagesRouter} nest />
|
||||||
<Route path="/" component={PagesRouter} nest />
|
<Route path="/" component={PagesRouter} nest />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Router>
|
</Router>
|
||||||
// </StrictMode>
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Link } from 'wouter'
|
import { Link } from 'wouter'
|
||||||
|
|
||||||
interface UploadProgress {
|
interface UploadProgress {
|
||||||
@@ -33,7 +33,7 @@ export default function AsyncTestPage() {
|
|||||||
isRunning: false,
|
isRunning: false,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
currentStep: '',
|
currentStep: '',
|
||||||
logs: []
|
logs: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
// 模拟实时数据更新
|
// 模拟实时数据更新
|
||||||
@@ -42,7 +42,7 @@ export default function AsyncTestPage() {
|
|||||||
if (isRealTimeActive) {
|
if (isRealTimeActive) {
|
||||||
interval = setInterval(() => {
|
interval = setInterval(() => {
|
||||||
const newData = `数据更新 ${new Date().toLocaleTimeString()}: ${Math.floor(Math.random() * 1000)}`
|
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)
|
}, 2000)
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
@@ -59,50 +59,53 @@ export default function AsyncTestPage() {
|
|||||||
progress: 0,
|
progress: 0,
|
||||||
status: 'uploading',
|
status: 'uploading',
|
||||||
speed: '0 KB/s',
|
speed: '0 KB/s',
|
||||||
timeRemaining: '计算中...'
|
timeRemaining: '计算中...',
|
||||||
}
|
}
|
||||||
|
|
||||||
setUploads(prev => [...prev, newUpload])
|
setUploads((prev) => [...prev, newUpload])
|
||||||
|
|
||||||
// 模拟上传进度
|
// 模拟上传进度
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setUploads(prev => prev.map(upload => {
|
setUploads((prev) =>
|
||||||
if (upload.id === uploadId) {
|
prev.map((upload) => {
|
||||||
const newProgress = Math.min(upload.progress + Math.random() * 15, 100)
|
if (upload.id === uploadId) {
|
||||||
const speed = `${(Math.random() * 500 + 100).toFixed(0)} KB/s`
|
const newProgress = Math.min(upload.progress + Math.random() * 15, 100)
|
||||||
const timeRemaining = newProgress >= 100 ? '完成' : `${Math.ceil((100 - newProgress) / 10)}秒`
|
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) {
|
if (newProgress > 50 && Math.random() < 0.1) {
|
||||||
clearInterval(interval)
|
clearInterval(interval)
|
||||||
return {
|
return {
|
||||||
...upload,
|
...upload,
|
||||||
status: 'error' as const,
|
status: 'error' as const,
|
||||||
speed: '0 KB/s',
|
speed: '0 KB/s',
|
||||||
timeRemaining: '失败'
|
timeRemaining: '失败',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newProgress >= 100) {
|
||||||
|
clearInterval(interval)
|
||||||
|
return {
|
||||||
|
...upload,
|
||||||
|
progress: 100,
|
||||||
|
status: 'completed' as const,
|
||||||
|
speed,
|
||||||
|
timeRemaining,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (newProgress >= 100) {
|
|
||||||
clearInterval(interval)
|
|
||||||
return {
|
return {
|
||||||
...upload,
|
...upload,
|
||||||
progress: 100,
|
progress: newProgress,
|
||||||
status: 'completed' as const,
|
|
||||||
speed,
|
speed,
|
||||||
timeRemaining
|
timeRemaining,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return upload
|
||||||
return {
|
})
|
||||||
...upload,
|
)
|
||||||
progress: newProgress,
|
|
||||||
speed,
|
|
||||||
timeRemaining
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return upload
|
|
||||||
}))
|
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,36 +120,38 @@ export default function AsyncTestPage() {
|
|||||||
title: '',
|
title: '',
|
||||||
content: '',
|
content: '',
|
||||||
timestamp: '',
|
timestamp: '',
|
||||||
status: 'loading'
|
status: 'loading',
|
||||||
}))
|
}))
|
||||||
setDataItems(skeletonItems)
|
setDataItems(skeletonItems)
|
||||||
|
|
||||||
// 逐个加载数据项
|
// 逐个加载数据项
|
||||||
for (let i = 0; i < 6; i++) {
|
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 {
|
return {
|
||||||
...item,
|
...item,
|
||||||
status: 'error',
|
status: 'loaded',
|
||||||
title: '加载失败',
|
title: `数据项 ${i + 1}`,
|
||||||
content: '数据加载失败,请重试'
|
content: `这是第 ${i + 1} 个数据项的内容,包含了一些示例文本用于展示加载效果。`,
|
||||||
|
timestamp: new Date().toLocaleString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return item
|
||||||
return {
|
})
|
||||||
...item,
|
)
|
||||||
status: 'loaded',
|
|
||||||
title: `数据项 ${i + 1}`,
|
|
||||||
content: `这是第 ${i + 1} 个数据项的内容,包含了一些示例文本用于展示加载效果。`,
|
|
||||||
timestamp: new Date().toLocaleString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoadingData(false)
|
setIsLoadingData(false)
|
||||||
@@ -158,7 +163,7 @@ export default function AsyncTestPage() {
|
|||||||
isRunning: true,
|
isRunning: true,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
currentStep: '初始化任务...',
|
currentStep: '初始化任务...',
|
||||||
logs: ['任务开始']
|
logs: ['任务开始'],
|
||||||
})
|
})
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
@@ -169,55 +174,55 @@ export default function AsyncTestPage() {
|
|||||||
{ name: '处理数据...', duration: 2500 },
|
{ name: '处理数据...', duration: 2500 },
|
||||||
{ name: '生成报告...', duration: 2000 },
|
{ name: '生成报告...', duration: 2000 },
|
||||||
{ name: '保存结果...', duration: 1000 },
|
{ name: '保存结果...', duration: 1000 },
|
||||||
{ name: '清理资源...', duration: 500 }
|
{ name: '清理资源...', duration: 500 },
|
||||||
]
|
]
|
||||||
|
|
||||||
for (let i = 0; i < steps.length; i++) {
|
for (let i = 0; i < steps.length; i++) {
|
||||||
const step = steps[i]
|
const step = steps[i]
|
||||||
|
|
||||||
setLongRunningTask(prev => ({
|
setLongRunningTask((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
currentStep: step.name,
|
currentStep: step.name,
|
||||||
logs: [...prev.logs, `开始: ${step.name}`]
|
logs: [...prev.logs, `开始: ${step.name}`],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// 模拟步骤执行时间
|
// 模拟步骤执行时间
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
while (Date.now() - startTime < step.duration) {
|
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 elapsed = Date.now() - startTime
|
||||||
const stepProgress = Math.min((elapsed / step.duration) * 100, 100)
|
const stepProgress = Math.min((elapsed / step.duration) * 100, 100)
|
||||||
const totalProgress = ((i + stepProgress / 100) / steps.length) * 100
|
const totalProgress = ((i + stepProgress / 100) / steps.length) * 100
|
||||||
|
|
||||||
setLongRunningTask(prev => ({
|
setLongRunningTask((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
progress: totalProgress
|
progress: totalProgress,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
setLongRunningTask(prev => ({
|
setLongRunningTask((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
logs: [...prev.logs, `完成: ${step.name}`]
|
logs: [...prev.logs, `完成: ${step.name}`],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// 模拟随机失败
|
// 模拟随机失败
|
||||||
if (i === 3 && Math.random() < 0.2) {
|
if (i === 3 && Math.random() < 0.2) {
|
||||||
setLongRunningTask(prev => ({
|
setLongRunningTask((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
isRunning: false,
|
isRunning: false,
|
||||||
currentStep: '任务失败',
|
currentStep: '任务失败',
|
||||||
logs: [...prev.logs, '错误: 数据下载失败,请重试']
|
logs: [...prev.logs, '错误: 数据下载失败,请重试'],
|
||||||
}))
|
}))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setLongRunningTask(prev => ({
|
setLongRunningTask((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
isRunning: false,
|
isRunning: false,
|
||||||
progress: 100,
|
progress: 100,
|
||||||
currentStep: '任务完成',
|
currentStep: '任务完成',
|
||||||
logs: [...prev.logs, '任务成功完成!']
|
logs: [...prev.logs, '任务成功完成!'],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,34 +231,38 @@ export default function AsyncTestPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const retryFailedUpload = (uploadId: string) => {
|
const retryFailedUpload = (uploadId: string) => {
|
||||||
const failedUpload = uploads.find(u => u.id === uploadId)
|
const failedUpload = uploads.find((u) => u.id === uploadId)
|
||||||
if (failedUpload) {
|
if (failedUpload) {
|
||||||
setUploads(prev => prev.filter(u => u.id !== uploadId))
|
setUploads((prev) => prev.filter((u) => u.id !== uploadId))
|
||||||
simulateFileUpload(failedUpload.name)
|
simulateFileUpload(failedUpload.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const retryDataLoad = (itemId: number) => {
|
const retryDataLoad = (itemId: number) => {
|
||||||
setDataItems(prev => prev.map(item => {
|
setDataItems((prev) =>
|
||||||
if (item.id === itemId) {
|
prev.map((item) => {
|
||||||
return { ...item, status: 'loading', title: '', content: '', timestamp: '' }
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
}))
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setDataItems(prev => prev.map(item => {
|
|
||||||
if (item.id === itemId) {
|
if (item.id === itemId) {
|
||||||
return {
|
return { ...item, status: 'loading', title: '', content: '', timestamp: '' }
|
||||||
...item,
|
|
||||||
status: 'loaded',
|
|
||||||
title: `数据项 ${itemId + 1}`,
|
|
||||||
content: `这是重新加载的第 ${itemId + 1} 个数据项的内容。`,
|
|
||||||
timestamp: new Date().toLocaleString()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return item
|
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)
|
}, 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="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
||||||
<div className="max-w-6xl mx-auto px-4">
|
<div className="max-w-6xl mx-auto px-4">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">异步操作测试</h1>
|
||||||
异步操作测试
|
|
||||||
</h1>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
测试等待、加载状态识别和异步操作处理能力
|
测试等待、加载状态识别和异步操作处理能力
|
||||||
</p>
|
</p>
|
||||||
@@ -301,29 +308,40 @@ export default function AsyncTestPage() {
|
|||||||
点击"开始上传"来模拟文件上传
|
点击"开始上传"来模拟文件上传
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
uploads.map(upload => (
|
uploads.map((upload) => (
|
||||||
<div key={upload.id} className="border border-gray-200 dark:border-gray-600 rounded-lg p-4">
|
<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">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<span className="font-medium text-gray-900 dark:text-white">
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
{upload.name}
|
{upload.name}
|
||||||
</span>
|
</span>
|
||||||
<span className={`text-sm ${
|
<span
|
||||||
upload.status === 'completed' ? 'text-green-600 dark:text-green-400' :
|
className={`text-sm ${
|
||||||
upload.status === 'error' ? 'text-red-600 dark:text-red-400' :
|
upload.status === 'completed'
|
||||||
'text-blue-600 dark:text-blue-400'
|
? 'text-green-600 dark:text-green-400'
|
||||||
}`}>
|
: upload.status === 'error'
|
||||||
{upload.status === 'completed' ? '✓ 完成' :
|
? 'text-red-600 dark:text-red-400'
|
||||||
upload.status === 'error' ? '✗ 失败' :
|
: 'text-blue-600 dark:text-blue-400'
|
||||||
'上传中...'}
|
}`}
|
||||||
|
>
|
||||||
|
{upload.status === 'completed'
|
||||||
|
? '✓ 完成'
|
||||||
|
: upload.status === 'error'
|
||||||
|
? '✗ 失败'
|
||||||
|
: '上传中...'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2">
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2">
|
||||||
<div
|
<div
|
||||||
className={`h-2 rounded-full transition-all duration-300 ${
|
className={`h-2 rounded-full transition-all duration-300 ${
|
||||||
upload.status === 'completed' ? 'bg-green-500' :
|
upload.status === 'completed'
|
||||||
upload.status === 'error' ? 'bg-red-500' :
|
? 'bg-green-500'
|
||||||
'bg-blue-500'
|
: upload.status === 'error'
|
||||||
|
? 'bg-red-500'
|
||||||
|
: 'bg-blue-500'
|
||||||
}`}
|
}`}
|
||||||
style={{ width: `${upload.progress}%` }}
|
style={{ width: `${upload.progress}%` }}
|
||||||
/>
|
/>
|
||||||
@@ -375,18 +393,16 @@ export default function AsyncTestPage() {
|
|||||||
点击"开始更新"来查看实时数据
|
点击"开始更新"来查看实时数据
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
realTimeData.map((data, index) => (
|
realTimeData.map((data) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={data}
|
||||||
className={`p-3 rounded-lg border transition-all duration-300 ${
|
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-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'
|
: 'bg-gray-50 dark:bg-gray-700 border-gray-200 dark:border-gray-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="text-sm text-gray-900 dark:text-white">
|
<span className="text-sm text-gray-900 dark:text-white">{data}</span>
|
||||||
{data}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -413,8 +429,11 @@ export default function AsyncTestPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{dataItems.map(item => (
|
{dataItems.map((item) => (
|
||||||
<div key={item.id} className="border border-gray-200 dark:border-gray-600 rounded-lg p-4">
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="border border-gray-200 dark:border-gray-600 rounded-lg p-4"
|
||||||
|
>
|
||||||
{item.status === 'loading' ? (
|
{item.status === 'loading' ? (
|
||||||
<div className="animate-pulse">
|
<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-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="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">长时间任务</h2>
|
||||||
长时间任务
|
|
||||||
</h2>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={startLongRunningTask}
|
onClick={startLongRunningTask}
|
||||||
@@ -496,11 +513,17 @@ export default function AsyncTestPage() {
|
|||||||
执行日志:
|
执行日志:
|
||||||
</h4>
|
</h4>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{longRunningTask.logs.map((log, index) => (
|
{longRunningTask.logs.map((log, logIdx) => {
|
||||||
<div key={index} className="text-sm text-gray-600 dark:text-gray-300 font-mono">
|
const logKey = `${logIdx + 1}-${log.substring(0, 30)}`
|
||||||
{log}
|
return (
|
||||||
</div>
|
<div
|
||||||
))}
|
key={logKey}
|
||||||
|
className="text-sm text-gray-600 dark:text-gray-300 font-mono"
|
||||||
|
>
|
||||||
|
{log}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default function NavigationTestPage() {
|
|||||||
const [notifications, setNotifications] = useState([
|
const [notifications, setNotifications] = useState([
|
||||||
{ id: 1, title: '新消息', content: '您有一条新的私信', time: '2分钟前', unread: true },
|
{ id: 1, title: '新消息', content: '您有一条新的私信', time: '2分钟前', unread: true },
|
||||||
{ id: 2, title: '系统通知', content: '系统将于今晚维护', time: '1小时前', 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) => {
|
const handleBreadcrumbClick = (index: number) => {
|
||||||
@@ -19,14 +19,12 @@ export default function NavigationTestPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const markNotificationAsRead = (id: number) => {
|
const markNotificationAsRead = (id: number) => {
|
||||||
setNotifications(prev =>
|
setNotifications((prev) =>
|
||||||
prev.map(notif =>
|
prev.map((notif) => (notif.id === id ? { ...notif, unread: false } : 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 (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
<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">
|
<div className="flex justify-between items-center h-16">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">
|
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">TestNav</div>
|
||||||
TestNav
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 主导航菜单 */}
|
{/* 主导航菜单 */}
|
||||||
<div className="hidden md:flex space-x-8">
|
<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>
|
</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"
|
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">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isDropdownOpen && (
|
{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="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">
|
<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>
|
||||||
<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>
|
||||||
<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>
|
||||||
<div className="border-t border-gray-200 dark:border-gray-600 my-1"></div>
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,10 +101,16 @@ export default function NavigationTestPage() {
|
|||||||
)}
|
)}
|
||||||
</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
|
||||||
|
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>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,7 +121,12 @@ export default function NavigationTestPage() {
|
|||||||
<div className="relative">
|
<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">
|
<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">
|
<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>
|
</svg>
|
||||||
{unreadCount > 0 && (
|
{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">
|
<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">
|
<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
|
user@example.com
|
||||||
</div>
|
</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>
|
||||||
<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>
|
||||||
<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>
|
||||||
<div className="border-t border-gray-200 dark:border-gray-600 my-1"></div>
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,25 +192,39 @@ export default function NavigationTestPage() {
|
|||||||
<div className="max-w-7xl mx-auto px-4 py-3">
|
<div className="max-w-7xl mx-auto px-4 py-3">
|
||||||
<nav className="flex" aria-label="Breadcrumb">
|
<nav className="flex" aria-label="Breadcrumb">
|
||||||
<ol className="flex items-center space-x-2">
|
<ol className="flex items-center space-x-2">
|
||||||
{breadcrumbs.map((crumb, index) => (
|
{breadcrumbs.map((crumb, crumbIdx) => {
|
||||||
<li key={index} className="flex items-center">
|
const isLast = crumbIdx === breadcrumbs.length - 1
|
||||||
{index > 0 && (
|
const showSeparator = crumbIdx > 0
|
||||||
<svg className="h-4 w-4 text-gray-400 mx-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
return (
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
<li key={`${crumb}-${crumbIdx + 1}`} className="flex items-center">
|
||||||
</svg>
|
{showSeparator && (
|
||||||
)}
|
<svg
|
||||||
<button
|
className="h-4 w-4 text-gray-400 mx-2"
|
||||||
onClick={() => handleBreadcrumbClick(index)}
|
fill="none"
|
||||||
className={`text-sm font-medium transition-colors ${
|
stroke="currentColor"
|
||||||
index === breadcrumbs.length - 1
|
viewBox="0 0 24 24"
|
||||||
? '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'
|
<path
|
||||||
}`}
|
strokeLinecap="round"
|
||||||
>
|
strokeLinejoin="round"
|
||||||
{crumb}
|
strokeWidth={2}
|
||||||
</button>
|
d="M9 5l7 7-7 7"
|
||||||
</li>
|
/>
|
||||||
))}
|
</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>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
@@ -183,7 +241,7 @@ export default function NavigationTestPage() {
|
|||||||
{ id: 'products', label: '产品列表', icon: '📱' },
|
{ id: 'products', label: '产品列表', icon: '📱' },
|
||||||
{ id: 'orders', label: '订单管理', icon: '📦' },
|
{ id: 'orders', label: '订单管理', icon: '📦' },
|
||||||
{ id: 'analytics', label: '数据分析', icon: '📊' },
|
{ id: 'analytics', label: '数据分析', icon: '📊' },
|
||||||
{ id: 'settings', label: '设置', icon: '⚙️' }
|
{ id: 'settings', label: '设置', icon: '⚙️' },
|
||||||
].map((tab) => (
|
].map((tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
@@ -231,8 +289,11 @@ export default function NavigationTestPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">产品列表</h2>
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">产品列表</h2>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{['iPhone 15 Pro', 'MacBook Air', 'iPad Pro', 'Apple Watch'].map((product, index) => (
|
{['iPhone 15 Pro', 'MacBook Air', 'iPad Pro', 'Apple Watch'].map((product) => (
|
||||||
<div key={index} className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-600 rounded-lg">
|
<div
|
||||||
|
key={product}
|
||||||
|
className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-600 rounded-lg"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium text-gray-900 dark:text-white">{product}</h3>
|
<h3 className="font-medium text-gray-900 dark:text-white">{product}</h3>
|
||||||
<p className="text-gray-500 dark:text-gray-400">产品描述...</p>
|
<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">
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
|
||||||
<thead className="bg-gray-50 dark:bg-gray-700">
|
<thead className="bg-gray-50 dark:bg-gray-700">
|
||||||
<tr>
|
<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 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>
|
||||||
<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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-600">
|
<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: '#001', customer: '张三', status: '已发货', amount: '¥1,299' },
|
||||||
{ id: '#002', customer: '李四', status: '处理中', amount: '¥2,599' },
|
{ id: '#002', customer: '李四', status: '处理中', amount: '¥2,599' },
|
||||||
{ id: '#003', customer: '王五', status: '已完成', amount: '¥899' }
|
{ id: '#003', customer: '王五', status: '已完成', amount: '¥899' },
|
||||||
].map((order, index) => (
|
].map((order) => (
|
||||||
<tr key={index}>
|
<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">
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{order.customer}</td>
|
{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">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<span className={`px-2 py-1 text-xs font-medium rounded-full ${
|
<span
|
||||||
order.status === '已完成' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' :
|
className={`px-2 py-1 text-xs font-medium rounded-full ${
|
||||||
order.status === '已发货' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' :
|
order.status === '已完成'
|
||||||
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
|
? '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}
|
{order.status}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -291,13 +370,17 @@ export default function NavigationTestPage() {
|
|||||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">数据分析</h2>
|
<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="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<div className="bg-gray-50 dark:bg-gray-700 p-6 rounded-lg">
|
<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 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>
|
</div>
|
||||||
<div className="bg-gray-50 dark:bg-gray-700 p-6 rounded-lg">
|
<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 className="h-32 bg-gradient-to-r from-green-400 to-blue-500 rounded-lg flex items-center justify-center text-white">
|
||||||
🗺️ 地图占位符
|
🗺️ 地图占位符
|
||||||
</div>
|
</div>
|
||||||
@@ -311,27 +394,46 @@ export default function NavigationTestPage() {
|
|||||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">设置</h2>
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">设置</h2>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<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">
|
<div className="space-y-2">
|
||||||
<label className="flex items-center">
|
<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>
|
<span className="ml-2 text-gray-700 dark:text-gray-300">邮件通知</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center">
|
<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>
|
<span className="ml-2 text-gray-700 dark:text-gray-300">短信通知</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<div className="space-y-2">
|
||||||
<label className="flex items-center">
|
<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>
|
<span className="ml-2 text-gray-700 dark:text-gray-300">公开个人资料</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center">
|
<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>
|
<span className="ml-2 text-gray-700 dark:text-gray-300">允许搜索</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -362,9 +464,9 @@ export default function NavigationTestPage() {
|
|||||||
title: '新通知',
|
title: '新通知',
|
||||||
content: `这是第 ${notifications.length + 1} 条通知`,
|
content: `这是第 ${notifications.length + 1} 条通知`,
|
||||||
time: '刚刚',
|
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"
|
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 justify-between items-start">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">{notification.title}</h4>
|
<h4 className="font-medium text-gray-900 dark:text-white">
|
||||||
<p className="text-gray-600 dark:text-gray-300 text-sm">{notification.content}</p>
|
{notification.title}
|
||||||
|
</h4>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 text-sm">
|
||||||
|
{notification.content}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<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 && (
|
{notification.unread && (
|
||||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
<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"
|
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">
|
<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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -84,12 +84,11 @@ export class LLM {
|
|||||||
usage: LanguageModelUsage
|
usage: LanguageModelUsage
|
||||||
}> {
|
}> {
|
||||||
const isClaude = this.config.modelName.slice(0, 8).includes('claude')
|
const isClaude = this.config.modelName.slice(0, 8).includes('claude')
|
||||||
const isQwen = this.config.modelName.slice(0, 6).includes('qwen')
|
// const isQwen = this.config.modelName.slice(0, 6).includes('qwen')
|
||||||
const isGPT = this.config.modelName.slice(0, 5).includes('gpt')
|
// const isGPT = this.config.modelName.slice(0, 5).includes('gpt')
|
||||||
|
|
||||||
return await withRetry(
|
return await withRetry(
|
||||||
async () => {
|
async () => {
|
||||||
// try {
|
|
||||||
const result = await generateText({
|
const result = await generateText({
|
||||||
model: this.#model,
|
model: this.#model,
|
||||||
messages,
|
messages,
|
||||||
@@ -165,16 +164,6 @@ export class LLM {
|
|||||||
toolResult,
|
toolResult,
|
||||||
usage,
|
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
|
// retry settings
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user