前端页面

This commit is contained in:
robin
2026-02-24 11:33:44 +08:00
parent f3af234308
commit 60dc87e0f2
141 changed files with 6845 additions and 133 deletions

View File

@@ -0,0 +1,10 @@
<second-menu>
<menu-item :href="'/httpdns/clusters/cluster?clusterId=' + currentCluster.id">{{currentCluster.name}}</menu-item>
<span class="item disabled" style="padding-left: 0; padding-right: 0">&raquo;</span>
<menu-item :href="'/httpdns/clusters/cluster?clusterId=' + clusterId">节点列表</menu-item>
<span class="item disabled">|</span>
<menu-item :href="'/httpdns/clusters/cluster/node?clusterId=' + clusterId + '&nodeId=' + nodeId" code="node">节点详情</menu-item>
<menu-item :href="'/httpdns/clusters/cluster/node/logs?clusterId=' + clusterId + '&nodeId=' + nodeId" code="log">运行日志</menu-item>
<menu-item :href="'/httpdns/clusters/cluster/node/update?clusterId=' + clusterId + '&nodeId=' + nodeId" code="update">修改设置</menu-item>
<menu-item :href="'/httpdns/clusters/cluster/node/install?clusterId=' + clusterId + '&nodeId=' + nodeId" code="install">安装节点</menu-item>
</second-menu>

View File

@@ -0,0 +1,4 @@
a.underline {
border-bottom: 1px #db2828 dashed;
}
/*# sourceMappingURL=index.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["index.less"],"names":[],"mappings":"AAAA,CAAC;EACA,iCAAA","file":"index.css"}

View File

@@ -0,0 +1,191 @@
{$layout}
{$template "node_menu"}
<h3>节点详情</h3>
<table class="ui table definition selectable">
<tr>
<td class="title">节点名称</td>
<td>{{node.name}}</td>
</tr>
<tr>
<td>状态</td>
<td><label-on :v-is-on="node.isOn"></label-on></td>
</tr>
<tr>
<td>IP地址</td>
<td>
<div v-if="node.ipAddresses.length > 0">
<div>
<div v-for="(address, index) in node.ipAddresses" class="ui label tiny basic">
{{address.ip}}
<span class="small" v-if="address.name.length > 0">{{address.name}}<span v-if="!address.canAccess">,不公开访问</span></span>&nbsp;
<span class="small red" v-if="!address.isOn">[off]</span>
<span class="small red" v-if="!address.isUp">[down]</span>
<span class="small" v-if="address.name.length == 0 && !address.canAccess">(不公开访问)</span>
</div>
</div>
</div>
<div v-else>
<span class="disabled">暂时还没有填写 IP 地址。</span>
</div>
</td>
</tr>
<tr>
<td colspan="2"><more-options-indicator></more-options-indicator></td>
</tr>
<tbody v-show="moreOptionsVisible">
<tr>
<td>SSH主机地址</td>
<td>
<div v-if="node.login != null && node.login.params != null && node.login.params.host != null">
<span v-if="node.login.params.host.length > 0">{{node.login.params.host}}</span>
<span v-else class="disabled">尚未设置</span>
</div>
<div v-else class="disabled">尚未设置</div>
</td>
</tr>
<tr>
<td>SSH主机端口</td>
<td>
<div v-if="node.login != null && node.login.params != null && node.login.params.host != null">
<span v-if="node.login.params.port > 0">{{node.login.params.port}}</span>
<span v-else class="disabled">尚未设置</span>
</div>
<span v-else class="disabled">尚未设置</span>
</td>
</tr>
<tr>
<td>SSH登录认证</td>
<td>
<div v-if="node.login != null && node.login.grant != null && node.login.grant.id > 0">
<a :href="'/clusters/grants/grant?grantId=' + node.login.grant.id">
{{node.login.grant.name}}
<span class="small grey">{{node.login.grant.methodName}}</span>
<span class="small grey" v-if="node.login.grant.username.length > 0">{{node.login.grant.username}}</span>
</a>
</div>
<span v-else class="disabled">尚未设置</span>
</td>
</tr>
<tr>
<td>API节点地址</td>
<td>
<div v-if="node.apiNodeAddrs != null && node.apiNodeAddrs.length > 0">
<span v-for="addr in node.apiNodeAddrs" class="ui label basic small">{{addr}}</span>
</div>
<span v-else class="disabled">使用全局设置</span>
</td>
</tr>
</tbody>
</table>
<div class="ui divider"></div>
<h3>运行状态</h3>
<table class="ui table definition selectable">
<tr>
<td class="title">运行状态</td>
<td>
<div v-if="node.status.isActive">
<span class="green">运行中</span>&nbsp;
<a href="" @click.prevent="stopNode()" v-if="!isStopping"><span>[通过SSH停止]</span></a>
<span v-if="isStopping">[停止中...]</span>
</div>
<div v-else>
<span class="red">已断开</span>&nbsp;
<a href="" @click.prevent="startNode()" v-if="node.isInstalled && !isStarting"><span>[通过SSH启动]</span></a>
<span v-if="node.isInstalled && isStarting">[启动中...]</span>
<a v-if="!node.isInstalled" :href="'/httpdns/clusters/cluster/node/install?clusterId=' + clusterId + '&nodeId=' + node.id"><span>去安装 &gt;</span></a>
</div>
</td>
</tr>
<tbody v-show="node.status.isActive">
<tr>
<td>CPU用量</td>
<td>
{{node.status.cpuUsageText}}
&nbsp;
<span v-if="node.status.cpuPhysicalCount > 0" class="small grey">{{node.status.cpuPhysicalCount}}核心/{{node.status.cpuLogicalCount}}线程)</span>
</td>
</tr>
<tr>
<td>内存用量</td>
<td>{{node.status.memUsageText}}</td>
</tr>
<tr>
<td>负载</td>
<td>
{{node.status.load1m}} &nbsp; {{node.status.load5m}} &nbsp; {{node.status.load15m}}
&nbsp;
<tip-icon content="三个数字分别代表1分钟、5分钟、15分钟平均负载"></tip-icon>
</td>
</tr>
</tbody>
<tbody>
<tr v-if="node.status.buildVersion != null && node.status.buildVersion.length > 0">
<td>版本</td>
<td>
v{{node.status.buildVersion}}
&nbsp;
<a :href="'/httpdns/clusters/upgradeRemote?clusterId=' + clusterId + '&nodeId=' + node.id" v-if="shouldUpgrade"><span class="red">发现新版本 v{{newVersion}} &raquo;</span></a>
</td>
</tr>
<tr v-if="node.status.exePath != null && node.status.exePath.length > 0">
<td>主程序位置</td>
<td>{{node.status.exePath}}</td>
</tr>
<tr>
<td>最近API连接状况</td>
<td>
<span v-if="node.status.apiSuccessPercent > 0 && node.status.apiAvgCostSeconds > 0">
<span v-if="node.status.apiSuccessPercent <= 50" class="red">连接错误异常严重({{round(100 - node.status.apiSuccessPercent)}}%失败请改善当前节点和API节点之间通讯</span>
<span v-else-if="node.status.apiSuccessPercent <= 80" class="red">连接错误较多({{round(100 - node.status.apiSuccessPercent)}}%失败请改善当前节点和API节点之间通讯</span>
<span v-else-if="node.status.apiSuccessPercent < 100" class="orange">有连接错误发生({{round(100 - node.status.apiSuccessPercent)}}%失败请改善当前节点和API节点之间通讯</span>
<span v-else>
<span v-if="node.status.apiAvgCostSeconds <= 1" class="green">连接良好</span>
<span v-else-if="node.status.apiAvgCostSeconds <= 5" class="orange">连接基本稳定(平均{{round(node.status.apiAvgCostSeconds)}}秒)</span>
<span v-else-if="node.status.apiAvgCostSeconds <= 10" class="orange">连接速度较慢(平均{{round(node.status.apiAvgCostSeconds)}}秒)</span>
<span v-else class="red">连接非常慢(平均{{round(node.status.apiAvgCostSeconds)}}秒请改善当前节点和API节点之间通讯</span>
</span>
</span>
<span v-else class="disabled">尚未上报数据</span>
</td>
</tr>
<tr v-if="nodeDatetime.length > 0">
<td>上次更新时间</td>
<td>
{{nodeDatetime}}
<p class="comment" v-if="nodeTimeDiff > 30"><span class="red">当前节点时间与API节点时间相差 {{nodeTimeDiff}} 秒,请同步节点时间。</span></p>
</td>
</tr>
</tbody>
</table>
<p class="comment" v-if="node.status.isActive">每隔30秒钟更新一次运行状态。</p>
<div class="ui divider"></div>
<h3>安装信息</h3>
<table class="ui table definition selectable">
<tr>
<td>节点ID <em>(id)</em></td>
<td>{{node.uniqueId}}</td>
</tr>
<tr>
<td>密钥 <em>(secret)</em></td>
<td>{{node.secret}}</td>
</tr>
<tr>
<td class="title">安装目录</td>
<td>
<div v-if="node.installDir.length == 0">使用集群设置<span v-if="node.cluster != null && node.cluster.installDir.length > 0">{{node.cluster.installDir}}</span></div>
<span v-else>{{node.installDir}}</span>
</td>
</tr>
<tr>
<td>已安装</td>
<td>
<span v-if="node.isInstalled" class="green">已安装</span>
<a v-else :href="'/httpdns/clusters/cluster/node/install?clusterId=' + clusterId + '&nodeId=' + nodeId" class="underline" title="点击进入安装界面"><span class="red">未安装</span></a>
</td>
</tr>
</table>

View File

@@ -0,0 +1,39 @@
Tea.context(function () {
this.isStarting = false
this.startNode = function () {
this.isStarting = true
this.$post('/httpdns/clusters/cluster/node/start')
.params({
nodeId: this.node.id
})
.success(function () {
teaweb.success('启动成功', function () {
teaweb.reload()
})
})
.done(function () {
this.isStarting = false
})
}
this.isStopping = false
this.stopNode = function () {
this.isStopping = true
this.$post('/httpdns/clusters/cluster/node/stop')
.params({
nodeId: this.node.id
})
.success(function () {
teaweb.success('执行成功', function () {
teaweb.reload()
})
})
.done(function () {
this.isStopping = false
})
}
this.round = function (f) {
return Math.round(f * 100) / 100
}
})

View File

@@ -0,0 +1,3 @@
a.underline {
border-bottom: 1px #db2828 dashed;
}

View File

@@ -0,0 +1,7 @@
.installing-box {
line-height: 1.8;
}
.installing-box .blue {
color: #2185d0;
}
/*# sourceMappingURL=install.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["install.less"],"names":[],"mappings":"AAAA;EACC,gBAAA;;AADD,eAGC;EACC,cAAA","file":"install.css"}

View File

@@ -0,0 +1,100 @@
{$layout}
{$template "node_menu"}
{$template "/code_editor"}
<!-- 已安装 -->
<div v-if="node.isInstalled">
<div class="ui message green">当前节点为已安装状态。</div>
<a href="" @click.prevent="updateNodeIsInstalled(false)">[重新安装]</a>
<h4>配置文件</h4>
<table class="ui table definition selectable">
<tr>
<td class="title">配置文件</td>
<td>
configs/api_httpdns.yaml &nbsp;
<download-link :v-element="'rpc-code'" :v-file="'api_httpdns.yaml'">[下载]</download-link>
</td>
</tr>
<tr>
<td>配置内容</td>
<td>
<source-code-box id="rpc-code" type="text/yaml">rpc.endpoints: [ {{apiEndpoints}} ]
nodeId: "{{node.uniqueId}}"
secret: "{{node.secret}}"</source-code-box>
<p class="comment">每个节点的配置文件内容均不相同,不能混用。</p>
</td>
</tr>
<tr>
<td class="title">安装目录</td>
<td>
<div v-if="node.installDir.length == 0">使用集群设置<span
v-if="node.cluster != null && node.cluster.installDir.length > 0">{{node.cluster.installDir}}</span>
</div>
<span v-else>{{node.installDir}}</span>
</td>
</tr>
</table>
</div>
<!-- 未安装 -->
<div v-if="!node.isInstalled">
<h4>方法1通过SSH自动安装</h4>
<table class="ui table definition selectable">
<tr>
<td class="title">SSH地址</td>
<td>
<span v-if="sshAddr.length > 0">{{sshAddr}} &nbsp; <a href=""
@click.prevent="showSSHPopup(nodeId)">[修改]</a></span>
<span v-else><span class="red">尚未设置</span> &nbsp; <a href=""
@click.prevent="showSSHPopup(nodeId)">[设置]</a></span>
</td>
</tr>
</table>
<div v-if="installStatus != null && (installStatus.isRunning || installStatus.isFinished)"
class="ui segment installing-box">
<div v-if="installStatus.isRunning" class="blue">安装中...</div>
<div v-if="installStatus.isFinished">
<span v-if="installStatus.isOk" class="green">已安装成功</span>
<span v-if="!installStatus.isOk" class="red">安装过程中发生错误:{{installStatus.error}}</span>
</div>
</div>
<div v-if="installStatus != null && installStatus.isFinished">
<button class="ui button small" type="button" @click.prevent="install()">重新安装</button>
</div>
<div v-if="installStatus == null || (!installStatus.isFinished && !installStatus.isRunning)">
<button class="ui button small primary" type="button" @click.prevent="install()">开始安装</button>
</div>
<h4>方法2手动安装</h4>
<table class="ui table definition selectable">
<tr>
<td class="title">配置文件</td>
<td>
configs/api_httpdns.yaml &nbsp;
<download-link :v-element="'rpc-code'" :v-file="'api_httpdns.yaml'">[下载]</download-link>
</td>
</tr>
<tr>
<td>配置内容</td>
<td>
<source-code-box id="rpc-code" type="text/yaml">rpc.endpoints: [ {{apiEndpoints}} ]
nodeId: "{{node.uniqueId}}"
secret: "{{node.secret}}"</source-code-box>
</td>
</tr>
<tr>
<td class="title">安装目录</td>
<td>
<div v-if="node.installDir.length == 0">使用集群设置<span
v-if="node.cluster != null && node.cluster.installDir.length > 0">{{node.cluster.installDir}}</span>
</div>
<span v-else>{{node.installDir}}</span>
</td>
</tr>
</table>
<a href="" @click.prevent="updateNodeIsInstalled(true)">[修改为已安装状态]</a>
</div>

View File

@@ -0,0 +1,115 @@
Tea.context(function () {
let isInstalling = false
this.$delay(function () {
this.reloadStatus(this.nodeId)
})
this.install = function () {
isInstalling = true
this.$post("$")
.params({
nodeId: this.nodeId
})
.success(function () {})
}
this.updateNodeIsInstalled = function (isInstalled) {
let msg = isInstalled
? "html:确认要将当前节点修改为 <strong>已安装</strong> 状态?"
: "html:确认要将当前节点修改为 <strong>未安装</strong> 状态?"
teaweb.confirm(msg, function () {
this.$post("/httpdns/clusters/cluster/node/updateInstallStatus")
.params({
nodeId: this.nodeId,
isInstalled: isInstalled ? 1 : 0
})
.refresh()
})
}
this.reloadStatus = function (nodeId) {
let that = this
this.$post("/httpdns/clusters/cluster/node/status")
.params({
nodeId: nodeId
})
.success(function (resp) {
this.installStatus = resp.data.installStatus
this.node.isInstalled = resp.data.isInstalled
if (!isInstalling) {
return
}
let currentNodeId = this.node.id
let installStatus = this.installStatus || {}
let errMsg = installStatus.error || ""
let errorCode = installStatus.errorCode || ""
if (errorCode.length > 0) {
isInstalling = false
}
switch (errorCode) {
case "EMPTY_LOGIN":
case "EMPTY_SSH_HOST":
case "EMPTY_SSH_PORT":
case "EMPTY_GRANT":
teaweb.warn("需要补充 SSH 登录信息", function () {
teaweb.popup("/httpdns/clusters/updateNodeSSH?nodeId=" + currentNodeId, {
height: "30em",
callback: function () {
that.install()
}
})
})
return
case "SSH_LOGIN_FAILED":
teaweb.warn("SSH 登录失败,请检查设置", function () {
teaweb.popup("/httpdns/clusters/updateNodeSSH?nodeId=" + currentNodeId, {
height: "30em",
callback: function () {
that.install()
}
})
})
return
case "CREATE_ROOT_DIRECTORY_FAILED":
teaweb.warn("创建根目录失败:" + errMsg)
return
case "INSTALL_HELPER_FAILED":
teaweb.warn("安装助手失败:" + errMsg)
return
case "TEST_FAILED":
teaweb.warn("环境测试失败:" + errMsg)
return
case "RPC_TEST_FAILED":
teaweb.confirm("html:节点到 API 的 RPC 连通性测试失败:" + errMsg + "<br/>现在去修改 API 信息?", function () {
window.location = "/settings/api"
})
return
default:
break
}
})
.done(function () {
this.$delay(function () {
this.reloadStatus(nodeId)
}, 1000)
})
}
this.showSSHPopup = function (nodeId) {
teaweb.popup("/httpdns/clusters/updateNodeSSH?nodeId=" + nodeId, {
height: "30em",
callback: function () {
teaweb.success("保存成功", function () {
teaweb.reload()
})
}
})
}
})

View File

@@ -0,0 +1,7 @@
.installing-box {
line-height: 1.8;
.blue {
color: #2185d0;
}
}

View File

@@ -0,0 +1,5 @@
pre.log-box {
margin: 0;
padding: 0;
}
/*# sourceMappingURL=logs.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["logs.less"],"names":[],"mappings":"AAAA,GAAG;EACF,SAAA;EACA,UAAA","file":"logs.css"}

View File

@@ -0,0 +1,51 @@
{$layout}
{$template "node_menu"}
{$template "/datepicker"}
<form method="get" action="/httpdns/clusters/cluster/node/logs" class="ui form" autocomplete="off">
<input type="hidden" name="clusterId" :value="clusterId"/>
<input type="hidden" name="nodeId" :value="nodeId"/>
<div class="ui fields inline">
<div class="ui field">
<input type="text" name="dayFrom" placeholder="开始日期" v-model="dayFrom" value="" style="width:8em" id="day-from-picker"/>
</div>
<div class="ui field">
<input type="text" name="dayTo" placeholder="结束日期" v-model="dayTo" value="" style="width:8em" id="day-to-picker"/>
</div>
<div class="ui field">
<select class="ui dropdown" name="level" v-model="level">
<option value="">[级别]</option>
<option value="error">错误</option>
<option value="warning">警告</option>
<option value="info">信息</option>
<option value="success">成功</option>
</select>
</div>
<div class="ui field">
<input type="text" name="keyword" style="width:10em" v-model="keyword" placeholder="关键词"/>
</div>
<div class="ui field">
<button type="submit" class="ui button">查询</button>
</div>
<div class="ui field" v-if="dayFrom.length > 0 || dayTo.length > 0 || keyword.length > 0 || level.length > 0">
<a :href="'/httpdns/clusters/cluster/node/logs?clusterId=' + clusterId + '&nodeId=' + nodeId">[清除条件]</a>
</div>
</div>
</form>
<p class="comment" v-if="logs.length == 0">暂时还没有日志。</p>
<table class="ui table selectable" v-if="logs.length > 0">
<thead>
<tr>
</tr>
</thead>
<tr v-for="log in logs">
<td>
<node-log-row :v-log="log" :v-keyword="keyword"></node-log-row>
</td>
</tr>
</table>
<div class="page" v-html="page"></div>

View File

@@ -0,0 +1,6 @@
Tea.context(function () {
this.$delay(function () {
teaweb.datepicker("day-from-picker")
teaweb.datepicker("day-to-picker")
})
})

View File

@@ -0,0 +1,4 @@
pre.log-box {
margin: 0;
padding: 0;
}

View File

@@ -0,0 +1,78 @@
{$layout}
{$template "node_menu"}
<h3>修改节点</h3>
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<input type="hidden" name="nodeId" :value="node.id" />
<input type="hidden" name="loginId" :value="loginId" />
<table class="ui table definition selectable">
<tr>
<td class="title">节点名称 *</td>
<td>
<input type="text" name="name" maxlength="50" ref="focus" v-model="node.name" />
</td>
</tr>
<tr>
<td>IP地址 *</td>
<td>
<node-ip-addresses-box role="ns" :v-ip-addresses="node.ipAddresses"></node-ip-addresses-box>
<p class="comment">用于访问节点和处理HTTPDNS解析请求等。</p>
</td>
</tr>
<tr>
<td>所属集群</td>
<td>
<select class="ui dropdown" name="clusterId" style="width:10em" v-model="clusterId">
<option v-for="cluster in clusters" :value="cluster.id">{{cluster.name}}</option>
</select>
</td>
</tr>
<tr>
<td colspan="2"><more-options-indicator></more-options-indicator></td>
</tr>
<tbody v-show="moreOptionsVisible">
<tr>
<td>SSH主机地址</td>
<td>
<input type="text" name="sshHost" maxlength="64" v-model="sshHost" />
<p class="comment">比如192.168.1.100</p>
</td>
</tr>
<tr>
<td>SSH主机端口</td>
<td>
<input type="text" name="sshPort" maxlength="5" v-model="sshPort" />
<p class="comment">比如22。</p>
</td>
</tr>
<tr>
<td>SSH登录认证</td>
<td>
<grant-selector :v-grant="grant" :v-node-cluster-id="clusterId"></grant-selector>
</td>
</tr>
<tr>
<td class="title">API节点地址</td>
<td>
<div style="margin-bottom: 0.5em">
<api-node-addresses-box :v-name="'apiNodeAddrsJSON'"
:v-addrs="apiNodeAddrs"></api-node-addresses-box>
</div>
<p class="comment">当前节点单独使用的API节点设置。<pro-warning-label></pro-warning-label></p>
</td>
</tr>
<tr>
<td>启用当前节点</td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="isOn" value="1" v-model="node.isOn" />
<label></label>
</div>
<p class="comment">如果不启用此节点,此节点上的所有网站将不能访问。</p>
</td>
</tr>
</tbody>
</table>
<submit-btn></submit-btn>
</form>

View File

@@ -0,0 +1,35 @@
Tea.context(function () {
this.clusterId = 0;
if (this.node.cluster != null && this.node.cluster.id > 0) {
this.clusterId = this.node.cluster.id;
}
this.success = NotifySuccess("保存成功", "/httpdns/clusters/cluster/node?clusterId=" + this.clusterId + "&nodeId=" + this.node.id);
// 认证相关
this.grant = null
this.sshHost = ""
this.sshPort = ""
this.loginId = 0
if (this.node.login != null) {
this.loginId = this.node.login.id
if (this.node.login.params != null) {
this.sshHost = this.node.login.params.host
if (this.node.login.params.port > 0) {
this.sshPort = this.node.login.params.port
}
}
if (this.node.login.grant != null && typeof this.node.login.grant.id != "undefined") {
this.grant = {
id: this.node.login.grant.id,
name: this.node.login.grant.name,
method: this.node.login.grant.method,
methodName: this.node.login.grant.methodName,
username: this.node.login.grant.username
}
}
}
})