Compare commits

...

3 Commits

Author SHA1 Message Date
yyhuni
410c543066 优化:大量ui 2025-12-23 20:03:27 +08:00
github-actions[bot]
66da140801 chore: bump version to v1.1.0 2025-12-23 11:20:10 +00:00
yyhuni
e60aac3622 更新输入框ui高度 2025-12-23 19:18:58 +08:00
15 changed files with 96 additions and 68 deletions

View File

@@ -1 +1 @@
v1.0.36
v1.1.0

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,

View File

@@ -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,

View File

@@ -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>

View File

@@ -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>

View File

@@ -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" />
),

View File

@@ -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>
{/* 分页控制 */}

View File

@@ -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
}}

View File

@@ -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,

View File

@@ -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>

View File

@@ -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}`)
},
})

View File

@@ -10,6 +10,8 @@ const nextConfig: NextConfig = {
eslint: {
ignoreDuringBuilds: true,
},
// 允许局域网 IP 访问开发服务器(消除跨域警告)
allowedDevOrigins: ['192.168.*.*', '10.*.*.*', '172.16.*.*'],
async rewrites() {
// Docker 环境使用 server 服务名,本地开发使用 localhost

View File

@@ -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);