docs+feat: record backlog tip and move custom protocols to More
Document delivered IA and backlog four items; put custom plugins under the wizard More group per design.
This commit is contained in:
1 parent
9984e21930
commit
76bfda8627
8 files changed
+50
-10
No files matched your search
@@ -182,6 +182,9 @@ func (s *Server) exportConnectionLogs(c *gin.Context) {
|
|||||||
q = q.Where("tunnel_id = ?", tid)
|
q = q.Where("tunnel_id = ?", tid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if v := c.Query("keyword"); v != "" {
|
||||||
|
q = q.Where("(detail LIKE ? OR ip_address LIKE ?)", "%"+v+"%", "%"+v+"%")
|
||||||
|
}
|
||||||
if v := c.Query("start"); v != "" {
|
if v := c.Query("start"); v != "" {
|
||||||
if t, err := time.Parse("2006-01-02", v); err == nil {
|
if t, err := time.Parse("2006-01-02", v); err == nil {
|
||||||
q = q.Where("created_at >= ?", t)
|
q = q.Where("created_at >= ?", t)
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# BACKLOG
|
||||||
|
|
||||||
|
## 已收
|
||||||
|
|
||||||
|
- **控制台 IA**(tip `f01d3dc`):对接与扩展分组(IPSec → 飞书/OIDC → Plugins → Nodes);Logs 去掉连接审计入口;Sessions `?tab=` 深链;资源侧仅拨号/设备/管理员。
|
||||||
|
- **本轮四项**(tip `9984e21`):
|
||||||
|
1. Logs 保留策略去掉 connection 条数统计。
|
||||||
|
2. 总览三卡深链:异常专线 → `/tunnels?filter=unhealthy`;在线会话 → `/sessions?tab=online`;待处理告警 → `/logs?tab=alert`。
|
||||||
|
3. Sessions 连接审计:`/connection-logs` 分页/筛选 + CSV 导出。
|
||||||
|
4. 创建专线向导:协议按场景分组(员工接入 / 站点对接 / 穿透)。
|
||||||
|
|
||||||
|
## 明确不做 / 另立
|
||||||
|
|
||||||
|
- **客户端系统直连 VPN**(本批与近期均不做)。
|
||||||
@@ -365,7 +365,8 @@
|
|||||||
"installFailed": "Failed to install {protocol}",
|
"installFailed": "Failed to install {protocol}",
|
||||||
"groupEmployee": "Employee access",
|
"groupEmployee": "Employee access",
|
||||||
"groupSite": "Site interconnect",
|
"groupSite": "Site interconnect",
|
||||||
"groupPenetrate": "Intranet penetration"
|
"groupPenetrate": "Intranet penetration",
|
||||||
|
"groupMore": "More"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"title": "Admins",
|
"title": "Admins",
|
||||||
|
|||||||
@@ -365,7 +365,8 @@
|
|||||||
"installFailed": "{protocol} 安装失败",
|
"installFailed": "{protocol} 安装失败",
|
||||||
"groupEmployee": "员工接入",
|
"groupEmployee": "员工接入",
|
||||||
"groupSite": "站点对接",
|
"groupSite": "站点对接",
|
||||||
"groupPenetrate": "内网穿透"
|
"groupPenetrate": "内网穿透",
|
||||||
|
"groupMore": "更多"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"title": "管理员",
|
"title": "管理员",
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ watch(() => route.query.tab, (name) => {
|
|||||||
router.replace({ path: '/sessions', query: { tab: 'audit' } })
|
router.replace({ path: '/sessions', query: { tab: 'audit' } })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const next = TAB_NAMES.includes(name) ? name : 'audit'
|
const next = normalizeLogTab(name)
|
||||||
if (activeTab.value !== next) activeTab.value = next
|
if (activeTab.value !== next) activeTab.value = next
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
<div class="row-between">
|
<div class="row-between">
|
||||||
<div class="filter-bar">
|
<div class="filter-bar">
|
||||||
<el-input v-model="auditFilters.username" :placeholder="$t('common.username')" clearable size="small" style="width:120px" @clear="searchAudit" @keyup.enter="searchAudit" />
|
<el-input v-model="auditFilters.username" :placeholder="$t('common.username')" clearable size="small" style="width:120px" @clear="searchAudit" @keyup.enter="searchAudit" />
|
||||||
|
<el-input v-model="auditFilters.keyword" :placeholder="$t('logs.keywordPlaceholder')" clearable size="small" style="width:160px" @clear="searchAudit" @keyup.enter="searchAudit" />
|
||||||
<el-select v-model="auditFilters.event" :placeholder="$t('logs.eventType')" clearable multiple size="small" style="width:220px" @change="searchAudit">
|
<el-select v-model="auditFilters.event" :placeholder="$t('logs.eventType')" clearable multiple size="small" style="width:220px" @change="searchAudit">
|
||||||
<el-option :label="$t('sessions.eventConnect')" value="connect" />
|
<el-option :label="$t('sessions.eventConnect')" value="connect" />
|
||||||
<el-option :label="$t('sessions.eventDisconnect')" value="disconnect" />
|
<el-option :label="$t('sessions.eventDisconnect')" value="disconnect" />
|
||||||
@@ -146,7 +147,7 @@ const auditExporting = ref(false)
|
|||||||
const auditPage = ref(1)
|
const auditPage = ref(1)
|
||||||
const auditPageSize = ref(20)
|
const auditPageSize = ref(20)
|
||||||
const auditTotal = ref(0)
|
const auditTotal = ref(0)
|
||||||
const auditFilters = reactive({ username: '', event: [] })
|
const auditFilters = reactive({ username: '', keyword: '', event: [] })
|
||||||
const auditDateRange = ref(null)
|
const auditDateRange = ref(null)
|
||||||
|
|
||||||
watch(() => route.query.tab, (name) => {
|
watch(() => route.query.tab, (name) => {
|
||||||
@@ -209,7 +210,8 @@ async function loadAudit() {
|
|||||||
const params = {
|
const params = {
|
||||||
page: auditPage.value,
|
page: auditPage.value,
|
||||||
page_size: auditPageSize.value,
|
page_size: auditPageSize.value,
|
||||||
username: auditFilters.username,
|
username: auditFilters.username || undefined,
|
||||||
|
keyword: auditFilters.keyword || undefined,
|
||||||
}
|
}
|
||||||
if (auditFilters.event?.length) params.event = auditFilters.event.join(',')
|
if (auditFilters.event?.length) params.event = auditFilters.event.join(',')
|
||||||
if (auditDateRange.value) {
|
if (auditDateRange.value) {
|
||||||
@@ -227,7 +229,10 @@ async function loadAudit() {
|
|||||||
|
|
||||||
async function exportAudit() {
|
async function exportAudit() {
|
||||||
auditExporting.value = true
|
auditExporting.value = true
|
||||||
const params = { username: auditFilters.username }
|
const params = {
|
||||||
|
username: auditFilters.username || undefined,
|
||||||
|
keyword: auditFilters.keyword || undefined,
|
||||||
|
}
|
||||||
if (auditFilters.event?.length) params.event = auditFilters.event.join(',')
|
if (auditFilters.event?.length) params.event = auditFilters.event.join(',')
|
||||||
if (auditDateRange.value) {
|
if (auditDateRange.value) {
|
||||||
params.start = auditDateRange.value[0]
|
params.start = auditDateRange.value[0]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<!-- 第一步:协议选择 -->
|
<!-- 第一步:协议选择 -->
|
||||||
<el-alert v-if="step === 0" type="info" :closable="false" :title="$t('tunnelWizard.onDemandHint')" style="margin-bottom:16px" />
|
<el-alert v-if="step === 0" type="info" :closable="false" :title="$t('tunnelWizard.onDemandHint')" style="margin-bottom:16px" />
|
||||||
<div v-if="step === 0" class="proto-groups">
|
<div v-if="step === 0" class="proto-groups">
|
||||||
<div v-for="group in protocolGroups" :key="group.id" class="proto-group">
|
<div v-for="group in protocolGroups.filter(g => g.keys.some(k => builtin[k]) || (g.id === 'more' && customProtocols.length))" :key="group.id" class="proto-group">
|
||||||
<div class="proto-group-title">{{ $t(group.titleKey) }}</div>
|
<div class="proto-group-title">{{ $t(group.titleKey) }}</div>
|
||||||
<div class="proto-grid">
|
<div class="proto-grid">
|
||||||
<div v-for="key in group.keys.filter(k => builtin[k])" :key="key"
|
<div v-for="key in group.keys.filter(k => builtin[k])" :key="key"
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="group.id === 'penetrate'">
|
<template v-if="group.id === 'more'">
|
||||||
<div v-for="p in customProtocols" :key="p.name" class="proto-card" :class="{ active: form.protocol === p.name }"
|
<div v-for="p in customProtocols" :key="p.name" class="proto-card" :class="{ active: form.protocol === p.name }"
|
||||||
@click="selectProtocol(p.name)">
|
@click="selectProtocol(p.name)">
|
||||||
<div class="proto-icon"><ProtocolIcon protocol="custom" :size="36" /></div>
|
<div class="proto-icon"><ProtocolIcon protocol="custom" :size="36" /></div>
|
||||||
@@ -177,6 +177,7 @@ const protocolGroups = [
|
|||||||
{ id: 'employee', titleKey: 'tunnelWizard.groupEmployee', keys: ['wireguard', 'ikev2', 'l2tp', 'openvpn'] },
|
{ id: 'employee', titleKey: 'tunnelWizard.groupEmployee', keys: ['wireguard', 'ikev2', 'l2tp', 'openvpn'] },
|
||||||
{ id: 'site', titleKey: 'tunnelWizard.groupSite', keys: ['ipsec', 'gre'] },
|
{ id: 'site', titleKey: 'tunnelWizard.groupSite', keys: ['ipsec', 'gre'] },
|
||||||
{ id: 'penetrate', titleKey: 'tunnelWizard.groupPenetrate', keys: ['frp'] },
|
{ id: 'penetrate', titleKey: 'tunnelWizard.groupPenetrate', keys: ['frp'] },
|
||||||
|
{ id: 'more', titleKey: 'tunnelWizard.groupMore', keys: [] },
|
||||||
]
|
]
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|||||||
@@ -118,7 +118,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { Plus, InfoFilled, ArrowDown, Refresh } from '@element-plus/icons-vue'
|
import { Plus, InfoFilled, ArrowDown, Refresh } from '@element-plus/icons-vue'
|
||||||
@@ -141,7 +141,14 @@ const changingIds = ref(new Set())
|
|||||||
let loadRevision = 0
|
let loadRevision = 0
|
||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
const protocolFilter = ref('')
|
const protocolFilter = ref('')
|
||||||
const stateFilter = ref(typeof route.query.state === 'string' ? route.query.state : '')
|
function initialStateFilter() {
|
||||||
|
const f = route.query.filter
|
||||||
|
const s = route.query.state
|
||||||
|
if (f === 'unhealthy') return 'error'
|
||||||
|
if (typeof s === 'string' && s) return s
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const stateFilter = ref(initialStateFilter())
|
||||||
const protocolOptions = ref([])
|
const protocolOptions = ref([])
|
||||||
// 批量操作
|
// 批量操作
|
||||||
const selectedRows = ref([])
|
const selectedRows = ref([])
|
||||||
@@ -268,6 +275,14 @@ function download(row) {
|
|||||||
})
|
})
|
||||||
.catch(e => ElMessage.error(e.message || t('tunnels.downloadFailed')))
|
.catch(e => ElMessage.error(e.message || t('tunnels.downloadFailed')))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(() => [route.query.filter, route.query.state], () => {
|
||||||
|
const next = initialStateFilter()
|
||||||
|
if (stateFilter.value !== next) {
|
||||||
|
stateFilter.value = next
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
})
|
||||||
let timer
|
let timer
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
load()
|
load()
|
||||||
|
|||||||
Reference in new issue
Block a user