mirror of
https://github.com/yyhuni/xingrin.git
synced 2026-01-31 19:53:11 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
410c543066 | ||
|
|
66da140801 | ||
|
|
e60aac3622 |
@@ -4,7 +4,7 @@ import React, { useState, useMemo } from "react"
|
||||
import { Settings, Search, Pencil, Trash2, Check, X, Plus } from "lucide-react"
|
||||
import * as yaml from "js-yaml"
|
||||
import Editor from "@monaco-editor/react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { useColorTheme } from "@/hooks/use-color-theme"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
@@ -86,7 +86,7 @@ export default function ScanEnginePage() {
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||
const [engineToDelete, setEngineToDelete] = useState<ScanEngine | null>(null)
|
||||
|
||||
const { theme } = useTheme()
|
||||
const { currentTheme } = useColorTheme()
|
||||
|
||||
// API Hooks
|
||||
const { data: engines = [], isLoading } = useEngines()
|
||||
@@ -295,7 +295,7 @@ export default function ScanEnginePage() {
|
||||
wordWrap: "on",
|
||||
padding: { top: 12, bottom: 12 },
|
||||
}}
|
||||
theme={theme === "dark" ? "vs-dark" : "light"}
|
||||
theme={currentTheme.isDark ? "vs-dark" : "light"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
Tag,
|
||||
User,
|
||||
} from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { useColorTheme } from "@/hooks/use-color-theme"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
@@ -91,7 +91,7 @@ export default function NucleiRepoDetailPage() {
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [editorValue, setEditorValue] = useState<string>("")
|
||||
|
||||
const { theme } = useTheme()
|
||||
const { currentTheme } = useColorTheme()
|
||||
|
||||
const numericRepoId = repoId ? Number(repoId) : null
|
||||
|
||||
@@ -321,7 +321,7 @@ export default function NucleiRepoDetailPage() {
|
||||
readOnly: true,
|
||||
padding: { top: 16 },
|
||||
}}
|
||||
theme={theme === "dark" ? "vs-dark" : "light"}
|
||||
theme={currentTheme.isDark ? "vs-dark" : "light"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { toast } from "sonner"
|
||||
import { useTheme } from "next-themes"
|
||||
import { useColorTheme } from "@/hooks/use-color-theme"
|
||||
|
||||
interface EngineCreateDialogProps {
|
||||
open: boolean
|
||||
@@ -37,7 +37,7 @@ export function EngineCreateDialog({
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [isEditorReady, setIsEditorReady] = useState(false)
|
||||
const [yamlError, setYamlError] = useState<{ message: string; line?: number; column?: number } | null>(null)
|
||||
const { theme } = useTheme()
|
||||
const { currentTheme } = useColorTheme()
|
||||
const editorRef = React.useRef<any>(null)
|
||||
|
||||
// 默认 YAML 模板
|
||||
@@ -202,7 +202,7 @@ export function EngineCreateDialog({
|
||||
value={yamlContent}
|
||||
onChange={handleEditorChange}
|
||||
onMount={handleEditorDidMount}
|
||||
theme={theme === "dark" ? "vs-dark" : "light"}
|
||||
theme={currentTheme.isDark ? "vs-dark" : "light"}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
fontSize: 13,
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { toast } from "sonner"
|
||||
import { useTheme } from "next-themes"
|
||||
import { useColorTheme } from "@/hooks/use-color-theme"
|
||||
import type { ScanEngine } from "@/types/engine.types"
|
||||
|
||||
interface EngineEditDialogProps {
|
||||
@@ -40,7 +40,7 @@ export function EngineEditDialog({
|
||||
const [hasChanges, setHasChanges] = useState(false)
|
||||
const [isEditorReady, setIsEditorReady] = useState(false)
|
||||
const [yamlError, setYamlError] = useState<{ message: string; line?: number; column?: number } | null>(null)
|
||||
const { theme } = useTheme()
|
||||
const { currentTheme } = useColorTheme()
|
||||
const editorRef = useRef<any>(null)
|
||||
|
||||
// 生成示例 YAML 配置
|
||||
@@ -276,7 +276,7 @@ url_fetch:
|
||||
value={yamlContent}
|
||||
onChange={handleEditorChange}
|
||||
onMount={handleEditorDidMount}
|
||||
theme={theme === "dark" ? "vs-dark" : "light"}
|
||||
theme={currentTheme.isDark ? "vs-dark" : "light"}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
fontSize: 13,
|
||||
|
||||
@@ -286,7 +286,7 @@ export function ScanHistoryDataTable({
|
||||
|
||||
{/* 表格容器 */}
|
||||
<div className="rounded-md border overflow-x-auto">
|
||||
<Table className="w-full" style={{ minWidth: table.getCenterTotalSize() }}>
|
||||
<Table style={{ minWidth: table.getCenterTotalSize() }}>
|
||||
{/* 表头 */}
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
@@ -314,9 +314,9 @@ export function ScanHistoryDataTable({
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
onDoubleClick={() => header.column.resetSize()}
|
||||
className="absolute -right-2.5 top-0 h-full w-5 cursor-col-resize select-none touch-none bg-transparent flex justify-center"
|
||||
className="absolute right-0 top-0 h-full w-4 cursor-col-resize select-none touch-none z-10"
|
||||
>
|
||||
<div className="w-1.5 h-full bg-transparent group-hover:bg-border" />
|
||||
<div className="absolute right-0 top-0 h-full w-1 bg-transparent group-hover:bg-border" />
|
||||
</div>
|
||||
)}
|
||||
</TableHead>
|
||||
|
||||
@@ -257,14 +257,14 @@ export function QuickScanDialog({ trigger }: QuickScanDialogProps) {
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="target">目标列表</Label>
|
||||
<div className="flex border rounded-md overflow-hidden h-[180px]">
|
||||
<div className="flex border rounded-md overflow-hidden h-[280px]">
|
||||
{/* 行号列 - 固定宽度 */}
|
||||
<div className="flex-shrink-0 w-10 border-r bg-muted/50">
|
||||
<div
|
||||
ref={lineNumbersRef}
|
||||
className="py-2 px-1.5 text-right font-mono text-xs text-muted-foreground leading-[1.4] h-full overflow-y-auto scrollbar-hide"
|
||||
>
|
||||
{Array.from({ length: Math.max(targetInput.split('\n').length, 8) }, (_, i) => (
|
||||
{Array.from({ length: Math.max(targetInput.split('\n').length, 12) }, (_, i) => (
|
||||
<div key={i + 1} className="h-[20px]">
|
||||
{i + 1}
|
||||
</div>
|
||||
|
||||
@@ -179,7 +179,7 @@ export const createScheduledScanColumns = ({
|
||||
// 任务名称列
|
||||
{
|
||||
accessorKey: "name",
|
||||
size: 350,
|
||||
size: 650,
|
||||
minSize: 250,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Task Name" />
|
||||
@@ -203,7 +203,6 @@ export const createScheduledScanColumns = ({
|
||||
accessorKey: "engineName",
|
||||
size: 120,
|
||||
minSize: 80,
|
||||
maxSize: 180,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Scan Engine" />
|
||||
),
|
||||
@@ -223,7 +222,6 @@ export const createScheduledScanColumns = ({
|
||||
header: "Cron Expression",
|
||||
size: 150,
|
||||
minSize: 100,
|
||||
maxSize: 200,
|
||||
cell: ({ row }) => {
|
||||
const cron = row.original.cronExpression
|
||||
return (
|
||||
@@ -246,7 +244,6 @@ export const createScheduledScanColumns = ({
|
||||
header: "Target",
|
||||
size: 180,
|
||||
minSize: 120,
|
||||
maxSize: 280,
|
||||
cell: ({ row }) => {
|
||||
const scanMode = row.original.scanMode
|
||||
const organizationName = row.original.organizationName
|
||||
@@ -279,7 +276,6 @@ export const createScheduledScanColumns = ({
|
||||
accessorKey: "isEnabled",
|
||||
size: 100,
|
||||
minSize: 80,
|
||||
maxSize: 130,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Status" />
|
||||
),
|
||||
@@ -307,7 +303,6 @@ export const createScheduledScanColumns = ({
|
||||
accessorKey: "nextRunTime",
|
||||
size: 150,
|
||||
minSize: 120,
|
||||
maxSize: 200,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Next Run" />
|
||||
),
|
||||
@@ -326,7 +321,6 @@ export const createScheduledScanColumns = ({
|
||||
accessorKey: "runCount",
|
||||
size: 80,
|
||||
minSize: 60,
|
||||
maxSize: 100,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Run Count" />
|
||||
),
|
||||
@@ -343,7 +337,6 @@ export const createScheduledScanColumns = ({
|
||||
accessorKey: "lastRunTime",
|
||||
size: 150,
|
||||
minSize: 120,
|
||||
maxSize: 200,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Last Run" />
|
||||
),
|
||||
|
||||
@@ -44,14 +44,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
|
||||
import type { ScheduledScan } from "@/types/scheduled-scan.types"
|
||||
|
||||
@@ -246,18 +238,24 @@ export function ScheduledScanDataTable({
|
||||
|
||||
{/* 表格容器 */}
|
||||
<div className="rounded-md border overflow-x-auto">
|
||||
<Table style={{ minWidth: table.getCenterTotalSize() }}>
|
||||
<table
|
||||
className="caption-bottom text-sm border-collapse"
|
||||
style={{ width: table.getTotalSize() }}
|
||||
>
|
||||
{/* 表头 */}
|
||||
<TableHeader>
|
||||
<thead className="[&_tr]:border-b">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
<tr
|
||||
key={headerGroup.id}
|
||||
className="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted"
|
||||
>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead
|
||||
<th
|
||||
key={header.id}
|
||||
colSpan={header.colSpan}
|
||||
style={{ width: header.getSize() }}
|
||||
className="relative group"
|
||||
className="h-10 px-2 text-left align-middle font-medium text-foreground whitespace-nowrap relative group"
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
@@ -270,49 +268,53 @@ export function ScheduledScanDataTable({
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
onDoubleClick={() => header.column.resetSize()}
|
||||
className="absolute -right-2.5 top-0 h-full w-8 cursor-col-resize select-none touch-none bg-transparent flex justify-center"
|
||||
className="absolute right-0 top-0 h-full w-4 cursor-col-resize select-none touch-none z-10"
|
||||
>
|
||||
<div className="w-1.5 h-full bg-transparent group-hover:bg-border" />
|
||||
<div className="absolute right-0 top-0 h-full w-1 bg-transparent group-hover:bg-border" />
|
||||
</div>
|
||||
)}
|
||||
</TableHead>
|
||||
</th>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
</tr>
|
||||
))}
|
||||
</TableHeader>
|
||||
</thead>
|
||||
|
||||
{/* 表体 */}
|
||||
<TableBody>
|
||||
<tbody className="[&_tr:last-child]:border-0">
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
<tr
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className="group"
|
||||
className="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted group"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} style={{ width: cell.column.getSize() }}>
|
||||
<td
|
||||
key={cell.id}
|
||||
style={{ width: cell.column.getSize() }}
|
||||
className="p-2 align-middle whitespace-nowrap"
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
</td>
|
||||
))}
|
||||
</TableRow>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
<tr className="border-b transition-colors">
|
||||
<td
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
className="h-24 text-center p-2 align-middle"
|
||||
>
|
||||
暂无数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 分页控制 */}
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
import { useEffect, useMemo, useRef } from "react"
|
||||
import Editor from "@monaco-editor/react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { useColorTheme } from "@/hooks/use-color-theme"
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { useSystemLogs } from "@/hooks/use-system-logs"
|
||||
|
||||
export function SystemLogsView() {
|
||||
const { theme } = useTheme()
|
||||
const { currentTheme } = useColorTheme()
|
||||
const { data } = useSystemLogs({ lines: 500 })
|
||||
|
||||
const content = useMemo(() => data?.content ?? "", [data?.content])
|
||||
@@ -34,7 +34,7 @@ export function SystemLogsView() {
|
||||
height="100%"
|
||||
defaultLanguage="log"
|
||||
value={content || "(暂无日志内容)"}
|
||||
theme={theme === "dark" ? "vs-dark" : "light"}
|
||||
theme={currentTheme.isDark ? "vs-dark" : "light"}
|
||||
onMount={(editor) => {
|
||||
editorRef.current = editor
|
||||
}}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "@/components/ui/dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { useTheme } from "next-themes"
|
||||
import { useColorTheme } from "@/hooks/use-color-theme"
|
||||
import { useWordlistContent, useUpdateWordlistContent } from "@/hooks/use-wordlists"
|
||||
import type { Wordlist } from "@/types/wordlist.types"
|
||||
|
||||
@@ -35,7 +35,7 @@ export function WordlistEditDialog({
|
||||
const [content, setContent] = useState("")
|
||||
const [hasChanges, setHasChanges] = useState(false)
|
||||
const [isEditorReady, setIsEditorReady] = useState(false)
|
||||
const { theme } = useTheme()
|
||||
const { currentTheme } = useColorTheme()
|
||||
const editorRef = useRef<any>(null)
|
||||
|
||||
// 获取字典内容
|
||||
@@ -145,7 +145,7 @@ export function WordlistEditDialog({
|
||||
value={content}
|
||||
onChange={handleEditorChange}
|
||||
onMount={handleEditorDidMount}
|
||||
theme={theme === "dark" ? "vs-dark" : "light"}
|
||||
theme={currentTheme.isDark ? "vs-dark" : "light"}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
fontSize: 13,
|
||||
|
||||
@@ -14,14 +14,14 @@ function Checkbox({
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input bg-background dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-sm border shadow-xs transition-all outline-none focus-visible:ring-[1px] cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"peer border-input bg-background dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-sm border shadow-xs transition-all outline-none focus-visible:ring-[1px] cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
className="flex items-center justify-center text-white transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
|
||||
@@ -90,6 +90,7 @@ export function useDeleteScheduledScan() {
|
||||
|
||||
/**
|
||||
* 切换定时扫描启用状态
|
||||
* 使用乐观更新,避免重新获取数据导致列表重新排序
|
||||
*/
|
||||
export function useToggleScheduledScan() {
|
||||
const queryClient = useQueryClient()
|
||||
@@ -97,11 +98,41 @@ export function useToggleScheduledScan() {
|
||||
return useMutation({
|
||||
mutationFn: ({ id, isEnabled }: { id: number; isEnabled: boolean }) =>
|
||||
toggleScheduledScan(id, isEnabled),
|
||||
onMutate: async ({ id, isEnabled }) => {
|
||||
// 取消正在进行的查询
|
||||
await queryClient.cancelQueries({ queryKey: ['scheduled-scans'] })
|
||||
|
||||
// 获取当前缓存的所有 scheduled-scans 查询
|
||||
const previousQueries = queryClient.getQueriesData({ queryKey: ['scheduled-scans'] })
|
||||
|
||||
// 乐观更新所有匹配的查询缓存
|
||||
queryClient.setQueriesData(
|
||||
{ queryKey: ['scheduled-scans'] },
|
||||
(old: any) => {
|
||||
if (!old?.results) return old
|
||||
return {
|
||||
...old,
|
||||
results: old.results.map((item: any) =>
|
||||
item.id === id ? { ...item, isEnabled } : item
|
||||
),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 返回上下文用于回滚
|
||||
return { previousQueries }
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
toast.success(result.message)
|
||||
queryClient.invalidateQueries({ queryKey: ['scheduled-scans'] })
|
||||
// 不调用 invalidateQueries,保持当前排序
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
onError: (error: Error, _variables, context) => {
|
||||
// 回滚到之前的状态
|
||||
if (context?.previousQueries) {
|
||||
context.previousQueries.forEach(([queryKey, data]) => {
|
||||
queryClient.setQueryData(queryKey, data)
|
||||
})
|
||||
}
|
||||
toast.error(`操作失败: ${error.message}`)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -10,6 +10,8 @@ const nextConfig: NextConfig = {
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
// 允许局域网 IP 访问开发服务器(消除跨域警告)
|
||||
allowedDevOrigins: ['192.168.*.*', '10.*.*.*', '172.16.*.*'],
|
||||
|
||||
async rewrites() {
|
||||
// Docker 环境使用 server 服务名,本地开发使用 localhost
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
--popover: oklch(0.2191 0.0214 284.4920);
|
||||
--popover-foreground: oklch(0.9085 0.0052 228.8253);
|
||||
--primary: oklch(0.9054 0.1546 194.7689);
|
||||
--primary-foreground: oklch(0.1786 0.0167 284.5816);
|
||||
--primary-foreground: oklch(0.1786 0.0167 284.5816);
|
||||
--secondary: oklch(0.3579 0.1469 302.2195);
|
||||
--secondary-foreground: oklch(0.9085 0.0052 228.8253);
|
||||
--muted: oklch(0.2973 0.0276 284.5974);
|
||||
|
||||
Reference in New Issue
Block a user