Files
par/web/src/views/Configs.vue

217 lines
7.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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 }} &nbsp;
<span class="label">版本:</span> {{ viewing.version }} &nbsp;
<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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
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>