fix
This commit is contained in:
@@ -21,6 +21,7 @@ export interface IpScanTask {
|
||||
online_count?: number
|
||||
scan_count?: number
|
||||
enable?: boolean
|
||||
priority?: number
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
@@ -41,9 +42,23 @@ export const fetchIpScanList = (params?: { page?: number; size?: number; keyword
|
||||
export const fetchIpScanDetail = (id: number) =>
|
||||
request.get<{ code: number; details?: IpScanTask; message?: string }>(`/DC-Control/v1/ipscans/${id}`)
|
||||
|
||||
/** 触发一次扫描(会创建 scan_run 并调用 Agent) */
|
||||
/** 触发一次扫描(会创建 scan_run 并调用 Agent);成功体见文档 { message: "scan triggered" } */
|
||||
export const triggerIpScan = (id: number) =>
|
||||
request.post<{ code: number; message?: string }>(`/DC-Control/v1/ipscans/${id}/trigger`)
|
||||
request.post<{ code: number; details?: { message?: string }; message?: string }>(
|
||||
`/DC-Control/v1/ipscans/${id}/trigger`,
|
||||
)
|
||||
|
||||
/** 启动扫描任务(文档:将 status 置为 running,与 Cron 调度语义相关) */
|
||||
export const startIpScan = (id: number) =>
|
||||
request.post<{ code: number; details?: { message?: string }; message?: string }>(
|
||||
`/DC-Control/v1/ipscans/${id}/start`,
|
||||
)
|
||||
|
||||
/** 停止扫描任务(文档:将 status 置为 stopped) */
|
||||
export const stopIpScan = (id: number) =>
|
||||
request.post<{ code: number; details?: { message?: string }; message?: string }>(
|
||||
`/DC-Control/v1/ipscans/${id}/stop`,
|
||||
)
|
||||
|
||||
/** 创建扫描任务 */
|
||||
export const createIpScan = (data: Partial<IpScanTask>) =>
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface ServerItem {
|
||||
collect_args: string
|
||||
collect_interval: number
|
||||
collect_last_result: string
|
||||
is_ip_scan_server: boolean
|
||||
ip_scan_port: number
|
||||
}
|
||||
|
||||
/** 服务器列表响应 */
|
||||
@@ -42,6 +44,7 @@ export interface ServerListParams {
|
||||
size?: number
|
||||
keyword?: string
|
||||
collect_on?: boolean
|
||||
is_ip_scan_server?: boolean
|
||||
}
|
||||
|
||||
/** 创建/更新服务器请求参数 */
|
||||
@@ -66,6 +69,8 @@ export interface ServerFormData {
|
||||
collect_args?: string
|
||||
collect_interval?: number
|
||||
collect_last_result?: string
|
||||
is_ip_scan_server?: boolean
|
||||
ip_scan_port?: number
|
||||
}
|
||||
|
||||
/** 获取服务器列表(分页) */
|
||||
|
||||
@@ -126,6 +126,19 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="8">
|
||||
<a-form-item field="is_ip_scan_server" label="IP扫描执行服务器">
|
||||
<a-switch v-model="formData.is_ip_scan_server" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item v-if="formData.is_ip_scan_server" field="ip_scan_port" label="IP扫描端口">
|
||||
<a-input-number v-model="formData.ip_scan_port" :min="1" :max="65535" placeholder="默认12429" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item field="description" label="描述信息">
|
||||
<a-textarea
|
||||
v-model="formData.description"
|
||||
@@ -178,6 +191,8 @@ const formData = reactive<ServerFormData>({
|
||||
status: 'unknown',
|
||||
collect_on: true,
|
||||
collect_interval: 60,
|
||||
is_ip_scan_server: false,
|
||||
ip_scan_port: 12429,
|
||||
})
|
||||
|
||||
const rules = {
|
||||
@@ -185,6 +200,21 @@ const rules = {
|
||||
host: [{ required: true, message: '请输入主机地址' }],
|
||||
}
|
||||
|
||||
function validateAgentConfigURL(raw?: string): string | null {
|
||||
const v = (raw || '').trim()
|
||||
if (!v) return null
|
||||
try {
|
||||
const u = new URL(v)
|
||||
const protocol = u.protocol.toLowerCase()
|
||||
if (protocol === 'https:' && u.port && u.port !== '443') {
|
||||
return `Agent 配置 URL 使用 https 且端口为 ${u.port},请确认该端口确实启用了 TLS;若为明文服务请改为 http://`
|
||||
}
|
||||
return null
|
||||
} catch {
|
||||
return 'Agent 配置 URL 格式不合法,请输入完整 http(s) URL'
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
@@ -208,6 +238,8 @@ watch(
|
||||
status: props.record.status || 'unknown',
|
||||
collect_on: props.record.collect_on ?? true,
|
||||
collect_interval: props.record.collect_interval || 60,
|
||||
is_ip_scan_server: props.record.is_ip_scan_server ?? false,
|
||||
ip_scan_port: props.record.ip_scan_port || 12429,
|
||||
})
|
||||
} else {
|
||||
Object.assign(formData, {
|
||||
@@ -228,6 +260,8 @@ watch(
|
||||
status: 'unknown',
|
||||
collect_on: true,
|
||||
collect_interval: 60,
|
||||
is_ip_scan_server: false,
|
||||
ip_scan_port: 12429,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -238,6 +272,12 @@ const handleOk = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
|
||||
const agentConfigErr = validateAgentConfigURL(formData.agent_config)
|
||||
if (agentConfigErr) {
|
||||
Message.warning(agentConfigErr)
|
||||
return
|
||||
}
|
||||
|
||||
confirmLoading.value = true
|
||||
|
||||
const submitData: ServerFormData = {
|
||||
@@ -258,6 +298,8 @@ const handleOk = async () => {
|
||||
status: formData.status,
|
||||
collect_on: formData.collect_on,
|
||||
collect_interval: formData.collect_interval,
|
||||
is_ip_scan_server: formData.is_ip_scan_server,
|
||||
ip_scan_port: formData.ip_scan_port,
|
||||
}
|
||||
|
||||
if (isEdit.value && props.record?.id) {
|
||||
|
||||
@@ -14,7 +14,19 @@
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-card :bordered="false">
|
||||
<a-card :bordered="false" title="任务列表">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-input
|
||||
v-model="keyword"
|
||||
allow-clear
|
||||
placeholder="按名称搜索(keyword)"
|
||||
style="width: 220px"
|
||||
@press-enter="runSearch"
|
||||
/>
|
||||
<a-button type="primary" @click="runSearch">查询</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-table
|
||||
row-key="id"
|
||||
:columns="columns"
|
||||
@@ -32,9 +44,17 @@
|
||||
<template #status="{ record }">
|
||||
<a-tag bordered>{{ record.status || '—' }}</a-tag>
|
||||
</template>
|
||||
<template #latestError="{ record }">
|
||||
<span v-if="latestErrorMap[record.id]" class="error-text" :title="latestErrorMap[record.id]">
|
||||
{{ latestErrorMap[record.id] }}
|
||||
</span>
|
||||
<span v-else class="text-muted">—</span>
|
||||
</template>
|
||||
<template #actions="{ record }">
|
||||
<a-space>
|
||||
<a-space wrap>
|
||||
<a-button type="text" size="small" @click="openEdit(record)">编辑</a-button>
|
||||
<a-button type="text" size="small" @click="handleStart(record)">启动</a-button>
|
||||
<a-button type="text" size="small" @click="handleStop(record)">停止</a-button>
|
||||
<a-button type="text" size="small" @click="handleTrigger(record)">触发扫描</a-button>
|
||||
<a-popconfirm content="确定删除该任务?" @ok="handleDelete(record)">
|
||||
<a-button type="text" size="small" status="danger">删除</a-button>
|
||||
@@ -101,6 +121,19 @@
|
||||
<a-form-item label="Cron(可选)">
|
||||
<a-input v-model="form.cron_expr" placeholder="定期扫描 Cron 表达式" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="优先级">
|
||||
<a-input-number v-model="form.priority" :min="0" :max="1000" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="扫描配置(JSON)" extra="须为合法 JSON;可不改,默认提交 {}">
|
||||
<a-textarea
|
||||
v-model="form.config"
|
||||
placeholder="{}"
|
||||
:auto-size="{ minRows: 2, maxRows: 6 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="启用">
|
||||
<a-switch v-model="formEnable" />
|
||||
</a-form-item>
|
||||
<a-form-item label="描述">
|
||||
<a-input v-model="form.description" allow-clear />
|
||||
</a-form-item>
|
||||
@@ -110,7 +143,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { IconPlus } from '@arco-design/web-vue/es/icon'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
@@ -121,9 +154,12 @@ import {
|
||||
updateIpScan,
|
||||
deleteIpScan,
|
||||
triggerIpScan,
|
||||
startIpScan,
|
||||
stopIpScan,
|
||||
type IpScanTask,
|
||||
} from '@/api/ops/ipScan'
|
||||
import { fetchServerList, type ServerItem } from '@/api/ops/server'
|
||||
import { fetchDiscoveryScanRuns, type DiscoveryScanRun } from '@/api/ops/discovery'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -132,6 +168,7 @@ const serversLoading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const tableData = ref<IpScanTask[]>([])
|
||||
const servers = ref<ServerItem[]>([])
|
||||
const latestErrorMap = ref<Record<number, string>>({})
|
||||
|
||||
const pagination = ref({
|
||||
current: 1,
|
||||
@@ -141,6 +178,7 @@ const pagination = ref({
|
||||
|
||||
const modalVisible = ref(false)
|
||||
const editingId = ref<number | null>(null)
|
||||
const keyword = ref('')
|
||||
|
||||
const defaultForm = (): Partial<IpScanTask> => ({
|
||||
name: '',
|
||||
@@ -148,15 +186,48 @@ const defaultForm = (): Partial<IpScanTask> => ({
|
||||
description: '',
|
||||
target_range: '',
|
||||
port_range: '',
|
||||
config: '{}',
|
||||
timeout: 5,
|
||||
concurrency: 100,
|
||||
cron_expr: '',
|
||||
server_id: undefined,
|
||||
status: 'stopped',
|
||||
enable: true,
|
||||
priority: 0,
|
||||
})
|
||||
|
||||
const form = reactive<Partial<IpScanTask>>(defaultForm())
|
||||
|
||||
/** 与 a-switch 绑定(form.enable 可能 undefined) */
|
||||
const formEnable = computed({
|
||||
get: () => form.enable !== false,
|
||||
set: (v: boolean) => {
|
||||
form.enable = v
|
||||
},
|
||||
})
|
||||
|
||||
/** 表单中的 config 转为提交用字符串:空或仅空白 → "{}";否则须可 JSON.parse */
|
||||
function configToSubmitJson(raw: string | undefined): { ok: true; value: string } | { ok: false } {
|
||||
const s = raw?.trim() ?? ''
|
||||
if (!s) return { ok: true, value: '{}' }
|
||||
try {
|
||||
return { ok: true, value: JSON.stringify(JSON.parse(s)) }
|
||||
} catch {
|
||||
return { ok: false }
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑回显:无配置或空串用 "{}";合法 JSON 则格式化便于阅读 */
|
||||
function configForForm(raw?: string): string {
|
||||
const s = raw?.trim() ?? ''
|
||||
if (!s) return '{}'
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(s), null, 2)
|
||||
} catch {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
function unwrapList(res: any): { total: number; data: IpScanTask[] } | undefined {
|
||||
if (!res || res.code !== 0) return undefined
|
||||
const d = res.details ?? res.data
|
||||
@@ -167,7 +238,7 @@ function unwrapList(res: any): { total: number; data: IpScanTask[] } | undefined
|
||||
const loadServers = async () => {
|
||||
serversLoading.value = true
|
||||
try {
|
||||
const res: any = await fetchServerList({ page: 1, size: 500 })
|
||||
const res: any = await fetchServerList({ page: 1, size: 500, is_ip_scan_server: true })
|
||||
if (res.code === 0) {
|
||||
const d = res.details || {}
|
||||
servers.value = d.data || []
|
||||
@@ -183,10 +254,12 @@ const loadTable = async () => {
|
||||
const res: any = await fetchIpScanList({
|
||||
page: pagination.value.current,
|
||||
size: pagination.value.pageSize,
|
||||
keyword: keyword.value.trim() || undefined,
|
||||
})
|
||||
const page = unwrapList(res)
|
||||
tableData.value = page?.data || []
|
||||
pagination.value.total = page?.total || 0
|
||||
await loadLatestErrorsForCurrentPage()
|
||||
if (res.code !== 0) {
|
||||
Message.error(res.message || '加载失败')
|
||||
}
|
||||
@@ -195,6 +268,31 @@ const loadTable = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadLatestErrorsForCurrentPage = async () => {
|
||||
const rows = tableData.value || []
|
||||
if (!rows.length) {
|
||||
latestErrorMap.value = {}
|
||||
return
|
||||
}
|
||||
const entries = await Promise.all(
|
||||
rows.map(async (row) => {
|
||||
try {
|
||||
const res: any = await fetchDiscoveryScanRuns({ scan_id: row.id, page: 1, size: 1 })
|
||||
if (res?.code !== 0) return [row.id, ''] as const
|
||||
const d = (res.details ?? res.data) as { data?: DiscoveryScanRun[] } | undefined
|
||||
const last = d?.data?.[0]
|
||||
if (last?.status === 'failed' && last.error_message) {
|
||||
return [row.id, last.error_message] as const
|
||||
}
|
||||
return [row.id, ''] as const
|
||||
} catch {
|
||||
return [row.id, ''] as const
|
||||
}
|
||||
}),
|
||||
)
|
||||
latestErrorMap.value = Object.fromEntries(entries)
|
||||
}
|
||||
|
||||
const serverLabel = (id?: number) => {
|
||||
if (!id) return '—'
|
||||
const s = servers.value.find((x) => x.id === id)
|
||||
@@ -206,6 +304,11 @@ const onPageChange = (current: number) => {
|
||||
loadTable()
|
||||
}
|
||||
|
||||
const runSearch = () => {
|
||||
pagination.value.current = 1
|
||||
loadTable()
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(form, defaultForm())
|
||||
}
|
||||
@@ -224,11 +327,14 @@ const openEdit = (row: IpScanTask) => {
|
||||
description: row.description || '',
|
||||
target_range: row.target_range,
|
||||
port_range: row.port_range || '',
|
||||
config: configForForm(row.config),
|
||||
timeout: row.timeout ?? 5,
|
||||
concurrency: row.concurrency ?? 100,
|
||||
cron_expr: row.cron_expr || '',
|
||||
server_id: row.server_id,
|
||||
status: row.status || 'stopped',
|
||||
enable: row.enable !== false,
|
||||
priority: row.priority ?? 0,
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
@@ -255,6 +361,12 @@ const submitForm = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
const cfg = configToSubmitJson(form.config)
|
||||
if (!cfg.ok) {
|
||||
Message.warning('扫描配置须为合法 JSON;不填时请保留 {}')
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const body: Partial<IpScanTask> = {
|
||||
@@ -263,11 +375,13 @@ const submitForm = async () => {
|
||||
description: form.description,
|
||||
target_range: form.target_range.trim(),
|
||||
port_range: form.port_range,
|
||||
config: cfg.value,
|
||||
timeout: form.timeout,
|
||||
concurrency: form.concurrency,
|
||||
cron_expr: form.cron_expr,
|
||||
server_id: form.server_id,
|
||||
enable: true,
|
||||
enable: form.enable !== false,
|
||||
priority: form.priority ?? 0,
|
||||
}
|
||||
if (!editingId.value) {
|
||||
body.status = 'stopped'
|
||||
@@ -317,7 +431,8 @@ const handleTrigger = async (row: IpScanTask) => {
|
||||
try {
|
||||
const res: any = await triggerIpScan(row.id)
|
||||
if (res?.code === 0) {
|
||||
Message.success('已触发扫描')
|
||||
const msg = res?.details?.message || res?.data?.message
|
||||
Message.success(msg === 'scan triggered' ? '已触发扫描' : msg || '已触发扫描')
|
||||
} else {
|
||||
Message.error(res?.message || '触发失败')
|
||||
}
|
||||
@@ -327,6 +442,36 @@ const handleTrigger = async (row: IpScanTask) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleStart = async (row: IpScanTask) => {
|
||||
try {
|
||||
const res: any = await startIpScan(row.id)
|
||||
if (res?.code === 0) {
|
||||
Message.success('已启动任务')
|
||||
await loadTable()
|
||||
} else {
|
||||
Message.error(res?.message || '启动失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
Message.error('启动失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleStop = async (row: IpScanTask) => {
|
||||
try {
|
||||
const res: any = await stopIpScan(row.id)
|
||||
if (res?.code === 0) {
|
||||
Message.success('已停止任务')
|
||||
await loadTable()
|
||||
} else {
|
||||
Message.error(res?.message || '停止失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
Message.error('停止失败')
|
||||
}
|
||||
}
|
||||
|
||||
const goAutoTopology = () => {
|
||||
router.push({ path: '/netarch/auto-topo' })
|
||||
}
|
||||
@@ -338,7 +483,8 @@ const columns: TableColumnData[] = [
|
||||
{ title: '目标范围', slotName: 'target', minWidth: 200 },
|
||||
{ title: 'Agent', slotName: 'server', width: 180, ellipsis: true, tooltip: true },
|
||||
{ title: '状态', slotName: 'status', width: 100 },
|
||||
{ title: '操作', slotName: 'actions', width: 220, fixed: 'right' },
|
||||
{ title: '最近错误', slotName: 'latestError', minWidth: 260, ellipsis: true, tooltip: true },
|
||||
{ title: '操作', slotName: 'actions', width: 300, fixed: 'right' },
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -384,4 +530,12 @@ export default {
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #f53f3f;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user