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

222 lines
9.8 KiB
Vue

<template>
<div>
<h2>站点列表</h2>
<el-card style="margin-top: 20px">
<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">
<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 prop="siteType" label="类型" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<el-tag size="small" :type="scope.row.status === 'active' ? 'success' : 'info'">
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" @click="handleViewConfig(scope.row)">配置</el-button>
<el-button size="small" @click="handleViewHistory(scope.row)">变更</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 编辑对话框 -->
<el-dialog v-model="editVisible" title="编辑站点" width="500px" :close-on-click-modal="false">
<el-form :model="editForm" label-width="80px" @submit.prevent="handleSave">
<el-form-item label="站点ID">
<el-input v-model="editForm.siteId" disabled />
</el-form-item>
<el-form-item label="名称">
<el-input v-model="editForm.name" type="textarea" :rows="3" placeholder="站点名称(一行一个)" />
</el-form-item>
<el-form-item label="地址">
<el-input v-model="editForm.baseUrl" type="textarea" :rows="3" placeholder="站点地址(一行一个)" />
</el-form-item>
<el-form-item label="类型">
<el-select v-model="editForm.siteType" placeholder="选择类型" style="width:100%">
<el-option v-for="t in siteTypes" :key="t.value" :label="t.label" :value="t.value" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="editForm.status" style="width:100%">
<el-option label="活跃" value="active" />
<el-option label="弃用" value="deprecated" />
</el-select>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="editForm.description" type="textarea" :rows="3" placeholder="站点描述" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave" :loading="saving">保存</el-button>
</template>
</el-dialog>
<!-- 查看配置对话框 -->
<el-dialog v-model="configVisible" :title="'配置:' + configSiteId" width="700px" :close-on-click-modal="false">
<div v-if="configLoading" style="text-align:center;padding:40px">加载中...</div>
<div v-else-if="configError" style="color:#f56c6c;text-align:center;padding:20px">{{ configError }}</div>
<div v-else-if="!configContent" style="text-align:center;padding:40px;color:#909399">暂无配置</div>
<pre v-else class="config-json" v-html="highlightedJson"></pre>
<template #footer>
<el-button @click="configVisible = false">关闭</el-button>
</template>
</el-dialog>
<!-- 变更记录对话框 -->
<el-dialog v-model="historyVisible" :title="'变更记录:' + historySiteId" width="800px" :close-on-click-modal="false">
<el-table :data="historyList" stripe size="small" v-loading="historyLoading" empty-text="暂无记录">
<el-table-column label="名称" width="160">
<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="180">
<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="tagForStatus(scope.row.siteStatus)">{{ scope.row.siteStatus }}</el-tag>
</template>
</el-table-column>
<el-table-column label="审核" width="80">
<template #default="scope">
<el-tag size="small" :type="scope.row.status === 'approved' ? 'success' : scope.row.status === 'pending' ? 'warning' : 'danger'">
{{ scope.row.status === 'approved' ? '已通过' : scope.row.status === 'pending' ? '待审核' : '已拒绝' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" min-width="100">
<template #default="scope">
<span class="text-xs">{{ scope.row.notes || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="时间" width="160" />
</el-table>
<template #footer>
<el-button @click="historyVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import api from '../api'
const sites = ref([])
const loading = ref(false)
const editVisible = ref(false), saving = ref(false)
const editForm = ref({ id: null, siteId: '', name: '', baseUrl: '', siteType: '', status: '', 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: '未知' },
]
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 { return syntaxHighlight(JSON.stringify(JSON.parse(configContent.value), null, 2)) }
catch { return escapeHtml(configContent.value) }
})
const fetchAll = async () => {
loading.value = true
try { const r = await api.sites.adminList(); sites.value = r.data || [] }
catch { ElMessage.error('获取站点列表失败') }
finally { loading.value = false }
}
const handleEdit = (row) => {
editForm.value = { id: row.id, siteId: row.siteId, name: (row.names||[]).join('\n'), baseUrl: (row.urls||[]).join('\n'), siteType: row.siteType||'', status: row.status||'active', description: row.description||'' }
editVisible.value = true
}
const handleSave = async () => {
if (!editForm.value.name.trim()||!editForm.value.baseUrl.trim()) { ElMessage.warning('名称和地址不能为空'); return }
saving.value = true
try {
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',
status: editForm.value.status||'active',
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
onMounted(fetchAll)
</script>
<style scoped>
.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;
max-height: 500px;
overflow: auto;
}
.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; }
</style>