refactor: 页面配置+风险规则改为左编辑器右可视化的脚本管理同款布局

This commit is contained in:
mediabot-pt
2026-07-05 20:08:06 +08:00
parent e6aaa3b238
commit 5381e2104d
2 changed files with 144 additions and 317 deletions

View File

@@ -1,192 +1,103 @@
<template>
<div>
<h2>页面配置</h2>
<!-- 站点类型默认模板 -->
<el-card style="margin-top:12px" v-if="defaults.length">
<template #header><span style="font-weight:500">站点类型默认模板</span></template>
<div style="display:flex;gap:12px;flex-wrap:wrap">
<div v-for="dt in defaults" :key="dt.type" style="border:1px solid #ebeef5;border-radius:4px;padding:10px 14px;min-width:200px">
<div style="font-weight:500;margin-bottom:6px">{{ dt.label }} <el-tag size="small">{{ dt.type }}</el-tag></div>
<div style="font-size:12px;color:#909399;margin-bottom:8px">{{ dt.entries.length }} 个入口</div>
<div style="display:flex;gap:4px">
<el-button size="small" @click="previewDefault(dt)">查看</el-button>
<el-button size="small" type="primary" @click="editDefault(dt)">编辑默认</el-button>
</div>
</div>
<div style="display:flex;flex-direction:column;height:calc(100vh - 80px)">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<h2 style="margin:0">页面配置</h2>
<div style="display:flex;gap:8px">
<el-select v-model="editSiteId" placeholder="选择站点" style="width:200px" filterable size="small">
<el-option v-for="s in allSiteOptions" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
<el-button size="small" @click="loadEditor">加载</el-button>
<el-button size="small" type="primary" @click="savePages" :loading="saving">保存</el-button>
</div>
</el-card>
</div>
<el-card style="margin-top:12px">
<div style="margin-bottom:12px;display:flex;gap:8px">
<el-button @click="fetchAll" :loading="loading">刷新</el-button>
</div>
<el-table :data="merged" stripe size="small" v-loading="loading" empty-text="暂无站点">
<el-table-column prop="siteId" label="站点" width="140" />
<el-table-column label="名称" min-width="130">
<template #default="s">{{ (s.row.names||[]).join(' / ') || '-' }}</template>
</el-table-column>
<el-table-column label="入口数" width="80" align="center">
<template #default="s">{{ s.row.entryCount || 0 }}</template>
</el-table-column>
<el-table-column label="版本" width="70" align="center">
<template #default="s">{{ s.row.version || '-' }}</template>
</el-table-column>
<el-table-column label="更新时间" width="150">
<template #default="s">{{ fmt(s.row.updatedAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="s">
<el-select v-model="viewVer" placeholder="历史" size="small" style="width:70px" clearable
@change="v=>loadVersion(s.row,v)" @click.stop>
<el-option v-for="v in s.row.versions||[]" :key="v" :label="'v'+v" :value="v" />
</el-select>
<el-button size="small" @click="toggleEdit(s.row)" :type="editSiteId===s.row.siteId?'warning':''">
{{ editSiteId===s.row.siteId ? '收起' : '编辑' }}
</el-button>
<el-button v-if="!s.row.entryCount" size="small" @click="loadDefault(s.row)">读取默认</el-button>
</template>
</el-table-column>
</el-table>
<div v-if="editSiteId" style="margin-top:16px;padding:12px;background:#fafafa;border-radius:4px">
<div style="margin-bottom:8px;display:flex;gap:8px;align-items:center">
<strong>编辑: {{ editSiteId }}</strong>
</div>
<div style="height:400px;border-radius:4px;overflow:hidden;margin-bottom:8px">
<div style="display:flex;gap:12px;flex:1;min-height:0">
<!-- 左侧:编辑器 -->
<div style="flex:0 0 440px;display:flex;flex-direction:column;min-height:0">
<div style="font-size:13px;font-weight:500;margin-bottom:4px">JSON 配置</div>
<div style="flex:1;min-height:0;border-radius:4px;overflow:hidden">
<CodeEditor v-model="editorJson" language="json" />
</div>
<div style="display:flex;gap:8px">
<el-button size="small" type="primary" @click="savePages" :loading="saving">保存</el-button>
<el-button size="small" @click="editSiteId=''">取消</el-button>
</div>
<!-- 右侧:可视化 -->
<div style="flex:1;overflow:auto;min-height:0">
<div v-if="!parsed.length" style="text-align:center;padding:60px;color:#909399">选择站点编辑</div>
<el-table v-else :data="parsed" stripe size="small" border>
<el-table-column prop="key" label="标识" width="100" />
<el-table-column prop="label" label="名称" width="110" />
<el-table-column prop="method" label="方法" width="70" />
<el-table-column label="完整URL" min-width="250">
<template #default="s">{{ resolveUrl(s.row.url) }}</template>
</el-table-column>
<el-table-column label="操作" width="60">
<template #default="s">
<el-button size="small" type="danger" text @click="removeEntry(s.$index)">×</el-button>
</template>
</el-table-column>
</el-table>
<div v-if="parsed.length" style="margin-top:8px">
<el-button size="small" @click="addEntry">+ 添加</el-button>
</div>
</div>
</el-card>
<el-dialog v-model="viewVisible" :title="'默认模板: '+viewType" width="700px">
<div style="height:400px;border-radius:4px;overflow:hidden">
<CodeEditor v-model="viewJson" language="json" readonly />
</div>
</el-dialog>
</div>
</div>
</template>
<script setup>
import api from '../api'
const loading = ref(false), saving = ref(false)
const sites = ref([]), pagesMap = ref({}), defaultMap = ref({}), viewVer = ref('')
const editSiteId = ref(''), editorJson = ref('')
const saving = ref(false), editorJson = ref('[]')
const allSiteOptions = ref([]), sitesList = ref([])
const editSiteId = ref('')
const fallbackPages = { nexusphp: [{key:'home',label:'首页',url:'{base}',method:'GET'}] }
const defaults = computed(() => {
const types = [...new Set((Array.isArray(sites.value)?sites.value:[]).map(s=>s.siteType).filter(Boolean))]
const base = ['nexusphp','mtorrent','gazelle','unit3d']
return [...new Set([...types,...base])].map(t => ({
type:t, label:t,
entries: defaultMap.value[t] || fallbackPages[t] || [{key:'home',label:'首页',url:'{base}',method:'GET'}]
}))
const parsed = computed(() => {
try { return JSON.parse(editorJson.value) } catch { return [] }
})
const fmt = (t) => t ? (t.substring(0,19)||'').replace('T',' ') : '-'
const resolveUrl = (url) => {
if (!url) return ''
const site = sitesList.value.find(s => s.siteId === editSiteId.value)
const base = site?.urls?.[0] || '{base}'
return url.replace('{base}', base)
}
const merged = computed(() => {
return (Array.isArray(sites.value)?sites.value:[]).map(s => ({
siteId: s.siteId,
names: s.names,
version: pagesMap.value[s.siteId]?.version || null,
entryCount: pagesMap.value[s.siteId]?.entryCount || 0,
updatedAt: pagesMap.value[s.siteId]?.updatedAt || null,
entriesJson: pagesMap.value[s.siteId]?.entriesJson || null,
versions: pagesMap.value[s.siteId]?.versions || [],
}))
})
const fetchAll = async () => {
loading.value = true
const fetchSites = async () => {
try {
const [sr, pr] = await Promise.allSettled([
api.raw.get('/admin/sites'),
api.raw.get('/admin/site-pages'),
])
sites.value = (sr.status==='fulfilled' ? (sr.value?.data || sr.value || []) : [])
const pages = pr.status==='fulfilled' ? (pr.value?.data || pr.value || []) : []
const map = {}, verMap = {}
for (const p of pages) {
const entries = (() => { try { return JSON.parse(p.entriesJson||'[]') } catch {return []} })()
const v = p.version
if (!map[p.siteId] || v > map[p.siteId].version) {
map[p.siteId] = { version: v, entryCount: entries.length, updatedAt: p.updatedAt, entriesJson: p.entriesJson }
}
if (!verMap[p.siteId]) verMap[p.siteId] = new Set()
verMap[p.siteId].add(v)
}
for (const k in verMap) map[k].versions = [...verMap[k]].sort((a,b)=>parseInt(b)-parseInt(a))
pagesMap.value = map
// 加载默认模板
for (const t of ['nexusphp','mtorrent','gazelle','unit3d']) {
try {
const r = await api.raw.get('/admin/default-pages/'+t)
const data = r?.data || r
if (data?.entries) defaultMap.value[t] = data.entries
} catch {}
}
} finally { loading.value = false }
sitesList.value = (await api.raw.get('/admin/sites'))?.data || []
allSiteOptions.value = sitesList.value.map(s => ({label: (s.names?.join('/')||s.siteId), value: s.siteId}))
} catch {}
}
const loadVersion = async (row, ver) => {
if (!ver) return
const loadEditor = async () => {
if (!editSiteId.value) return
try {
const r = await api.raw.get('/admin/site-pages/'+row.siteId+'/version/'+ver)
editorJson.value = JSON.stringify(JSON.parse((r?.data||r)?.content||'[]'), null, 2)
} catch { ElMessage.error('加载版本失败') }
const r = await api.raw.get('/admin/site-pages/' + editSiteId.value)
const data = r?.data || r
editorJson.value = data?.entries ? JSON.stringify(data.entries, null, 2) : '[]'
} catch { editorJson.value = '[]' }
}
const loadDefault = (row) => {
editSiteId.value = row.siteId
const entries = defaultMap.value[row.siteType] || fallbackPages[row.siteType] || [{key:'home',label:'首页',url:'{base}',method:'GET'}]
editorJson.value = JSON.stringify(entries, null, 2)
window.scrollTo({top: document.body.scrollHeight, behavior:'smooth'})
const addEntry = () => {
const arr = parsed.value
arr.push({key:'',label:'',url:'{base}',method:'GET'})
editorJson.value = JSON.stringify(arr, null, 2)
}
const editDefault = (dt) => {
editSiteId.value = dt.type
editorJson.value = JSON.stringify(dt.entries, null, 2)
window.scrollTo({top: document.body.scrollHeight, behavior:'smooth'})
}
const viewVisible = ref(false), viewType = ref(''), viewJson = ref('')
const previewDefault = (dt) => {
viewType.value = dt.type
viewJson.value = JSON.stringify(dt.entries, null, 2)
viewVisible.value = true
}
const toggleEdit = (row) => {
if (editSiteId.value === row.siteId) { editSiteId.value = ''; return }
editSiteId.value = row.siteId
try { editorJson.value = JSON.stringify(JSON.parse(row.entriesJson||'[]'), null, 2) }
catch { editorJson.value = JSON.stringify([{key:'home',label:'首页',url:'{base}',method:'GET'}], null, 2) }
const removeEntry = (idx) => {
const arr = parsed.value
arr.splice(idx, 1)
editorJson.value = JSON.stringify(arr, null, 2)
}
const savePages = async () => {
saving.value = true
try {
const url = editSiteId.value.startsWith('default:')
? '/admin/default-pages/'+editSiteId.value.replace('default:','')
: '/sites/'+editSiteId.value+'/pages'
await api.raw.put(url, { entries: JSON.parse(editorJson.value) })
ElMessage.success('已保存'); editSiteId.value = ''; fetchAll()
await api.raw.put('/sites/'+editSiteId.value+'/pages', { entries: parsed.value })
ElMessage.success('已保存')
} catch { ElMessage.error('保存失败') }
finally { saving.value = false }
}
onMounted(fetchAll)
onMounted(fetchSites)
</script>