diff --git a/web/src/router/index.js b/web/src/router/index.js index f702deb..dca7835 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -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', diff --git a/web/src/views/Layout.vue b/web/src/views/Layout.vue index 8f2e027..d93be69 100644 --- a/web/src/views/Layout.vue +++ b/web/src/views/Layout.vue @@ -16,7 +16,11 @@ - 站点管理 + 站点列表 + + + + 站点审核 diff --git a/web/src/views/SiteReview.vue b/web/src/views/SiteReview.vue new file mode 100644 index 0000000..f26adb4 --- /dev/null +++ b/web/src/views/SiteReview.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/web/src/views/Sites.vue b/web/src/views/Sites.vue index 8874078..6ec79a1 100644 --- a/web/src/views/Sites.vue +++ b/web/src/views/Sites.vue @@ -1,49 +1,8 @@ @@ -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 `${match}` - } - ) -} - -const escapeHtml = (str) => { - return str.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 `${match}` +}) + +const escapeHtml = (str) => str.replace(/&/g,'&').replace(//g,'>') + const handleEdit = (row) => { editForm.value = { id: row.id,