217 lines
7.2 KiB
Vue
217 lines
7.2 KiB
Vue
<template>
|
||
<div>
|
||
<h2>配置审核</h2>
|
||
<el-card style="margin-top: 20px">
|
||
<template #header>
|
||
<span>待审核配置</span>
|
||
</template>
|
||
<el-table :data="pendingConfigs" v-loading="loading" stripe>
|
||
<el-table-column prop="siteId" label="站点ID" width="120" />
|
||
<el-table-column prop="version" label="版本" width="100" />
|
||
|
||
<el-table-column prop="submitterId" label="提交者" width="100" />
|
||
<el-table-column prop="createdAt" label="提交时间" width="180" />
|
||
<el-table-column label="操作" width="100">
|
||
<template #default="scope">
|
||
<el-button size="small" type="primary" @click="handleView(scope.row)">查看审核</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-card>
|
||
|
||
<el-dialog v-model="viewVisible" title="配置详情" width="900px" :close-on-click-modal="false">
|
||
<div class="config-info">
|
||
<span class="label">站点:</span> {{ viewing.siteId }}
|
||
<span class="label">版本:</span> {{ viewing.version }}
|
||
<span class="label">提交者:</span> {{ viewing.submitterId || '-' }}
|
||
<span style="margin-left: 16px">
|
||
<el-button size="small" @click="toggleHistory">历史版本 ({{ historyConfigs.length }})</el-button>
|
||
</span>
|
||
</div>
|
||
<div style="display: flex; gap: 12px; margin-top: 12px; max-height: 500px">
|
||
<div :style="{ flex: showHistory ? '1' : '1' }" style="overflow: auto">
|
||
<div class="section-title">待审核配置</div>
|
||
<pre class="config-json" v-html="highlightedJson"></pre>
|
||
</div>
|
||
<div v-if="showHistory && historyConfigs.length" style="flex: none; width: 280px; overflow: auto; border-left: 1px solid #ebeef5; padding-left: 12px">
|
||
<div class="section-title">已审批版本</div>
|
||
<div v-for="h in historyConfigs" :key="h.id" class="history-item" @click="loadHistoryConfig(h)">
|
||
<span class="history-version">v{{ h.version }}</span>
|
||
<span class="history-time">{{ h.createdAt }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="viewVisible = false">关闭</el-button>
|
||
<el-button type="danger" @click="handleReject(viewing)">拒绝</el-button>
|
||
<el-button type="success" @click="handleApprove(viewing)" :loading="auditing">通过</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import api from '../api'
|
||
|
||
const pendingConfigs = ref([])
|
||
const loading = ref(false)
|
||
const viewVisible = ref(false)
|
||
const viewing = ref({})
|
||
const configContent = ref('')
|
||
const historyConfigs = ref([])
|
||
const showHistory = ref(false)
|
||
const loadingHistory = ref(false)
|
||
|
||
const fetchPending = async () => {
|
||
loading.value = true
|
||
try {
|
||
const res = await api.configs.pending()
|
||
pendingConfigs.value = res.data || []
|
||
} catch (e) {
|
||
ElMessage.error('获取待审核列表失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const handleView = async (row) => {
|
||
viewing.value = row
|
||
configContent.value = ''
|
||
historyConfigs.value = []
|
||
showHistory.value = false
|
||
viewVisible.value = true
|
||
try {
|
||
const resp = await api.configs.content(row.siteId, row.version)
|
||
const content = resp.data
|
||
if (content) {
|
||
configContent.value = typeof content === 'string' ? content : JSON.stringify(content, null, 2)
|
||
} else {
|
||
configContent.value = '(无配置内容)'
|
||
}
|
||
} catch (e) {
|
||
configContent.value = '(加载失败: ' + (e.response?.data?.message || e.message || '未知错误') + ')'
|
||
}
|
||
}
|
||
|
||
const loadHistoryConfig = async (h) => {
|
||
try {
|
||
const resp = await api.configs.content(h.siteId, h.version)
|
||
const content = resp.data
|
||
if (content) {
|
||
configContent.value = typeof content === 'string' ? content : JSON.stringify(content, null, 2)
|
||
} else {
|
||
configContent.value = '(无配置内容)'
|
||
}
|
||
} catch (e) {
|
||
configContent.value = '(加载失败: ' + (e.response?.data?.message || e.message || '未知错误') + ')'
|
||
}
|
||
}
|
||
|
||
const toggleHistory = async () => {
|
||
showHistory.value = !showHistory.value
|
||
if (showHistory.value && historyConfigs.value.length === 0 && viewing.value.siteId) {
|
||
loadingHistory.value = true
|
||
try {
|
||
const res = await api.configs.bySite(viewing.value.siteId)
|
||
historyConfigs.value = (res.data || []).filter(c => c.status === 'approved').reverse()
|
||
} catch {}
|
||
loadingHistory.value = false
|
||
}
|
||
}
|
||
|
||
const highlightedJson = computed(() => {
|
||
try {
|
||
const obj = JSON.parse(configContent.value)
|
||
return syntaxHighlight(JSON.stringify(obj, null, 2))
|
||
} catch {
|
||
return escapeHtml(configContent.value)
|
||
}
|
||
})
|
||
|
||
const syntaxHighlight = (json) => {
|
||
return 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>`
|
||
}
|
||
)
|
||
}
|
||
|
||
const escapeHtml = (str) => {
|
||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||
}
|
||
|
||
const auditing = ref(false)
|
||
|
||
const handleApprove = async (row) => {
|
||
try {
|
||
await ElMessageBox.confirm(`确认通过站点 ${row.siteId} 的配置版本 ${row.version}?`, '提示')
|
||
auditing.value = true
|
||
await api.configs.approve(row.id)
|
||
ElMessage.success('已通过')
|
||
viewVisible.value = false
|
||
fetchPending()
|
||
} catch (e) {
|
||
if (e !== 'cancel') ElMessage.error(e.response?.data?.message || '操作失败')
|
||
} finally { auditing.value = false }
|
||
}
|
||
|
||
const handleReject = async (row) => {
|
||
try {
|
||
const { value: comment } = await ElMessageBox.prompt('请输入拒绝原因', '拒绝配置', {
|
||
confirmButtonText: '确认', cancelButtonText: '取消',
|
||
inputValidator: (v) => v ? true : '请输入拒绝原因'
|
||
})
|
||
auditing.value = true
|
||
await api.configs.reject(row.id, comment)
|
||
ElMessage.success('已拒绝')
|
||
viewVisible.value = false
|
||
fetchPending()
|
||
} catch (e) {
|
||
if (e !== 'cancel') ElMessage.error(e.response?.data?.message || '操作失败')
|
||
} finally { auditing.value = false }
|
||
}
|
||
|
||
onMounted(fetchPending)
|
||
</script>
|
||
|
||
<style scoped>
|
||
.config-info {
|
||
font-size: 13px;
|
||
color: #606266;
|
||
}
|
||
.config-info .label {
|
||
font-weight: bold;
|
||
color: #303133;
|
||
}
|
||
.config-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;
|
||
}
|
||
.config-json :deep(.key) { color: #9cdcfe; }
|
||
.config-json :deep(.string) { color: #ce9178; }
|
||
.config-json :deep(.number) { color: #b5cea8; }
|
||
.config-json :deep(.boolean) { color: #569cd6; }
|
||
.config-json :deep(.null) { color: #569cd6; }
|
||
.section-title { font-size: 12px; font-weight: bold; color: #303133; margin-bottom: 8px; }
|
||
.history-item { padding: 6px 8px; cursor: pointer; border-radius: 4px; margin-bottom: 4px; }
|
||
.history-item:hover { background: #f0f5ff; }
|
||
.history-version { font-size: 12px; font-weight: bold; color: #409EFF; }
|
||
.history-time { font-size: 11px; color: #909399; margin-left: 8px; }
|
||
</style>
|