初始化1
This commit is contained in:
400
nginx-manager-frontend/src/views/NginxForwarding.vue
Normal file
400
nginx-manager-frontend/src/views/NginxForwarding.vue
Normal file
@@ -0,0 +1,400 @@
|
||||
<template>
|
||||
<div class="nginx-forwarding-container">
|
||||
<!-- 顶部标题栏 -->
|
||||
<div class="page-header">
|
||||
<h1>Nginx转发管理</h1>
|
||||
<p class="subtitle">管理 Nginx 反向代理规则与 HTTPS 证书</p>
|
||||
</div>
|
||||
|
||||
<!-- 操作区域 -->
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-tag type="warning" size="large">
|
||||
<el-icon style="margin-right: 4px"><InfoFilled /></el-icon>
|
||||
新增/修改/删除请在宝塔面板上操作
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索域名..."
|
||||
clearable
|
||||
style="width: 240px"
|
||||
:prefix-icon="Search"
|
||||
/>
|
||||
<el-select v-model="protocolFilter" placeholder="协议筛选" clearable style="width: 120px; margin-left: 10px">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="HTTP" value="http" />
|
||||
<el-option label="HTTPS" value="https" />
|
||||
</el-select>
|
||||
<el-button @click="fetchRules" :loading="loading" style="margin-left: 10px">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 桌面端:表格视图 -->
|
||||
<div v-if="!isMobile" class="table-container">
|
||||
<el-table
|
||||
:data="filteredRules"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
border
|
||||
style="width: 100%"
|
||||
empty-text="暂无转发规则,点击 [新增规则] 添加"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="domain" label="域名" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<span class="domain-text">{{ row.domain }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="protocol" label="协议类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.protocol === 'https' ? 'success' : 'info'" effect="light">
|
||||
{{ row.protocol.toUpperCase() }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="target" label="转发目标地址" min-width="280">
|
||||
<template #default="{ row }">
|
||||
<code class="target-address">{{ row.target }}</code>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sslCert" label="SSL证书" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.sslCert">
|
||||
<el-tooltip :content="row.sslCert" placement="top">
|
||||
<el-tag type="warning" effect="plain" size="small">已配置</el-tag>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span style="color: #c0c4cc">-</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdBy" label="创建人" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.createdBy || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ formatDate(row.createdAt) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="isSystemDomain(row.domain)">
|
||||
<el-tag type="info" size="small">系统域名</el-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button type="primary" link size="small" @click="showDetailDialog(row)">
|
||||
<el-icon><View /></el-icon> 查看
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 移动端:卡片视图 -->
|
||||
<div v-if="isMobile" class="mobile-card-list" v-loading="loading">
|
||||
<div v-if="filteredRules.length === 0" class="mobile-empty">暂无转发规则</div>
|
||||
<div v-for="row in filteredRules" :key="row.id" class="mobile-card">
|
||||
<div class="card-top">
|
||||
<el-tag :type="row.protocol === 'https' ? 'success' : 'info'" size="small" effect="dark">
|
||||
{{ row.protocol.toUpperCase() }}
|
||||
</el-tag>
|
||||
<span class="card-domain">{{ row.domain }}</span>
|
||||
<el-tag v-if="row.sslCert" type="warning" size="small" effect="plain">SSL</el-tag>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-row">
|
||||
<span class="card-label">转发目标</span>
|
||||
<code class="card-value">{{ row.target }}</code>
|
||||
</div>
|
||||
<div class="card-row">
|
||||
<span class="card-label">创建人</span>
|
||||
<span style="font-size:13px;color:#606266;">{{ row.createdBy || '-' }}</span>
|
||||
</div>
|
||||
<div class="card-row">
|
||||
<span class="card-label">创建时间</span>
|
||||
<span style="font-size:12px;color:#909399;">{{ formatDate(row.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<template v-if="!isSystemDomain(row.domain)">
|
||||
<el-button type="primary" size="small" @click="showDetailDialog(row)">
|
||||
<el-icon><View /></el-icon> 查看详情
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详情查看对话框(只读) -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="'转发规则详情 — ' + detailData.domain"
|
||||
width="900px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-loading="detailLoading" style="min-height: 300px">
|
||||
<!-- 基本信息 -->
|
||||
<el-descriptions :column="2" border size="default" label-width="100px" style="margin-bottom: 16px">
|
||||
<el-descriptions-item label="域名">
|
||||
<span class="detail-domain">{{ detailData.domain }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="协议类型">
|
||||
<el-tag :type="detailData.protocol === 'https' ? 'success' : 'info'" effect="light">
|
||||
{{ detailData.protocol?.toUpperCase() }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="转发目标" :span="2">
|
||||
<code class="target-address">{{ detailData.target }}</code>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- SSL证书信息 -->
|
||||
<template v-if="detailData.protocol === 'https' || detailData.sslStatus">
|
||||
<el-divider content-position="left">
|
||||
<el-icon><Lock /></el-icon> SSL 证书
|
||||
</el-divider>
|
||||
<el-descriptions :column="2" border size="small" style="margin-bottom: 16px">
|
||||
<el-descriptions-item label="证书状态">
|
||||
<el-tag v-if="detailData.sslStatus === 'valid'" type="success" size="small">有效</el-tag>
|
||||
<el-tag v-else-if="detailData.sslStatus === 'none'" type="info" size="small">未配置</el-tag>
|
||||
<el-tag v-else-if="detailData.sslStatus === 'error'" type="danger" size="small">获取失败</el-tag>
|
||||
<el-tag v-else type="warning" size="small">{{ detailData.sslStatus || '未知' }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="到期时间">
|
||||
<span v-if="detailData.sslExpireDate">{{ formatDate(detailData.sslExpireDate) }}</span>
|
||||
<span v-else style="color:#C0C4CC">-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="剩余天数" v-if="detailData.sslDaysLeft !== undefined">
|
||||
<span :style="{ color: detailData.sslDaysLeft <= 15 ? '#F56C6C' : detailData.sslDaysLeft <= 30 ? '#E6A23C' : '#67C23A', fontWeight: 'bold' }">
|
||||
{{ detailData.sslDaysLeft }} 天
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="验证方式" v-if="detailData.sslAuthType">
|
||||
<el-tag size="small" effect="plain">{{ detailData.sslAuthType?.toUpperCase() }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
|
||||
<!-- Nginx 配置文件内容 -->
|
||||
<el-divider content-position="left">
|
||||
<el-icon><Document /></el-icon> 配置文件
|
||||
</el-divider>
|
||||
<el-input
|
||||
:model-value="detailData.configContent || '加载中...'"
|
||||
type="textarea"
|
||||
:rows="16"
|
||||
readonly
|
||||
style="font-family: 'Consolas', 'Courier New', monospace; font-size: 13px;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Refresh, View, InfoFilled, Lock, Document } from '@element-plus/icons-vue'
|
||||
import { getRules, getRuleConfig } from '../api/nginxApi'
|
||||
import { useMobile } from '../composables/useMobile'
|
||||
|
||||
const { isMobile } = useMobile()
|
||||
|
||||
// ─── 状态 ───────────────────────────────────
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
|
||||
const rules = ref([])
|
||||
const searchKeyword = ref('')
|
||||
const protocolFilter = ref('')
|
||||
|
||||
const detailData = reactive({
|
||||
domain: '',
|
||||
protocol: '',
|
||||
target: '',
|
||||
configContent: '',
|
||||
sslStatus: '',
|
||||
sslExpireDate: '',
|
||||
sslDaysLeft: null,
|
||||
sslAuthType: ''
|
||||
})
|
||||
|
||||
// ─── 域名判断 ────────────────────────────────
|
||||
const SYSTEM_DOMAINS = ['zgitm.com', 'www.zgitm.com']
|
||||
function isSystemDomain(domain) {
|
||||
return SYSTEM_DOMAINS.includes(domain?.toLowerCase())
|
||||
}
|
||||
|
||||
/** 格式化日期为本地字符串 */
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '-'
|
||||
try {
|
||||
// 处理 ISO 格式或时间戳
|
||||
const d = new Date(dateStr)
|
||||
if (isNaN(d.getTime())) return dateStr
|
||||
return d.toLocaleString('zh-CN', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
})
|
||||
} catch {
|
||||
return dateStr || '-'
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 计算属性 ────────────────────────────────
|
||||
const filteredRules = computed(() => {
|
||||
let result = rules.value
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
result = result.filter(r => r.domain.toLowerCase().includes(keyword))
|
||||
}
|
||||
if (protocolFilter.value) {
|
||||
result = result.filter(r => r.protocol === protocolFilter.value)
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
// ─── 方法 ───────────────────────────────────
|
||||
|
||||
/** 获取规则列表 */
|
||||
async function fetchRules() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getRules()
|
||||
rules.value = res.data || []
|
||||
} catch (e) {
|
||||
ElMessage.error('获取规则列表失败:' + e.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 显示详情对话框 — 调用后端获取配置文件 + SSL信息 */
|
||||
async function showDetailDialog(row) {
|
||||
dialogVisible.value = true
|
||||
detailLoading.value = true
|
||||
|
||||
// 先填充基本信息
|
||||
detailData.domain = row.domain
|
||||
detailData.protocol = row.protocol
|
||||
detailData.target = row.target
|
||||
detailData.configContent = ''
|
||||
detailData.sslStatus = ''
|
||||
detailData.sslExpireDate = ''
|
||||
detailData.sslDaysLeft = null
|
||||
detailData.sslAuthType = ''
|
||||
|
||||
try {
|
||||
const res = await getRuleConfig(row.id)
|
||||
const data = res.data?.data || res.data
|
||||
if (data) {
|
||||
detailData.configContent = data.configContent || ''
|
||||
detailData.sslStatus = data.sslStatus || ''
|
||||
detailData.sslExpireDate = data.sslExpireDate || ''
|
||||
detailData.sslDaysLeft = data.sslDaysLeft
|
||||
detailData.sslAuthType = data.sslAuthType || ''
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.warning('获取详情失败:' + e.message)
|
||||
detailData.configContent = '获取配置文件失败'
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 生命周期 ────────────────────────────────
|
||||
onMounted(() => {
|
||||
fetchRules()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.nginx-forwarding-container { max-width: 1200px; margin: 0 auto; padding: 24px; }
|
||||
.page-header { margin-bottom: 20px; }
|
||||
.page-header h1 { margin: 0; font-size: 24px; color: #303133; }
|
||||
.page-header .subtitle { margin: 4px 0 0; color: #909399; font-size: 14px; }
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
.toolbar-left, .toolbar-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
|
||||
.table-container {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.domain-text { font-weight: 500; color: #303133; }
|
||||
.target-address { background: #f5f7fa; padding: 2px 8px; border-radius: 4px; font-size: 13px; color: #e6a23c; }
|
||||
|
||||
.detail-domain { font-size: 16px; font-weight: 600; color: #303133; }
|
||||
|
||||
/* ════════════════════════════════════════════════ */
|
||||
/* 移动端 (≤768px) */
|
||||
/* ════════════════════════════════════════════════ */
|
||||
@media (max-width: 768px) {
|
||||
.nginx-forwarding-container { padding: 8px; }
|
||||
.page-header h1 { font-size: 18px; }
|
||||
.page-header .subtitle { font-size: 12px; }
|
||||
|
||||
.toolbar { flex-direction: column; align-items: stretch; }
|
||||
.toolbar-left, .toolbar-right { width: 100%; }
|
||||
.toolbar-right :deep(.el-input) { width: 100% !important; }
|
||||
.toolbar-right :deep(.el-select) { width: 100% !important; margin-left: 0 !important; }
|
||||
|
||||
/* 隐藏桌面表格,改用卡片 */
|
||||
.table-container { display: none; }
|
||||
|
||||
/* 移动端卡片列表 */
|
||||
.mobile-card-list { display: flex; flex-direction: column; gap: 10px; }
|
||||
.mobile-empty { text-align: center; color: #C0C4CC; padding: 40px 0; font-size: 14px; }
|
||||
|
||||
.mobile-card {
|
||||
background: #fff; border-radius: 10px; padding: 14px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||
}
|
||||
.card-top {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding-bottom: 10px; border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.card-domain { font-size: 15px; font-weight: 600; color: #303133; flex:1; word-break: break-all; }
|
||||
.card-body { padding: 10px 0; }
|
||||
.card-row { display: flex; align-items: flex-start; margin-bottom: 6px; }
|
||||
.card-label { color: #909399; font-size: 12px; width: 56px; flex-shrink: 0; padding-top: 1px; }
|
||||
.card-value { font-size: 13px; color: #E6A23C; background: #fdf6ec; padding: 2px 6px; border-radius: 4px; word-break: break-all; }
|
||||
|
||||
.card-actions {
|
||||
display: flex; gap: 8px; padding-top: 10px;
|
||||
border-top: 1px solid #f0f0f0; justify-content: flex-end;
|
||||
}
|
||||
.card-actions .el-button { min-height: 36px; min-width: 64px; }
|
||||
|
||||
/* 对话框全屏 */
|
||||
:deep(.el-dialog) { width: 95% !important; margin: 2vh auto !important; }
|
||||
:deep(.el-dialog__body) { padding: 12px 8px; }
|
||||
:deep(.el-form-item__label) { width: 70px !important; font-size: 13px; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user