diff --git a/par-api/src/main/java/com/par/api/controller/AdminController.java b/par-api/src/main/java/com/par/api/controller/AdminController.java index 545eed1..2723cd9 100644 --- a/par-api/src/main/java/com/par/api/controller/AdminController.java +++ b/par-api/src/main/java/com/par/api/controller/AdminController.java @@ -7,6 +7,7 @@ import com.par.core.dto.SiteConfigDTO; import com.par.core.entity.Account; import com.par.core.entity.Psk; import com.par.core.entity.SiteConfig; +import com.par.core.entity.SiteSnapshot; import com.par.core.enums.ConfigStatus; import com.par.core.enums.TrustLevel; import com.par.core.event.SnapshotUpdateEvent; @@ -14,6 +15,7 @@ import com.par.core.mapper.AccountMapper; import com.par.core.mapper.ConfigReviewMapper; import com.par.core.mapper.SiteConfigMapper; import com.par.core.mapper.SiteMapper; +import com.par.core.mapper.SiteSnapshotMapper; import com.par.core.service.AccountService; import com.par.core.service.ConfigService; import com.par.core.service.PskService; @@ -44,6 +46,7 @@ public class AdminController { private final ConfigReviewMapper configReviewMapper; private final AccountMapper accountMapper; private final SiteMapper siteMapper; + private final SiteSnapshotMapper snapshotMapper; private final AccountService accountService; private final PskService pskService; private final ConfigService configService; @@ -418,6 +421,47 @@ public class AdminController { } } + /** 查询站点快照状态(draft / published 版本信息) */ + @GetMapping("/sites/{siteId}/snapshot-status") + public ApiResponse> getSnapshotStatus(@PathVariable("siteId") String siteId, + HttpServletRequest request) { + checkAdmin(request); + SiteSnapshot draft = snapshotMapper.selectLatestDraft(siteId); + SiteSnapshot published = snapshotMapper.selectLatestPublished(siteId); + + Map result = new LinkedHashMap<>(); + result.put("siteId", siteId); + + Map draftInfo = new LinkedHashMap<>(); + if (draft != null) { + draftInfo.put("exists", true); + draftInfo.put("version", draft.getVersion()); + draftInfo.put("checksum", draft.getChecksum() != null ? draft.getChecksum().substring(0, 8) : null); + draftInfo.put("updatedAt", draft.getUpdatedAt() != null ? draft.getUpdatedAt().toString() : null); + } else { + draftInfo.put("exists", false); + } + result.put("draft", draftInfo); + + Map pubInfo = new LinkedHashMap<>(); + if (published != null) { + pubInfo.put("exists", true); + pubInfo.put("version", published.getVersion()); + pubInfo.put("checksum", published.getChecksum() != null ? published.getChecksum().substring(0, 8) : null); + pubInfo.put("publishedAt", published.getPublishedAt() != null ? published.getPublishedAt().toString() : null); + } else { + pubInfo.put("exists", false); + } + result.put("published", pubInfo); + + // 是否有新 draft 待发布(draft 存在且与 published 不同) + boolean hasPendingPublish = draft != null && (published == null + || (draft.getChecksum() != null && !draft.getChecksum().equals(published.getChecksum()))); + result.put("hasPendingPublish", hasPendingPublish); + + return ApiResponse.success(result); + } + @GetMapping("/sites") public ApiResponse> listAllSites(HttpServletRequest request) { checkAdmin(request); diff --git a/web/src/views/AccessResultCheck.vue b/web/src/views/AccessResultCheck.vue index df87bbc..785d3ca 100644 --- a/web/src/views/AccessResultCheck.vue +++ b/web/src/views/AccessResultCheck.vue @@ -2,7 +2,13 @@
-

适配校验:{{ siteId }}

+
+

适配校验:{{ siteId }}

+ + + {{ snapshotStatus.hasPendingPublish ? '预览版 v' + snapshotStatus.draft.version : snapshotStatus.draft.exists ? '预览版 v' + snapshotStatus.draft.version : snapshotStatus.published.exists ? '正式版 v' + snapshotStatus.published.version : '未发布' }} + +
← 返回列表
@@ -49,6 +55,10 @@ 拒绝 发布 + + 保存预览版 + 发布正式版 + 未保存 保存
@@ -150,11 +160,17 @@ const siteId = route.params.siteId const loading = ref(false), parseLoading = ref(false), scriptExpanded = ref(true), historyVersion = ref(null), urlFilter = ref(''), deleting = ref(false) const selectedConfigId = ref(null), selectedConfig = ref(null) -const scriptContent = ref(''), saving = ref(false), approving = ref(false), rejecting = ref(false) +const scriptContent = ref(''), originalScript = ref(''), saving = ref(false), approving = ref(false), rejecting = ref(false) +const savingPreview = ref(false), publishing = ref(false) +const snapshotStatus = ref(null) const allConfigVersions = ref([]), allResults = ref([]) const pageParsed = ref({}), reparsingUrls = reactive(new Set()), expanded = reactive(new Set()) const rawVisible = ref(false), rawLoading = ref(false), rawContent = ref('') +const hasUnsavedChanges = computed(() => { + return originalScript.value !== '' && scriptContent.value !== originalScript.value +}) + const totalResults = computed(() => allResults.value.length) const filteredPages = computed(() => { @@ -215,8 +231,27 @@ const onConfigChange = () => { const loadScript = async () => { if (!selectedConfig.value) return - try { const r = await api.adminConfigs.getScript(selectedConfig.value.id); scriptContent.value = r.data?.script||'' } - catch { scriptContent.value = '' } + try { const r = await api.adminConfigs.getScript(selectedConfig.value.id); scriptContent.value = r.data?.script||''; originalScript.value = scriptContent.value } + catch { scriptContent.value = ''; originalScript.value = '' } +} + +const fetchSnapshotStatus = async () => { + try { snapshotStatus.value = (await api.raw.get('/admin/sites/' + siteId + '/snapshot-status')).data || null } + catch { snapshotStatus.value = null } +} + +const savePreview = async () => { + savingPreview.value = true + try { await api.raw.post('/admin/sites/' + siteId + '/save-preview'); ElMessage.success('预览版已保存'); fetchSnapshotStatus() } + catch { ElMessage.error('保存预览版失败') } + finally { savingPreview.value = false } +} + +const publishSnapshot = async () => { + publishing.value = true + try { await api.raw.post('/admin/sites/' + siteId + '/publish'); ElMessage.success('已发布为正式版'); fetchSnapshotStatus() } + catch { ElMessage.error('发布失败') } + finally { publishing.value = false } } watch(selectedConfig, (v) => { if (v) loadScript() }) @@ -240,7 +275,7 @@ const revertScript = async () => { const saveScript = async () => { if (!selectedConfig.value) return saving.value = true - try { await api.adminConfigs.updateScript(selectedConfig.value.id, scriptContent.value); ElMessage.success('已保存') } + try { await api.adminConfigs.updateScript(selectedConfig.value.id, scriptContent.value); originalScript.value = scriptContent.value; ElMessage.success('已保存') } catch(e) { ElMessage.error(e.response?.data?.message||'保存失败') } finally { saving.value = false } } @@ -333,6 +368,7 @@ const statusTag = (c) => c>=200&&c<300?'success':c>=300&&c<400?'warning':'danger onMounted(async () => { await refresh() await loadConfigs() + fetchSnapshotStatus() if (selectedConfigId.value) { selectedConfig.value = allConfigVersions.value.find(v=>v.id===selectedConfigId.value) } diff --git a/web/src/views/RiskRules.vue b/web/src/views/RiskRules.vue index 0e5f034..d16eced 100644 --- a/web/src/views/RiskRules.vue +++ b/web/src/views/RiskRules.vue @@ -32,11 +32,18 @@

编辑: {{ editSiteName }}

+ + + {{ snapshotStatus.hasPendingPublish ? '预览版 v' + snapshotStatus.draft.version : snapshotStatus.draft.exists ? '预览版 v' + snapshotStatus.draft.version : snapshotStatus.published.exists ? '正式版 v' + snapshotStatus.published.version : '未发布' }} + + 未保存 {{ parsed.length }} 条规则
保存 上传 + 保存预览版 + 发布正式版 返回列表
@@ -272,11 +279,18 @@ const presets = [ ] // ============ 状态 ============ -const loading = ref(false), saving = ref(false), uploading = ref(false) +const loading = ref(false), saving = ref(false), uploading = ref(false), savingPreview = ref(false), publishing = ref(false) const sites = ref([]), rulesMap = ref({}) -const editType = ref(''), editorJson = ref('[]') +const editType = ref(''), editorJson = ref('[]'), originalJson = ref('') +const snapshotStatus = ref(null) const dragIdx = ref(null) +const hasUnsavedChanges = computed(() => { + if (!editType.value) return false + try { return JSON.stringify(JSON.parse(editorJson.value)) !== JSON.stringify(JSON.parse(originalJson.value || '[]')) } + catch { return false } +}) + const fmt = (t) => t ? (t.substring(0,19)||'').replace('T',' ') : '-' const parsed = computed({ @@ -366,6 +380,27 @@ const editRule = async (row) => { const r = await api.raw.get('/admin/risk-rules/'+row.siteId) editorJson.value = (r?.data||r)?.rules ? JSON.stringify((r.data||r).rules, null, 2) : '[]' } catch { editorJson.value = '[]' } + originalJson.value = editorJson.value + fetchSnapshotStatus() +} + +const fetchSnapshotStatus = async () => { + try { snapshotStatus.value = (await api.raw.get('/admin/sites/' + editType.value + '/snapshot-status')).data || null } + catch { snapshotStatus.value = null } +} + +const savePreview = async () => { + savingPreview.value = true + try { await api.raw.post('/admin/sites/' + editType.value + '/save-preview'); ElMessage.success('预览版已保存'); fetchSnapshotStatus() } + catch { ElMessage.error('保存预览版失败') } + finally { savingPreview.value = false } +} + +const publishSnapshot = async () => { + publishing.value = true + try { await api.raw.post('/admin/sites/' + editType.value + '/publish'); ElMessage.success('已发布为正式版'); fetchSnapshotStatus() } + catch { ElMessage.error('发布失败') } + finally { publishing.value = false } } const sync = () => { editorJson.value = JSON.stringify(parsed.value, null, 2) } @@ -429,6 +464,7 @@ const saveRules = async () => { saving.value = true try { await api.raw.post('/risk-rules/'+editType.value, { rules: parsed.value }) + originalJson.value = editorJson.value ElMessage.success('已保存') } catch { ElMessage.error('保存失败') } finally { saving.value = false } diff --git a/web/src/views/SitePages.vue b/web/src/views/SitePages.vue index 4a0da5c..3d3c5c3 100644 --- a/web/src/views/SitePages.vue +++ b/web/src/views/SitePages.vue @@ -43,10 +43,19 @@
-

编辑: {{ editSiteId }}

+
+

编辑: {{ editSiteId }}

+ + + {{ snapshotStatus.hasPendingPublish ? '预览版 v' + snapshotStatus.draft.version : snapshotStatus.draft.exists ? '预览版 v' + snapshotStatus.draft.version : snapshotStatus.published.exists ? '正式版 v' + snapshotStatus.published.version : '未发布' }} + + 未保存 +
保存 上传 + 保存预览版 + 发布正式版 删除 返回列表
@@ -113,8 +122,10 @@