Fix CSS issues, add missing files, and optimize documentation page. Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
675 lines
24 KiB
TypeScript
675 lines
24 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect, useRef } from "react"
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Textarea } from "@/components/ui/textarea"
|
||
import { Checkbox } from "@/components/ui/checkbox"
|
||
import { Progress } from "@/components/ui/progress"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||
import { toast } from "@/components/ui/use-toast"
|
||
import {
|
||
Camera,
|
||
Download,
|
||
FileText,
|
||
ImageIcon,
|
||
Settings,
|
||
Eye,
|
||
Trash2,
|
||
RefreshCw,
|
||
Monitor,
|
||
Smartphone,
|
||
Tablet,
|
||
Globe,
|
||
CheckCircle,
|
||
XCircle,
|
||
Clock,
|
||
Zap,
|
||
} from "lucide-react"
|
||
import {
|
||
enhancedScreenshotService,
|
||
type EnhancedScreenshotOptions,
|
||
} from "@/lib/documentation/enhanced-screenshot-service"
|
||
import { generateDocx } from "@/lib/documentation/docx-generator"
|
||
import { pageRegistry } from "@/lib/documentation/page-registry"
|
||
|
||
interface Screenshot {
|
||
id: string
|
||
name: string
|
||
url: string
|
||
dataUrl?: string
|
||
timestamp: Date
|
||
status: "pending" | "success" | "failed" | "capturing"
|
||
error?: string
|
||
method?: string
|
||
size?: { width: number; height: number }
|
||
}
|
||
|
||
interface DocumentSettings {
|
||
title: string
|
||
author: string
|
||
description: string
|
||
includeTimestamp: boolean
|
||
includePageUrls: boolean
|
||
imageFormat: "png" | "jpeg" | "webp"
|
||
imageQuality: number
|
||
pageSize: "A4" | "A3" | "Letter"
|
||
orientation: "portrait" | "landscape"
|
||
}
|
||
|
||
export default function DocumentationPage() {
|
||
const [screenshots, setScreenshots] = useState<Screenshot[]>([])
|
||
const [selectedPages, setSelectedPages] = useState<string[]>([])
|
||
const [isCapturing, setIsCapturing] = useState(false)
|
||
const [captureProgress, setCaptureProgress] = useState(0)
|
||
const [isGeneratingDoc, setIsGeneratingDoc] = useState(false)
|
||
const [previewMode, setPreviewMode] = useState<"desktop" | "tablet" | "mobile">("desktop")
|
||
const [currentPreviewUrl, setCurrentPreviewUrl] = useState("")
|
||
const [documentSettings, setDocumentSettings] = useState<DocumentSettings>({
|
||
title: "用户数据资产中台文档",
|
||
author: "系统管理员",
|
||
description: "自动生成的系统文档",
|
||
includeTimestamp: true,
|
||
includePageUrls: true,
|
||
imageFormat: "png",
|
||
imageQuality: 0.9,
|
||
pageSize: "A4",
|
||
orientation: "portrait",
|
||
})
|
||
|
||
const previewRef = useRef<HTMLIFrameElement>(null)
|
||
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))
|
||
}, [])
|
||
|
||
const handlePageSelection = (pagePath: string, checked: boolean) => {
|
||
if (checked) {
|
||
setSelectedPages((prev) => [...prev, pagePath])
|
||
} else {
|
||
setSelectedPages((prev) => prev.filter((p) => p !== pagePath))
|
||
}
|
||
}
|
||
|
||
const selectAllPages = () => {
|
||
setSelectedPages(pages.map((p) => p.path))
|
||
}
|
||
|
||
const deselectAllPages = () => {
|
||
setSelectedPages([])
|
||
}
|
||
|
||
const getViewportSize = () => {
|
||
switch (previewMode) {
|
||
case "mobile":
|
||
return { width: 375, height: 667 }
|
||
case "tablet":
|
||
return { width: 768, height: 1024 }
|
||
default:
|
||
return { width: 1920, height: 1080 }
|
||
}
|
||
}
|
||
|
||
const captureScreenshot = async (page: (typeof pages)[0]): Promise<Screenshot> => {
|
||
const screenshot: Screenshot = {
|
||
id: `${page.path}-${Date.now()}`,
|
||
name: page.name,
|
||
url: page.path,
|
||
timestamp: new Date(),
|
||
status: "capturing",
|
||
}
|
||
|
||
try {
|
||
const result = await enhancedScreenshotService.captureViewport({
|
||
...screenshotOptions,
|
||
...getViewportSize(),
|
||
watermarkText: `${page.name} - ${new Date().toLocaleString()}`,
|
||
})
|
||
|
||
if (result.success && result.dataUrl) {
|
||
return {
|
||
...screenshot,
|
||
status: "success",
|
||
dataUrl: result.dataUrl,
|
||
method: result.method,
|
||
size: getViewportSize(),
|
||
}
|
||
} else {
|
||
throw new Error(result.error || "截图失败")
|
||
}
|
||
} catch (error) {
|
||
console.error(`截图失败 ${page.name}:`, error)
|
||
return {
|
||
...screenshot,
|
||
status: "failed",
|
||
error: error instanceof Error ? error.message : "未知错误",
|
||
}
|
||
}
|
||
}
|
||
|
||
const startCapture = async () => {
|
||
if (selectedPages.length === 0) {
|
||
toast({
|
||
title: "请选择页面",
|
||
description: "请至少选择一个页面进行截图",
|
||
variant: "destructive",
|
||
})
|
||
return
|
||
}
|
||
|
||
setIsCapturing(true)
|
||
setCaptureProgress(0)
|
||
setScreenshots([])
|
||
|
||
const selectedPageObjects = pages.filter((p) => selectedPages.includes(p.path))
|
||
const newScreenshots: Screenshot[] = []
|
||
|
||
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,
|
||
url: page.path,
|
||
timestamp: new Date(),
|
||
status: "capturing",
|
||
}
|
||
|
||
newScreenshots.push(pendingScreenshot)
|
||
setScreenshots([...newScreenshots])
|
||
|
||
try {
|
||
const screenshot = await captureScreenshot(page)
|
||
|
||
const updatedScreenshots = [...newScreenshots]
|
||
updatedScreenshots[i] = screenshot
|
||
setScreenshots(updatedScreenshots)
|
||
newScreenshots[i] = screenshot
|
||
|
||
if (screenshot.status === "success") {
|
||
toast({
|
||
title: "截图成功",
|
||
description: `${page.name} 截图完成`,
|
||
})
|
||
} else {
|
||
toast({
|
||
title: "截图失败",
|
||
description: `${page.name}: ${screenshot.error}`,
|
||
variant: "destructive",
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.error(`截图过程出错:`, error)
|
||
const failedScreenshot: Screenshot = {
|
||
...pendingScreenshot,
|
||
status: "failed",
|
||
error: error instanceof Error ? error.message : "未知错误",
|
||
}
|
||
|
||
const updatedScreenshots = [...newScreenshots]
|
||
updatedScreenshots[i] = failedScreenshot
|
||
setScreenshots(updatedScreenshots)
|
||
newScreenshots[i] = failedScreenshot
|
||
}
|
||
|
||
if (i < selectedPageObjects.length - 1) {
|
||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||
}
|
||
}
|
||
|
||
setCaptureProgress(100)
|
||
setIsCapturing(false)
|
||
|
||
const successCount = newScreenshots.filter((s) => s.status === "success").length
|
||
const failedCount = newScreenshots.filter((s) => s.status === "failed").length
|
||
|
||
toast({
|
||
title: "截图完成",
|
||
description: `成功: ${successCount}, 失败: ${failedCount}`,
|
||
variant: successCount > 0 ? "default" : "destructive",
|
||
})
|
||
}
|
||
|
||
const generateDocument = async () => {
|
||
const successfulScreenshots = screenshots.filter((s) => s.status === "success" && s.dataUrl)
|
||
|
||
if (successfulScreenshots.length === 0) {
|
||
toast({
|
||
title: "没有可用的截图",
|
||
description: "请先成功截图至少一个页面",
|
||
variant: "destructive",
|
||
})
|
||
return
|
||
}
|
||
|
||
setIsGeneratingDoc(true)
|
||
|
||
try {
|
||
const docBuffer = await generateDocx(successfulScreenshots, documentSettings)
|
||
|
||
const blob = new Blob([docBuffer], {
|
||
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||
})
|
||
|
||
const url = URL.createObjectURL(blob)
|
||
const a = document.createElement("a")
|
||
a.href = url
|
||
a.download = `${documentSettings.title.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, "_")}_${new Date().toISOString().split("T")[0]}.docx`
|
||
document.body.appendChild(a)
|
||
a.click()
|
||
document.body.removeChild(a)
|
||
URL.revokeObjectURL(url)
|
||
|
||
toast({
|
||
title: "文档生成成功",
|
||
description: `已生成包含 ${successfulScreenshots.length} 个截图的文档`,
|
||
})
|
||
} catch (error) {
|
||
console.error("文档生成失败:", error)
|
||
toast({
|
||
title: "文档生成失败",
|
||
description: error instanceof Error ? error.message : "未知错误",
|
||
variant: "destructive",
|
||
})
|
||
} finally {
|
||
setIsGeneratingDoc(false)
|
||
}
|
||
}
|
||
|
||
const deleteScreenshot = (id: string) => {
|
||
setScreenshots((prev) => prev.filter((s) => s.id !== id))
|
||
toast({
|
||
title: "截图已删除",
|
||
description: "截图已从列表中移除",
|
||
})
|
||
}
|
||
|
||
const clearAllScreenshots = () => {
|
||
setScreenshots([])
|
||
toast({
|
||
title: "已清空所有截图",
|
||
description: "所有截图已从列表中移除",
|
||
})
|
||
}
|
||
|
||
const previewPage = (url: string) => {
|
||
setCurrentPreviewUrl(url)
|
||
}
|
||
|
||
const getStatusIcon = (status: Screenshot["status"]) => {
|
||
switch (status) {
|
||
case "success":
|
||
return <CheckCircle className="h-4 w-4 text-green-500" />
|
||
case "failed":
|
||
return <XCircle className="h-4 w-4 text-red-500" />
|
||
case "capturing":
|
||
return <Clock className="h-4 w-4 text-blue-500 animate-spin" />
|
||
default:
|
||
return <Clock className="h-4 w-4 text-gray-400" />
|
||
}
|
||
}
|
||
|
||
const getPreviewIcon = () => {
|
||
switch (previewMode) {
|
||
case "mobile":
|
||
return <Smartphone className="h-4 w-4" />
|
||
case "tablet":
|
||
return <Tablet className="h-4 w-4" />
|
||
default:
|
||
return <Monitor className="h-4 w-4" />
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="container mx-auto p-6 space-y-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-3xl font-bold">文档生成器</h1>
|
||
<p className="text-muted-foreground mt-1">自动截图并生成系统文档</p>
|
||
</div>
|
||
<div className="flex items-center space-x-2">
|
||
<Badge variant="outline" className="flex items-center space-x-1">
|
||
<Zap className="h-3 w-3" />
|
||
<span>增强版截图</span>
|
||
</Badge>
|
||
</div>
|
||
</div>
|
||
|
||
<Tabs defaultValue="capture" className="space-y-6">
|
||
<TabsList className="grid w-full grid-cols-3">
|
||
<TabsTrigger value="capture">页面截图</TabsTrigger>
|
||
<TabsTrigger value="screenshots">截图管理</TabsTrigger>
|
||
<TabsTrigger value="settings">文档设置</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent value="capture" className="space-y-6">
|
||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||
<div className="lg:col-span-2">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center space-x-2">
|
||
<Globe className="h-5 w-5" />
|
||
<span>选择页面</span>
|
||
</CardTitle>
|
||
<CardDescription>选择需要截图的页面,支持多选</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center space-x-2">
|
||
<Button variant="outline" size="sm" onClick={selectAllPages}>
|
||
全选
|
||
</Button>
|
||
<Button variant="outline" size="sm" onClick={deselectAllPages}>
|
||
取消全选
|
||
</Button>
|
||
</div>
|
||
<Badge variant="secondary">
|
||
已选择 {selectedPages.length} / {pages.length} 个页面
|
||
</Badge>
|
||
</div>
|
||
|
||
<ScrollArea className="h-[400px]">
|
||
<div className="space-y-2">
|
||
{pages.map((page) => (
|
||
<div
|
||
key={page.path}
|
||
className="flex items-center space-x-3 p-3 border rounded-lg hover:bg-gray-50"
|
||
>
|
||
<Checkbox
|
||
checked={selectedPages.includes(page.path)}
|
||
onCheckedChange={(checked) => handlePageSelection(page.path, checked as boolean)}
|
||
/>
|
||
<div className="flex-1 min-w-0">
|
||
<div className="font-medium">{page.name}</div>
|
||
<div className="text-sm text-muted-foreground truncate">{page.path}</div>
|
||
{page.description && (
|
||
<div className="text-xs text-muted-foreground mt-1">{page.description}</div>
|
||
)}
|
||
</div>
|
||
<Button variant="ghost" size="sm" onClick={() => previewPage(page.path)}>
|
||
<Eye className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</ScrollArea>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
<div className="space-y-6">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center space-x-2">
|
||
<Camera className="h-5 w-5" />
|
||
<span>开始截图</span>
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
{isCapturing && (
|
||
<div className="space-y-2">
|
||
<div className="flex items-center justify-between text-sm">
|
||
<span>截图进度</span>
|
||
<span>{Math.round(captureProgress)}%</span>
|
||
</div>
|
||
<Progress value={captureProgress} />
|
||
</div>
|
||
)}
|
||
|
||
<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>
|
||
|
||
<div className="text-sm text-muted-foreground">
|
||
<div className="flex items-center justify-between">
|
||
<span>总页面数:</span>
|
||
<span>{pages.length}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span>已选择:</span>
|
||
<span>{selectedPages.length}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span>已截图:</span>
|
||
<span>{screenshots.length}</span>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="screenshots" className="space-y-6">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center justify-between">
|
||
<div className="flex items-center space-x-2">
|
||
<ImageIcon className="h-5 w-5" />
|
||
<span>截图管理</span>
|
||
</div>
|
||
<div className="flex items-center space-x-2">
|
||
<Badge variant="outline">{screenshots.filter((s) => s.status === "success").length} 成功</Badge>
|
||
<Badge variant="destructive">{screenshots.filter((s) => s.status === "failed").length} 失败</Badge>
|
||
{screenshots.length > 0 && (
|
||
<Button variant="outline" size="sm" onClick={clearAllScreenshots}>
|
||
<Trash2 className="h-4 w-4 mr-1" />
|
||
清空
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</CardTitle>
|
||
<CardDescription>管理已截取的页面截图</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{screenshots.length === 0 ? (
|
||
<div className="text-center py-12 text-muted-foreground">
|
||
<ImageIcon className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||
<p>暂无截图</p>
|
||
<p className="text-sm">请先在"页面截图"标签页中截取页面</p>
|
||
</div>
|
||
) : (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
{screenshots.map((screenshot) => (
|
||
<Card key={screenshot.id} className="overflow-hidden">
|
||
<div className="aspect-video bg-gray-100 relative">
|
||
{screenshot.dataUrl ? (
|
||
<img
|
||
src={screenshot.dataUrl || "/placeholder.svg"}
|
||
alt={screenshot.name}
|
||
className="w-full h-full object-cover"
|
||
/>
|
||
) : (
|
||
<div className="flex items-center justify-center h-full">
|
||
{screenshot.status === "capturing" ? (
|
||
<RefreshCw className="h-8 w-8 animate-spin text-blue-500" />
|
||
) : (
|
||
<ImageIcon className="h-8 w-8 text-gray-400" />
|
||
)}
|
||
</div>
|
||
)}
|
||
<div className="absolute top-2 right-2">{getStatusIcon(screenshot.status)}</div>
|
||
</div>
|
||
<CardContent className="p-4">
|
||
<div className="space-y-2">
|
||
<div className="font-medium truncate">{screenshot.name}</div>
|
||
<div className="text-sm text-muted-foreground truncate">{screenshot.url}</div>
|
||
{screenshot.method && (
|
||
<Badge variant="outline" className="text-xs">
|
||
{screenshot.method}
|
||
</Badge>
|
||
)}
|
||
{screenshot.error && <div className="text-xs text-red-500 truncate">{screenshot.error}</div>}
|
||
<div className="text-xs text-muted-foreground">{screenshot.timestamp.toLocaleString()}</div>
|
||
<div className="flex items-center justify-between">
|
||
<div className="text-xs text-muted-foreground">
|
||
{screenshot.size && `${screenshot.size.width}×${screenshot.size.height}`}
|
||
</div>
|
||
<Button variant="ghost" size="sm" onClick={() => deleteScreenshot(screenshot.id)}>
|
||
<Trash2 className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{screenshots.some((s) => s.status === "success") && (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center space-x-2">
|
||
<FileText className="h-5 w-5" />
|
||
<span>生成文档</span>
|
||
</CardTitle>
|
||
<CardDescription>将成功的截图生成为Word文档</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<Button onClick={generateDocument} disabled={isGeneratingDoc} className="w-full">
|
||
{isGeneratingDoc ? (
|
||
<>
|
||
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
|
||
生成中...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Download className="mr-2 h-4 w-4" />
|
||
生成Word文档
|
||
</>
|
||
)}
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</TabsContent>
|
||
|
||
<TabsContent value="settings" className="space-y-6">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center space-x-2">
|
||
<Settings className="h-5 w-5" />
|
||
<span>文档设置</span>
|
||
</CardTitle>
|
||
<CardDescription>配置生成的Word文档格式和内容</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-6">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<div className="space-y-4">
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-medium">文档标题</label>
|
||
<Input
|
||
value={documentSettings.title}
|
||
onChange={(e) =>
|
||
setDocumentSettings((prev) => ({
|
||
...prev,
|
||
title: e.target.value,
|
||
}))
|
||
}
|
||
placeholder="输入文档标题"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-medium">作者</label>
|
||
<Input
|
||
value={documentSettings.author}
|
||
onChange={(e) =>
|
||
setDocumentSettings((prev) => ({
|
||
...prev,
|
||
author: e.target.value,
|
||
}))
|
||
}
|
||
placeholder="输入作者姓名"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-medium">文档描述</label>
|
||
<Textarea
|
||
value={documentSettings.description}
|
||
onChange={(e) =>
|
||
setDocumentSettings((prev) => ({
|
||
...prev,
|
||
description: e.target.value,
|
||
}))
|
||
}
|
||
placeholder="输入文档描述"
|
||
rows={3}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div className="space-y-3">
|
||
<label className="text-sm font-medium">包含内容</label>
|
||
<div className="space-y-2">
|
||
<div className="flex items-center space-x-2">
|
||
<Checkbox
|
||
checked={documentSettings.includeTimestamp}
|
||
onCheckedChange={(checked) =>
|
||
setDocumentSettings((prev) => ({
|
||
...prev,
|
||
includeTimestamp: checked as boolean,
|
||
}))
|
||
}
|
||
/>
|
||
<label className="text-sm">包含时间戳</label>
|
||
</div>
|
||
<div className="flex items-center space-x-2">
|
||
<Checkbox
|
||
checked={documentSettings.includePageUrls}
|
||
onCheckedChange={(checked) =>
|
||
setDocumentSettings((prev) => ({
|
||
...prev,
|
||
includePageUrls: checked as boolean,
|
||
}))
|
||
}
|
||
/>
|
||
<label className="text-sm">包含页面URL</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
</Tabs>
|
||
</div>
|
||
)
|
||
}
|