feat: 新增适配验证接口,mediabot 可提交站点访问结果,管理员可校验适配是否正确
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
package com.par.api.controller;
|
||||
|
||||
import com.par.core.dto.AccessResultDTO;
|
||||
import com.par.core.dto.ApiResponse;
|
||||
import com.par.core.entity.AccessResult;
|
||||
import com.par.core.service.AccessResultService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 站点访问结果控制器
|
||||
* 提交需 HMAC 鉴权(mediabot 调用),查看需管理员权限
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class AccessResultController {
|
||||
|
||||
private final AccessResultService accessResultService;
|
||||
|
||||
// ==================== mediabot 提交接口(PSK 鉴权) ====================
|
||||
|
||||
/**
|
||||
* mediabot 提交访问站点的返回数据
|
||||
*/
|
||||
@PostMapping("/api/v1/sites/{siteId}/access-result")
|
||||
public ApiResponse<AccessResultDTO> submitAccessResult(
|
||||
@PathVariable("siteId") String siteId,
|
||||
@RequestBody Map<String, Object> payload,
|
||||
HttpServletRequest request) {
|
||||
|
||||
String configVersion = (String) payload.get("configVersion");
|
||||
String contentType = (String) payload.getOrDefault("contentType", "html");
|
||||
String content = (String) payload.get("content");
|
||||
String url = (String) payload.get("url");
|
||||
Integer statusCode = payload.get("statusCode") instanceof Integer i ? i : null;
|
||||
Integer responseTimeMs = payload.get("responseTimeMs") instanceof Integer i ? i : null;
|
||||
|
||||
if (content == null || content.isBlank()) {
|
||||
return ApiResponse.error(400, "content is required");
|
||||
}
|
||||
|
||||
String clientId = (String) request.getAttribute("clientId");
|
||||
Long submitterId = (Long) request.getAttribute("accountId");
|
||||
|
||||
AccessResult result = accessResultService.submit(
|
||||
siteId, configVersion, clientId, contentType, content,
|
||||
url, statusCode, responseTimeMs, submitterId);
|
||||
|
||||
return ApiResponse.success(accessResultService.toDTO(result));
|
||||
}
|
||||
|
||||
// ==================== 管理员查看接口(JWT 鉴权) ====================
|
||||
|
||||
/**
|
||||
* 获取所有访问结果列表
|
||||
*/
|
||||
@GetMapping("/api/v1/admin/access-results")
|
||||
public ApiResponse<List<AccessResultDTO>> listAll() {
|
||||
return ApiResponse.success(accessResultService.listAll());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定站点的访问结果
|
||||
*/
|
||||
@GetMapping("/api/v1/admin/access-results/by-site/{siteId}")
|
||||
public ApiResponse<List<AccessResultDTO>> listBySiteId(@PathVariable("siteId") String siteId) {
|
||||
return ApiResponse.success(accessResultService.listBySiteId(siteId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个访问结果详情(含完整内容)
|
||||
*/
|
||||
@GetMapping("/api/v1/admin/access-results/{id}")
|
||||
public ApiResponse<AccessResultDTO> getById(@PathVariable("id") Long id) {
|
||||
AccessResult result = accessResultService.getById(id);
|
||||
if (result == null) {
|
||||
return ApiResponse.error(404, "访问结果不存在");
|
||||
}
|
||||
return ApiResponse.success(accessResultService.toDTO(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员验证适配结果
|
||||
*/
|
||||
@PostMapping("/api/v1/admin/access-results/{id}/validate")
|
||||
public ApiResponse<AccessResultDTO> validate(
|
||||
@PathVariable("id") Long id,
|
||||
@RequestBody Map<String, Object> payload) {
|
||||
|
||||
Boolean isValid = payload.get("isValid") instanceof Boolean b ? b : null;
|
||||
String note = (String) payload.getOrDefault("note", "");
|
||||
|
||||
if (isValid == null) {
|
||||
return ApiResponse.error(400, "isValid is required");
|
||||
}
|
||||
|
||||
try {
|
||||
AccessResult result = accessResultService.validate(id, isValid, note);
|
||||
return ApiResponse.success(accessResultService.toDTO(result));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ApiResponse.error(404, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ public class WebConfig implements WebMvcConfigurer {
|
||||
// HMAC 鉴权拦截器:拦截所有写入端点
|
||||
registry.addInterceptor(hmacAuthInterceptor)
|
||||
.addPathPatterns("/api/v1/configs/*/submit", "/api/v1/sites/*/submit",
|
||||
"/api/v1/sites/*/access-result",
|
||||
"/api/v1/admin/**", "/api/v1/account/**");
|
||||
}
|
||||
}
|
||||
|
||||
26
par-core/src/main/java/com/par/core/dto/AccessResultDTO.java
Normal file
26
par-core/src/main/java/com/par/core/dto/AccessResultDTO.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.par.core.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 站点访问结果 DTO
|
||||
*/
|
||||
@Data
|
||||
public class AccessResultDTO {
|
||||
|
||||
private Long id;
|
||||
private String siteId;
|
||||
private String configVersion;
|
||||
private String clientId;
|
||||
private String contentType;
|
||||
private String content;
|
||||
private String url;
|
||||
private Integer statusCode;
|
||||
private Integer responseTimeMs;
|
||||
private Boolean isValid;
|
||||
private String validationNote;
|
||||
private Long submitterId;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
53
par-core/src/main/java/com/par/core/entity/AccessResult.java
Normal file
53
par-core/src/main/java/com/par/core/entity/AccessResult.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package com.par.core.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 站点访问结果实体(mediabot 提交访问站点返回的 HTML/JSON 数据)
|
||||
*/
|
||||
@Data
|
||||
@TableName("access_results")
|
||||
public class AccessResult {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@TableField("site_id")
|
||||
private String siteId;
|
||||
|
||||
@TableField("config_version")
|
||||
private String configVersion;
|
||||
|
||||
@TableField("client_id")
|
||||
private String clientId;
|
||||
|
||||
@TableField("content_type")
|
||||
private String contentType;
|
||||
|
||||
@TableField("content")
|
||||
private String content;
|
||||
|
||||
@TableField("url")
|
||||
private String url;
|
||||
|
||||
@TableField("status_code")
|
||||
private Integer statusCode;
|
||||
|
||||
@TableField("response_time_ms")
|
||||
private Integer responseTimeMs;
|
||||
|
||||
@TableField("is_valid")
|
||||
private Boolean isValid;
|
||||
|
||||
@TableField("validation_note")
|
||||
private String validationNote;
|
||||
|
||||
@TableField("submitter_id")
|
||||
private Long submitterId;
|
||||
|
||||
@TableField(value = "created_at", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.par.core.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.par.core.entity.AccessResult;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 站点访问结果 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface AccessResultMapper extends BaseMapper<AccessResult> {
|
||||
|
||||
@Select("SELECT * FROM access_results WHERE site_id = #{siteId} ORDER BY created_at DESC")
|
||||
List<AccessResult> selectBySiteId(@Param("siteId") String siteId);
|
||||
|
||||
@Select("SELECT * FROM access_results ORDER BY created_at DESC")
|
||||
List<AccessResult> selectAll();
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.par.core.service;
|
||||
|
||||
import com.par.core.dto.AccessResultDTO;
|
||||
import com.par.core.entity.AccessResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 站点访问结果服务接口
|
||||
*/
|
||||
public interface AccessResultService {
|
||||
|
||||
/**
|
||||
* 提交访问结果(mediabot 调用)
|
||||
*/
|
||||
AccessResult submit(String siteId, String configVersion, String clientId,
|
||||
String contentType, String content, String url,
|
||||
Integer statusCode, Integer responseTimeMs, Long submitterId);
|
||||
|
||||
/**
|
||||
* 获取所有访问结果
|
||||
*/
|
||||
List<AccessResultDTO> listAll();
|
||||
|
||||
/**
|
||||
* 获取指定站点的访问结果
|
||||
*/
|
||||
List<AccessResultDTO> listBySiteId(String siteId);
|
||||
|
||||
/**
|
||||
* 获取单个访问结果详情
|
||||
*/
|
||||
AccessResult getById(Long id);
|
||||
|
||||
/**
|
||||
* 标记验证结果(有效/无效)
|
||||
*/
|
||||
AccessResult validate(Long id, Boolean isValid, String note);
|
||||
|
||||
/**
|
||||
* 实体转 DTO
|
||||
*/
|
||||
AccessResultDTO toDTO(AccessResult entity);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.par.core.service.impl;
|
||||
|
||||
import com.par.core.dto.AccessResultDTO;
|
||||
import com.par.core.entity.AccessResult;
|
||||
import com.par.core.mapper.AccessResultMapper;
|
||||
import com.par.core.service.AccessResultService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AccessResultServiceImpl implements AccessResultService {
|
||||
|
||||
private final AccessResultMapper mapper;
|
||||
|
||||
@Override
|
||||
public AccessResult submit(String siteId, String configVersion, String clientId,
|
||||
String contentType, String content, String url,
|
||||
Integer statusCode, Integer responseTimeMs, Long submitterId) {
|
||||
AccessResult result = new AccessResult();
|
||||
result.setSiteId(siteId);
|
||||
result.setConfigVersion(configVersion);
|
||||
result.setClientId(clientId);
|
||||
result.setContentType(contentType != null ? contentType : "html");
|
||||
result.setContent(content);
|
||||
result.setUrl(url);
|
||||
result.setStatusCode(statusCode);
|
||||
result.setResponseTimeMs(responseTimeMs);
|
||||
result.setSubmitterId(submitterId != null ? submitterId : 0L);
|
||||
mapper.insert(result);
|
||||
log.info("Access result submitted: siteId={}, version={}, clientId={}, id={}",
|
||||
siteId, configVersion, clientId, result.getId());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AccessResultDTO> listAll() {
|
||||
return mapper.selectAll().stream().map(this::toDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AccessResultDTO> listBySiteId(String siteId) {
|
||||
return mapper.selectBySiteId(siteId).stream().map(this::toDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccessResult getById(Long id) {
|
||||
return mapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccessResult validate(Long id, Boolean isValid, String note) {
|
||||
AccessResult result = mapper.selectById(id);
|
||||
if (result == null) throw new IllegalArgumentException("访问结果不存在: " + id);
|
||||
result.setIsValid(isValid);
|
||||
result.setValidationNote(note);
|
||||
mapper.updateById(result);
|
||||
log.info("Access result validated: id={}, isValid={}", id, isValid);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccessResultDTO toDTO(AccessResult entity) {
|
||||
if (entity == null) return null;
|
||||
AccessResultDTO dto = new AccessResultDTO();
|
||||
dto.setId(entity.getId());
|
||||
dto.setSiteId(entity.getSiteId());
|
||||
dto.setConfigVersion(entity.getConfigVersion());
|
||||
dto.setClientId(entity.getClientId());
|
||||
dto.setContentType(entity.getContentType());
|
||||
dto.setContent(entity.getContent());
|
||||
dto.setUrl(entity.getUrl());
|
||||
dto.setStatusCode(entity.getStatusCode());
|
||||
dto.setResponseTimeMs(entity.getResponseTimeMs());
|
||||
dto.setIsValid(entity.getIsValid());
|
||||
dto.setValidationNote(entity.getValidationNote());
|
||||
dto.setSubmitterId(entity.getSubmitterId());
|
||||
dto.setCreatedAt(entity.getCreatedAt());
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
-- PAR Flyway V10: 站点访问结果表(mediabot 提交访问站点返回的 HTML/JSON 数据,用于验证适配是否正确)
|
||||
CREATE TABLE access_results (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
site_id VARCHAR(64) NOT NULL COMMENT '站点ID',
|
||||
config_version VARCHAR(20) COMMENT '使用的配置版本号',
|
||||
client_id VARCHAR(128) COMMENT '提交者标识(mediabot 客户端 ID)',
|
||||
content_type VARCHAR(10) NOT NULL DEFAULT 'html' COMMENT '数据类型:html / json',
|
||||
content MEDIUMTEXT NOT NULL COMMENT '访问站点返回的内容',
|
||||
url VARCHAR(500) COMMENT '访问的站点 URL',
|
||||
status_code INT COMMENT 'HTTP 状态码',
|
||||
response_time_ms INT COMMENT '响应耗时(毫秒)',
|
||||
is_valid TINYINT(1) COMMENT '适配是否有效(null=未验证, 1=有效, 0=无效)',
|
||||
validation_note VARCHAR(500) COMMENT '管理员验证备注',
|
||||
submitter_id BIGINT UNSIGNED COMMENT '提交者账户 ID(PSK 提交时为 0)',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '提交时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_site_id (site_id),
|
||||
KEY idx_created (created_at),
|
||||
KEY idx_valid (is_valid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='站点访问结果表';
|
||||
@@ -66,5 +66,11 @@ export default {
|
||||
emailStats: () => api.get('/admin/logs/stats/email'),
|
||||
pathStats: () => api.get('/admin/logs/stats/path'),
|
||||
},
|
||||
accessResults: {
|
||||
list: () => api.get('/admin/access-results'),
|
||||
bySite: (siteId) => api.get(`/admin/access-results/by-site/${siteId}`),
|
||||
get: (id) => api.get(`/admin/access-results/${id}`),
|
||||
validate: (id, isValid, note) => api.post(`/admin/access-results/${id}/validate`, { isValid, note: note || '' }),
|
||||
},
|
||||
health: () => api.get('/health', { baseURL: '' })
|
||||
}
|
||||
|
||||
@@ -53,6 +53,12 @@ const routes = [
|
||||
name: 'ApiLogs',
|
||||
component: () => import('../views/ApiLogs.vue'),
|
||||
meta: { title: '访问日志' }
|
||||
},
|
||||
{
|
||||
path: 'access-results',
|
||||
name: 'AccessResults',
|
||||
component: () => import('../views/AccessResults.vue'),
|
||||
meta: { title: '适配验证' }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
235
web/src/views/AccessResults.vue
Normal file
235
web/src/views/AccessResults.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>适配验证</h2>
|
||||
<el-card style="margin-top: 20px">
|
||||
<div style="margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center">
|
||||
<div>
|
||||
<el-select v-model="filterSiteId" placeholder="按站点筛选" clearable @change="fetchAll" style="width: 200px">
|
||||
<el-option v-for="s in siteOptions" :key="s.siteId" :label="s.siteId" :value="s.siteId" />
|
||||
</el-select>
|
||||
<el-select v-model="filterValid" placeholder="验证状态" clearable @change="fetchAll" style="width: 140px; margin-left: 8px">
|
||||
<el-option label="已验证-有效" value="1" />
|
||||
<el-option label="已验证-无效" value="0" />
|
||||
<el-option label="未验证" value="null" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-button @click="fetchAll" :icon="Refresh">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="filteredList" v-loading="loading" stripe size="small" empty-text="暂无访问记录">
|
||||
<el-table-column type="index" label="#" width="50" />
|
||||
<el-table-column prop="siteId" label="站点ID" width="130" />
|
||||
<el-table-column prop="configVersion" label="配置版本" width="100">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.configVersion" class="text-xs">{{ scope.row.configVersion }}</span>
|
||||
<span v-else class="text-xs" style="color:#909399">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="clientId" label="来源" width="120">
|
||||
<template #default="scope">
|
||||
<span class="text-xs">{{ scope.row.clientId || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="url" label="访问URL" min-width="200">
|
||||
<template #default="scope">
|
||||
<span class="text-xs" style="word-break: break-all">{{ scope.row.url || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="statusCode" label="HTTP" width="70">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.statusCode" size="small" :type="statusTag(scope.row.statusCode)">
|
||||
{{ scope.row.statusCode }}
|
||||
</el-tag>
|
||||
<span v-else class="text-xs" style="color:#909399">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="responseTimeMs" label="耗时" width="80">
|
||||
<template #default="scope">
|
||||
<span class="text-xs">{{ scope.row.responseTimeMs != null ? scope.row.responseTimeMs + 'ms' : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="验证" width="110">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.isValid === true" size="small" type="success">有效</el-tag>
|
||||
<el-tag v-else-if="scope.row.isValid === false" size="small" type="danger">无效</el-tag>
|
||||
<el-tag v-else size="small" type="info">未验证</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="时间" width="160" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleView(scope.row)">查看内容</el-button>
|
||||
<el-button v-if="scope.row.isValid == null" size="small" type="success" @click="handleValidate(scope.row, true)">有效</el-button>
|
||||
<el-button v-if="scope.row.isValid == null" size="small" type="danger" @click="handleValidate(scope.row, false)">无效</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 查看内容对话框 -->
|
||||
<el-dialog v-model="detailVisible" title="访问结果详情" width="900px" :close-on-click-modal="false">
|
||||
<el-descriptions :column="2" border size="small" style="margin-bottom: 16px">
|
||||
<el-descriptions-item label="站点">{{ detail.siteId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="配置版本">{{ detail.configVersion || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="来源">{{ detail.clientId || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="数据类型">{{ detail.contentType }}</el-descriptions-item>
|
||||
<el-descriptions-item label="访问URL" :span="2">{{ detail.url || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="HTTP状态码">
|
||||
<el-tag v-if="detail.statusCode" size="small" :type="statusTag(detail.statusCode)">{{ detail.statusCode }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="响应耗时">{{ detail.responseTimeMs != null ? detail.responseTimeMs + 'ms' : '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="验证结果">
|
||||
<el-tag v-if="detail.isValid === true" size="small" type="success">有效</el-tag>
|
||||
<el-tag v-else-if="detail.isValid === false" size="small" type="danger">无效</el-tag>
|
||||
<el-tag v-else size="small" type="info">未验证</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="验证备注">{{ detail.validationNote || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="提交时间" :span="2">{{ detail.createdAt }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div style="margin-bottom: 4px; font-size: 13px; color: #606266; font-weight: 500">返回内容:</div>
|
||||
<pre v-if="detail.contentType === 'json'" class="content-json" v-html="highlightedJson"></pre>
|
||||
<pre v-else class="content-html" v-html="highlightedHtml"></pre>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
<el-button v-if="detail.isValid == null" type="success" @click="handleValidate(detail, true); detailVisible = false">标记有效</el-button>
|
||||
<el-button v-if="detail.isValid == null" type="danger" @click="handleValidate(detail, false); detailVisible = false">标记无效</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import api from '../api'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const filterSiteId = ref('')
|
||||
const filterValid = ref('')
|
||||
const siteOptions = ref([])
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const detail = ref({})
|
||||
|
||||
const filteredList = computed(() => {
|
||||
let items = list.value
|
||||
if (filterSiteId.value) {
|
||||
items = items.filter(r => r.siteId === filterSiteId.value)
|
||||
}
|
||||
if (filterValid.value === '1') {
|
||||
items = items.filter(r => r.isValid === true)
|
||||
} else if (filterValid.value === '0') {
|
||||
items = items.filter(r => r.isValid === false)
|
||||
} else if (filterValid.value === 'null') {
|
||||
items = items.filter(r => r.isValid == null)
|
||||
}
|
||||
return items
|
||||
})
|
||||
|
||||
const highlightedJson = computed(() => {
|
||||
try {
|
||||
const json = typeof detail.value.content === 'string' ? JSON.parse(detail.value.content) : detail.value.content
|
||||
return syntaxHighlight(JSON.stringify(json, null, 2))
|
||||
} catch {
|
||||
return escapeHtml(detail.value.content || '')
|
||||
}
|
||||
})
|
||||
|
||||
const highlightedHtml = computed(() => {
|
||||
return escapeHtml(detail.value.content || '')
|
||||
.replace(/(<\/?)(\w+)([\s\S]*?)(\/?>)/g,
|
||||
'<span class="html-tag">$1</span><span class="html-name">$2</span><span class="html-tag">$3$4</span>')
|
||||
.replace(/(\w+)=(".*?")/g,
|
||||
'<span class="html-attr">$1</span>=<span class="html-value">$2</span>')
|
||||
})
|
||||
|
||||
const fetchAll = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const r = await api.accessResults.list()
|
||||
list.value = r.data || []
|
||||
// 提取去重的站点列表用于筛选
|
||||
const siteIds = [...new Set(list.value.map(r => r.siteId))]
|
||||
siteOptions.value = siteIds.map(id => ({ siteId: id }))
|
||||
} catch { ElMessage.error('获取访问结果失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const handleView = (row) => {
|
||||
detail.value = { ...row }
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
const handleValidate = async (row, isValid) => {
|
||||
try {
|
||||
await api.accessResults.validate(row.id, isValid)
|
||||
ElMessage.success(`已标记为${isValid ? '有效' : '无效'}`)
|
||||
fetchAll()
|
||||
} catch { ElMessage.error('操作失败') }
|
||||
}
|
||||
|
||||
const statusTag = (code) => {
|
||||
if (code >= 200 && code < 300) return 'success'
|
||||
if (code >= 300 && code < 400) return 'warning'
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
const escapeHtml = (str) => {
|
||||
if (!str) return ''
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
}
|
||||
|
||||
const syntaxHighlight = (json) => escapeHtml(json).replace(
|
||||
/("(\\u[a-fA-F0-9]{4}|\\[^u]|[^"\\])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
|
||||
(match) => {
|
||||
let cls = 'number'
|
||||
if (/^"/.test(match)) cls = /:$/.test(match) ? 'key' : 'string'
|
||||
else if (/true|false/.test(match)) cls = 'boolean'
|
||||
else if (/null/.test(match)) cls = 'null'
|
||||
return `<span class="${cls}">${match}</span>`
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(fetchAll)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content-json {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 500px;
|
||||
overflow: auto;
|
||||
}
|
||||
.content-html {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 500px;
|
||||
overflow: auto;
|
||||
}
|
||||
.content-json :deep(.key) { color: #9cdcfe; }
|
||||
.content-json :deep(.string) { color: #ce9178; }
|
||||
.content-json :deep(.number) { color: #b5cea8; }
|
||||
.content-json :deep(.boolean) { color: #569cd6; }
|
||||
.content-json :deep(.null) { color: #569cd6; }
|
||||
|
||||
.content-html :deep(.html-tag) { color: #808080; }
|
||||
.content-html :deep(.html-name) { color: #569cd6; }
|
||||
.content-html :deep(.html-attr) { color: #9cdcfe; }
|
||||
.content-html :deep(.html-value) { color: #ce9178; }
|
||||
</style>
|
||||
@@ -38,6 +38,10 @@
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>访问日志</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/access-results">
|
||||
<el-icon><Connection /></el-icon>
|
||||
<span>适配验证</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
|
||||
Reference in New Issue
Block a user