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:
Codex committed 2026-10-23 16:17:43 +08:00
1 parent 9984e21930
commit 76bfda8627
8 files changed
+50 -10

No files matched your search

+3
View File
@@ -182,6 +182,9 @@ func (s *Server) exportConnectionLogs(c *gin.Context) {
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 t, err := time.Parse("2006-01-02", v); err == nil {
q = q.Where("created_at >= ?", t)
+14
View File
@@ -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**(本批与近期均不做)。
+2 -1
View File
@@ -365,7 +365,8 @@
"installFailed": "Failed to install {protocol}",
"groupEmployee": "Employee access",
"groupSite": "Site interconnect",
"groupPenetrate": "Intranet penetration"
"groupPenetrate": "Intranet penetration",
"groupMore": "More"
},
"users": {
"title": "Admins",
+2 -1
View File
@@ -365,7 +365,8 @@
"installFailed": "{protocol} 安装失败",
"groupEmployee": "员工接入",
"groupSite": "站点对接",
"groupPenetrate": "内网穿透"
"groupPenetrate": "内网穿透",
"groupMore": "更多"
},
"users": {
"title": "管理员",
+1 -1
View File
@@ -123,7 +123,7 @@ watch(() => route.query.tab, (name) => {
router.replace({ path: '/sessions', query: { tab: 'audit' } })
return
}
const next = TAB_NAMES.includes(name) ? name : 'audit'
const next = normalizeLogTab(name)
if (activeTab.value !== next) activeTab.value = next
}, { immediate: true })
+8 -3
View File
@@ -67,6 +67,7 @@
<div class="row-between">
<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.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-option :label="$t('sessions.eventConnect')" value="connect" />
<el-option :label="$t('sessions.eventDisconnect')" value="disconnect" />
@@ -146,7 +147,7 @@ const auditExporting = ref(false)
const auditPage = ref(1)
const auditPageSize = ref(20)
const auditTotal = ref(0)
const auditFilters = reactive({ username: '', event: [] })
const auditFilters = reactive({ username: '', keyword: '', event: [] })
const auditDateRange = ref(null)
watch(() => route.query.tab, (name) => {
@@ -209,7 +210,8 @@ async function loadAudit() {
const params = {
page: auditPage.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 (auditDateRange.value) {
@@ -227,7 +229,10 @@ async function loadAudit() {
async function exportAudit() {
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 (auditDateRange.value) {
params.start = auditDateRange.value[0]
+3 -2
View File
@@ -9,7 +9,7 @@
<!-- 第一步:协议选择 -->
<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-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-grid">
<div v-for="key in group.keys.filter(k => builtin[k])" :key="key"
@@ -32,7 +32,7 @@
</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 }"
@click="selectProtocol(p.name)">
<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: 'site', titleKey: 'tunnelWizard.groupSite', keys: ['ipsec', 'gre'] },
{ id: 'penetrate', titleKey: 'tunnelWizard.groupPenetrate', keys: ['frp'] },
{ id: 'more', titleKey: 'tunnelWizard.groupMore', keys: [] },
]
const { t } = useI18n()
+17 -2
View File
@@ -118,7 +118,7 @@
</template>
<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 { useI18n } from 'vue-i18n'
import { Plus, InfoFilled, ArrowDown, Refresh } from '@element-plus/icons-vue'
@@ -141,7 +141,14 @@ const changingIds = ref(new Set())
let loadRevision = 0
const keyword = 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 selectedRows = ref([])
@@ -268,6 +275,14 @@ function download(row) {
})
.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
onMounted(async () => {
load()