Files
par/web/src/views/SitePages.vue
2026-07-06 01:17:56 +08:00

178 lines
8.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- 列表模式 -->
<div v-if="!editSiteId">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<h2 style="margin:0">页面配置</h2>
<el-button size="small" @click="fetchAll" :icon="Refresh">刷新</el-button>
</div>
<!-- 默认模板 -->
<el-card style="margin-bottom:12px">
<template #header><span style="font-weight:500">站点类型默认模板</span></template>
<div style="display:flex;gap:12px;flex-wrap:wrap">
<div v-for="dt in defaultTypes" :key="dt" style="border:1px solid #ebeef5;border-radius:4px;padding:8px 12px;min-width:140px">
<div style="display:flex;align-items:center;gap:8px;justify-content:space-between">
<span><el-tag size="small">{{ dt }}</el-tag></span>
<el-button size="small" @click="editPage({siteId:'default:'+dt})">编辑</el-button>
</div>
</div>
</div>
</el-card>
<!-- 站点列表 -->
<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 v-if="isDefaultEditor" size="small" @click="uploadPages" :loading="uploading">上传</el-button>
<el-button size="small" type="danger" @click="deletePages" :loading="deleting">删除</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 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">
<div style="font-size:13px;font-weight:500;margin-bottom:4px">双向可视化</div>
<el-table :data="parsed" stripe border style="font-size:13px" :row-style="{height:'42px'}" v-if="parsed.length">
<el-table-column prop="key" label="key" width="120">
<template #default="s">
<el-select v-model="s.row.key" size="small" @change="onKeyChange(s.row)" filterable>
<el-option v-for="k in pageKeys" :key="k.key" :label="k.key+' - '+k.label" :value="k.key" />
</el-select>
</template>
</el-table-column>
<el-table-column prop="method" label="方法" width="70">
<template #default="s"><el-select v-model="s.row.method" size="small" @change="syncToJson"><el-option label="GET" value="GET"/><el-option label="POST" value="POST"/></el-select></template>
</el-table-column>
<el-table-column label="URL" min-width="180">
<template #default="s"><el-input v-model="s.row.url" size="small" @input="syncToJson"/></template>
</el-table-column>
<el-table-column label="Headers" width="130">
<template #default="s"><el-input v-model="s.row.headers" size="small" @input="syncToJson" placeholder="k:v,k:v"/></template>
</el-table-column>
<el-table-column width="50">
<template #default="s"><el-button size="small" type="danger" text @click="remove(s.$index)">×</el-button></template>
</el-table-column>
</el-table>
<div v-else style="text-align:center;padding:40px;color:#909399">暂无页面入口</div>
<div style="margin-top:8px"><el-button size="small" @click="addEntry">+ 添加</el-button></div>
</div>
</div>
</div>
</template>
<script setup>
import api from '../api'; import { Refresh } from '@element-plus/icons-vue'
const loading = ref(false), saving = ref(false), uploading = ref(false), deleting = ref(false), sites = ref([]), pagesMap = ref({}), sitesList = ref([])
const editSiteId = ref(''), editorJson = ref('[]')
const defaultTypes = ['nexusphp','mtorrent','gazelle','unit3d']
const isDefaultEditor = computed(() => editSiteId.value.startsWith('default:'))
const pageKeys = ref([])
const loadPageKeys = async () => {
try { const r = await api.raw.get('/admin/page-keys'); pageKeys.value = r?.data || r || [] }
catch {}
}
const onKeyChange = (row) => {
const pk = pageKeys.value.find(k => k.key === row.key)
if (pk?.defaultMethod) row.method = pk.defaultMethod
syncToJson()
}
const fmt = (t) => t ? (t.substring(0,19)||'').replace('T',' ') : '-'
const parsed = computed(() => { try { return JSON.parse(editorJson.value) } catch { return [] } })
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||[]) : []
const map = {}
for (const p of pages) {
const entries = (() => { try { return JSON.parse(p.entriesJson||'[]') } catch {return []} })()
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 editPage = async (row) => {
editSiteId.value = row.siteId
const url = row.siteId.startsWith('default:')
? '/admin/default-pages/' + row.siteId.replace('default:','')
: '/admin/site-pages/' + row.siteId
try { const r = await api.raw.get(url); editorJson.value = (r?.data||r)?.entries ? JSON.stringify((r.data||r).entries, null, 2) : '[]' }
catch { editorJson.value = '[]' }
}
const syncToJson = () => { editorJson.value = JSON.stringify(parsed.value, null, 2) }
const remove = (idx) => { parsed.value.splice(idx,1); syncToJson() }
const addEntry = () => { parsed.value.push({key:'',url:'{base}',method:'GET'}); syncToJson() }
const deletePages = async () => {
try { await ElMessageBox.confirm('删除后站点将使用默认模板','确认',{type:'warning',confirmButtonText:'确认删除'}) } catch { return }
deleting.value = true
try { await api.raw.delete('/admin/site-pages/'+editSiteId.value); ElMessage.success('已删除'); editSiteId.value=''; fetchAll() }
catch { ElMessage.error('删除失败') }
finally { deleting.value = false }
}
const uploadPages = async () => {
uploading.value = true
try {
const type = editSiteId.value.replace('default:','')
await api.raw.put('/admin/default-pages/'+type+'/upload', { entries: parsed.value })
ElMessage.success('已上传到云存储')
} catch { ElMessage.error('上传失败') }
finally { uploading.value = false }
}
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: parsed.value })
ElMessage.success('已保存'); editSiteId.value=''; fetchAll()
} catch { ElMessage.error('保存失败') }
finally { saving.value = false }
}
onMounted(() => { fetchAll(); loadPageKeys() })
</script>