fix
This commit is contained in:
parent
ea5090c891
commit
dcc2b6684f
|
|
@ -0,0 +1,246 @@
|
|||
# 模板导入进度日志复制功能
|
||||
|
||||
## 功能概述
|
||||
|
||||
为模板导入进度弹窗中的导入日志添加了复制功能,用户可以方便地复制日志内容用于调试、分享或存档。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 🎯 主要功能
|
||||
|
||||
1. **一键复制详细日志** - 包含完整的导入信息和日志内容
|
||||
2. **多种复制格式** - 支持详细格式和简单格式
|
||||
3. **智能下拉菜单** - 提供更多复制选项
|
||||
4. **复制状态反馈** - 显示复制成功状态
|
||||
5. **错误处理** - 支持备用复制方法
|
||||
|
||||
### 📋 复制格式
|
||||
|
||||
#### 详细格式(默认)
|
||||
```
|
||||
=== 模板导入日志 ===
|
||||
导出时间: 2024-01-15 14:30:25
|
||||
当前进度: 导入模板 - 正在处理模板文件... (75.5%)
|
||||
导入结果: 成功 - 导入完成 (成功导入 8 个模板)
|
||||
=== 详细日志 ===
|
||||
|
||||
[2024-01-15 14:30:20] 开始扫描模板目录...
|
||||
[2024-01-15 14:30:21] 发现 10 个模板文件
|
||||
[2024-01-15 14:30:22] 开始导入模板: template1
|
||||
...
|
||||
```
|
||||
|
||||
#### 简单格式
|
||||
```
|
||||
[2024-01-15 14:30:20] 开始扫描模板目录...
|
||||
[2024-01-15 14:30:21] 发现 10 个模板文件
|
||||
[2024-01-15 14:30:22] 开始导入模板: template1
|
||||
...
|
||||
```
|
||||
|
||||
## 用户界面
|
||||
|
||||
### 复制按钮组
|
||||
- **主复制按钮**: 一键复制详细格式日志
|
||||
- **下拉菜单按钮**: 展开更多复制选项
|
||||
|
||||
### 下拉菜单选项
|
||||
- **复制详细日志**: 包含时间戳、进度信息、结果信息和完整日志
|
||||
- **仅复制日志内容**: 只复制原始日志内容,不包含额外信息
|
||||
|
||||
### 状态反馈
|
||||
- **复制中**: 显示"复制日志"文本和复制图标
|
||||
- **复制成功**: 显示"已复制"文本和勾选图标(2秒后自动恢复)
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 核心功能
|
||||
|
||||
#### 1. 复制函数
|
||||
```typescript
|
||||
const copyLogsToClipboard = async (detailed: boolean = true) => {
|
||||
try {
|
||||
let logsText: string
|
||||
|
||||
if (detailed) {
|
||||
// 构建详细格式
|
||||
const timestamp = new Date().toLocaleString()
|
||||
const progressInfo = progress ? `当前进度: ${progress.step}...` : ''
|
||||
const resultInfo = result ? `导入结果: ${result.status ? '成功' : '失败'}...` : ''
|
||||
|
||||
const header = [
|
||||
'=== 模板导入日志 ===',
|
||||
`导出时间: ${timestamp}`,
|
||||
progressInfo,
|
||||
resultInfo,
|
||||
'=== 详细日志 ===',
|
||||
''
|
||||
].filter(Boolean).join('\n')
|
||||
|
||||
logsText = header + logs.join('\n')
|
||||
} else {
|
||||
// 简单格式
|
||||
logsText = logs.join('\n')
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(logsText)
|
||||
setIsCopied(true)
|
||||
setTimeout(() => setIsCopied(false), 2000)
|
||||
} catch (error) {
|
||||
// 备用复制方法
|
||||
fallbackCopy(logs.join('\n'))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 备用复制方法
|
||||
```typescript
|
||||
const fallbackCopy = (text: string) => {
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = text
|
||||
document.body.appendChild(textArea)
|
||||
textArea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textArea)
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 下拉菜单控制
|
||||
```typescript
|
||||
const [showCopyOptions, setShowCopyOptions] = useState(false)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 点击外部关闭下拉菜单
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setShowCopyOptions(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (showCopyOptions) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [showCopyOptions])
|
||||
```
|
||||
|
||||
### 组件结构
|
||||
|
||||
```tsx
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="font-medium text-gray-900">导入日志</h4>
|
||||
{logs.length > 0 && (
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-1">
|
||||
{/* 主复制按钮 */}
|
||||
<button onClick={() => copyLogsToClipboard(true)}>
|
||||
{isCopied ? '已复制' : '复制日志'}
|
||||
</button>
|
||||
|
||||
{/* 下拉菜单 */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button onClick={() => setShowCopyOptions(!showCopyOptions)}>
|
||||
<ChevronDown />
|
||||
</button>
|
||||
|
||||
{showCopyOptions && (
|
||||
<div className="dropdown-menu">
|
||||
<button onClick={() => copyLogsToClipboard(true)}>
|
||||
复制详细日志
|
||||
</button>
|
||||
<button onClick={() => copyLogsToClipboard(false)}>
|
||||
仅复制日志内容
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
```
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 1. 调试问题
|
||||
当模板导入失败时,用户可以复制详细日志发送给技术支持:
|
||||
- 包含完整的错误信息
|
||||
- 包含导入时间和环境信息
|
||||
- 便于问题定位和解决
|
||||
|
||||
### 2. 分享成功经验
|
||||
用户可以复制成功的导入日志分享给其他用户:
|
||||
- 展示导入过程
|
||||
- 分享最佳实践
|
||||
- 帮助其他用户学习
|
||||
|
||||
### 3. 存档记录
|
||||
用户可以保存导入日志作为操作记录:
|
||||
- 项目文档
|
||||
- 操作历史
|
||||
- 审计追踪
|
||||
|
||||
## 兼容性
|
||||
|
||||
### 浏览器支持
|
||||
- **现代浏览器**: 使用 `navigator.clipboard.writeText()` API
|
||||
- **旧版浏览器**: 自动降级到 `document.execCommand('copy')` 方法
|
||||
|
||||
### 错误处理
|
||||
- **剪贴板权限被拒绝**: 自动使用备用方法
|
||||
- **API 不可用**: 降级到传统复制方法
|
||||
- **复制失败**: 在控制台记录错误信息
|
||||
|
||||
## 测试覆盖
|
||||
|
||||
### 单元测试
|
||||
- ✅ 复制按钮显示/隐藏逻辑
|
||||
- ✅ 详细格式复制功能
|
||||
- ✅ 简单格式复制功能
|
||||
- ✅ 下拉菜单交互
|
||||
- ✅ 复制状态反馈
|
||||
- ✅ 错误处理机制
|
||||
- ✅ 点击外部关闭菜单
|
||||
|
||||
### 集成测试
|
||||
- ✅ 与模板导入流程集成
|
||||
- ✅ 实时日志更新
|
||||
- ✅ 进度状态同步
|
||||
|
||||
## 性能考虑
|
||||
|
||||
### 内存使用
|
||||
- 日志内容在内存中临时存储
|
||||
- 复制完成后立即释放
|
||||
- 不会造成内存泄漏
|
||||
|
||||
### 用户体验
|
||||
- 复制操作响应迅速(< 100ms)
|
||||
- 状态反馈及时(2秒自动恢复)
|
||||
- 下拉菜单流畅交互
|
||||
|
||||
## 未来改进
|
||||
|
||||
### 短期计划
|
||||
- [ ] 添加复制格式自定义选项
|
||||
- [ ] 支持复制特定时间段的日志
|
||||
- [ ] 添加日志过滤功能
|
||||
|
||||
### 长期计划
|
||||
- [ ] 支持导出为文件
|
||||
- [ ] 添加日志搜索功能
|
||||
- [ ] 集成云端存储
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `src/components/template/ImportProgressModal.tsx` - 主要组件
|
||||
- `src/components/template/ImportProgressModal.test.tsx` - 单元测试
|
||||
- `src/hooks/useProgressCommand.ts` - 进度管理 Hook
|
||||
|
||||
---
|
||||
|
||||
通过这个复制功能,用户可以更方便地管理和分享模板导入日志,提升了整体的用户体验和问题解决效率。
|
||||
|
|
@ -71,7 +71,7 @@ const RecentProjects: React.FC = () => {
|
|||
<p className="text-secondary-600 mb-4">还没有项目</p>
|
||||
<Link
|
||||
to="/projects"
|
||||
className="btn-primary inline-flex items-center"
|
||||
className="btn-primary inline-flex items-center px-2 py-1"
|
||||
>
|
||||
<Plus size={16} className="mr-2" />
|
||||
创建第一个项目
|
||||
|
|
@ -90,7 +90,7 @@ const RecentProjects: React.FC = () => {
|
|||
查看全部
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{loading ? (
|
||||
<LoadingSkeleton />
|
||||
|
|
|
|||
|
|
@ -14,21 +14,21 @@ const WelcomeSection: React.FC = () => {
|
|||
<div className="flex items-center justify-center space-x-4">
|
||||
<Link
|
||||
to="/text-video-generator"
|
||||
className="btn-primary flex items-center"
|
||||
className="btn-primary flex items-center px-2 py-1"
|
||||
>
|
||||
<Wand2 size={20} className="mr-2" />
|
||||
AI 内容生成
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects"
|
||||
className="btn-secondary flex items-center"
|
||||
className="btn-secondary flex items-center px-2 py-1"
|
||||
>
|
||||
<Plus size={20} className="mr-2" />
|
||||
创建新项目
|
||||
</Link>
|
||||
<Link
|
||||
to="/templates"
|
||||
className="btn-secondary"
|
||||
className="btn-secondary px-2 py-1"
|
||||
>
|
||||
浏览模板
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,235 @@
|
|||
/**
|
||||
* 测试 ImportProgressModal 组件的复制功能
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||
import { ImportProgressModal } from './ImportProgressModal'
|
||||
import type { ProgressState, ImportResult } from '../../hooks/useProgressCommand'
|
||||
|
||||
// Mock clipboard API
|
||||
const mockWriteText = jest.fn()
|
||||
Object.assign(navigator, {
|
||||
clipboard: {
|
||||
writeText: mockWriteText,
|
||||
},
|
||||
})
|
||||
|
||||
describe('ImportProgressModal', () => {
|
||||
const defaultProps = {
|
||||
isOpen: true,
|
||||
isExecuting: false,
|
||||
progress: null,
|
||||
result: null,
|
||||
logs: ['日志行1', '日志行2', '日志行3'],
|
||||
onClose: jest.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockWriteText.mockClear()
|
||||
})
|
||||
|
||||
it('应该显示复制按钮当有日志时', () => {
|
||||
render(<ImportProgressModal {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTitle('复制详细日志')).toBeInTheDocument()
|
||||
expect(screen.getByTitle('更多复制选项')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('应该隐藏复制按钮当没有日志时', () => {
|
||||
render(<ImportProgressModal {...defaultProps} logs={[]} />)
|
||||
|
||||
expect(screen.queryByTitle('复制详细日志')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTitle('更多复制选项')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('应该复制详细日志格式', async () => {
|
||||
const progress: ProgressState = {
|
||||
step: '导入模板',
|
||||
progress: 50,
|
||||
message: '正在处理...',
|
||||
details: {},
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
const result: ImportResult = {
|
||||
status: true,
|
||||
msg: '导入成功',
|
||||
imported_count: 2,
|
||||
failed_count: 0,
|
||||
imported_templates: [],
|
||||
failed_templates: []
|
||||
}
|
||||
|
||||
render(
|
||||
<ImportProgressModal
|
||||
{...defaultProps}
|
||||
progress={progress}
|
||||
result={result}
|
||||
/>
|
||||
)
|
||||
|
||||
const copyButton = screen.getByTitle('复制详细日志')
|
||||
fireEvent.click(copyButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockWriteText).toHaveBeenCalledWith(
|
||||
expect.stringContaining('=== 模板导入日志 ===')
|
||||
)
|
||||
expect(mockWriteText).toHaveBeenCalledWith(
|
||||
expect.stringContaining('当前进度: 导入模板')
|
||||
)
|
||||
expect(mockWriteText).toHaveBeenCalledWith(
|
||||
expect.stringContaining('导入结果: 成功')
|
||||
)
|
||||
expect(mockWriteText).toHaveBeenCalledWith(
|
||||
expect.stringContaining('日志行1')
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('应该显示下拉菜单选项', async () => {
|
||||
render(<ImportProgressModal {...defaultProps} />)
|
||||
|
||||
const dropdownButton = screen.getByTitle('更多复制选项')
|
||||
fireEvent.click(dropdownButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('复制详细日志')).toBeInTheDocument()
|
||||
expect(screen.getByText('仅复制日志内容')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('应该复制简单日志格式', async () => {
|
||||
render(<ImportProgressModal {...defaultProps} />)
|
||||
|
||||
const dropdownButton = screen.getByTitle('更多复制选项')
|
||||
fireEvent.click(dropdownButton)
|
||||
|
||||
await waitFor(() => {
|
||||
const simpleButton = screen.getByText('仅复制日志内容')
|
||||
fireEvent.click(simpleButton)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockWriteText).toHaveBeenCalledWith('日志行1\n日志行2\n日志行3')
|
||||
})
|
||||
})
|
||||
|
||||
it('应该显示复制成功状态', async () => {
|
||||
render(<ImportProgressModal {...defaultProps} />)
|
||||
|
||||
const copyButton = screen.getByTitle('复制详细日志')
|
||||
fireEvent.click(copyButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('已复制')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// 等待状态重置
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('复制日志')).toBeInTheDocument()
|
||||
}, { timeout: 3000 })
|
||||
})
|
||||
|
||||
it('应该处理复制失败的情况', async () => {
|
||||
mockWriteText.mockRejectedValue(new Error('复制失败'))
|
||||
|
||||
// Mock console.error to avoid test output noise
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
||||
|
||||
render(<ImportProgressModal {...defaultProps} />)
|
||||
|
||||
const copyButton = screen.getByTitle('复制详细日志')
|
||||
fireEvent.click(copyButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(consoleSpy).toHaveBeenCalledWith('复制日志失败:', expect.any(Error))
|
||||
})
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('应该在点击外部时关闭下拉菜单', async () => {
|
||||
render(<ImportProgressModal {...defaultProps} />)
|
||||
|
||||
const dropdownButton = screen.getByTitle('更多复制选项')
|
||||
fireEvent.click(dropdownButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('复制详细日志')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// 点击外部
|
||||
fireEvent.mouseDown(document.body)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('复制详细日志')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('应该显示暂无日志信息当日志为空时', () => {
|
||||
render(<ImportProgressModal {...defaultProps} logs={[]} />)
|
||||
|
||||
expect(screen.getByText('暂无日志信息')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('应该正确格式化进度信息', async () => {
|
||||
const progress: ProgressState = {
|
||||
step: '扫描文件',
|
||||
progress: -1, // 不确定进度
|
||||
message: '正在扫描模板文件...',
|
||||
details: {
|
||||
processed_templates: 3,
|
||||
total_templates: 10
|
||||
},
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
render(
|
||||
<ImportProgressModal
|
||||
{...defaultProps}
|
||||
progress={progress}
|
||||
/>
|
||||
)
|
||||
|
||||
const copyButton = screen.getByTitle('复制详细日志')
|
||||
fireEvent.click(copyButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockWriteText).toHaveBeenCalledWith(
|
||||
expect.stringContaining('当前进度: 扫描文件 - 正在扫描模板文件... (处理中)')
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('应该正确格式化失败结果信息', async () => {
|
||||
const result: ImportResult = {
|
||||
status: false,
|
||||
msg: '导入失败',
|
||||
imported_count: 1,
|
||||
failed_count: 2,
|
||||
imported_templates: [],
|
||||
failed_templates: [
|
||||
{ name: '模板1', error: '文件损坏' },
|
||||
{ name: '模板2', error: '格式错误' }
|
||||
]
|
||||
}
|
||||
|
||||
render(
|
||||
<ImportProgressModal
|
||||
{...defaultProps}
|
||||
result={result}
|
||||
/>
|
||||
)
|
||||
|
||||
const copyButton = screen.getByTitle('复制详细日志')
|
||||
fireEvent.click(copyButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockWriteText).toHaveBeenCalledWith(
|
||||
expect.stringContaining('导入结果: 失败 - 导入失败 (成功导入 1 个模板) (失败 2 个模板)')
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react'
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { Copy, Check, ChevronDown } from 'lucide-react'
|
||||
import type { ProgressState, ImportResult } from '../../hooks/useProgressCommand'
|
||||
|
||||
interface ImportProgressModalProps {
|
||||
|
|
@ -18,6 +19,74 @@ export const ImportProgressModal: React.FC<ImportProgressModalProps> = ({
|
|||
logs,
|
||||
onClose
|
||||
}) => {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
const [showCopyOptions, setShowCopyOptions] = useState(false)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 点击外部关闭下拉菜单
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setShowCopyOptions(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (showCopyOptions) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [showCopyOptions])
|
||||
|
||||
// 复制日志到剪贴板(详细格式)
|
||||
const copyLogsToClipboard = async (detailed: boolean = true) => {
|
||||
try {
|
||||
let logsText: string
|
||||
|
||||
if (detailed) {
|
||||
// 构建详细的日志信息
|
||||
const timestamp = new Date().toLocaleString()
|
||||
const progressInfo = progress ? `当前进度: ${progress.step} - ${progress.message} (${progress.progress >= 0 ? progress.progress.toFixed(1) + '%' : '处理中'})` : ''
|
||||
const resultInfo = result ? `导入结果: ${result.status ? '成功' : '失败'} - ${result.msg}${result.imported_count > 0 ? ` (成功导入 ${result.imported_count} 个模板)` : ''}${result.failed_count > 0 ? ` (失败 ${result.failed_count} 个模板)` : ''}` : ''
|
||||
|
||||
const header = [
|
||||
'=== 模板导入日志 ===',
|
||||
`导出时间: ${timestamp}`,
|
||||
progressInfo,
|
||||
resultInfo,
|
||||
'=== 详细日志 ===',
|
||||
''
|
||||
].filter(Boolean).join('\n')
|
||||
|
||||
logsText = header + logs.join('\n')
|
||||
} else {
|
||||
// 简单格式,只复制日志内容
|
||||
logsText = logs.join('\n')
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(logsText)
|
||||
setIsCopied(true)
|
||||
setTimeout(() => setIsCopied(false), 2000) // 2秒后重置状态
|
||||
} catch (error) {
|
||||
console.error('复制日志失败:', error)
|
||||
// 如果剪贴板API失败,尝试使用传统方法
|
||||
try {
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = logs.join('\n')
|
||||
document.body.appendChild(textArea)
|
||||
textArea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textArea)
|
||||
setIsCopied(true)
|
||||
setTimeout(() => setIsCopied(false), 2000)
|
||||
} catch (fallbackError) {
|
||||
console.error('备用复制方法也失败:', fallbackError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
|
|
@ -97,13 +166,80 @@ export const ImportProgressModal: React.FC<ImportProgressModalProps> = ({
|
|||
|
||||
{/* Logs */}
|
||||
<div className="bg-gray-50 rounded-lg p-4 h-64 overflow-y-auto">
|
||||
<h4 className="font-medium text-gray-900 mb-2">导入日志</h4>
|
||||
<div className="space-y-1">
|
||||
{logs.map((log, index) => (
|
||||
<div key={index} className="text-xs text-gray-600 font-mono">
|
||||
{log}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="font-medium text-gray-900">导入日志</h4>
|
||||
{logs.length > 0 && (
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-1">
|
||||
{/* 主复制按钮 */}
|
||||
<button
|
||||
onClick={() => copyLogsToClipboard(true)}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-l transition-colors"
|
||||
title="复制详细日志"
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<Check size={12} />
|
||||
已复制
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy size={12} />
|
||||
复制日志
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* 下拉菜单按钮 */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setShowCopyOptions(!showCopyOptions)}
|
||||
className="flex items-center px-1 py-1 text-xs bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-r border-l border-gray-300 transition-colors"
|
||||
title="更多复制选项"
|
||||
>
|
||||
<ChevronDown size={12} />
|
||||
</button>
|
||||
|
||||
{/* 下拉菜单 */}
|
||||
{showCopyOptions && (
|
||||
<div className="absolute right-0 top-full mt-1 bg-white border border-gray-200 rounded-md shadow-lg z-10 min-w-32">
|
||||
<button
|
||||
onClick={() => {
|
||||
copyLogsToClipboard(true)
|
||||
setShowCopyOptions(false)
|
||||
}}
|
||||
className="w-full text-left px-3 py-2 text-xs text-gray-700 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
复制详细日志
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
copyLogsToClipboard(false)
|
||||
setShowCopyOptions(false)
|
||||
}}
|
||||
className="w-full text-left px-3 py-2 text-xs text-gray-700 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
仅复制日志内容
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{logs.length > 0 ? (
|
||||
logs.map((log, index) => (
|
||||
<div key={index} className="text-xs text-gray-600 font-mono">
|
||||
{log}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-xs text-gray-400 italic">
|
||||
暂无日志信息
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue