Initial commit
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<div class="viewer-wrap">
|
||||
<!-- Toolbar -->
|
||||
<div class="viewer-bar">
|
||||
<el-button size="small" text @click="router.back()">
|
||||
<el-icon><ArrowLeft /></el-icon> 返回
|
||||
</el-button>
|
||||
<div class="viewer-title">
|
||||
<el-icon><component :is="pageIcon" /></el-icon>
|
||||
<span>{{ page?.name || '...' }}</span>
|
||||
<el-tag v-if="page?.use_proxy" size="small" type="warning" style="margin-left:4px">代理</el-tag>
|
||||
</div>
|
||||
<div class="viewer-url">{{ page?.url }}</div>
|
||||
<div class="viewer-actions">
|
||||
<el-tooltip content="在新标签页打开">
|
||||
<el-button size="small" text @click="openExternal">
|
||||
<el-icon><TopRight /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="刷新">
|
||||
<el-button size="small" text @click="reload">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading overlay -->
|
||||
<div v-if="iframeLoading" class="iframe-loading">
|
||||
<el-icon class="spin" :size="32"><Loading /></el-icon>
|
||||
<p>正在加载页面…</p>
|
||||
</div>
|
||||
|
||||
<!-- Error state (removed to let browser natively show iframe state) -->
|
||||
|
||||
<iframe
|
||||
v-show="!iframeLoading"
|
||||
ref="iframeRef"
|
||||
:src="iframeSrc"
|
||||
class="page-iframe"
|
||||
@load="onLoad"
|
||||
@error="onError"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch, markRaw } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
ArrowLeft, TopRight, Refresh, Loading, WarningFilled,
|
||||
Link as LinkIcon, Monitor, SetUp, Reading, Cpu, DataLine,
|
||||
Grid, Connection, Ticket, Wallet, Key, Tools, Star, House,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { customPagesApi, type CustomPageData } from '@/api'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const iconMap: Record<string, any> = {
|
||||
Link: markRaw(LinkIcon), Monitor: markRaw(Monitor), SetUp: markRaw(SetUp),
|
||||
Reading: markRaw(Reading), Cpu: markRaw(Cpu), DataLine: markRaw(DataLine),
|
||||
Grid: markRaw(Grid), Connection: markRaw(Connection), Ticket: markRaw(Ticket),
|
||||
Wallet: markRaw(Wallet), Key: markRaw(Key), Tools: markRaw(Tools),
|
||||
Star: markRaw(Star), House: markRaw(House),
|
||||
}
|
||||
|
||||
const page = ref<CustomPageData | null>(null)
|
||||
const iframeRef = ref<HTMLIFrameElement>()
|
||||
const iframeLoading = ref(true)
|
||||
const loadError = ref(false)
|
||||
|
||||
const pageIcon = computed(() => iconMap[page.value?.icon || 'Link'] || LinkIcon)
|
||||
|
||||
// Build the iframe src: use backend proxy if use_proxy=true
|
||||
// Pass JWT token as query param since iframe can't set Authorization header
|
||||
const iframeSrc = computed(() => {
|
||||
if (!page.value) return ''
|
||||
if (page.value.use_proxy) {
|
||||
const encoded = encodeURIComponent(page.value.url)
|
||||
const token = encodeURIComponent(auth.token || '')
|
||||
return `/api/custom-pages/frame-proxy?url=${encoded}&token=${token}`
|
||||
}
|
||||
return page.value.url
|
||||
})
|
||||
|
||||
async function loadPage(id: number) {
|
||||
iframeLoading.value = true
|
||||
loadError.value = false
|
||||
try {
|
||||
const res = await customPagesApi.list()
|
||||
page.value = res.data.find(p => p.id === id) || null
|
||||
if (!page.value) {
|
||||
ElMessage.error('页面不存在')
|
||||
router.push('/custom-pages')
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('加载失败')
|
||||
}
|
||||
}
|
||||
|
||||
function onLoad() {
|
||||
iframeLoading.value = false
|
||||
}
|
||||
|
||||
function onError() {
|
||||
iframeLoading.value = false
|
||||
}
|
||||
|
||||
function openExternal() {
|
||||
if (page.value?.url) window.open(page.value.url, '_blank', 'noopener')
|
||||
}
|
||||
|
||||
function reload() {
|
||||
if (!iframeRef.value || !page.value) return
|
||||
iframeLoading.value = true
|
||||
loadError.value = false
|
||||
iframeRef.value.src = iframeSrc.value
|
||||
}
|
||||
|
||||
watch(() => route.params.id, (id) => {
|
||||
if (id) loadPage(Number(id))
|
||||
}, { immediate: false })
|
||||
|
||||
onMounted(() => {
|
||||
loadPage(Number(route.params.id))
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.viewer-wrap {
|
||||
/* Expand to full viewport minus topbar, ignoring parent padding */
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
top: var(--topbar-height);
|
||||
left: var(--sidebar-width);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-base);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.viewer-bar {
|
||||
height: 44px;
|
||||
background: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.viewer-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.viewer-url {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-elevated);
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.viewer-actions { display: flex; gap: 4px; }
|
||||
|
||||
.page-iframe {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.iframe-loading, .iframe-error {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.error-icon { color: #f59e0b; }
|
||||
.error-title { font-size: 16px; font-weight: 600; color: var(--text-primary); }
|
||||
.error-sub { font-size: 13px; color: var(--text-muted); margin-bottom: 8px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user