Merge remote-tracking branch 'origin/main'

# Conflicts:
#	internal/httpapi/dist/assets/index-CL42Ur3_.css
#	internal/httpapi/dist/assets/index-CbqC9m-j.js
#	internal/httpapi/dist/assets/index-RFHYrCiX.css
#	internal/httpapi/dist/assets/index-jvouiCgh.css
#	internal/httpapi/dist/index.html
#	internal/store/store.go
#	web/src/api.ts
#	web/src/views/ClusterView.vue
This commit is contained in:
JianFeeeee
2026-08-24 23:30:19 +08:00
28 changed files with 1143 additions and 1084 deletions

View File

@ -62,13 +62,13 @@
<!-- Action toolbar -->
<div class="toolbar w4f-card">
<template v-if="!isMember">
<button class="w4f-btn" :disabled="busy" @click="onCreate">⊕ 创建集群</button>
<button class="w4f-btn ghost" :disabled="busy" @click="joinDialog.visible = true">↪ 加入集群…</button>
<button v-if="canWrite" class="w4f-btn" :disabled="busy" @click="onCreate">⊕ 创建集群</button>
<button v-if="canWrite" class="w4f-btn ghost" :disabled="busy" @click="joinDialog.visible = true">↪ 加入集群…</button>
<span class="act-hint" v-if="isStandalone && isLeader">本节点为初始 leader,已就绪;可直接创建,或输入对端地址加入既有集群</span>
<span class="act-hint" v-else-if="!selfInRing">本节点已脱离集群,可创建新集群或加入既有集群</span>
</template>
<template v-else>
<button class="w4f-btn danger" :disabled="busy" @click="onLeave">⏏ 退出集群</button>
<button v-if="canWrite" class="w4f-btn danger" :disabled="busy" @click="onLeave">⏏ 退出集群</button>
<span class="act-hint">退出后本节点仅保留 localOnly 转发;可重新创建/加入</span>
</template>
<button class="w4f-btn ghost small" style="margin-left: auto" :disabled="busy" @click="load">⟳ 刷新</button>
@ -92,7 +92,7 @@
<div class="w4f-bar"><i :style="{ width: Math.min(100, n.load.memPct + n.load.netPct) + '%' }" /></div>
</div>
<div class="rn-addr">{{ n.addr }}</div>
<button v-if="n.id !== ring.selfId && n.alive" class="w4f-btn danger small rn-remove" :disabled="busy" @click="onRemoveNode(n.id)">移除节点</button>
<button v-if="canWrite && n.id !== ring.selfId && n.alive" class="w4f-btn danger small rn-remove" :disabled="busy" @click="onRemoveNode(n.id)">移除节点</button>
</div>
<span v-if="i < ring.nodes.length - 1" class="ring-arrow">→</span>
</template>
@ -183,16 +183,16 @@
<el-dialog v-model="joinDialog.visible" title="加入集群" width="420px">
<div class="form-row">
<label>对端地址</label>
<el-input v-model="joinDialog.addr" placeholder="对端 IP:port(如 192.168.1.10:7500)" @keyup.enter="onJoin" />
<el-input v-model="joinDialog.addr" :disabled="!canWrite" placeholder="对端 IP:port(如 192.168.1.10:7500)" @keyup.enter="onJoin" />
</div>
<div class="form-row">
<label>加入密钥</label>
<el-input v-model="joinDialog.key" placeholder="对端节点的加入密钥(nodeKey)" @keyup.enter="onJoin" />
<el-input v-model="joinDialog.key" :disabled="!canWrite" placeholder="对端节点的加入密钥(nodeKey)" @keyup.enter="onJoin" />
</div>
<div class="form-hint">填入对端节点可被本机路由到达的真实 IP:port(或域名:port),本节点将向对端请求加入并采纳其环状态,令牌到达即并入环。需提供对端节点的加入密钥以通过验证。</div>
<template #footer>
<button class="w4f-btn ghost small" @click="joinDialog.visible = false">取消</button>
<button class="w4f-btn small" :disabled="busy || !joinDialog.addr.trim() || !joinDialog.key.trim()" @click="onJoin">加入</button>
<button class="w4f-btn small" :disabled="busy || !canWrite || !joinDialog.addr.trim() || !joinDialog.key.trim()" @click="onJoin">加入</button>
</template>
</el-dialog>
</div>
@ -202,6 +202,7 @@
import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue'
import { ElMessage } from 'element-plus'
import { api, downloadAuditCsv } from '../api'
import { canWrite } from '../auth'
import type { RingSnapshot, RingTaskInfo, RingLogEntry } from '../types'
const ring = ref<RingSnapshot | null>(null)
@ -517,7 +518,7 @@ onBeforeUnmount(() => {
.ring-node { position: relative; border: 1px solid var(--w4f-line); border-radius: $radius-md; padding: 12px 14px; min-width: 168px;
background: var(--w4f-card-2); box-shadow: var(--w4f-sh-sm);
&.leader { border-color: var(--w4f-secondary); background: var(--w4f-secondary-50); }
&.self { outline: 2px solid rgba(255, 127, 172, 0.45); box-shadow: 0 0 0 4px rgba(255, 127, 172, 0.14); border-color: var(--w4f-primary); }
&.self { outline: 2px solid var(--w4f-primary); box-shadow: 0 0 0 4px var(--w4f-glow-soft); border-color: var(--w4f-primary); }
&.dead { border-color: var(--w4f-danger); background: var(--w4f-danger-50); opacity: 0.72; }
}
.rn-top { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
@ -533,8 +534,8 @@ onBeforeUnmount(() => {
.task-list, .topo-list, .log-list { display: flex; flex-direction: column; gap: 6px; }
.log-list { max-height: 340px; overflow-y: auto; padding-right: 2px; }
.task-row, .topo-row { display: flex; align-items: center; gap: 8px; border: 1px solid var(--w4f-line); border-radius: $radius-xs; padding: 8px 11px; font-size: 13px; background: var(--w4f-card-2); box-shadow: var(--w4f-sh-sm); }
.task-row.pending { background: var(--w4f-primary-50); border-color: rgba(255, 127, 172, 0.25); }
.task-row.revoke, .task-row.remove { background: var(--w4f-danger-50); border-color: rgba(219, 54, 148, 0.25); }
.task-row.pending { background: var(--w4f-primary-50); border-color: var(--w4f-primary-200); }
.task-row.revoke, .task-row.remove { background: var(--w4f-danger-50); border-color: var(--w4f-danger); }
.tk-id, .tp-id { font-weight: 700; color: $color-text-secondary; font-size: 12px; font-family: var(--w4f-mono); }
.tk-name, .tp-name { font-weight: 600; }
.tk-arrow { color: var(--w4f-primary); font-weight: 700; }