refactor: 站点列表和站点审核拆分为两个独立页面
- Sites.vue: 精简为纯站点列表(编辑/配置/变更) - SiteReview.vue: 新文件,站点审核队列(通过/拒绝/匹配) - router: 新增 /sites/review 路由 - Layout: 导航栏新增「站点审核」菜单项
This commit is contained in:
@@ -22,7 +22,13 @@ const routes = [
|
||||
path: 'sites',
|
||||
name: 'Sites',
|
||||
component: () => import('../views/Sites.vue'),
|
||||
meta: { title: '站点管理' }
|
||||
meta: { title: '站点列表' }
|
||||
},
|
||||
{
|
||||
path: 'sites/review',
|
||||
name: 'SiteReview',
|
||||
component: () => import('../views/SiteReview.vue'),
|
||||
meta: { title: '站点审核' }
|
||||
},
|
||||
{
|
||||
path: 'configs',
|
||||
|
||||
@@ -16,7 +16,11 @@
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/sites">
|
||||
<el-icon><OfficeBuilding /></el-icon>
|
||||
<span>站点管理</span>
|
||||
<span>站点列表</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/sites/review">
|
||||
<el-icon><Checked /></el-icon>
|
||||
<span>站点审核</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/configs">
|
||||
<el-icon><DocumentChecked /></el-icon>
|
||||
|
||||
91
web/src/views/SiteReview.vue
Normal file
91
web/src/views/SiteReview.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>站点审核</h2>
|
||||
<el-card style="margin-top: 20px" v-if="submissions.length === 0">
|
||||
<div style="text-align:center;padding:40px;color:#909399">暂无待审核提交</div>
|
||||
</el-card>
|
||||
<el-card style="margin-top: 20px" v-else>
|
||||
<el-table :data="submissions" stripe size="small">
|
||||
<el-table-column prop="siteId" label="站点ID" width="120" />
|
||||
<el-table-column label="名称" width="180">
|
||||
<template #default="scope"><div v-for="(n,i) in scope.row.names" :key="i" class="text-xs">{{ n }}</div></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="地址" min-width="200">
|
||||
<template #default="scope"><div v-for="(u,i) in scope.row.urls" :key="i" class="text-xs">{{ u }}</div></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="站点状态" width="90">
|
||||
<template #default="scope">
|
||||
<el-tag size="small" :type="scope.row.siteStatus==='正常'?'success':scope.row.siteStatus==='关停'?'danger':'warning'">{{ scope.row.siteStatus }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" min-width="120">
|
||||
<template #default="scope"><span class="text-xs text-muted">{{ scope.row.notes||'-' }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="提交时间" width="160" />
|
||||
<el-table-column label="操作" width="210">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="success" @click="handleApprove(scope.row)">通过</el-button>
|
||||
<el-button size="small" type="danger" @click="handleReject(scope.row)">拒绝</el-button>
|
||||
<el-button size="small" @click="handleMatch(scope.row)">匹配</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="matchVisible" title="关联到已有站点" width="500px">
|
||||
<el-input v-model="matchQuery" placeholder="搜索站点名称或URL..." clearable @input="handleSearch" style="margin-bottom:12px" />
|
||||
<el-table :data="matchResults" stripe size="small" v-loading="matchSearching" max-height="300" @row-click="handleLinkSite" highlight-current-row>
|
||||
<el-table-column prop="siteId" label="站点ID" width="120" />
|
||||
<el-table-column label="名称" min-width="150"><template #default="scope"><div v-for="(n,i) in scope.row.names" :key="i" class="text-xs">{{ n }}</div></template></el-table-column>
|
||||
<el-table-column label="地址" min-width="150"><template #default="scope"><div v-for="(u,i) in scope.row.urls" :key="i" class="text-xs">{{ u }}</div></template></el-table-column>
|
||||
</el-table>
|
||||
<template #footer><el-button @click="matchVisible = false">取消</el-button></template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import api from '../api'
|
||||
|
||||
const submissions = ref([])
|
||||
const matchVisible = ref(false), matchQuery = ref(''), matchResults = ref([]), matchSearching = ref(false)
|
||||
let matchTargetId = null
|
||||
|
||||
const fetch = async () => {
|
||||
try { const r = await api.sites.pendingChanges(); submissions.value = r.data || [] }
|
||||
catch { ElMessage.error('获取待审核列表失败') }
|
||||
}
|
||||
|
||||
const handleApprove = async (row) => {
|
||||
try { await api.sites.approveChange(row.id); ElMessage.success('已通过'); fetch() }
|
||||
catch (e) { ElMessage.error(e.response?.data?.message||'操作失败') }
|
||||
}
|
||||
|
||||
const handleReject = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认拒绝 "${(row.names||[])[0]||row.siteId}" 的提交?`, '警告', { type: 'warning' })
|
||||
await api.sites.rejectChange(row.id); ElMessage.success('已拒绝'); fetch()
|
||||
} catch (e) { if (e !== 'cancel') ElMessage.error(e.response?.data?.message||'操作失败') }
|
||||
}
|
||||
|
||||
const handleMatch = (row) => { matchTargetId = row.id; matchQuery.value = (row.names||[])[0]||''; matchVisible.value = true; handleSearch() }
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (!matchQuery.value.trim()) { matchResults.value = []; return }
|
||||
matchSearching.value = true
|
||||
try { const r = await api.sites.search(matchQuery.value.trim()); matchResults.value = r.data||[] }
|
||||
catch { matchResults.value = [] }
|
||||
finally { matchSearching.value = false }
|
||||
}
|
||||
|
||||
const handleLinkSite = async (row) => {
|
||||
try { await api.sites.linkChange(matchTargetId, row.siteId); ElMessage.success(`已关联到 ${row.siteId}`); matchVisible.value = false; fetch() }
|
||||
catch (e) { ElMessage.error(e.response?.data?.message||'关联失败') }
|
||||
}
|
||||
|
||||
onMounted(fetch)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-muted { color: #909399; }
|
||||
</style>
|
||||
@@ -1,49 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>站点管理</h2>
|
||||
|
||||
<!-- 审核中的提交 -->
|
||||
<el-card style="margin-top: 20px" v-if="submissions.length > 0">
|
||||
<template #header><span style="font-weight:bold">待审核</span></template>
|
||||
<el-table :data="submissions" stripe size="small">
|
||||
<el-table-column prop="siteId" label="站点ID" width="120" />
|
||||
<el-table-column label="名称" width="180">
|
||||
<template #default="scope">
|
||||
<div v-for="(n, i) in scope.row.names" :key="i" class="text-xs">{{ n }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="地址" min-width="200">
|
||||
<template #default="scope">
|
||||
<div v-for="(u, i) in scope.row.urls" :key="i" class="text-xs">{{ u }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="站点状态" width="90">
|
||||
<template #default="scope">
|
||||
<el-tag size="small" :type="scope.row.siteStatus === '正常' ? 'success' : scope.row.siteStatus === '关停' ? 'danger' : 'warning'">
|
||||
{{ scope.row.siteStatus }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" min-width="120">
|
||||
<template #default="scope">
|
||||
<span class="text-xs text-muted">{{ scope.row.notes || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="提交时间" width="160" />
|
||||
<el-table-column label="操作" width="210">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="success" @click="handleApprove(scope.row)">通过</el-button>
|
||||
<el-button size="small" type="danger" @click="handleReject(scope.row)">拒绝</el-button>
|
||||
<el-button size="small" @click="handleMatch(scope.row)">匹配</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 已审核站点 -->
|
||||
<h2>站点列表</h2>
|
||||
<el-card style="margin-top: 20px">
|
||||
<template #header><span style="font-weight:bold">已审核站点</span></template>
|
||||
<el-table :data="sites" v-loading="loading" stripe size="small">
|
||||
<el-table :data="sites" v-loading="loading" stripe size="small" empty-text="暂无站点">
|
||||
<el-table-column prop="siteId" label="站点ID" width="120" />
|
||||
<el-table-column label="名称" width="180">
|
||||
<template #default="scope">
|
||||
@@ -148,26 +107,6 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 手动匹配对话框 -->
|
||||
<el-dialog v-model="matchVisible" title="关联到已有站点" width="500px" :close-on-click-modal="false">
|
||||
<el-input v-model="matchQuery" placeholder="搜索站点名称或URL..." clearable @input="handleSearch" style="margin-bottom:12px" />
|
||||
<el-table :data="matchResults" stripe size="small" v-loading="matchSearching" max-height="300" @row-click="handleLinkSite" highlight-current-row>
|
||||
<el-table-column prop="siteId" label="站点ID" width="120" />
|
||||
<el-table-column label="名称" min-width="150">
|
||||
<template #default="scope">
|
||||
<div v-for="(n, i) in scope.row.names" :key="i" class="text-xs">{{ n }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="地址" min-width="150">
|
||||
<template #default="scope">
|
||||
<div v-for="(u, i) in scope.row.urls" :key="i" class="text-xs">{{ u }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="matchVisible = false">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -175,182 +114,81 @@
|
||||
import api from '../api'
|
||||
|
||||
const sites = ref([])
|
||||
const submissions = ref([])
|
||||
const loading = ref(false)
|
||||
const editVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
const editVisible = ref(false), saving = ref(false)
|
||||
const editForm = ref({ id: null, siteId: '', name: '', baseUrl: '', siteType: '', description: '' })
|
||||
const siteTypes = [
|
||||
{ value: 'nexusphp', label: 'NexusPHP' },
|
||||
{ value: 'mtorrent', label: 'MTorrent' },
|
||||
{ value: 'mteam', label: 'M-Team API' },
|
||||
{ value: 'pttime', label: 'PTTime' },
|
||||
{ value: 'gazelle', label: 'Gazelle' },
|
||||
{ value: 'unit3d', label: 'UNIT3D' },
|
||||
{ value: 'jsonapi', label: 'JSON API' },
|
||||
{ value: 'unknown', label: '未知' },
|
||||
{ value: 'nexusphp', label: 'NexusPHP' }, { value: 'mtorrent', label: 'MTorrent' },
|
||||
{ value: 'mteam', label: 'M-Team API' }, { value: 'pttime', label: 'PTTime' },
|
||||
{ value: 'gazelle', label: 'Gazelle' }, { value: 'unit3d', label: 'UNIT3D' },
|
||||
{ value: 'jsonapi', label: 'JSON API' }, { value: 'unknown', label: '未知' },
|
||||
]
|
||||
|
||||
// 配置查看
|
||||
const matchVisible = ref(false)
|
||||
const matchQuery = ref('')
|
||||
const matchResults = ref([])
|
||||
const matchSearching = ref(false)
|
||||
let matchTargetId = ref(null)
|
||||
|
||||
const handleMatch = (row) => {
|
||||
matchTargetId.value = row.id
|
||||
matchQuery.value = (row.names || [])[0] || ''
|
||||
matchVisible.value = true
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (!matchQuery.value.trim()) { matchResults.value = []; return }
|
||||
matchSearching.value = true
|
||||
try {
|
||||
const res = await api.sites.search(matchQuery.value.trim())
|
||||
matchResults.value = res.data || []
|
||||
} catch { matchResults.value = [] }
|
||||
finally { matchSearching.value = false }
|
||||
}
|
||||
|
||||
const handleLinkSite = async (row) => {
|
||||
try {
|
||||
await api.sites.linkChange(matchTargetId.value, row.siteId)
|
||||
ElMessage.success(`已关联到 ${row.siteId}`)
|
||||
matchVisible.value = false
|
||||
fetchAll()
|
||||
} catch (e) {
|
||||
ElMessage.error(e.response?.data?.message || '关联失败')
|
||||
}
|
||||
}
|
||||
|
||||
const historyVisible = ref(false)
|
||||
const historyLoading = ref(false)
|
||||
const historyList = ref([])
|
||||
const historySiteId = ref('')
|
||||
|
||||
const tagForStatus = (s) => {
|
||||
if (s === '正常') return 'success'
|
||||
if (s === '关停') return 'danger'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
const handleViewHistory = async (row) => {
|
||||
historySiteId.value = row.siteId
|
||||
historyList.value = []
|
||||
historyVisible.value = true
|
||||
historyLoading.value = true
|
||||
try {
|
||||
const res = await api.sites.changesBySite(row.siteId)
|
||||
historyList.value = res.data || []
|
||||
} catch (e) {
|
||||
ElMessage.error('获取变更记录失败')
|
||||
} finally {
|
||||
historyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const configVisible = ref(false)
|
||||
const configLoading = ref(false)
|
||||
const configError = ref('')
|
||||
const configContent = ref('')
|
||||
const configSiteId = ref('')
|
||||
const configVisible = ref(false), configLoading = ref(false), configError = ref(''), configContent = ref(''), configSiteId = ref('')
|
||||
const historyVisible = ref(false), historyLoading = ref(false), historyList = ref([]), historySiteId = ref('')
|
||||
|
||||
const highlightedJson = computed(() => {
|
||||
try {
|
||||
const obj = JSON.parse(configContent.value)
|
||||
return syntaxHighlight(JSON.stringify(obj, null, 2))
|
||||
} catch {
|
||||
return escapeHtml(configContent.value)
|
||||
}
|
||||
try { return syntaxHighlight(JSON.stringify(JSON.parse(configContent.value), null, 2)) }
|
||||
catch { return escapeHtml(configContent.value) }
|
||||
})
|
||||
|
||||
const handleViewConfig = async (row) => {
|
||||
configVisible.value = true
|
||||
configSiteId.value = row.siteId
|
||||
configLoading.value = true
|
||||
configError.value = ''
|
||||
configContent.value = ''
|
||||
try {
|
||||
const res = await api.configs.bySite(row.siteId)
|
||||
const configs = res.data || []
|
||||
if (configs.length === 0) {
|
||||
configContent.value = ''
|
||||
} else {
|
||||
const latest = configs[0] // already sorted by created_at desc
|
||||
const resp = await api.configs.content(latest.siteId, latest.version)
|
||||
const content = resp.data
|
||||
configContent.value = typeof content === 'string' ? content : JSON.stringify(content, null, 2)
|
||||
}
|
||||
} catch (e) {
|
||||
configError.value = e.response?.data?.message || '获取配置失败'
|
||||
} finally {
|
||||
configLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// syntax highlighting (reused from Configs.vue)
|
||||
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 fetchAll = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const [siteRes, subRes] = await Promise.all([
|
||||
api.sites.adminList(),
|
||||
api.sites.pendingChanges(),
|
||||
])
|
||||
sites.value = siteRes.data || []
|
||||
submissions.value = subRes.data || []
|
||||
} catch (e) {
|
||||
ElMessage.error('获取数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
try { const r = await api.sites.adminList(); sites.value = r.data || [] }
|
||||
catch { ElMessage.error('获取站点列表失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const handleApprove = async (row) => {
|
||||
try {
|
||||
await api.sites.approveChange(row.id)
|
||||
ElMessage.success('已通过')
|
||||
fetchAll()
|
||||
} catch (e) {
|
||||
ElMessage.error(e.response?.data?.message || '操作失败')
|
||||
}
|
||||
const handleEdit = (row) => {
|
||||
editForm.value = { id: row.id, siteId: row.siteId, name: (row.names||[]).join('\n'), baseUrl: (row.urls||[]).join('\n'), siteType: row.siteType||'', description: row.description||'' }
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
const handleReject = async (row) => {
|
||||
const handleSave = async () => {
|
||||
if (!editForm.value.name.trim()||!editForm.value.baseUrl.trim()) { ElMessage.warning('名称和地址不能为空'); return }
|
||||
saving.value = true
|
||||
try {
|
||||
const name = (row.names || [])[0] || row.siteId
|
||||
await ElMessageBox.confirm(`确认拒绝 "${name}" 的提交?`, '警告', { type: 'warning' })
|
||||
await api.sites.rejectChange(row.id)
|
||||
ElMessage.success('已拒绝')
|
||||
fetchAll()
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') ElMessage.error(e.response?.data?.message || '操作失败')
|
||||
}
|
||||
await api.sites.update(editForm.value.siteId, {
|
||||
name: editForm.value.name.trim().split('\n').filter(Boolean)[0]||'',
|
||||
baseUrl: editForm.value.baseUrl.trim().split('\n').filter(Boolean)[0]||'',
|
||||
siteType: editForm.value.siteType.trim()||'unknown',
|
||||
description: editForm.value.description.trim()||null,
|
||||
})
|
||||
ElMessage.success('保存成功'); editVisible.value = false; fetchAll()
|
||||
} catch (e) { ElMessage.error(e.response?.data?.message||'保存失败') }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
|
||||
const handleViewConfig = async (row) => {
|
||||
configVisible.value = true; configSiteId.value = row.siteId; configLoading.value = true; configError.value = ''; configContent.value = ''
|
||||
try {
|
||||
const r = await api.configs.bySite(row.siteId)
|
||||
const configs = r.data || []
|
||||
if (configs.length) {
|
||||
const resp = await api.configs.content(configs[0].siteId, configs[0].version)
|
||||
configContent.value = typeof resp.data==='string' ? resp.data : JSON.stringify(resp.data,null,2)
|
||||
}
|
||||
} catch (e) { configError.value = e.response?.data?.message||'获取配置失败' }
|
||||
finally { configLoading.value = false }
|
||||
}
|
||||
|
||||
const handleViewHistory = async (row) => {
|
||||
historySiteId.value = row.siteId; historyList.value = []; historyVisible.value = true; historyLoading.value = true
|
||||
try { const r = await api.sites.changesBySite(row.siteId); historyList.value = r.data || [] }
|
||||
catch { ElMessage.error('获取变更记录失败') }
|
||||
finally { historyLoading.value = false }
|
||||
}
|
||||
|
||||
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>`
|
||||
})
|
||||
|
||||
const escapeHtml = (str) => str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
||||
|
||||
const handleEdit = (row) => {
|
||||
editForm.value = {
|
||||
id: row.id,
|
||||
|
||||
Reference in New Issue
Block a user