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

209 lines
11 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="margin-bottom:12px;padding:10px 12px;border:1px solid #e6a23c;border-radius:4px;background:#fdf6ec">
<div style="font-weight:500;margin-bottom:6px;font-size:13px">认证方式</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<el-select v-model="authType" size="small" @change="syncAuth" style="width:120px">
<el-option label="无" value="NONE"/>
<el-option label="Cookie" value="COOKIE"/>
<el-option label="Token" value="TOKEN"/>
<el-option label="Basic" value="BASIC"/>
<el-option label="API Key" value="API_KEY"/>
</el-select>
<el-input v-if="authType!=='NONE'" v-model="authValue" size="small" placeholder="cookie/token值或占位符" @input="syncAuth" style="flex:1;max-width:300px"/>
</div>
</div>
<!-- 页面入口 -->
<div style="font-size:13px;font-weight:500;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center">
<span>页面入口</span>
<el-button size="small" @click="addEntry">+ 添加</el-button>
</div>
<div v-if="!parsed.length" style="text-align:center;padding:40px;color:#909399">暂无,点击添加</div>
<div v-for="(entry, idx) in parsed" :key="idx" style="margin-bottom:12px;padding:12px;border:1px solid #ebeef5;border-radius:4px;background:#fafafa">
<div style="display:flex;gap:8px;align-items:center;margin-bottom:8px">
<el-select v-model="entry.key" size="small" @change="onKeyChange(entry)" filterable style="width:140px" placeholder="选择操作">
<el-option v-for="k in pageKeys" :key="k.key" :label="k.key+' - '+k.label" :value="k.key" />
</el-select>
<el-select v-model="entry.method" size="small" @change="syncToJson" style="width:80px">
<el-option label="GET" value="GET"/><el-option label="POST" value="POST"/>
</el-select>
<div style="flex:1"></div>
<el-button size="small" type="danger" text @click="remove(idx)">删除</el-button>
</div>
<el-input v-model="entry.url" size="small" placeholder="URL,支持{base}{keyword}{page}{id}{passkey}" @input="syncToJson" style="margin-bottom:6px" />
<el-input v-model="entry.headers" size="small" placeholder="Headers: key:value,key:value(可选)" @input="syncToJson" style="margin-bottom:6px" />
<el-input v-if="entry.method==='POST'" v-model="entry.body" size="small" type="textarea" :rows="2" placeholder="POST Body(可选)" @input="syncToJson" />
</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 authType = ref('NONE'), authValue = 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).entries || [] } catch { return [] } })
const syncAuth = () => {
const obj = (() => { try { return JSON.parse(editorJson.value) } catch { return {} } })()
obj.authType = authType.value
if (authType.value !== 'NONE') obj.authValue = authValue.value
else delete obj.authValue
obj.entries = parsed.value
editorJson.value = JSON.stringify(obj, null, 2)
}
const syncToJson = () => {
const obj = { authType: authType.value, entries: parsed.value }
if (authType.value !== 'NONE') obj.authValue = authValue.value
editorJson.value = JSON.stringify(obj, null, 2)
}
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)
const data = r?.data || r
authType.value = data?.authType || 'NONE'
authValue.value = data?.authValue || ''
editorJson.value = JSON.stringify({ authType: authType.value, authValue: authValue.value || undefined, entries: data?.entries || [] }, null, 2)
} catch { editorJson.value = JSON.stringify({ authType:'NONE', entries:[] }, null, 2) }
}
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:'',method:'GET',url:'{base}'}); 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, authType: authType.value, authValue: authType.value!=='NONE'?authValue.value:null })
ElMessage.success('已保存'); editSiteId.value=''; fetchAll()
} catch { ElMessage.error('保存失败') }
finally { saving.value = false }
}
onMounted(() => { fetchAll(); loadPageKeys() })
</script>