feat: 合并配置审核到适配验证页,支持版本切换、编辑、审批/拒绝
This commit is contained in:
@@ -2,6 +2,7 @@ package com.par.api.controller;
|
||||
|
||||
import com.par.core.dto.AccountDTO;
|
||||
import com.par.core.dto.ApiResponse;
|
||||
import com.par.core.dto.SiteConfigDTO;
|
||||
import com.par.core.entity.Account;
|
||||
import com.par.core.entity.SiteConfig;
|
||||
import com.par.core.enums.ConfigStatus;
|
||||
@@ -12,13 +13,16 @@ import com.par.core.mapper.SiteConfigMapper;
|
||||
import com.par.core.mapper.SiteMapper;
|
||||
import com.par.core.entity.Psk;
|
||||
import com.par.core.service.AccountService;
|
||||
import com.par.core.service.ConfigService;
|
||||
import com.par.core.service.PskService;
|
||||
import com.par.core.storage.CachedStorageService;
|
||||
import com.par.common.util.HmacUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -39,6 +43,7 @@ public class AdminController {
|
||||
private final SiteMapper siteMapper;
|
||||
private final AccountService accountService;
|
||||
private final PskService pskService;
|
||||
private final ConfigService configService;
|
||||
private final CachedStorageService storageService;
|
||||
private final com.par.core.service.SiteService siteService;
|
||||
|
||||
@@ -124,7 +129,7 @@ public class AdminController {
|
||||
@PostMapping("/reviews/{reviewId}/reject")
|
||||
public ApiResponse<Void> rejectConfig(
|
||||
@PathVariable Long reviewId,
|
||||
@RequestParam("comment") String comment,
|
||||
@RequestParam(value = "comment", required = false, defaultValue = "") String comment,
|
||||
HttpServletRequest request) {
|
||||
|
||||
checkAdmin(request);
|
||||
@@ -151,6 +156,69 @@ public class AdminController {
|
||||
return ApiResponse.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定站点的待审核配置列表
|
||||
*/
|
||||
@GetMapping("/configs/pending/{siteId}")
|
||||
public ApiResponse<List<SiteConfigDTO>> listPendingConfigsBySite(
|
||||
@PathVariable String siteId, HttpServletRequest request) {
|
||||
checkAdmin(request);
|
||||
List<SiteConfig> configs = siteConfigMapper.selectList(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SiteConfig>()
|
||||
.eq(SiteConfig::getSiteId, siteId)
|
||||
.eq(SiteConfig::getStatus, ConfigStatus.PENDING)
|
||||
.eq(SiteConfig::getDeleted, 0));
|
||||
return ApiResponse.success(configs.stream().map(configService::toDTO).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看指定版本配置内容
|
||||
*/
|
||||
@GetMapping("/configs/{configId}/content")
|
||||
public ApiResponse<Map<String, Object>> getConfigContent(
|
||||
@PathVariable Long configId, HttpServletRequest request) {
|
||||
checkAdmin(request);
|
||||
SiteConfig config = siteConfigMapper.selectById(configId);
|
||||
if (config == null) return ApiResponse.error(404, "Config not found");
|
||||
String content = null;
|
||||
try { content = storageService.read(config.getStoragePath()); } catch (Exception e) {
|
||||
return ApiResponse.error(500, "读取配置失败: " + e.getMessage());
|
||||
}
|
||||
return ApiResponse.success(Map.of("id", config.getId(), "content", content != null ? content : ""));
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑待审核配置内容
|
||||
*/
|
||||
@PutMapping("/configs/{configId}/content")
|
||||
public ApiResponse<SiteConfigDTO> updateConfigContent(
|
||||
@PathVariable Long configId,
|
||||
@RequestBody Map<String, String> payload,
|
||||
HttpServletRequest request) {
|
||||
checkAdmin(request);
|
||||
SiteConfig config = siteConfigMapper.selectById(configId);
|
||||
if (config == null) return ApiResponse.error(404, "Config not found");
|
||||
if (config.getStatus() != ConfigStatus.PENDING) {
|
||||
return ApiResponse.error(400, "只能编辑待审核的配置");
|
||||
}
|
||||
|
||||
String content = payload.get("content");
|
||||
if (content == null || content.isBlank()) {
|
||||
return ApiResponse.error(400, "content is required");
|
||||
}
|
||||
|
||||
// 更新存储文件
|
||||
storageService.store(config.getStoragePath(), content);
|
||||
|
||||
// 更新 hash 和文件大小
|
||||
config.setConfigHash(HmacUtil.sha256(content));
|
||||
config.setFileSize((long) content.getBytes(StandardCharsets.UTF_8).length);
|
||||
siteConfigMapper.updateById(config);
|
||||
|
||||
log.info("Config content updated: id={}, siteId={}", configId, config.getSiteId());
|
||||
return ApiResponse.success(configService.toDTO(config));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户信任等级(禁止修改管理员、禁止提升为管理员)
|
||||
*/
|
||||
|
||||
@@ -16,6 +16,7 @@ public class SiteAccessSummaryDTO {
|
||||
private int validCount;
|
||||
private int invalidCount;
|
||||
private int uncheckedCount;
|
||||
private int pendingConfigCount;
|
||||
private LocalDateTime lastSubmitted;
|
||||
private List<AccessPageSummaryDTO> pages;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import com.par.core.dto.AccessPageSummaryDTO;
|
||||
import com.par.core.dto.AccessResultDTO;
|
||||
import com.par.core.dto.SiteAccessSummaryDTO;
|
||||
import com.par.core.entity.AccessResult;
|
||||
import com.par.core.entity.SiteConfig;
|
||||
import com.par.core.mapper.AccessResultMapper;
|
||||
import com.par.core.mapper.SiteConfigMapper;
|
||||
import com.par.core.service.AccessResultService;
|
||||
import com.par.core.storage.CachedStorageService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -22,6 +24,7 @@ import java.util.stream.Collectors;
|
||||
public class AccessResultServiceImpl implements AccessResultService {
|
||||
|
||||
private final AccessResultMapper mapper;
|
||||
private final SiteConfigMapper siteConfigMapper;
|
||||
private final CachedStorageService storageService;
|
||||
|
||||
@Override
|
||||
@@ -154,6 +157,19 @@ public class AccessResultServiceImpl implements AccessResultService {
|
||||
site.setUncheckedCount(site.getTotalResults() - site.getValidCount() - site.getInvalidCount());
|
||||
}
|
||||
|
||||
// 补充待审核配置数量
|
||||
List<SiteConfig> pendingConfigs = siteConfigMapper.selectPending();
|
||||
for (SiteConfig c : pendingConfigs) {
|
||||
String sid = c.getSiteId();
|
||||
SiteAccessSummaryDTO site = siteMap.computeIfAbsent(sid, k -> {
|
||||
SiteAccessSummaryDTO s = new SiteAccessSummaryDTO();
|
||||
s.setSiteId(k);
|
||||
s.setPages(new ArrayList<>());
|
||||
return s;
|
||||
});
|
||||
site.setPendingConfigCount(site.getPendingConfigCount() + 1);
|
||||
}
|
||||
|
||||
return new ArrayList<>(siteMap.values());
|
||||
}
|
||||
|
||||
|
||||
@@ -74,5 +74,12 @@ export default {
|
||||
validate: (id, isValid, note) => api.post(`/admin/access-results/${id}/validate`, { isValid, note: note || '' }),
|
||||
batchValidate: (siteId, url, isValid) => api.post('/admin/access-results/batch-validate', { siteId, url, isValid }),
|
||||
},
|
||||
adminConfigs: {
|
||||
pendingBySite: (siteId) => api.get(`/admin/configs/pending/${siteId}`),
|
||||
getContent: (configId) => api.get(`/admin/configs/${configId}/content`),
|
||||
updateContent: (configId, content) => api.put(`/admin/configs/${configId}/content`, { content }),
|
||||
approve: (configId) => api.post(`/admin/reviews/${configId}/approve`),
|
||||
reject: (configId, comment) => api.post(`/admin/reviews/${configId}/reject`, null, { params: { comment: comment || '' } }),
|
||||
},
|
||||
health: () => api.get('/health', { baseURL: '' })
|
||||
}
|
||||
|
||||
@@ -32,9 +32,7 @@ const routes = [
|
||||
},
|
||||
{
|
||||
path: 'configs',
|
||||
name: 'Configs',
|
||||
component: () => import('../views/Configs.vue'),
|
||||
meta: { title: '配置审核' }
|
||||
redirect: '/access-results'
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
|
||||
@@ -5,36 +5,80 @@
|
||||
<el-button @click="$router.push('/access-results')">← 返回列表</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 统计概览 -->
|
||||
<el-card style="margin-bottom: 16px" shadow="never">
|
||||
<div style="display:flex;gap:20px;align-items:center;flex-wrap:wrap">
|
||||
<div><strong>配置版本:</strong>{{ configVersion || '-' }}</div>
|
||||
<div><strong>页面数:</strong>{{ groupedPages.length }}</div>
|
||||
<div><strong>总次数:</strong>{{ allResults.length }}</div>
|
||||
<div style="display:flex;gap:16px;height:calc(100vh - 200px)">
|
||||
<!-- 左侧:适配配置面板 -->
|
||||
<el-card style="flex:0 0 380px;overflow:auto" shadow="never">
|
||||
<template #header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px">
|
||||
<span><strong>适配配置</strong></span>
|
||||
<span v-if="selectedConfig" class="text-xs" style="color:#909399">
|
||||
v{{ selectedConfig.version }}
|
||||
<el-tag size="small" :type="selectedConfig.status === 'approved' ? 'success' : 'warning'">
|
||||
{{ selectedConfig.status === 'approved' ? '已批准' : '待审核' }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 版本选择 -->
|
||||
<div style="margin-bottom:12px">
|
||||
<el-tabs v-model="configTab" @tab-change="onConfigTabChange" type="card" size="small">
|
||||
<el-tab-pane v-for="v in allConfigVersions" :key="v.id"
|
||||
:label="'v' + v.version" :name="String(v.id)">
|
||||
<template #label>
|
||||
<span style="display:flex;align-items:center;gap:4px">
|
||||
v{{ v.version }}
|
||||
<el-tag v-if="v.status === 'pending'" size="small" type="warning" style="margin-left:2px">待审</el-tag>
|
||||
<el-tag v-else-if="v.isLatest" size="small" type="success" style="margin-left:2px">当前</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<div v-if="configLoading" style="text-align:center;padding:20px;color:#909399">加载配置中...</div>
|
||||
<div v-else-if="!selectedConfig" style="text-align:center;padding:20px;color:#909399">暂无配置</div>
|
||||
<template v-else>
|
||||
<!-- 编辑模式 -->
|
||||
<div v-if="editing">
|
||||
<el-input v-model="editContent" type="textarea" :rows="18" style="font-family:monospace;font-size:11px" />
|
||||
<div style="display:flex;gap:8px;margin-top:8px;flex-wrap:wrap">
|
||||
<el-button size="small" type="primary" @click="saveEdit" :loading="saving">保存</el-button>
|
||||
<el-button size="small" @click="cancelEdit">取消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 只读模式 -->
|
||||
<div v-else>
|
||||
<pre v-if="selectedConfigJson" class="config-json" v-html="highlightedConfig"></pre>
|
||||
<!-- 待审核配置操作栏 -->
|
||||
<div v-if="selectedConfig.status === 'pending'" style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap">
|
||||
<el-button size="small" type="success" @click="approveConfig" :loading="approving">审批通过</el-button>
|
||||
<el-button size="small" type="danger" @click="rejectConfig" :loading="rejecting">拒绝</el-button>
|
||||
<el-button size="small" @click="startEdit">编辑</el-button>
|
||||
</div>
|
||||
<!-- 已批准配置操作栏 -->
|
||||
<div v-else style="display:flex;gap:8px;margin-top:10px">
|
||||
<el-button size="small" @click="startEdit" v-if="selectedConfig.status === 'pending'">编辑</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 右侧:访问结果 -->
|
||||
<div style="flex:1;overflow:auto;display:flex;flex-direction:column;gap:8px">
|
||||
<!-- 统计 -->
|
||||
<el-card shadow="never" size="small">
|
||||
<div style="display:flex;gap:16px;align-items:center;flex-wrap:wrap">
|
||||
<span class="text-xs"><strong>页面数:</strong>{{ groupedPages.length }}</span>
|
||||
<span class="text-xs"><strong>总次数:</strong>{{ allResults.length }}</span>
|
||||
<el-tag size="small" type="success">{{ validCount }} 有效</el-tag>
|
||||
<el-tag size="small" type="danger">{{ invalidCount }} 无效</el-tag>
|
||||
<el-tag size="small" type="warning">{{ uncheckedCount }} 待验</el-tag>
|
||||
<el-button size="small" type="primary" @click="refresh" :loading="loading">刷新</el-button>
|
||||
<el-button size="small" @click="refresh" :loading="loading">刷新</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<div style="display:flex;gap:16px;height:calc(100vh - 200px)">
|
||||
<!-- 左侧:适配配置 -->
|
||||
<el-card style="flex:0 0 360px;overflow:auto" shadow="never">
|
||||
<template #header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<span><strong>适配配置</strong></span>
|
||||
<el-button size="small" text @click="toggleConfigCollapse">{{ configCollapsed ? '展开' : '折叠' }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="configLoading" style="text-align:center;padding:20px;color:#909399">加载配置中...</div>
|
||||
<div v-else-if="configError" style="padding:10px;color:#f56c6c">{{ configError }}</div>
|
||||
<div v-else-if="configCollapsed" style="text-align:center;padding:20px;color:#909399">配置已折叠</div>
|
||||
<pre v-else class="config-json" v-html="highlightedConfig"></pre>
|
||||
</el-card>
|
||||
|
||||
<!-- 右侧:页面列表 -->
|
||||
<div style="flex:1;overflow:auto;display:flex;flex-direction:column;gap:8px">
|
||||
<!-- 页面列表 -->
|
||||
<el-card v-for="page in groupedPages" :key="page.url" shadow="never" size="small">
|
||||
<template #header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px">
|
||||
@@ -50,38 +94,28 @@
|
||||
<el-button v-if="page.totalCount > 1" size="small" text @click="toggleExpand(page.url)">
|
||||
{{ expandedUrls.has(page.url) ? '收起' : `显示全部(${page.totalCount}条)` }}
|
||||
</el-button>
|
||||
<el-button v-if="page.hasUnchecked" size="small" type="success" @click="batchValidate(page.url, true)">
|
||||
全部有效
|
||||
</el-button>
|
||||
<el-button v-if="page.hasUnchecked" size="small" type="danger" @click="batchValidate(page.url, false)">
|
||||
全部无效
|
||||
</el-button>
|
||||
<el-button v-if="page.hasUnchecked" size="small" type="success" @click="batchValidate(page.url, true)">全部有效</el-button>
|
||||
<el-button v-if="page.hasUnchecked" size="small" type="danger" @click="batchValidate(page.url, false)">全部无效</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 该页面的结果(默认最近1条,展开后最多5条) -->
|
||||
<div v-for="(r, ri) in page.visibleResults" :key="r.id"
|
||||
style="padding:6px 0;display:flex;justify-content:space-between;align-items:center"
|
||||
:style="ri < page.visibleResults.length - 1 ? 'border-bottom:1px solid #ebeef5' : ''">
|
||||
<div style="display:flex;align-items:center;gap:10px;flex:1;min-width:0">
|
||||
<el-tag v-if="r.statusCode" size="small" :type="statusTag(r.statusCode)" style="flex-shrink:0">
|
||||
{{ r.statusCode }}
|
||||
</el-tag>
|
||||
<span v-if="r.responseTimeMs != null" class="text-xs" style="color:#909399;flex-shrink:0">{{ r.responseTimeMs }}ms</span>
|
||||
<span class="text-xs" style="color:#909399;flex-shrink:0">{{ formatTime(r.createdAt) }}</span>
|
||||
<span v-if="r.clientId" class="text-xs" style="color:#909399;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:80px">{{ r.clientId }}</span>
|
||||
<el-tag v-if="r.statusCode" size="small" :type="statusTag(r.statusCode)">{{ r.statusCode }}</el-tag>
|
||||
<span v-if="r.responseTimeMs != null" class="text-xs" style="color:#909399">{{ r.responseTimeMs }}ms</span>
|
||||
<span class="text-xs" style="color:#909399">{{ formatTime(r.createdAt) }}</span>
|
||||
<span v-if="r.clientId" class="text-xs" style="color:#909399;max-width:80px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ r.clientId }}</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:6px;flex-shrink:0">
|
||||
<div style="display:flex;align-items:center;gap:6px">
|
||||
<el-tag v-if="r.isValid === true" size="small" type="success">有效</el-tag>
|
||||
<el-tag v-else-if="r.isValid === false" size="small" type="danger">无效</el-tag>
|
||||
<el-button size="small" @click="viewContent(r)">查看</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 收起时提示还有更多 -->
|
||||
<div v-if="!expandedUrls.has(page.url) && page.totalCount > 5"
|
||||
style="padding:2px 0;color:#909399;font-size:12px;text-align:center">
|
||||
<div v-if="!expandedUrls.has(page.url) && page.totalCount > 5" style="padding:2px 0;color:#909399;font-size:12px;text-align:center">
|
||||
还有 {{ page.totalCount - 5 }} 条更早记录
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -123,17 +157,24 @@ const siteId = route.params.siteId
|
||||
|
||||
const loading = ref(false)
|
||||
const allResults = ref([])
|
||||
const configVersion = ref('')
|
||||
|
||||
const allConfigVersions = ref([])
|
||||
const selectedConfig = ref(null)
|
||||
const selectedConfigJson = ref('')
|
||||
const configTab = ref('')
|
||||
const configLoading = ref(true)
|
||||
const configError = ref('')
|
||||
const configContent = ref('')
|
||||
const configCollapsed = ref(false)
|
||||
const editing = ref(false)
|
||||
const editContent = ref('')
|
||||
const saving = ref(false)
|
||||
const approving = ref(false)
|
||||
const rejecting = ref(false)
|
||||
|
||||
const contentVisible = ref(false)
|
||||
const contentLoading = ref(false)
|
||||
const viewingResult = ref({})
|
||||
const viewingContentType = ref('html')
|
||||
const viewingContent = ref('')
|
||||
|
||||
const expandedUrls = reactive(new Set())
|
||||
const MAX_VISIBLE = 5
|
||||
|
||||
@@ -142,7 +183,6 @@ const toggleExpand = (url) => {
|
||||
else expandedUrls.add(url)
|
||||
}
|
||||
|
||||
// 按 URL 分组(最新在前),默认显示最近5条
|
||||
const groupedPages = computed(() => {
|
||||
const map = new Map()
|
||||
for (const r of allResults.value) {
|
||||
@@ -156,8 +196,7 @@ const groupedPages = computed(() => {
|
||||
const hasUnchecked = results.some(r => r.isValid == null)
|
||||
const totalCount = results.length
|
||||
const visibleResults = expandedUrls.has(url) ? results.slice(0, MAX_VISIBLE) : results.slice(0, 1)
|
||||
const visibleCount = visibleResults.length
|
||||
return { url, results, totalCount, visibleResults, visibleCount, allValid, allInvalid, hasUnchecked }
|
||||
return { url, results, totalCount, visibleResults, visibleCount: visibleResults.length, allValid, allInvalid, hasUnchecked }
|
||||
})
|
||||
})
|
||||
|
||||
@@ -167,31 +206,104 @@ const uncheckedCount = computed(() => allResults.value.filter(r => r.isValid ==
|
||||
|
||||
const refresh = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const r = await api.accessResults.bySite(siteId)
|
||||
allResults.value = r.data || []
|
||||
} catch { ElMessage.error('获取访问结果失败') }
|
||||
try { const r = await api.accessResults.bySite(siteId); allResults.value = r.data || [] }
|
||||
catch { ElMessage.error('获取访问结果失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const loadConfig = async () => {
|
||||
// 加载所有配置版本(已批准 + 待审核)
|
||||
const loadConfigs = async () => {
|
||||
configLoading.value = true
|
||||
try {
|
||||
const r = await api.configs.bySite(siteId)
|
||||
const configs = r.data || []
|
||||
if (configs.length) {
|
||||
configVersion.value = configs[0].version
|
||||
const resp = await api.configs.content(siteId, configs[0].version)
|
||||
configContent.value = typeof resp.data === 'string' ? resp.data : JSON.stringify(resp.data, null, 2)
|
||||
} else {
|
||||
configError.value = '该站点暂无适配配置'
|
||||
const [approvedRes, pendingRes] = await Promise.allSettled([
|
||||
api.configs.bySite(siteId),
|
||||
api.adminConfigs.pendingBySite(siteId)
|
||||
])
|
||||
const approved = approvedRes.status === 'fulfilled' ? (approvedRes.value.data || []) : []
|
||||
const pending = pendingRes.status === 'fulfilled' ? (pendingRes.value.data || []) : []
|
||||
// 合并:待审核在前,已批准在后
|
||||
allConfigVersions.value = [...pending, ...approved]
|
||||
} catch {
|
||||
allConfigVersions.value = []
|
||||
} finally {
|
||||
configLoading.value = false
|
||||
// 默认选中第一个待审核,否则第一个已批准
|
||||
if (allConfigVersions.value.length) {
|
||||
const first = allConfigVersions.value[0]
|
||||
configTab.value = String(first.id)
|
||||
selectConfig(first)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
configError.value = e.response?.data?.message || '获取配置失败'
|
||||
} finally { configLoading.value = false }
|
||||
}
|
||||
|
||||
const toggleConfigCollapse = () => { configCollapsed.value = !configCollapsed.value }
|
||||
const onConfigTabChange = (tabName) => {
|
||||
const cfg = allConfigVersions.value.find(v => String(v.id) === tabName)
|
||||
if (cfg) selectConfig(cfg)
|
||||
}
|
||||
|
||||
const selectConfig = async (cfg) => {
|
||||
selectedConfig.value = cfg
|
||||
editing.value = false
|
||||
// 加载配置内容
|
||||
try {
|
||||
if (cfg.status === 'pending') {
|
||||
const r = await api.adminConfigs.getContent(cfg.id)
|
||||
selectedConfigJson.value = r.data?.content || ''
|
||||
} else {
|
||||
const r = await api.configs.content(siteId, cfg.version)
|
||||
selectedConfigJson.value = typeof r.data === 'string' ? r.data : JSON.stringify(r.data || {}, null, 2)
|
||||
}
|
||||
} catch {
|
||||
selectedConfigJson.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const startEdit = () => {
|
||||
editContent.value = selectedConfigJson.value
|
||||
editing.value = true
|
||||
}
|
||||
|
||||
const cancelEdit = () => { editing.value = false }
|
||||
|
||||
const saveEdit = async () => {
|
||||
if (!selectedConfig.value) return
|
||||
saving.value = true
|
||||
try {
|
||||
await api.adminConfigs.updateContent(selectedConfig.value.id, editContent.value)
|
||||
selectedConfigJson.value = editContent.value
|
||||
editing.value = false
|
||||
ElMessage.success('配置已保存')
|
||||
} catch (e) { ElMessage.error(e.response?.data?.message || '保存失败') }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
|
||||
const approveConfig = async () => {
|
||||
if (!selectedConfig.value) return
|
||||
try {
|
||||
await ElMessageBox.confirm('确认审批通过?通过后版本号将自动加1。', '审批配置', { confirmButtonText: '确认' })
|
||||
} catch { return }
|
||||
approving.value = true
|
||||
try {
|
||||
await api.adminConfigs.approve(selectedConfig.value.id)
|
||||
ElMessage.success('配置已审批通过')
|
||||
loadConfigs()
|
||||
} catch (e) { ElMessage.error(e.response?.data?.message || '审批失败') }
|
||||
finally { approving.value = false }
|
||||
}
|
||||
|
||||
const rejectConfig = async () => {
|
||||
if (!selectedConfig.value) return
|
||||
try {
|
||||
await ElMessageBox.confirm('确认拒绝该配置?', '拒绝配置', { confirmButtonText: '确认', type: 'warning' })
|
||||
} catch { return }
|
||||
rejecting.value = true
|
||||
try {
|
||||
await api.adminConfigs.reject(selectedConfig.value.id, '')
|
||||
ElMessage.success('配置已拒绝')
|
||||
loadConfigs()
|
||||
} catch (e) { ElMessage.error(e.response?.data?.message || '拒绝失败') }
|
||||
finally { rejecting.value = false }
|
||||
}
|
||||
|
||||
const batchValidate = async (url, isValid) => {
|
||||
try {
|
||||
@@ -220,51 +332,36 @@ const viewContent = async (row) => {
|
||||
const r = await api.accessResults.get(row.id)
|
||||
viewingResult.value = r.data || {}
|
||||
viewingContent.value = r.data?.content || ''
|
||||
} catch {
|
||||
ElMessage.error('获取内容失败')
|
||||
viewingContent.value = '加载失败'
|
||||
} finally { contentLoading.value = false }
|
||||
}
|
||||
|
||||
const formatTime = (t) => {
|
||||
if (!t) return '-'
|
||||
return t.substring(5, 16) // MM-DD HH:mm
|
||||
} catch { ElMessage.error('获取内容失败'); viewingContent.value = '加载失败' }
|
||||
finally { contentLoading.value = false }
|
||||
}
|
||||
|
||||
const formatTime = (t) => t ? t.substring(5, 16) : '-'
|
||||
const statusTag = (code) => {
|
||||
if (code >= 200 && code < 300) return 'success'
|
||||
if (code >= 300 && code < 400) return 'warning'
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
// 配置 JSON 语法高亮
|
||||
// JSON 语法高亮
|
||||
const highlightedConfig = computed(() => {
|
||||
try {
|
||||
const json = typeof configContent.value === 'string' ? JSON.parse(configContent.value) : configContent.value
|
||||
return syntaxHighlight(JSON.stringify(json, null, 2))
|
||||
} catch { return escapeHtml(configContent.value || '') }
|
||||
try { return syntaxHighlight(JSON.stringify(JSON.parse(selectedConfigJson.value), null, 2)) }
|
||||
catch { return escapeHtml(selectedConfigJson.value || '') }
|
||||
})
|
||||
|
||||
const highlightedViewJson = computed(() => {
|
||||
try {
|
||||
const json = typeof viewingContent.value === 'string' ? JSON.parse(viewingContent.value) : viewingContent.value
|
||||
return syntaxHighlight(JSON.stringify(json, null, 2))
|
||||
} catch { return escapeHtml(viewingContent.value || '') }
|
||||
try { return syntaxHighlight(JSON.stringify(typeof viewingContent.value === 'string' ? JSON.parse(viewingContent.value) : viewingContent.value, null, 2)) }
|
||||
catch { return escapeHtml(viewingContent.value || '') }
|
||||
})
|
||||
|
||||
const highlightedViewHtml = computed(() => {
|
||||
return escapeHtml(viewingContent.value || '')
|
||||
.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>')
|
||||
.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 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) => {
|
||||
@@ -278,7 +375,7 @@ const syntaxHighlight = (json) => escapeHtml(json).replace(
|
||||
|
||||
onMounted(() => {
|
||||
refresh()
|
||||
loadConfig()
|
||||
loadConfigs()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -293,7 +390,7 @@ onMounted(() => {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: calc(100vh - 280px);
|
||||
max-height: calc(100vh - 440px);
|
||||
overflow: auto;
|
||||
}
|
||||
.content-json, .content-html {
|
||||
|
||||
@@ -27,6 +27,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="待审核" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.pendingConfigCount" size="small" type="danger">{{ scope.row.pendingConfigCount }}</el-tag>
|
||||
<span v-else class="text-xs" style="color:#909399">0</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近提交" width="160">
|
||||
<template #default="scope">
|
||||
<span class="text-xs">{{ scope.row.lastSubmitted || '-' }}</span>
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
<el-icon><Checked /></el-icon>
|
||||
<span>站点审核</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/configs">
|
||||
<el-icon><DocumentChecked /></el-icon>
|
||||
<span>配置审核</span>
|
||||
<el-menu-item index="/access-results">
|
||||
<el-icon><Connection /></el-icon>
|
||||
<span>适配验证</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/users">
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
@@ -38,10 +38,6 @@
|
||||
<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