refactor: 统一四页面交互模式(列表全宽→编辑双栏)
This commit is contained in:
@@ -1,144 +1,109 @@
|
||||
<template>
|
||||
<div style="display:flex;flex-direction:column;height:calc(100vh - 80px)">
|
||||
<!-- 列表模式 -->
|
||||
<div v-if="!editType">
|
||||
<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" v-if="editType">
|
||||
<el-button size="small" @click="fetchAll" :icon="Refresh">刷新</el-button>
|
||||
</div>
|
||||
<el-table :data="merged" stripe v-loading="loading" border empty-text="暂无站点" style="font-size:13px" :row-style="{height:'44px'}">
|
||||
<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 prop="siteType" label="类型" width="100" />
|
||||
<el-table-column label="规则数" width="80" align="center">
|
||||
<template #default="s"><el-tag size="small">{{ s.row.ruleCount || 0 }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="版本" width="80" 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="100">
|
||||
<template #default="s"><el-button size="small" @click="editRule(s.row)">编辑</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 编辑模式:全屏双栏 -->
|
||||
<div v-else 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">编辑: {{ editType }}</h2>
|
||||
<div style="display:flex;gap:8px">
|
||||
<el-button size="small" type="primary" @click="saveRules" :loading="saving">保存</el-button>
|
||||
<el-button size="small" @click="editType=''">返回列表</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表模式 -->
|
||||
<template v-if="!editType">
|
||||
<div style="display:flex;gap:12px;flex:1;min-height:0">
|
||||
<div style="flex:1;overflow:auto;min-height:0">
|
||||
<el-table :data="merged" stripe v-loading="loading" border empty-text="暂无站点"
|
||||
style="font-size:13px" :row-style="{height:'44px'}">
|
||||
<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 prop="siteType" label="类型" width="100" />
|
||||
<el-table-column label="规则数" width="80" align="center">
|
||||
<template #default="s">
|
||||
<el-tag size="small">{{ s.row.ruleCount || 0 }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="版本" width="80" 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="100">
|
||||
<template #default="s">
|
||||
<el-button size="small" @click="selectType(s.row)">编辑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="display:flex;gap:12px;flex:1;min-height:0">
|
||||
<div style="flex:0 0 400px;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>
|
||||
</template>
|
||||
|
||||
<!-- 编辑模式 -->
|
||||
<template v-else>
|
||||
<div style="display:flex;gap:12px;flex:1;min-height:0">
|
||||
<div style="flex:0 0 420px;display:flex;flex-direction:column;min-height:0">
|
||||
<div style="font-size:13px;font-weight:500;margin-bottom:4px">
|
||||
编辑: {{ editType }}
|
||||
<span style="color:#909399;font-size:12px;font-weight:normal">(影响所有同类型站点)</span>
|
||||
</div>
|
||||
<div style="flex:1;min-height:0;border-radius:4px;overflow:hidden">
|
||||
<CodeEditor v-model="editorJson" language="json" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex:1;overflow:auto;min-height:0">
|
||||
<div v-if="!parsed.length" style="text-align:center;padding:40px;color:#909399">该类型暂无规则,配置后所有同类型站点生效</div>
|
||||
<el-table v-else :data="parsed" stripe border style="font-size:13px" :row-style="{height:'42px'}">
|
||||
<el-table-column label="级别" width="80">
|
||||
<template #default="s">
|
||||
<el-tag :type="s.row.level==='CRITICAL'?'danger':'warning'" size="small">{{ s.row.level }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="类型" width="90" />
|
||||
<el-table-column prop="name" label="名称" min-width="180" />
|
||||
<el-table-column label="条件" width="180">
|
||||
<template #default="s">{{ s.row.metric }} {{ condText(s.row.condition) }} {{ s.row.threshold }} {{ s.row.unit||'' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="适用条件" width="140">
|
||||
<template #default="s">{{ appText(s.row) || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="margin-top:8px">
|
||||
<el-button size="small" @click="addRule">+ 添加规则</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex:1;overflow:auto;min-height:0">
|
||||
<div v-if="!parsed.length" style="text-align:center;padding:40px;color:#909399">该类型暂无规则</div>
|
||||
<el-table v-else :data="parsed" stripe border style="font-size:13px" :row-style="{height:'42px'}">
|
||||
<el-table-column label="级别" width="80">
|
||||
<template #default="s"><el-tag :type="s.row.level==='CRITICAL'?'danger':'warning'" size="small">{{ s.row.level }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="类型" width="90" />
|
||||
<el-table-column prop="name" label="名称" min-width="180" />
|
||||
<el-table-column label="条件" width="180">
|
||||
<template #default="s">{{ s.row.metric }} {{ condText(s.row.condition) }} {{ s.row.threshold }} {{ s.row.unit||'' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="适用" width="130">
|
||||
<template #default="s">{{ appText(s.row) || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="margin-top:8px"><el-button size="small" @click="addRule">+ 添加</el-button></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import api from '../api'
|
||||
import api from '../api'; import { Refresh } from '@element-plus/icons-vue'
|
||||
|
||||
const loading = ref(false), saving = ref(false)
|
||||
const sites = ref([]), rulesMap = ref({})
|
||||
const loading = ref(false), saving = ref(false), sites = ref([]), rulesMap = ref({})
|
||||
const editType = ref(''), editorJson = ref('[]')
|
||||
|
||||
const fmt = (t) => t ? (t.substring(0,19)||'').replace('T',' ') : '-'
|
||||
const condText = (c) => ({GT:'>',LT:'<',GTE:'>=',LTE:'<=',EQ:'='})[c]||c
|
||||
const appText = (r) => r.applicable ? Object.entries(r.applicable).map(([k,v])=>k+'>='+v).join(', ') : null
|
||||
const parsed = computed(() => { try { return JSON.parse(editorJson.value) } catch { return [] } })
|
||||
|
||||
const merged = computed(() => {
|
||||
return (Array.isArray(sites.value)?sites.value:[]).map(s => ({
|
||||
siteId: s.siteId, names: s.names, siteType: s.siteType,
|
||||
ruleCount: rulesMap.value[s.siteType]?.ruleCount || 0,
|
||||
version: rulesMap.value[s.siteType]?.version || null,
|
||||
updatedAt: rulesMap.value[s.siteType]?.updatedAt || null,
|
||||
rulesJson: rulesMap.value[s.siteType]?.rulesJson || null,
|
||||
}))
|
||||
})
|
||||
const merged = computed(() => (Array.isArray(sites.value)?sites.value:[]).map(s => ({
|
||||
siteId: s.siteId, names: s.names, siteType: s.siteType,
|
||||
ruleCount: rulesMap.value[s.siteType]?.ruleCount || 0, version: rulesMap.value[s.siteType]?.version || null,
|
||||
updatedAt: rulesMap.value[s.siteType]?.updatedAt || null, rulesJson: rulesMap.value[s.siteType]?.rulesJson || null,
|
||||
})))
|
||||
|
||||
const fetchAll = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const [sr, rr] = await Promise.allSettled([api.raw.get('/admin/sites'), api.raw.get('/admin/risk-rules')])
|
||||
sites.value = sr.status==='fulfilled' ? (sr.value?.data || sr.value || []) : []
|
||||
const rulesData = rr.status==='fulfilled' ? (rr.value?.data || rr.value || []) : []
|
||||
sites.value = sr.status==='fulfilled' ? (sr.value?.data||sr.value||[]) : []
|
||||
const rulesData = rr.status==='fulfilled' ? (rr.value?.data||rr.value||[]) : []
|
||||
const map = {}
|
||||
for (const r of rulesData) {
|
||||
const cnt = (() => { try { return JSON.parse(r.rulesJson||'[]').length } catch { return 0 } })()
|
||||
if (!map[r.siteType] || r.version > map[r.siteType].version) {
|
||||
if (!map[r.siteType] || r.version > map[r.siteType].version)
|
||||
map[r.siteType] = { ruleCount: cnt, version: r.version, updatedAt: r.updatedAt, rulesJson: r.rulesJson }
|
||||
}
|
||||
}
|
||||
rulesMap.value = map
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
const selectType = async (row) => {
|
||||
const editRule = async (row) => {
|
||||
editType.value = row.siteType
|
||||
try {
|
||||
const r = await api.raw.get('/admin/risk-rules/' + row.siteType)
|
||||
const data = r?.data || r
|
||||
editorJson.value = data?.rules ? JSON.stringify(data.rules, null, 2) : '[]'
|
||||
} catch { editorJson.value = '[]' }
|
||||
try { const r = await api.raw.get('/admin/risk-rules/'+row.siteType); editorJson.value = (r?.data||r)?.rules ? JSON.stringify((r.data||r).rules, null, 2) : '[]' }
|
||||
catch { editorJson.value = '[]' }
|
||||
}
|
||||
|
||||
const addRule = () => {
|
||||
parsed.value.push({type:'TRAFFIC',name:'',metric:'',threshold:0,unit:'DAY',condition:'GT',level:'WARN'})
|
||||
editorJson.value = JSON.stringify(parsed.value, null, 2)
|
||||
}
|
||||
|
||||
const addRule = () => { parsed.value.push({type:'TRAFFIC',name:'',metric:'',threshold:0,unit:'DAY',condition:'GT',level:'WARN'}); editorJson.value = JSON.stringify(parsed.value, null, 2) }
|
||||
const saveRules = async () => {
|
||||
saving.value = true
|
||||
try {
|
||||
await api.raw.post('/risk-rules/'+editType.value, { rules: parsed.value })
|
||||
ElMessage.success('已保存'); editType.value = ''; fetchAll()
|
||||
} catch { ElMessage.error('保存失败') }
|
||||
try { await api.raw.post('/risk-rules/'+editType.value, { rules: parsed.value }); ElMessage.success('已保存'); editType.value=''; fetchAll() }
|
||||
catch { ElMessage.error('保存失败') }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
|
||||
onMounted(fetchAll)
|
||||
</script>
|
||||
|
||||
@@ -1,138 +1,104 @@
|
||||
<template>
|
||||
<div style="display:flex;flex-direction:column;height:calc(100vh - 80px)">
|
||||
<!-- 列表模式 -->
|
||||
<div v-if="!editSiteId">
|
||||
<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" v-if="editSiteId">
|
||||
<el-button size="small" @click="fetchAll" :icon="Refresh">刷新</el-button>
|
||||
</div>
|
||||
<el-table :data="merged" stripe v-loading="loading" border empty-text="暂无站点" style="font-size:13px" :row-style="{height:'44px'}">
|
||||
<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="80" 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="100">
|
||||
<template #default="s"><el-button size="small" @click="editPage(s.row)">编辑</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 编辑模式:全屏双栏 -->
|
||||
<div v-else 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">编辑: {{ editSiteId }}</h2>
|
||||
<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>
|
||||
|
||||
<!-- 列表模式 -->
|
||||
<template v-if="!editSiteId">
|
||||
<div style="display:flex;gap:12px;flex:1;min-height:0">
|
||||
<div style="flex:1;overflow:auto;min-height:0">
|
||||
<el-table :data="merged" stripe v-loading="loading" border empty-text="暂无站点"
|
||||
style="font-size:13px" :row-style="{height:'44px'}">
|
||||
<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="80" 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="100">
|
||||
<template #default="s">
|
||||
<el-button size="small" @click="selectSite(s.row)">编辑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="display:flex;gap:12px;flex:1;min-height:0">
|
||||
<div style="flex:0 0 400px;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>
|
||||
</template>
|
||||
|
||||
<!-- 编辑模式 -->
|
||||
<template v-else>
|
||||
<div style="display:flex;gap:12px;flex:1;min-height:0">
|
||||
<div style="flex:0 0 420px;display:flex;flex-direction:column;min-height:0">
|
||||
<div style="font-size:13px;font-weight:500;margin-bottom:4px">编辑: {{ editSiteId }}</div>
|
||||
<div style="flex:1;min-height:0;border-radius:4px;overflow:hidden">
|
||||
<CodeEditor v-model="editorJson" language="json" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex:1;overflow:auto;min-height:0">
|
||||
<div v-if="!parsed.length" style="text-align:center;padding:40px;color:#909399">暂无页面入口</div>
|
||||
<el-table v-else :data="parsed" stripe border style="font-size:13px" :row-style="{height:'42px'}">
|
||||
<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>
|
||||
<div style="margin-top:8px;display:flex;gap:8px">
|
||||
<el-button size="small" @click="addEntry">+ 添加入口</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex:1;overflow:auto;min-height:0">
|
||||
<div v-if="!parsed.length" style="text-align:center;padding:40px;color:#909399">暂无页面入口</div>
|
||||
<el-table v-else :data="parsed" stripe border style="font-size:13px" :row-style="{height:'42px'}">
|
||||
<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>
|
||||
<div style="margin-top:8px"><el-button size="small" @click="addEntry">+ 添加</el-button></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import api from '../api'
|
||||
import api from '../api'; import { Refresh } from '@element-plus/icons-vue'
|
||||
|
||||
const loading = ref(false), saving = ref(false)
|
||||
const sites = ref([]), pagesMap = ref({})
|
||||
const loading = ref(false), saving = ref(false), sites = ref([]), pagesMap = ref({}), sitesList = ref([])
|
||||
const editSiteId = ref(''), editorJson = ref('[]')
|
||||
const sitesList = ref([])
|
||||
|
||||
const fmt = (t) => t ? (t.substring(0,19)||'').replace('T',' ') : '-'
|
||||
|
||||
const parsed = computed(() => { try { return JSON.parse(editorJson.value) } catch { return [] } })
|
||||
|
||||
const resolveUrl = (url) => {
|
||||
if (!url) return ''
|
||||
const site = sitesList.value.find(s => s.siteId === editSiteId.value)
|
||||
return url.replace('{base}', site?.urls?.[0] || '{base}')
|
||||
}
|
||||
|
||||
const merged = computed(() => {
|
||||
return (Array.isArray(sitesList.value)?sitesList.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,
|
||||
}))
|
||||
})
|
||||
const resolveUrl = (url) => url?.replace('{base}', sitesList.value.find(s=>s.siteId===editSiteId.value)?.urls?.[0] || '{base}') || ''
|
||||
const merged = computed(() => (Array.isArray(sitesList.value)?sitesList.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,
|
||||
})))
|
||||
|
||||
const fetchAll = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const [sr, pr] = await Promise.allSettled([api.raw.get('/admin/sites'), api.raw.get('/admin/site-pages')])
|
||||
sitesList.value = sr.status==='fulfilled' ? (sr.value?.data || sr.value || []) : []
|
||||
const pages = pr.status==='fulfilled' ? (pr.value?.data || pr.value || []) : []
|
||||
sitesList.value = sr.status==='fulfilled' ? (sr.value?.data||sr.value||[]) : []
|
||||
const pages = pr.status==='fulfilled' ? (pr.value?.data||pr.value||[]) : []
|
||||
const map = {}
|
||||
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 (!map[p.siteId] || p.version > map[p.siteId].version)
|
||||
map[p.siteId] = { version: p.version, entryCount: entries.length, updatedAt: p.updatedAt, entriesJson: p.entriesJson }
|
||||
}
|
||||
pagesMap.value = map
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
const selectSite = async (row) => {
|
||||
const editPage = async (row) => {
|
||||
editSiteId.value = row.siteId
|
||||
try {
|
||||
const r = await api.raw.get('/admin/site-pages/' + row.siteId)
|
||||
const data = r?.data || r
|
||||
editorJson.value = data?.entries ? JSON.stringify(data.entries, null, 2) : '[]'
|
||||
} catch { editorJson.value = '[]' }
|
||||
}
|
||||
|
||||
const addEntry = () => {
|
||||
parsed.value.push({key:'',label:'',url:'{base}',method:'GET'})
|
||||
editorJson.value = JSON.stringify(parsed.value, null, 2)
|
||||
try { const r = await api.raw.get('/admin/site-pages/'+row.siteId); editorJson.value = (r?.data||r)?.entries ? JSON.stringify((r.data||r).entries, null, 2) : '[]' }
|
||||
catch { editorJson.value = '[]' }
|
||||
}
|
||||
|
||||
const addEntry = () => { parsed.value.push({key:'',label:'',url:'{base}',method:'GET'}); editorJson.value = JSON.stringify(parsed.value, null, 2) }
|
||||
const savePages = async () => {
|
||||
saving.value = true
|
||||
try {
|
||||
await api.raw.put('/sites/'+editSiteId.value+'/pages', { entries: parsed.value })
|
||||
ElMessage.success('已保存'); editSiteId.value = ''; fetchAll()
|
||||
} catch { ElMessage.error('保存失败') }
|
||||
try { await api.raw.put('/sites/'+editSiteId.value+'/pages', { entries: parsed.value }); ElMessage.success('已保存'); editSiteId.value=''; fetchAll() }
|
||||
catch { ElMessage.error('保存失败') }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
|
||||
onMounted(fetchAll)
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div style="display:flex;flex-direction:column;height:calc(100vh - 80px)">
|
||||
<!-- 列表模式 -->
|
||||
<div v-if="!editSiteId">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
||||
<div style="display:flex;align-items:center;gap:12px">
|
||||
<h2 style="margin:0">站点列表</h2>
|
||||
@@ -9,53 +10,62 @@
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<el-button size="small" @click="refresh" :loading="loading">刷新</el-button>
|
||||
<el-button size="small" @click="newSite">新建</el-button>
|
||||
<el-button size="small" type="primary" @click="saveSite" :loading="saving" v-if="editSiteId">保存</el-button>
|
||||
<el-button size="small" type="primary" @click="newSite">新建站点</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="sites" stripe v-loading="loading" border empty-text="暂无站点" style="font-size:13px" :row-style="{height:'44px'}">
|
||||
<el-table-column prop="siteId" label="站点ID" width="110" />
|
||||
<el-table-column label="名称" min-width="130">
|
||||
<template #default="s">{{ (s.row.names||[]).join(' / ') || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="地址" min-width="180" show-overflow-tooltip>
|
||||
<template #default="s">{{ (s.row.urls||[]).join(' / ') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="siteType" label="类型" width="100" />
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="s">
|
||||
<el-tag size="small" :type="s.row.status==='active'?'success':'info'">{{ s.row.status||'active' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="适配" width="90">
|
||||
<template #default="s">
|
||||
<el-tag v-if="s.row.latestVersion" size="small" type="primary">v{{ s.row.latestVersion }}</el-tag>
|
||||
<span v-else style="color:#909399">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="s">
|
||||
<el-button size="small" @click="editSite(s.row)">编辑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 编辑模式:全屏双栏 -->
|
||||
<div v-else 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">站点编辑: {{ editSiteId }}</h2>
|
||||
<div style="display:flex;gap:8px">
|
||||
<el-button size="small" type="primary" @click="saveSite" :loading="saving">保存</el-button>
|
||||
<el-button size="small" @click="editSiteId=''">返回列表</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:12px;flex:1;min-height:0">
|
||||
<!-- 左侧:编辑器(当前选中站点) -->
|
||||
<div style="flex:0 0 420px;display:flex;flex-direction:column;min-height:0">
|
||||
<div style="font-size:13px;font-weight:500;margin-bottom:4px;display:flex;justify-content:space-between;align-items:center">
|
||||
<span>{{ editSiteId ? '编辑: '+editSiteId : '选择站点编辑' }}</span>
|
||||
</div>
|
||||
<div style="flex:0 0 400px;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>
|
||||
|
||||
<!-- 右侧:所有站点可视化 -->
|
||||
<div style="flex:1;overflow:auto;min-height:0">
|
||||
<el-table :data="sites" stripe v-loading="loading" border empty-text="暂无站点"
|
||||
style="font-size:13px" :row-style="{height:'44px'}" @row-click="editSite" highlight-current-row
|
||||
:row-class-name="rowClass">
|
||||
<el-table-column prop="siteId" label="站点ID" width="110" />
|
||||
<el-table-column label="名称" min-width="130">
|
||||
<template #default="s">{{ (s.row.names||[]).join(' / ') || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="地址" min-width="180" show-overflow-tooltip>
|
||||
<template #default="s">{{ (s.row.urls||[]).join(' / ') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="siteType" label="类型" width="100" />
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="s">
|
||||
<el-tag size="small" :type="s.row.status==='active'?'success':'info'">{{ s.row.status||'active' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="适配" width="90">
|
||||
<template #default="s">
|
||||
<el-tag v-if="s.row.latestVersion" size="small" type="primary">v{{ s.row.latestVersion }}</el-tag>
|
||||
<span v-else style="color:#909399">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="s">
|
||||
<el-button size="small" @click.stop="editSite(s.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click.stop="deleteSite(s.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="font-size:13px;font-weight:500;margin-bottom:8px">预览</div>
|
||||
<el-form label-width="70px" label-position="left" size="small">
|
||||
<el-form-item label="站点ID"><span style="font-weight:500">{{ preview.siteId||'-' }}</span></el-form-item>
|
||||
<el-form-item label="名称"><span>{{ (preview.names||[]).join(' / ') }}</span></el-form-item>
|
||||
<el-form-item label="地址"><span>{{ (preview.urls||[]).join(' / ') }}</span></el-form-item>
|
||||
<el-form-item label="类型"><el-tag size="small">{{ preview.siteType }}</el-tag></el-form-item>
|
||||
<el-form-item label="状态"><el-tag size="small" :type="preview.status==='active'?'success':'info'">{{ preview.status }}</el-tag></el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,56 +76,37 @@ import api from '../api'
|
||||
|
||||
const loading = ref(false), saving = ref(false), pendingCount = ref(0)
|
||||
const sites = ref([]), editSiteId = ref(''), editorJson = ref('{}')
|
||||
|
||||
const rowClass = ({row}) => row.siteId === editSiteId.value ? 'current-row' : ''
|
||||
const preview = computed(() => { try { return JSON.parse(editorJson.value) } catch { return {} } })
|
||||
|
||||
const refresh = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const r = await api.raw.get('/admin/sites')
|
||||
sites.value = Array.isArray(r?.data) ? r.data : r || []
|
||||
try { pendingCount.value = ((await api.sites.pendingChanges()).data || []).length } catch { pendingCount.value = 0 }
|
||||
const r = await api.raw.get('/admin/sites'); sites.value = r?.data || r || []
|
||||
try { pendingCount.value = ((await api.sites.pendingChanges()).data||[]).length } catch { pendingCount.value=0 }
|
||||
} catch { ElMessage.error('加载失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const editSite = (row) => {
|
||||
editSiteId.value = row.siteId
|
||||
editorJson.value = JSON.stringify({siteId: row.siteId, names: row.names, urls: row.urls, siteType: row.siteType, status: row.status||'active'}, null, 2)
|
||||
editorJson.value = JSON.stringify({siteId:row.siteId,names:row.names,urls:row.urls,siteType:row.siteType,status:row.status||'active'}, null, 2)
|
||||
}
|
||||
|
||||
const newSite = () => {
|
||||
editSiteId.value = ''
|
||||
editSiteId.value = '(新建)'
|
||||
editorJson.value = JSON.stringify({siteId:'',names:[''],urls:[''],siteType:'nexusphp',status:'active'}, null, 2)
|
||||
}
|
||||
|
||||
const deleteSite = async (row) => {
|
||||
try { await ElMessageBox.confirm('确认删除 '+row.siteId+' ?','删除',{type:'warning'}) } catch { return }
|
||||
try {
|
||||
await api.raw.put('/admin/sites/'+row.siteId, {siteId:row.siteId,names:row.names,urls:row.urls,siteType:row.siteType,status:'deprecated'})
|
||||
ElMessage.success('已弃用')
|
||||
refresh()
|
||||
if (editSiteId.value === row.siteId) editSiteId.value = ''
|
||||
} catch { ElMessage.error('操作失败') }
|
||||
}
|
||||
|
||||
const saveSite = async () => {
|
||||
saving.value = true
|
||||
try {
|
||||
const site = JSON.parse(editorJson.value)
|
||||
if (!site.siteId) { ElMessage.warning('siteId不能为空'); return }
|
||||
await api.raw.put('/admin/sites/'+site.siteId, {
|
||||
names: site.names||[], urls: site.urls||[], siteType: site.siteType||'unknown', status: site.status||'active',
|
||||
})
|
||||
ElMessage.success('已保存')
|
||||
refresh()
|
||||
const s = JSON.parse(editorJson.value)
|
||||
if (!s.siteId) { ElMessage.warning('siteId不能为空'); return }
|
||||
await api.raw.put('/admin/sites/'+s.siteId, {names:s.names||[],urls:s.urls||[],siteType:s.siteType||'unknown',status:s.status||'active'})
|
||||
ElMessage.success('已保存'); editSiteId.value = ''; refresh()
|
||||
} catch { ElMessage.error('保存失败') }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
|
||||
onMounted(refresh)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.current-row) { background-color: #ecf5ff !important }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user