初始化1

This commit is contained in:
zg
2026-07-16 13:03:11 +08:00
parent 0bdfcbc1c8
commit e6b0f287cc
100 changed files with 12184 additions and 0 deletions

View 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>