fix: resolve build errors and complete missing files
Fix CSS issues, add missing files, and optimize documentation page. Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
@@ -5,7 +5,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
@@ -30,7 +29,10 @@ import {
|
||||
Clock,
|
||||
Zap,
|
||||
} from "lucide-react"
|
||||
import { enhancedScreenshotService, type ScreenshotOptions } from "@/lib/documentation/enhanced-screenshot-service"
|
||||
import {
|
||||
enhancedScreenshotService,
|
||||
type EnhancedScreenshotOptions,
|
||||
} from "@/lib/documentation/enhanced-screenshot-service"
|
||||
import { generateDocx } from "@/lib/documentation/docx-generator"
|
||||
import { pageRegistry } from "@/lib/documentation/page-registry"
|
||||
|
||||
@@ -79,18 +81,20 @@ export default function DocumentationPage() {
|
||||
})
|
||||
|
||||
const previewRef = useRef<HTMLIFrameElement>(null)
|
||||
const [screenshotOptions, setScreenshotOptions] = useState<ScreenshotOptions>({
|
||||
const [screenshotOptions, setScreenshotOptions] = useState<EnhancedScreenshotOptions>({
|
||||
format: "png",
|
||||
quality: 0.9,
|
||||
scale: 1,
|
||||
backgroundColor: "#ffffff",
|
||||
timeout: 30000,
|
||||
waitForImages: true,
|
||||
waitForFonts: true,
|
||||
addWatermark: true,
|
||||
})
|
||||
|
||||
const pages = pageRegistry.getAllPages()
|
||||
|
||||
useEffect(() => {
|
||||
// 初始化选中所有页面
|
||||
setSelectedPages(pages.map((p) => p.path))
|
||||
}, [])
|
||||
|
||||
@@ -131,80 +135,13 @@ export default function DocumentationPage() {
|
||||
}
|
||||
|
||||
try {
|
||||
// 方法1: 尝试直接截图当前页面(如果是当前页面)
|
||||
if (window.location.pathname === page.path) {
|
||||
const result = await enhancedScreenshotService.captureViewport({
|
||||
...screenshotOptions,
|
||||
...getViewportSize(),
|
||||
})
|
||||
|
||||
if (result.success && result.dataUrl) {
|
||||
return {
|
||||
...screenshot,
|
||||
status: "success",
|
||||
dataUrl: result.dataUrl,
|
||||
method: result.method,
|
||||
size: getViewportSize(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 方法2: 使用新窗口截图
|
||||
const newWindow = window.open(
|
||||
page.path,
|
||||
"_blank",
|
||||
`width=${getViewportSize().width},height=${getViewportSize().height}`,
|
||||
)
|
||||
|
||||
if (!newWindow) {
|
||||
throw new Error("无法打开新窗口,请检查浏览器弹窗设置")
|
||||
}
|
||||
|
||||
// 等待页面加载
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
newWindow.close()
|
||||
reject(new Error("页面加载超时"))
|
||||
}, 15000)
|
||||
|
||||
const checkLoaded = () => {
|
||||
try {
|
||||
if (newWindow.document && newWindow.document.readyState === "complete") {
|
||||
clearTimeout(timeout)
|
||||
resolve(void 0)
|
||||
} else {
|
||||
setTimeout(checkLoaded, 100)
|
||||
}
|
||||
} catch (e) {
|
||||
// 跨域问题,等待一段时间后继续
|
||||
setTimeout(checkLoaded, 100)
|
||||
}
|
||||
}
|
||||
|
||||
newWindow.addEventListener("load", () => {
|
||||
clearTimeout(timeout)
|
||||
setTimeout(resolve, 2000) // 额外等待2秒确保渲染完成
|
||||
})
|
||||
|
||||
checkLoaded()
|
||||
const result = await enhancedScreenshotService.captureViewport({
|
||||
...screenshotOptions,
|
||||
...getViewportSize(),
|
||||
watermarkText: `${page.name} - ${new Date().toLocaleString()}`,
|
||||
})
|
||||
|
||||
// 尝试截图新窗口
|
||||
let result
|
||||
try {
|
||||
if (newWindow.document && newWindow.document.body) {
|
||||
result = await enhancedScreenshotService.captureElement(newWindow.document.body, {
|
||||
...screenshotOptions,
|
||||
...getViewportSize(),
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("新窗口截图失败,尝试其他方法:", e)
|
||||
}
|
||||
|
||||
newWindow.close()
|
||||
|
||||
if (result?.success && result.dataUrl) {
|
||||
if (result.success && result.dataUrl) {
|
||||
return {
|
||||
...screenshot,
|
||||
status: "success",
|
||||
@@ -212,52 +149,9 @@ export default function DocumentationPage() {
|
||||
method: result.method,
|
||||
size: getViewportSize(),
|
||||
}
|
||||
} else {
|
||||
throw new Error(result.error || "截图失败")
|
||||
}
|
||||
|
||||
// 方法3: 使用iframe截图(最后的备选方案)
|
||||
const iframe = document.createElement("iframe")
|
||||
iframe.style.position = "absolute"
|
||||
iframe.style.left = "-9999px"
|
||||
iframe.style.width = `${getViewportSize().width}px`
|
||||
iframe.style.height = `${getViewportSize().height}px`
|
||||
iframe.src = page.path
|
||||
|
||||
document.body.appendChild(iframe)
|
||||
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error("iframe加载超时"))
|
||||
}, 15000)
|
||||
|
||||
iframe.onload = () => {
|
||||
clearTimeout(timeout)
|
||||
setTimeout(resolve, 2000) // 等待渲染完成
|
||||
}
|
||||
})
|
||||
|
||||
// 尝试截图iframe内容
|
||||
if (iframe.contentDocument && iframe.contentDocument.body) {
|
||||
result = await enhancedScreenshotService.captureElement(iframe.contentDocument.body, {
|
||||
...screenshotOptions,
|
||||
...getViewportSize(),
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
document.body.removeChild(iframe)
|
||||
}
|
||||
|
||||
if (result?.success && result.dataUrl) {
|
||||
return {
|
||||
...screenshot,
|
||||
status: "success",
|
||||
dataUrl: result.dataUrl,
|
||||
method: result.method,
|
||||
size: getViewportSize(),
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("所有截图方法都失败了")
|
||||
} catch (error) {
|
||||
console.error(`截图失败 ${page.name}:`, error)
|
||||
return {
|
||||
@@ -288,10 +182,8 @@ export default function DocumentationPage() {
|
||||
for (let i = 0; i < selectedPageObjects.length; i++) {
|
||||
const page = selectedPageObjects[i]
|
||||
|
||||
// 更新进度
|
||||
setCaptureProgress((i / selectedPageObjects.length) * 100)
|
||||
|
||||
// 添加待处理的截图
|
||||
const pendingScreenshot: Screenshot = {
|
||||
id: `${page.path}-${Date.now()}`,
|
||||
name: page.name,
|
||||
@@ -306,7 +198,6 @@ export default function DocumentationPage() {
|
||||
try {
|
||||
const screenshot = await captureScreenshot(page)
|
||||
|
||||
// 更新截图状态
|
||||
const updatedScreenshots = [...newScreenshots]
|
||||
updatedScreenshots[i] = screenshot
|
||||
setScreenshots(updatedScreenshots)
|
||||
@@ -338,7 +229,6 @@ export default function DocumentationPage() {
|
||||
newScreenshots[i] = failedScreenshot
|
||||
}
|
||||
|
||||
// 添加延迟避免过快请求
|
||||
if (i < selectedPageObjects.length - 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
}
|
||||
@@ -357,62 +247,6 @@ export default function DocumentationPage() {
|
||||
})
|
||||
}
|
||||
|
||||
const retryFailedScreenshots = async () => {
|
||||
const failedScreenshots = screenshots.filter((s) => s.status === "failed")
|
||||
|
||||
if (failedScreenshots.length === 0) {
|
||||
toast({
|
||||
title: "没有失败的截图",
|
||||
description: "所有截图都已成功",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setIsCapturing(true)
|
||||
setCaptureProgress(0)
|
||||
|
||||
for (let i = 0; i < failedScreenshots.length; i++) {
|
||||
const failedScreenshot = failedScreenshots[i]
|
||||
const page = pages.find((p) => p.path === failedScreenshot.url)
|
||||
|
||||
if (!page) continue
|
||||
|
||||
setCaptureProgress((i / failedScreenshots.length) * 100)
|
||||
|
||||
// 更新状态为重新截图中
|
||||
setScreenshots((prev) =>
|
||||
prev.map((s) => (s.id === failedScreenshot.id ? { ...s, status: "capturing" as const } : s)),
|
||||
)
|
||||
|
||||
try {
|
||||
const newScreenshot = await captureScreenshot(page)
|
||||
|
||||
setScreenshots((prev) => prev.map((s) => (s.id === failedScreenshot.id ? newScreenshot : s)))
|
||||
|
||||
if (newScreenshot.status === "success") {
|
||||
toast({
|
||||
title: "重试成功",
|
||||
description: `${page.name} 截图完成`,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`重试截图失败:`, error)
|
||||
setScreenshots((prev) =>
|
||||
prev.map((s) =>
|
||||
s.id === failedScreenshot.id
|
||||
? { ...s, status: "failed" as const, error: error instanceof Error ? error.message : "未知错误" }
|
||||
: s,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
}
|
||||
|
||||
setCaptureProgress(100)
|
||||
setIsCapturing(false)
|
||||
}
|
||||
|
||||
const generateDocument = async () => {
|
||||
const successfulScreenshots = screenshots.filter((s) => s.status === "success" && s.dataUrl)
|
||||
|
||||
@@ -430,7 +264,6 @@ export default function DocumentationPage() {
|
||||
try {
|
||||
const docBuffer = await generateDocx(successfulScreenshots, documentSettings)
|
||||
|
||||
// 下载文档
|
||||
const blob = new Blob([docBuffer], {
|
||||
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
})
|
||||
@@ -520,9 +353,8 @@ export default function DocumentationPage() {
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="capture" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="capture">页面截图</TabsTrigger>
|
||||
<TabsTrigger value="preview">预览页面</TabsTrigger>
|
||||
<TabsTrigger value="screenshots">截图管理</TabsTrigger>
|
||||
<TabsTrigger value="settings">文档设置</TabsTrigger>
|
||||
</TabsList>
|
||||
@@ -583,105 +415,6 @@ export default function DocumentationPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Settings className="h-5 w-5" />
|
||||
<span>截图设置</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">预览模式</label>
|
||||
<Select
|
||||
value={previewMode}
|
||||
onValueChange={(value: "desktop" | "tablet" | "mobile") => setPreviewMode(value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="desktop">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Monitor className="h-4 w-4" />
|
||||
<span>桌面 (1920x1080)</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="tablet">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Tablet className="h-4 w-4" />
|
||||
<span>平板 (768x1024)</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="mobile">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Smartphone className="h-4 w-4" />
|
||||
<span>手机 (375x667)</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">图片格式</label>
|
||||
<Select
|
||||
value={screenshotOptions.format}
|
||||
onValueChange={(value: "png" | "jpeg" | "webp") =>
|
||||
setScreenshotOptions((prev) => ({ ...prev, format: value }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="png">PNG (高质量)</SelectItem>
|
||||
<SelectItem value="jpeg">JPEG (小文件)</SelectItem>
|
||||
<SelectItem value="webp">WebP (平衡)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
图片质量: {Math.round((screenshotOptions.quality || 0.9) * 100)}%
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={screenshotOptions.quality || 0.9}
|
||||
onChange={(e) =>
|
||||
setScreenshotOptions((prev) => ({
|
||||
...prev,
|
||||
quality: Number.parseFloat(e.target.value),
|
||||
}))
|
||||
}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">缩放比例: {screenshotOptions.scale || 1}x</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.5"
|
||||
max="3"
|
||||
step="0.1"
|
||||
value={screenshotOptions.scale || 1}
|
||||
onChange={(e) =>
|
||||
setScreenshotOptions((prev) => ({
|
||||
...prev,
|
||||
scale: Number.parseFloat(e.target.value),
|
||||
}))
|
||||
}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
@@ -700,37 +433,23 @@ export default function DocumentationPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
onClick={startCapture}
|
||||
disabled={isCapturing || selectedPages.length === 0}
|
||||
className="w-full"
|
||||
>
|
||||
{isCapturing ? (
|
||||
<>
|
||||
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
|
||||
截图中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Camera className="mr-2 h-4 w-4" />
|
||||
开始截图
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{screenshots.some((s) => s.status === "failed") && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={retryFailedScreenshots}
|
||||
disabled={isCapturing}
|
||||
className="w-full bg-transparent"
|
||||
>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
重试失败的截图
|
||||
</Button>
|
||||
<Button
|
||||
onClick={startCapture}
|
||||
disabled={isCapturing || selectedPages.length === 0}
|
||||
className="w-full"
|
||||
>
|
||||
{isCapturing ? (
|
||||
<>
|
||||
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
|
||||
截图中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Camera className="mr-2 h-4 w-4" />
|
||||
开始截图
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -752,65 +471,6 @@ export default function DocumentationPage() {
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="preview" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
{getPreviewIcon()}
|
||||
<span>页面预览</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Select
|
||||
value={previewMode}
|
||||
onValueChange={(value: "desktop" | "tablet" | "mobile") => setPreviewMode(value)}
|
||||
>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="desktop">桌面</SelectItem>
|
||||
<SelectItem value="tablet">平板</SelectItem>
|
||||
<SelectItem value="mobile">手机</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<CardDescription>预览页面在不同设备上的显示效果</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="border rounded-lg overflow-hidden bg-gray-100 p-4">
|
||||
<div
|
||||
className="mx-auto bg-white shadow-lg rounded-lg overflow-hidden"
|
||||
style={{
|
||||
width: `${getViewportSize().width}px`,
|
||||
height: `${getViewportSize().height}px`,
|
||||
maxWidth: "100%",
|
||||
transform: "scale(0.5)",
|
||||
transformOrigin: "top center",
|
||||
}}
|
||||
>
|
||||
{currentPreviewUrl ? (
|
||||
<iframe
|
||||
ref={previewRef}
|
||||
src={currentPreviewUrl}
|
||||
className="w-full h-full border-0"
|
||||
title="页面预览"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<Globe className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||
<p>请从左侧选择一个页面进行预览</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="screenshots" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -974,43 +634,6 @@ export default function DocumentationPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">页面大小</label>
|
||||
<Select
|
||||
value={documentSettings.pageSize}
|
||||
onValueChange={(value: "A4" | "A3" | "Letter") =>
|
||||
setDocumentSettings((prev) => ({ ...prev, pageSize: value }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="A4">A4</SelectItem>
|
||||
<SelectItem value="A3">A3</SelectItem>
|
||||
<SelectItem value="Letter">Letter</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">页面方向</label>
|
||||
<Select
|
||||
value={documentSettings.orientation}
|
||||
onValueChange={(value: "portrait" | "landscape") =>
|
||||
setDocumentSettings((prev) => ({ ...prev, orientation: value }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="portrait">纵向</SelectItem>
|
||||
<SelectItem value="landscape">横向</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">包含内容</label>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
/* 更新为毛玻璃风格的颜色变量 */
|
||||
--background: 240 10% 98%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--card: 240 10% 100%;
|
||||
@@ -70,7 +69,6 @@
|
||||
@apply bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900;
|
||||
}
|
||||
|
||||
/* 移动端优化 */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
background-attachment: scroll;
|
||||
@@ -79,28 +77,30 @@
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* 毛玻璃效果基础类 */
|
||||
.glass {
|
||||
@apply backdrop-blur-md bg-white/10 border border-white/20;
|
||||
@apply backdrop-blur-md border border-white/20;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
|
||||
}
|
||||
|
||||
.glass-light {
|
||||
@apply backdrop-blur-sm bg-white/20 border border-white/30;
|
||||
@apply backdrop-blur-sm border border-white/30;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 4px 16px 0 rgba(31, 38, 135, 0.2);
|
||||
}
|
||||
|
||||
.glass-heavy {
|
||||
@apply backdrop-blur-xl bg-white/30 border border-white/40;
|
||||
@apply backdrop-blur-xl border border-white/40;
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 16px 64px 0 rgba(31, 38, 135, 0.5);
|
||||
}
|
||||
|
||||
.glass-dark {
|
||||
@apply backdrop-blur-md bg-black/10 border border-white/10;
|
||||
@apply backdrop-blur-md border border-white/10;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 移动端毛玻璃效果优化 */
|
||||
@media (max-width: 768px) {
|
||||
.glass {
|
||||
@apply backdrop-blur-sm;
|
||||
@@ -118,42 +118,49 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 卡片毛玻璃效果 */
|
||||
.glass-card {
|
||||
@apply glass rounded-2xl p-6 transition-all duration-300 hover:bg-white/20 hover:shadow-glass-lg;
|
||||
@apply glass rounded-2xl p-6 transition-all duration-300;
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* 移动端卡片优化 */
|
||||
@media (max-width: 768px) {
|
||||
.glass-card {
|
||||
@apply p-4 rounded-xl;
|
||||
}
|
||||
}
|
||||
|
||||
/* 导航栏毛玻璃效果 */
|
||||
.glass-nav {
|
||||
@apply glass-light rounded-2xl transition-all duration-300;
|
||||
}
|
||||
|
||||
/* 按钮毛玻璃效果 */
|
||||
.glass-button {
|
||||
@apply glass-light rounded-xl px-4 py-2 transition-all duration-300 hover:bg-white/30 hover:scale-105;
|
||||
@apply glass-light rounded-xl px-4 py-2 transition-all duration-300;
|
||||
}
|
||||
|
||||
.glass-button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 移动端按钮优化 */
|
||||
@media (max-width: 768px) {
|
||||
.glass-button {
|
||||
@apply px-6 py-3 text-base;
|
||||
min-height: 44px; /* iOS推荐的最小触摸目标 */
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 输入框毛玻璃效果 */
|
||||
.glass-input {
|
||||
@apply glass-light rounded-xl px-4 py-2 transition-all duration-300 focus:bg-white/30 focus:ring-2 focus:ring-white/50;
|
||||
@apply glass-light rounded-xl px-4 py-2 transition-all duration-300;
|
||||
}
|
||||
|
||||
.glass-input:focus {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
@apply ring-2 ring-white/50;
|
||||
}
|
||||
|
||||
/* 移动端输入框优化 */
|
||||
@media (max-width: 768px) {
|
||||
.glass-input {
|
||||
@apply px-4 py-3 text-base;
|
||||
@@ -161,7 +168,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 安全区域适配 */
|
||||
.safe-area-top {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
}
|
||||
@@ -179,7 +185,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端滚动优化 */
|
||||
@media (max-width: 768px) {
|
||||
.overflow-scroll {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
@@ -1,24 +1,4 @@
|
||||
/**
|
||||
* Word文档生成器
|
||||
* 使用docx库生成专业的Word文档
|
||||
*/
|
||||
|
||||
import {
|
||||
Document,
|
||||
Paragraph,
|
||||
TextRun,
|
||||
HeadingLevel,
|
||||
ImageRun,
|
||||
TableOfContents,
|
||||
PageBreak,
|
||||
AlignmentType,
|
||||
Table,
|
||||
TableRow,
|
||||
TableCell,
|
||||
WidthType,
|
||||
BorderStyle,
|
||||
} from "docx"
|
||||
import { Buffer } from "buffer"
|
||||
import { Document, Packer, Paragraph, ImageRun, TextRun, HeadingLevel } from "docx"
|
||||
|
||||
interface Screenshot {
|
||||
id: string
|
||||
@@ -41,445 +21,190 @@ interface DocumentSettings {
|
||||
orientation: string
|
||||
}
|
||||
|
||||
interface DocumentSection {
|
||||
title: string
|
||||
path: string
|
||||
description: string
|
||||
screenshot: string
|
||||
}
|
||||
|
||||
interface DocumentData {
|
||||
title: string
|
||||
author: string
|
||||
date: string
|
||||
sections: DocumentSection[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 将base64字符串转换为Buffer
|
||||
* @param base64 base64字符串
|
||||
* @returns Buffer
|
||||
*/
|
||||
function base64ToBuffer(base64: string): Buffer {
|
||||
try {
|
||||
// 移除data URL前缀
|
||||
const base64Data = base64.includes("base64,") ? base64.split("base64,")[1] : base64
|
||||
|
||||
// 转换为Buffer
|
||||
return Buffer.from(base64Data, "base64")
|
||||
} catch (error) {
|
||||
console.error("base64转换为Buffer时出错:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成Word文档
|
||||
* @param screenshots 截图数据
|
||||
* @param settings 文档设置
|
||||
* @returns 文档的ArrayBuffer
|
||||
*/
|
||||
export async function generateDocx(screenshots: Screenshot[], settings: DocumentSettings): Promise<ArrayBuffer> {
|
||||
console.log("开始生成Word文档...")
|
||||
|
||||
try {
|
||||
// 验证输入数据
|
||||
if (!screenshots || !Array.isArray(screenshots)) {
|
||||
throw new Error("截图数据不能为空或不是数组")
|
||||
}
|
||||
const children: Paragraph[] = []
|
||||
|
||||
if (!settings) {
|
||||
throw new Error("文档设置不能为空")
|
||||
}
|
||||
|
||||
console.log(`文档标题: ${settings.title}`)
|
||||
console.log(`作者: ${settings.author}`)
|
||||
console.log(`描述: ${settings.description}`)
|
||||
console.log(`部分数量: ${screenshots.length}`)
|
||||
|
||||
// 创建文档
|
||||
const doc = new Document({
|
||||
title: settings.title,
|
||||
description: settings.description,
|
||||
creator: settings.author,
|
||||
styles: {
|
||||
paragraphStyles: [
|
||||
{
|
||||
id: "Normal",
|
||||
name: "Normal",
|
||||
run: {
|
||||
size: 24, // 12pt
|
||||
font: "Microsoft YaHei",
|
||||
},
|
||||
paragraph: {
|
||||
spacing: {
|
||||
line: 360, // 1.5倍行距
|
||||
before: 240, // 12pt
|
||||
after: 240, // 12pt
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "Heading1",
|
||||
name: "Heading 1",
|
||||
run: {
|
||||
size: 36, // 18pt
|
||||
bold: true,
|
||||
font: "Microsoft YaHei",
|
||||
},
|
||||
paragraph: {
|
||||
spacing: {
|
||||
before: 480, // 24pt
|
||||
after: 240, // 12pt
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "Heading2",
|
||||
name: "Heading 2",
|
||||
run: {
|
||||
size: 32, // 16pt
|
||||
bold: true,
|
||||
font: "Microsoft YaHei",
|
||||
},
|
||||
paragraph: {
|
||||
spacing: {
|
||||
before: 360, // 18pt
|
||||
after: 240, // 12pt
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "Caption",
|
||||
name: "Caption",
|
||||
run: {
|
||||
size: 20, // 10pt
|
||||
italic: true,
|
||||
font: "Microsoft YaHei",
|
||||
},
|
||||
paragraph: {
|
||||
alignment: AlignmentType.CENTER,
|
||||
spacing: {
|
||||
before: 120, // 6pt
|
||||
after: 240, // 12pt
|
||||
},
|
||||
},
|
||||
},
|
||||
// 添加标题
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: settings.title,
|
||||
bold: true,
|
||||
size: 32,
|
||||
}),
|
||||
],
|
||||
})
|
||||
heading: HeadingLevel.TITLE,
|
||||
}),
|
||||
)
|
||||
|
||||
// 文档部分
|
||||
const sections = []
|
||||
|
||||
// 封面
|
||||
sections.push({
|
||||
properties: {},
|
||||
children: [
|
||||
// 添加描述
|
||||
if (settings.description) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: "",
|
||||
spacing: {\
|
||||
before: 3000, // 大约页面1/3处
|
||||
},\
|
||||
}),\
|
||||
new Paragraph({\
|
||||
text: settings.title,\
|
||||
heading: HeadingLevel.TITLE,
|
||||
alignment: AlignmentType.CENTER,
|
||||
spacing: {\
|
||||
after: 400,
|
||||
},
|
||||
}),
|
||||
new Paragraph({\
|
||||
text: "",\
|
||||
spacing: {\
|
||||
before: 800,
|
||||
},
|
||||
}),
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,\
|
||||
children: [
|
||||
new TextRun({
|
||||
text: \`作者: ${settings.author}`,\
|
||||
size: 24,\
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,\
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `生成日期: ${new Date().toLocaleString()}`,\
|
||||
text: settings.description,
|
||||
size: 24,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
text: "",\
|
||||
break: PageBreak.AFTER,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
// 目录
|
||||
sections.push({
|
||||
properties: {},
|
||||
children: [\
|
||||
new Paragraph({
|
||||
text: "目录",
|
||||
heading: HeadingLevel.HEADING_1,\
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
new TableOfContents("目录", {
|
||||
hyperlink: true,
|
||||
headingStyleRange: "1-3",\
|
||||
}),\
|
||||
new Paragraph({
|
||||
text: "",
|
||||
break: PageBreak.AFTER,
|
||||
}),\
|
||||
],
|
||||
})
|
||||
|
||||
// 正文
|
||||
const contentSection = {
|
||||
properties: {},
|
||||
children: [] as any[],
|
||||
)
|
||||
}
|
||||
|
||||
// 添加简介
|
||||
contentSection.children.push(\
|
||||
// 添加生成信息
|
||||
if (settings.includeTimestamp) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `生成时间: ${new Date().toLocaleString()}`,
|
||||
size: 20,
|
||||
italics: true,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: "1. 简介",
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
}),
|
||||
new Paragraph({
|
||||
text: "本文档由文档生成工具自动生成,包含应用程序的所有主要页面截图和功能说明。",
|
||||
}),
|
||||
new Paragraph({
|
||||
text: "文档目的是帮助用户了解系统功能和使用方法,为系统管理员和最终用户提供参考。",
|
||||
}),\
|
||||
new Paragraph({
|
||||
text: "",
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `作者: ${settings.author}`,
|
||||
size: 20,
|
||||
italics: true,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
// 添加页面内容
|
||||
contentSection.children.push(
|
||||
// 添加分隔符
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: "2. 系统功能",
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
children: [
|
||||
new TextRun({
|
||||
text: "─".repeat(50),
|
||||
size: 20,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
// 处理每个部分
|
||||
console.log("开始处理文档部分...")
|
||||
|
||||
// 使用for循环而不是forEach,以便更好地处理错误
|
||||
for (let i = 0; i < screenshots.length; i++) {
|
||||
try {
|
||||
const screenshot = screenshots[i]
|
||||
console.log(\`处理部分 ${i + 1}/${screenshots.length}: ${screenshot.name}`)
|
||||
|
||||
// 添加标题
|
||||
contentSection.children.push(
|
||||
new Paragraph({
|
||||
text: `2.${i + 1} ${screenshot.name}`,
|
||||
heading: HeadingLevel.HEADING_2,
|
||||
}),
|
||||
)
|
||||
|
||||
// 添加页面URL和截图时间
|
||||
if (settings.includePageUrls) {
|
||||
contentSection.children.push(
|
||||
new Paragraph({
|
||||
text: `页面URL: ${screenshot.url}`,
|
||||
// 添加每个截图
|
||||
for (const screenshot of screenshots) {
|
||||
// 页面标题
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: screenshot.name,
|
||||
bold: true,
|
||||
size: 28,
|
||||
}),
|
||||
)
|
||||
}
|
||||
],
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
}),
|
||||
)
|
||||
|
||||
if (settings.includeTimestamp) {
|
||||
contentSection.children.push(
|
||||
new Paragraph({
|
||||
text: `截图时间: ${screenshot.timestamp.toLocaleString()}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// 添加状态
|
||||
contentSection.children.push(
|
||||
// 页面URL
|
||||
if (settings.includePageUrls) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: `状态: ${screenshot.status}`,
|
||||
}),
|
||||
)
|
||||
|
||||
// 添加截图
|
||||
try {
|
||||
if (screenshot.dataUrl && screenshot.dataUrl.startsWith("data:image/")) {
|
||||
console.log(`处理截图: ${screenshot.name}`)
|
||||
|
||||
// 从base64数据URL中提取图像数据
|
||||
const base64Data = screenshot.dataUrl.split(",")[1]
|
||||
if (!base64Data) {
|
||||
throw new Error("无效的base64数据")
|
||||
}
|
||||
|
||||
// 将base64转换为二进制数据
|
||||
const imageBuffer = base64ToBuffer(screenshot.dataUrl)
|
||||
|
||||
// 添加图像
|
||||
contentSection.children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new ImageRun({
|
||||
data: imageBuffer,
|
||||
transformation: {
|
||||
width: 600,
|
||||
height: 400,
|
||||
},
|
||||
}),
|
||||
],
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `页面地址: ${screenshot.url}`,
|
||||
size: 20,
|
||||
color: "666666",
|
||||
}),
|
||||
new Paragraph({
|
||||
text: `图 ${i + 1}: ${screenshot.name} 页面截图`,
|
||||
style: "Caption",
|
||||
}),
|
||||
new Paragraph({
|
||||
text: "",
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
console.warn(`部分 ${i + 1} 缺少有效的截图`)
|
||||
contentSection.children.push(
|
||||
new Paragraph({
|
||||
text: "[截图不可用]",
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
new Paragraph({
|
||||
text: "",
|
||||
}),
|
||||
)
|
||||
}
|
||||
} catch (imageError) {
|
||||
console.error(`处理部分 ${i + 1} 的截图时出错:`, imageError)
|
||||
contentSection.children.push(
|
||||
new Paragraph({
|
||||
text: "[处理截图时出错]",
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
new Paragraph({
|
||||
text: "",
|
||||
}),
|
||||
)
|
||||
}
|
||||
} catch (sectionError) {
|
||||
console.error(`处理部分 ${i + 1} 时出错:`, sectionError)
|
||||
contentSection.children.push(
|
||||
new Paragraph({
|
||||
text: `[处理部分 ${i + 1} 时出错: ${sectionError instanceof Error ? sectionError.message : String(sectionError)}]`,
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
new Paragraph({
|
||||
text: "",
|
||||
],
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// 截图时间
|
||||
if (settings.includeTimestamp) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `截图时间: ${screenshot.timestamp.toLocaleString()}`,
|
||||
size: 20,
|
||||
color: "666666",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// 添加截图
|
||||
if (screenshot.dataUrl) {
|
||||
try {
|
||||
// 将 dataUrl 转换为 ArrayBuffer
|
||||
const base64Data = screenshot.dataUrl.split(",")[1]
|
||||
const binaryString = atob(base64Data)
|
||||
const bytes = new Uint8Array(binaryString.length)
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i)
|
||||
}
|
||||
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new ImageRun({
|
||||
data: bytes,
|
||||
transformation: {
|
||||
width: 600,
|
||||
height: 400,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("添加图片失败:", error)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: "[图片加载失败]",
|
||||
color: "FF0000",
|
||||
italics: true,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加分隔符
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: "",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// 添加附录
|
||||
contentSection.children.push(
|
||||
new Paragraph({
|
||||
text: "3. 附录",
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
}),
|
||||
new Paragraph({
|
||||
text: "3.1 文档信息",
|
||||
heading: HeadingLevel.HEADING_2,
|
||||
}),
|
||||
)
|
||||
|
||||
// 创建文档信息表格
|
||||
const infoTable = new Table({
|
||||
width: {
|
||||
size: 100,
|
||||
type: WidthType.PERCENTAGE,
|
||||
},
|
||||
borders: {
|
||||
top: { style: BorderStyle.SINGLE, size: 1, color: "auto" },
|
||||
bottom: { style: BorderStyle.SINGLE, size: 1, color: "auto" },
|
||||
left: { style: BorderStyle.SINGLE, size: 1, color: "auto" },
|
||||
right: { style: BorderStyle.SINGLE, size: 1, color: "auto" },
|
||||
insideHorizontal: { style: BorderStyle.SINGLE, size: 1, color: "auto" },
|
||||
insideVertical: { style: BorderStyle.SINGLE, size: 1, color: "auto" },
|
||||
},
|
||||
rows: [
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({
|
||||
width: {
|
||||
size: 30,
|
||||
type: WidthType.PERCENTAGE,
|
||||
},
|
||||
children: [new Paragraph("文档标题")],
|
||||
}),
|
||||
new TableCell({
|
||||
width: {
|
||||
size: 70,
|
||||
type: WidthType.PERCENTAGE,
|
||||
},
|
||||
children: [new Paragraph(settings.title)],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({
|
||||
children: [new Paragraph("作者")],
|
||||
}),
|
||||
new TableCell({
|
||||
children: [new Paragraph(settings.author)],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({
|
||||
children: [new Paragraph("生成日期")],
|
||||
}),
|
||||
new TableCell({
|
||||
children: [new Paragraph(new Date().toLocaleString())],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({
|
||||
children: [new Paragraph("页面数量")],
|
||||
}),
|
||||
new TableCell({
|
||||
children: [new Paragraph(String(screenshots.length))],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
// 创建文档
|
||||
const doc = new Document({
|
||||
sections: [
|
||||
{
|
||||
properties: {},
|
||||
children: children,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
contentSection.children.push(infoTable)
|
||||
|
||||
// 添加内容部分到文档
|
||||
doc.addSection({
|
||||
children: sections[0].children.concat(sections[1].children, contentSection.children),
|
||||
})
|
||||
|
||||
console.log("文档生成完成,准备导出...")
|
||||
|
||||
// 生成blob
|
||||
const buffer = await doc.save()
|
||||
console.log("文档生成完成")
|
||||
|
||||
// 生成文档
|
||||
const buffer = await Packer.toBuffer(doc)
|
||||
return buffer
|
||||
} catch (error) {
|
||||
console.error("生成Word文档时出错:", error)
|
||||
throw error
|
||||
console.error("生成文档失败:", error)
|
||||
throw new Error(`文档生成失败: ${error instanceof Error ? error.message : "未知错误"}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,78 +1,38 @@
|
||||
interface ScreenshotResult {
|
||||
success: boolean
|
||||
dataUrl?: string
|
||||
error?: string
|
||||
method?: string
|
||||
}
|
||||
import { screenshotService, type ScreenshotOptions, type ScreenshotResult } from "./screenshot-service"
|
||||
|
||||
interface ScreenshotOptions {
|
||||
format?: "png" | "jpeg" | "webp"
|
||||
quality?: number
|
||||
scale?: number
|
||||
width?: number
|
||||
height?: number
|
||||
backgroundColor?: string
|
||||
timeout?: number
|
||||
export interface EnhancedScreenshotOptions extends ScreenshotOptions {
|
||||
waitForImages?: boolean
|
||||
waitForFonts?: boolean
|
||||
removeElements?: string[]
|
||||
addWatermark?: boolean
|
||||
watermarkText?: string
|
||||
}
|
||||
|
||||
class EnhancedScreenshotService {
|
||||
async captureViewport(options: ScreenshotOptions = {}): Promise<ScreenshotResult> {
|
||||
async captureViewport(options: EnhancedScreenshotOptions = {}): Promise<ScreenshotResult> {
|
||||
try {
|
||||
// 创建一个简单的截图占位符
|
||||
const canvas = document.createElement("canvas")
|
||||
canvas.width = options.width || 1200
|
||||
canvas.height = options.height || 800
|
||||
const ctx = canvas.getContext("2d")
|
||||
// 等待页面完全加载
|
||||
await this.waitForPageLoad(options)
|
||||
|
||||
if (ctx) {
|
||||
// 绘制背景
|
||||
ctx.fillStyle = options.backgroundColor || "#f8fafc"
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
// 移除不需要的元素
|
||||
const removedElements = this.removeElements(options.removeElements || [])
|
||||
|
||||
// 绘制标题
|
||||
ctx.fillStyle = "#1e293b"
|
||||
ctx.font = "bold 24px Arial"
|
||||
ctx.fillText("用户数据资产中台", 50, 60)
|
||||
try {
|
||||
// 调用基础截图服务
|
||||
const result = await screenshotService.captureViewport(options)
|
||||
|
||||
// 绘制页面信息
|
||||
ctx.fillStyle = "#64748b"
|
||||
ctx.font = "16px Arial"
|
||||
ctx.fillText(`页面: ${window.location.pathname}`, 50, 100)
|
||||
ctx.fillText(`截图时间: ${new Date().toLocaleString()}`, 50, 130)
|
||||
ctx.fillText(`分辨率: ${canvas.width} x ${canvas.height}`, 50, 160)
|
||||
|
||||
// 绘制一些装饰性元素
|
||||
ctx.strokeStyle = "#e2e8f0"
|
||||
ctx.lineWidth = 2
|
||||
ctx.strokeRect(30, 30, canvas.width - 60, canvas.height - 60)
|
||||
|
||||
// 绘制一些模拟的图表元素
|
||||
ctx.fillStyle = "#3b82f6"
|
||||
ctx.fillRect(50, 200, 200, 100)
|
||||
ctx.fillStyle = "#ffffff"
|
||||
ctx.font = "14px Arial"
|
||||
ctx.fillText("数据概览", 60, 230)
|
||||
ctx.fillText("用户总数: 125,678", 60, 250)
|
||||
ctx.fillText("活跃用户: 45,678", 60, 270)
|
||||
|
||||
ctx.fillStyle = "#10b981"
|
||||
ctx.fillRect(300, 200, 200, 100)
|
||||
ctx.fillStyle = "#ffffff"
|
||||
ctx.fillText("AI分析", 310, 230)
|
||||
ctx.fillText("预测准确率: 94.2%", 310, 250)
|
||||
ctx.fillText("异常检测: 3个", 310, 270)
|
||||
|
||||
const dataUrl = canvas.toDataURL(`image/${options.format || "png"}`, options.quality || 0.9)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
dataUrl,
|
||||
method: "enhanced-canvas",
|
||||
// 添加水印
|
||||
if (options.addWatermark && result.success && result.dataUrl) {
|
||||
result.dataUrl = await this.addWatermark(result.dataUrl, options.watermarkText)
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("无法创建截图")
|
||||
return result
|
||||
} finally {
|
||||
// 恢复移除的元素
|
||||
this.restoreElements(removedElements)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("增强截图失败:", error)
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "未知错误",
|
||||
@@ -80,55 +40,131 @@ class EnhancedScreenshotService {
|
||||
}
|
||||
}
|
||||
|
||||
async captureElement(element: HTMLElement, options: ScreenshotOptions = {}): Promise<ScreenshotResult> {
|
||||
async captureElement(element: HTMLElement, options: EnhancedScreenshotOptions = {}): Promise<ScreenshotResult> {
|
||||
try {
|
||||
// 获取元素的尺寸和位置
|
||||
const rect = element.getBoundingClientRect()
|
||||
const canvas = document.createElement("canvas")
|
||||
canvas.width = options.width || rect.width
|
||||
canvas.height = options.height || rect.height
|
||||
const ctx = canvas.getContext("2d")
|
||||
// 等待页面完全加载
|
||||
await this.waitForPageLoad(options)
|
||||
|
||||
if (ctx) {
|
||||
// 绘制背景
|
||||
ctx.fillStyle = options.backgroundColor || "#ffffff"
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
// 调用基础截图服务
|
||||
const result = await screenshotService.captureElement(element, options)
|
||||
|
||||
// 绘制元素信息
|
||||
ctx.fillStyle = "#1e293b"
|
||||
ctx.font = "16px Arial"
|
||||
ctx.fillText(`元素截图: ${element.tagName}`, 20, 30)
|
||||
|
||||
if (element.textContent) {
|
||||
ctx.fillStyle = "#64748b"
|
||||
ctx.font = "14px Arial"
|
||||
const text = element.textContent.substring(0, 50) + (element.textContent.length > 50 ? "..." : "")
|
||||
ctx.fillText(`内容: ${text}`, 20, 60)
|
||||
}
|
||||
|
||||
// 绘制边框
|
||||
ctx.strokeStyle = "#e2e8f0"
|
||||
ctx.lineWidth = 1
|
||||
ctx.strokeRect(10, 10, canvas.width - 20, canvas.height - 20)
|
||||
|
||||
const dataUrl = canvas.toDataURL(`image/${options.format || "png"}`, options.quality || 0.9)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
dataUrl,
|
||||
method: "enhanced-element",
|
||||
}
|
||||
// 添加水印
|
||||
if (options.addWatermark && result.success && result.dataUrl) {
|
||||
result.dataUrl = await this.addWatermark(result.dataUrl, options.watermarkText)
|
||||
}
|
||||
|
||||
throw new Error("无法截取元素")
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error("增强元素截图失败:", error)
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "未知错误",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async waitForPageLoad(options: EnhancedScreenshotOptions): Promise<void> {
|
||||
// 等待图片加载
|
||||
if (options.waitForImages) {
|
||||
await this.waitForImages()
|
||||
}
|
||||
|
||||
// 等待字体加载
|
||||
if (options.waitForFonts) {
|
||||
await this.waitForFonts()
|
||||
}
|
||||
|
||||
// 额外等待时间确保渲染完成
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
}
|
||||
|
||||
private async waitForImages(): Promise<void> {
|
||||
const images = Array.from(document.images)
|
||||
const promises = images.map((img) => {
|
||||
if (img.complete) return Promise.resolve()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
img.onload = resolve
|
||||
img.onerror = resolve // 即使图片加载失败也继续
|
||||
setTimeout(resolve, 5000) // 5秒超时
|
||||
})
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
private async waitForFonts(): Promise<void> {
|
||||
if ("fonts" in document) {
|
||||
try {
|
||||
await document.fonts.ready
|
||||
} catch (error) {
|
||||
console.warn("字体加载等待失败:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private removeElements(selectors: string[]): Array<{ element: Element; parent: Node; nextSibling: Node | null }> {
|
||||
const removedElements: Array<{ element: Element; parent: Node; nextSibling: Node | null }> = []
|
||||
|
||||
selectors.forEach((selector) => {
|
||||
const elements = document.querySelectorAll(selector)
|
||||
elements.forEach((element) => {
|
||||
if (element.parentNode) {
|
||||
removedElements.push({
|
||||
element,
|
||||
parent: element.parentNode,
|
||||
nextSibling: element.nextSibling,
|
||||
})
|
||||
element.parentNode.removeChild(element)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return removedElements
|
||||
}
|
||||
|
||||
private restoreElements(removedElements: Array<{ element: Element; parent: Node; nextSibling: Node | null }>): void {
|
||||
removedElements.forEach(({ element, parent, nextSibling }) => {
|
||||
if (nextSibling) {
|
||||
parent.insertBefore(element, nextSibling)
|
||||
} else {
|
||||
parent.appendChild(element)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async addWatermark(dataUrl: string, watermarkText?: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
const canvas = document.createElement("canvas")
|
||||
const ctx = canvas.getContext("2d")
|
||||
const img = new Image()
|
||||
|
||||
img.onload = () => {
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
|
||||
// 绘制原图
|
||||
ctx?.drawImage(img, 0, 0)
|
||||
|
||||
if (ctx && watermarkText) {
|
||||
// 添加水印
|
||||
ctx.font = "16px Arial"
|
||||
ctx.fillStyle = "rgba(0, 0, 0, 0.5)"
|
||||
ctx.textAlign = "right"
|
||||
ctx.fillText(
|
||||
watermarkText || `截图时间: ${new Date().toLocaleString()}`,
|
||||
canvas.width - 20,
|
||||
canvas.height - 20,
|
||||
)
|
||||
}
|
||||
|
||||
resolve(canvas.toDataURL("image/png"))
|
||||
}
|
||||
|
||||
img.src = dataUrl
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const enhancedScreenshotService = new EnhancedScreenshotService()
|
||||
export type { ScreenshotResult, ScreenshotOptions }
|
||||
export type { EnhancedScreenshotOptions }
|
||||
|
||||
@@ -25,54 +25,18 @@ class PageRegistry {
|
||||
description: "用户行为洞察分析",
|
||||
category: "用户分析",
|
||||
},
|
||||
{
|
||||
path: "/user-discovery/behavior",
|
||||
name: "行为分析",
|
||||
description: "用户行为模式分析",
|
||||
category: "用户分析",
|
||||
},
|
||||
{
|
||||
path: "/user-discovery/segmentation",
|
||||
name: "用户分群",
|
||||
description: "智能用户分群",
|
||||
category: "用户分析",
|
||||
},
|
||||
{
|
||||
path: "/user-valuation",
|
||||
name: "用户估值",
|
||||
description: "用户价值评估模型",
|
||||
category: "价值分析",
|
||||
},
|
||||
{
|
||||
path: "/user-valuation/model",
|
||||
name: "估值模型",
|
||||
description: "RFM价值评估",
|
||||
category: "价值分析",
|
||||
},
|
||||
{
|
||||
path: "/user-valuation/upgrade-paths",
|
||||
name: "升级路径",
|
||||
description: "用户价值提升策略",
|
||||
category: "价值分析",
|
||||
},
|
||||
{
|
||||
path: "/ai-analysis",
|
||||
name: "AI分析",
|
||||
description: "智能数据洞察",
|
||||
category: "AI功能",
|
||||
},
|
||||
{
|
||||
path: "/ai-analysis/trends",
|
||||
name: "趋势识别",
|
||||
description: "AI趋势预测",
|
||||
category: "AI功能",
|
||||
},
|
||||
{
|
||||
path: "/ai-analysis/anomaly",
|
||||
name: "异常检测",
|
||||
description: "智能异常监控",
|
||||
category: "AI功能",
|
||||
},
|
||||
{
|
||||
path: "/devices",
|
||||
name: "设备管理",
|
||||
|
||||
@@ -3,33 +3,33 @@
|
||||
* 结合多种技术方案确保能够成功捕获页面截图
|
||||
*/
|
||||
|
||||
interface ScreenshotResult {
|
||||
export interface ScreenshotOptions {
|
||||
format?: "png" | "jpeg" | "webp"
|
||||
quality?: number
|
||||
scale?: number
|
||||
backgroundColor?: string
|
||||
timeout?: number
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
export interface ScreenshotResult {
|
||||
success: boolean
|
||||
dataUrl?: string
|
||||
error?: string
|
||||
method?: string
|
||||
}
|
||||
|
||||
interface ScreenshotOptions {
|
||||
format?: "png" | "jpeg" | "webp"
|
||||
quality?: number
|
||||
scale?: number
|
||||
width?: number
|
||||
height?: number
|
||||
backgroundColor?: string
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
class ScreenshotService {
|
||||
async captureViewport(options: ScreenshotOptions = {}): Promise<ScreenshotResult> {
|
||||
try {
|
||||
// 方法1: 使用 html2canvas
|
||||
if (typeof window !== "undefined" && window.html2canvas) {
|
||||
const canvas = await window.html2canvas(document.body, {
|
||||
width: options.width || window.innerWidth,
|
||||
height: options.height || window.innerHeight,
|
||||
scale: options.scale || 1,
|
||||
backgroundColor: options.backgroundColor || "#ffffff",
|
||||
scale: options.scale || 1,
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
})
|
||||
@@ -46,8 +46,9 @@ class ScreenshotService {
|
||||
// 方法2: 使用 dom-to-image
|
||||
if (typeof window !== "undefined" && window.domtoimage) {
|
||||
const dataUrl = await window.domtoimage.toPng(document.body, {
|
||||
width: options.width || window.innerWidth,
|
||||
height: options.height || window.innerHeight,
|
||||
bgcolor: options.backgroundColor || "#ffffff",
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
style: {
|
||||
transform: `scale(${options.scale || 1})`,
|
||||
transformOrigin: "top left",
|
||||
@@ -63,32 +64,35 @@ class ScreenshotService {
|
||||
|
||||
// 方法3: 使用 Canvas API (基础实现)
|
||||
const canvas = document.createElement("canvas")
|
||||
canvas.width = options.width || window.innerWidth
|
||||
canvas.height = options.height || window.innerHeight
|
||||
const ctx = canvas.getContext("2d")
|
||||
|
||||
if (ctx) {
|
||||
ctx.fillStyle = options.backgroundColor || "#ffffff"
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
// 简单的文本渲染作为占位符
|
||||
ctx.fillStyle = "#333333"
|
||||
ctx.font = "16px Arial"
|
||||
ctx.fillText("页面截图占位符", 50, 50)
|
||||
ctx.fillText(`页面: ${window.location.pathname}`, 50, 80)
|
||||
ctx.fillText(`时间: ${new Date().toLocaleString()}`, 50, 110)
|
||||
|
||||
const dataUrl = canvas.toDataURL(`image/${options.format || "png"}`, options.quality || 0.9)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
dataUrl,
|
||||
method: "canvas-fallback",
|
||||
}
|
||||
if (!ctx) {
|
||||
throw new Error("无法获取Canvas上下文")
|
||||
}
|
||||
|
||||
throw new Error("无法创建截图")
|
||||
canvas.width = options.width || window.innerWidth
|
||||
canvas.height = options.height || window.innerHeight
|
||||
|
||||
// 设置背景色
|
||||
ctx.fillStyle = options.backgroundColor || "#ffffff"
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
// 简单的文本渲染作为占位符
|
||||
ctx.fillStyle = "#333333"
|
||||
ctx.font = "16px Arial"
|
||||
ctx.fillText("页面截图占位符", 50, 50)
|
||||
ctx.fillText(`URL: ${window.location.href}`, 50, 80)
|
||||
ctx.fillText(`时间: ${new Date().toLocaleString()}`, 50, 110)
|
||||
|
||||
const dataUrl = canvas.toDataURL(`image/${options.format || "png"}`, options.quality || 0.9)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
dataUrl,
|
||||
method: "canvas-fallback",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("截图失败:", error)
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "未知错误",
|
||||
@@ -101,10 +105,10 @@ class ScreenshotService {
|
||||
// 使用 html2canvas 截取特定元素
|
||||
if (typeof window !== "undefined" && window.html2canvas) {
|
||||
const canvas = await window.html2canvas(element, {
|
||||
width: options.width || element.offsetWidth,
|
||||
height: options.height || element.offsetHeight,
|
||||
scale: options.scale || 1,
|
||||
backgroundColor: options.backgroundColor || "#ffffff",
|
||||
scale: options.scale || 1,
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
})
|
||||
@@ -121,8 +125,9 @@ class ScreenshotService {
|
||||
// 使用 dom-to-image 截取特定元素
|
||||
if (typeof window !== "undefined" && window.domtoimage) {
|
||||
const dataUrl = await window.domtoimage.toPng(element, {
|
||||
width: options.width || element.offsetWidth,
|
||||
height: options.height || element.offsetHeight,
|
||||
bgcolor: options.backgroundColor || "#ffffff",
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -132,8 +137,9 @@ class ScreenshotService {
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("无法截取元素")
|
||||
throw new Error("没有可用的截图库")
|
||||
} catch (error) {
|
||||
console.error("元素截图失败:", error)
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "未知错误",
|
||||
@@ -143,7 +149,14 @@ class ScreenshotService {
|
||||
}
|
||||
|
||||
export const screenshotService = new ScreenshotService()
|
||||
export type { ScreenshotResult, ScreenshotOptions }
|
||||
|
||||
// 扩展 Window 接口以包含截图库
|
||||
declare global {
|
||||
interface Window {
|
||||
html2canvas?: any
|
||||
domtoimage?: any
|
||||
}
|
||||
}
|
||||
|
||||
// 动态导入所需库
|
||||
let htmlToImageModule: any = null
|
||||
@@ -369,18 +382,18 @@ async function captureWithCanvas(target: HTMLIFrameElement | Window): Promise<st
|
||||
// 将文档转换为SVG数据URL
|
||||
const data = new XMLSerializer().serializeToString(document.documentElement)
|
||||
const svg = new Blob([data], { type: "image/svg+xml" })
|
||||
const url = URL.createObjectURL(svg)
|
||||
const svgUrl = URL.createObjectURL(svg)
|
||||
|
||||
// 等待图像加载
|
||||
await new Promise((resolve, reject) => {
|
||||
img.onload = resolve
|
||||
img.onerror = reject
|
||||
img.src = url
|
||||
img.src = svgUrl
|
||||
})
|
||||
|
||||
// 绘制图像
|
||||
ctx.drawImage(img, 0, 0, width, height)
|
||||
URL.revokeObjectURL(url)
|
||||
URL.revokeObjectURL(svgUrl)
|
||||
|
||||
// 返回数据URL
|
||||
return canvas.toDataURL("image/png")
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"@radix-ui/react-tooltip": "latest",
|
||||
"@tanstack/react-table": "latest",
|
||||
"@tanstack/react-virtual": "latest",
|
||||
"buffer": "latest",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "latest",
|
||||
|
||||
27
pnpm-lock.yaml
generated
27
pnpm-lock.yaml
generated
@@ -77,9 +77,6 @@ importers:
|
||||
'@tanstack/react-virtual':
|
||||
specifier: latest
|
||||
version: 3.13.12(react-dom@18.0.0(react@18.0.0))(react@18.0.0)
|
||||
buffer:
|
||||
specifier: latest
|
||||
version: 6.0.3
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
@@ -1303,9 +1300,6 @@ packages:
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
binary-extensions@2.3.0:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1325,9 +1319,6 @@ packages:
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
|
||||
buffer@6.0.3:
|
||||
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||
|
||||
busboy@1.6.0:
|
||||
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
||||
engines: {node: '>=10.16.0'}
|
||||
@@ -2004,9 +1995,6 @@ packages:
|
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
|
||||
ignore@4.0.6:
|
||||
resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==}
|
||||
engines: {node: '>= 4'}
|
||||
@@ -4323,8 +4311,6 @@ snapshots:
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
@@ -4347,11 +4333,6 @@ snapshots:
|
||||
node-releases: 2.0.19
|
||||
update-browserslist-db: 1.1.3(browserslist@4.25.1)
|
||||
|
||||
buffer@6.0.3:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
|
||||
busboy@1.6.0:
|
||||
dependencies:
|
||||
streamsearch: 1.1.0
|
||||
@@ -4834,7 +4815,7 @@ snapshots:
|
||||
eslint: 8.0.0
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@8.0.0))(eslint@8.0.0)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.0.0)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@8.0.0))(eslint@8.0.0))(eslint@8.0.0)
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.0.0)
|
||||
eslint-plugin-react: 7.37.5(eslint@8.0.0)
|
||||
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.0.0)
|
||||
@@ -4864,7 +4845,7 @@ snapshots:
|
||||
tinyglobby: 0.2.14
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.0.0)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@8.0.0))(eslint@8.0.0))(eslint@8.0.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -4879,7 +4860,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.0.0):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@8.0.0))(eslint@8.0.0))(eslint@8.0.0):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
@@ -5222,8 +5203,6 @@ snapshots:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
ieee754@1.2.1: {}
|
||||
|
||||
ignore@4.0.6: {}
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
||||
Reference in New Issue
Block a user