feat: status-server主题增加agent账单信息展示 (#424)

1. 首页通过在后台配置PublicNote字段,实现agent账单信息展示
2. 优化顶部导航,增加当前位置标识
3. 优化network页移动端样式
4. 优化后台server页展示,隐藏掉一些不重要的信息
5. 一些其他小优化
使用说明:https://github.com/naiba/nezha/pull/424#issuecomment-2386837658
This commit is contained in:
nap0o 2024-10-01 22:05:53 -04:00 committed by GitHub
parent 7940e010e0
commit 60ada33ee9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 517 additions and 34 deletions

View File

@ -690,3 +690,18 @@ other = "Cancel"
[FMError]
other = "Agent returned an error, please view the console for details. To open a new connection, reopen the FM again."
[Remaining]
other = "Remaining"
[Lifetime]
other = "Lifetime"
[Price]
other = "Price"
[Expired]
other = "Expired"
[Days]
other = "d"

View File

@ -690,3 +690,18 @@ other = "Cancelar"
[FMError]
other = "Agent devolvió un error, consulte la consola para obtener más detalles. Para abrir una nueva conexión, vuelva a abrir el FM."
[Remaining]
other = "Remaining"
[Lifetime]
other = "Lifetime"
[Price]
other = "Price"
[Expired]
other = "Expired"
[Days]
other = "d"

View File

@ -690,3 +690,18 @@ other = "取消"
[FMError]
other = "Agent 返回了错误,请查看控制台获取详细信息。要建立新连接,请重新打开 FM。"
[Remaining]
other = "剩余"
[Lifetime]
other = "永续"
[Price]
other = "价格"
[Expired]
other = "已到期"
[Days]
other = "天"

View File

@ -690,3 +690,18 @@ other = "取消"
[FMError]
other = "Agent 回傳了錯誤,請查看主控台獲取詳細資訊。要建立新連線,請重新開啟 FM。"
[Remaining]
other = "剩餘"
[Lifetime]
other = "永續"
[Price]
other = "價格"
[Expired]
other = "已到期"
[Days]
other = "天"

View File

@ -8,6 +8,11 @@ body[theme="dark"] .navbar .navbar-brand {
color: #ffffff;
}
body[theme="dark"] .navbar .navbar-nav li.pc-active a,
body[theme="dark"] .navbar .navbar-nav li.m-active a{
color: rgba(73, 146, 255, 1);
}
body[theme="dark"] .navbar .dropdown-menu {
background-color: rgba(23, 26, 30, 1);
border-color: rgba(49, 54, 59, 1);
@ -88,8 +93,8 @@ body[theme="dark"] .table > tbody > tr.expandRow.even > td:before{
/* expandRow展开部分样式结束 */
body[theme="dark"] .progress {
background-image: linear-gradient(#2c2c2c 0,rgba(28, 29, 38, 1) 100%);
background-color: rgba(28, 29, 38, 1);
background-image: none;
background-color: rgba(255, 255, 255, 0.075);
}
body[theme="dark"] .progress-bar {
@ -174,5 +179,5 @@ body[theme="dark"] .toolbox i{
}
body[theme="dark"] .network-box .network-box-header{
border-bottom: 1px solid rgba(110, 112, 121, 0.25);
border-bottom-color: rgba(110, 112, 121, 0.25);
}

View File

@ -16,10 +16,15 @@ body[theme="light"] .navbar .navbar-brand {
color: rgba(0, 0, 0, 0.87);
}
body[theme="light"] .navbar .navbar-nav > li > a{
body[theme="light"] .navbar .navbar-nav > li > a {
color: rgba(0, 0, 0, 0.87);
}
body[theme="light"] .navbar .navbar-nav li.pc-active a,
body[theme="light"] .navbar .navbar-nav li.m-active a {
color: rgba(73, 146, 255, 1);
}
body[theme="light"] .navbar .navbar-nav > .open > a:focus,
body[theme="light"] .navbar .navbar-nav > .open > a:hover,
body[theme="light"] .navbar .navbar-nav > .active > a,
@ -198,7 +203,7 @@ body[theme="light"] .modal-header i.xclose{
}
body[theme="light"] .network-box .network-box-header{
border-bottom: 1px solid rgba(224, 230, 241, 0.6);
border-bottom-color: rgba(224, 230, 241, 0.6);
}
@media only screen and (max-width: 767px) {

View File

@ -237,8 +237,8 @@ tr.accordion-toggle{
}
.node-cell.network {
min-width: 100px;
max-width: 100px;
min-width: 110px;
max-width: 110px;
}
.node-cell.traffic {
@ -250,6 +250,24 @@ tr.accordion-toggle{
max-width: 50px;
}
.node-cell.remaining {
min-width: 65px;
max-width: 65px;
}
.node-cell.remaining .additional {
position: absolute;
left: 0;
font-size: 12px;
text-align: center;
width: 100%;
}
.node-cell.remaining .additional small{
white-space: nowrap;
font-size: 80%;
}
.temp-detail {
cursor: pointer;
}
@ -331,6 +349,8 @@ td.ping-network-quality {
padding: 5px 0px 15px 5px;
cursor: pointer;
width: 100%;
border-bottom-width: 1px;
border-bottom-style: solid;
}
.network-box .network-box-header .dropdown-menu {
@ -529,6 +549,10 @@ footer p{
display: none;
visibility: hidden;
}
.node-cell.remaining {
min-width: 75px;
max-width: 75px;
}
.accordian-body{
margin: 5px 0px 5px 10px;
}
@ -572,6 +596,7 @@ footer p{
.network-box .network-box-header {
margin: 8px 0px 0px 8px;
font-size: 16px;
border-bottom: none;
}
.network-box .chartTitle {
font-size: 16px;

View File

@ -155,6 +155,11 @@ const mixinsVue = {
checkIsMobile() { // 检测设备类型,页面宽度小于768px认为是移动设备
return window.innerWidth <= 768;
},
isMenuActive(page){
if(page == this.$root.page) {
return this.isMobile ? 'm-active' : 'pc-active';
}
},
setBenchmarkHeight() {
let vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);

View File

@ -30,9 +30,6 @@
<th>{{tr "VersionNumber"}}</th>
<th>{{tr "HideForGuest"}}</th>
<th>{{tr "EnableDDNS"}}</th>
<th>{{tr "EnableIPv4"}}</th>
<th>{{tr "EnableIpv6"}}</th>
<th>{{tr "DDNSDomain"}}</th>
<th>{{tr "Secret"}}</th>
<th>{{tr "OneKeyInstall"}}</th>
<th>{{tr "Note"}}</th>
@ -51,9 +48,6 @@
<td>{{$server.Host.Version}}</td>
<td>{{$server.HideForGuest}}</td>
<td>{{$server.EnableDDNS}}</td>
<td>{{$server.EnableIPv4}}</td>
<td>{{$server.EnableIpv6}}</td>
<td>{{$server.DDNSDomain}}</td>
<td>
<button class="ui icon green mini button" data-clipboard-text="{{$server.Secret}}" data-tooltip="{{tr "ClickToCopy"}}">
<i class="copy icon"></i>
@ -76,8 +70,16 @@
<i class="apple icon"></i>
</button>
</td>
<td style="word-break: break-word;white-space: pre-wrap;">{{$server.Note}}</td>
<td style="word-break: break-word;white-space: pre-wrap;">{{$server.PublicNote}}</td>
<td>
{{if $server.Note}}
<button class="ui icon green mini button" title="{{ $server.Note }}" onclick="addOrEditServer({{$server.MarshalForDashboard}})"><i class="sticky note icon"></i></button>
{{end}}
</td>
<td>
{{if $server.PublicNote}}
<button class="ui icon green mini button" title="{{$server.PublicNote}}" onclick="addOrEditServer({{$server.MarshalForDashboard}})"><i class="sticky clipboard icon"></i></button>
{{end}}
</td>
<td>
<div class="ui mini icon buttons">
<button class="ui button" onclick="connectToServer({{$server.ID}})">

View File

@ -26,10 +26,10 @@
<script src="https://unpkg.com/bootstrap@3.4.1/dist/js/bootstrap.min.js"></script>
<script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script>
<script src="https://unpkg.com/echarts@5.5.0/dist/echarts.min.js"></script>
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20240909">
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v202408011">
<link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20240811">
<script src="/static/theme-server-status/js/mixin.js?v20240907"></script>
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20240929">
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v202409029">
<link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20240916">
<script src="/static/theme-server-status/js/mixin.js?v20240915"></script>
</head>
<body>
<div id="app">

View File

@ -6,6 +6,7 @@
<th class="node-cell name center">{{tr "Name"}}</th>
<th class="node-cell os center">{{tr "Platform"}}</th>
<th class="node-cell location center">{{tr "Location"}}</th>
<th v-if="additional && Object.values(additional).filter(item => item.price).length > 0" class="node-cell price center">{{tr "Price"}}</th>
<th class="node-cell uptime center">{{tr "Uptime"}}</th>
<th class="node-cell load center">{{tr "Load"}}</th>
<th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
@ -13,6 +14,7 @@
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
<th class="node-cell memory center">{{tr "MemUsed"}}</th>
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
<th v-if="additional && Object.values(additional).filter(item => item.remaining).length > 0" class="node-cell remaining center">{{tr "Remaining"}}</th>
</tr>
</thead>
<tbody id="servers">
@ -36,6 +38,12 @@
<i :class="'fi fi-' + node.location"></i>
<span class="node-cell-location-text text-uppercase">@#node.location#@</span>
</td>
<td v-if="additional && Object.values(additional).filter(item => item.price).length > 0" class="node-cell price center">
<template v-if="additional[node.ID] && additional[node.ID].price">
<span v-if="additional[node.ID].price.amount == 0" class="node-cell-price-text">Free</span>
<span v-else class="node-cell-price-text">@#additional[node.ID].price.amount#@@#(additional[node.ID].price.cycle ? '/' + additional[node.ID].price.cycle : '')#@</span>
</template>
</td>
<td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td>
<td style="text-align: center;" class="node-cell load">@#node.load#@</td>
<td style="text-align: center;" class="node-cell network">@#node.network#@</td>
@ -59,6 +67,21 @@
</div>
</div>
</td>
<td v-if="additional && Object.values(additional).filter(item => item.remaining).length > 0" class="node-cell remaining">
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
<template v-if="additional[node.ID] && additional[node.ID].remaining.format">
<div :style="additional[node.ID].remaining.format.style" :class="additional[node.ID].remaining.days == 'lifetime' ? '' : additional[node.ID].remaining.format.class"></div>
<div class="additional">
<small v-if="additional[node.ID].remaining.days == 'lifetime'">{{tr "Lifetime"}}</small>
<small v-else-if="additional[node.ID].remaining.days < 0">{{tr "Expired"}}</small>
<small v-else>
<span class="node-cell-remaining-days">@#additional[node.ID].remaining.days#@{{tr "Days"}}</span>
<span class="node-cell-remaining-percent">@#additional[node.ID].remaining.percent#@%</span>
</small>
</div>
</template>
</div>
</td>
</tr>
<tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'">
<td colspan="16">

View File

@ -9,6 +9,7 @@
<th class="node-cell name center">{{tr "Name"}}</th>
<th class="node-cell os center">{{tr "Platform"}}</th>
<th class="node-cell location center">{{tr "Location"}}</th>
<th v-if="additional && Object.values(additional).filter(item => item.price).length > 0" class="node-cell price center">{{tr "Price"}}</th>
<th class="node-cell uptime center">{{tr "Uptime"}}</th>
<th class="node-cell load center">{{tr "Load"}}</th>
<th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
@ -16,6 +17,7 @@
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
<th class="node-cell memory center">{{tr "MemUsed"}}</th>
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
<th v-if="additional && Object.values(additional).filter(item => item.remaining).length > 0" class="node-cell remaining center">{{tr "Remaining"}}</th>
</tr>
</thead>
<tbody id="servers">
@ -39,6 +41,12 @@
<i :class="'fi fi-' + node.location"></i>
<span class="node-cell-location-text text-uppercase">&nbsp;@#node.location#@</span>
</td>
<td v-if="additional && Object.values(additional).filter(item => item.price).length > 0" class="node-cell price center">
<template v-if="additional[node.ID] && additional[node.ID].price">
<span v-if="additional[node.ID].price.amount == 0" class="node-cell-price-text">Free</span>
<span v-else class="node-cell-price-text">@#additional[node.ID].price.amount#@@#(additional[node.ID].price.cycle ? '/' + additional[node.ID].price.cycle : '')#@</span>
</template>
</td>
<td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td>
<td style="text-align: center;" class="node-cell load">@#node.load#@</td>
<td style="text-align: center;" class="node-cell network">@#node.network#@</td>
@ -62,6 +70,21 @@
</div>
</div>
</td>
<td v-if="additional && Object.values(additional).filter(item => item.remaining).length > 0" class="node-cell remaining">
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
<template v-if="additional[node.ID] && additional[node.ID].remaining.format">
<div :style="additional[node.ID].remaining.format.style" :class="additional[node.ID].remaining.days == 'lifetime' ? '' : additional[node.ID].remaining.format.class"></div>
<div class="additional">
<small v-if="additional[node.ID].remaining.days == 'lifetime'">{{tr "Lifetime"}}</small>
<small v-else-if="additional[node.ID].remaining.days < 0">{{tr "Expired"}}</small>
<small v-else>
<span class="node-cell-remaining-days">@#additional[node.ID].remaining.days#@{{tr "Days"}}</span>
<span class="node-cell-remaining-percent">@#additional[node.ID].remaining.percent#@%</span>
</small>
</div>
</template>
</div>
</td>
</tr>
<tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'">
<td colspan="16">

View File

@ -42,6 +42,7 @@
servers: [],
nodesTag: [],
nodesNoTag: [],
additional: {},
chartDataList: [],
ws: null,
language: {{.Conf.Language}},
@ -81,7 +82,8 @@
this.countryNameMap = this.initCountryNameMap();
this.countryServer = this.initCountryServer();
this.countryMapChartData = this.initCountryMapChartData();
})
});
this.additional = this.initAdditional(this.servers);
},
mounted() {
// 初始化时建立WebSocket连接
@ -98,6 +100,25 @@
});
},
methods: {
initAdditional(servers) {
let nodes = {};
servers?.forEach(server => {
if(server.PublicNote) {
nodes[server.ID] = {
"remaining": {
format: this.getRemainingFormat(server.live, server.PublicNote),
days: this.getRemainingDays(this.getNoteElementValue(server.PublicNote, "billingDataMod", "endDate"),server.PublicNote),
percent: this.toFixed2(100 - this.getRemainingPercent(this.getNoteElementValue(server.PublicNote, "billingDataMod", "startDate"), this.getNoteElementValue(server.PublicNote, "billingDataMod", "endDate"), server.PublicNote))
},
"price": {
amount: this.getNoteElementValue(server.PublicNote, "billingDataMod", "amount"),
cycle: this.getNoteElementValue(server.PublicNote, "billingDataMod", "cycle")
}
};
}
});
return nodes;
},
initCountryMap() {
return fetch(this.staticUrl + '/maps/nezha.countrymap.json')
.then(response => response.json())
@ -722,6 +743,331 @@
this.renderCharts(id,true);
}
});
},
getNoteElementValue(string, elementName, childElementName) {
let obj;
try {
obj = JSON.parse(string);
} catch (e) {
return null;
}
if (!elementName) return null;
if (childElementName) {
return obj[elementName] && childElementName in obj[elementName] ? obj[elementName][childElementName] : null;
} else {
return elementName in obj ? obj[elementName] : null;
}
},
getBillingCycle(billingCycle) {
// 统一转换为小写进行比较
const cycle = billingCycle.toLowerCase();
switch (cycle) {
case '月':
case 'month':
case 'monthly':
case 'm':
return "M";
case '季':
case 'quarterly':
case 'q':
return "Q";
case '半':
case '半年':
case 'half':
case 'semi-annually':
case 'h':
return "H";
case '年':
case 'year':
case 'annually':
case 'y':
return "Y";
default:
return null;
}
},
getAdjustTimezone(reference, target) {
// 获取时区
const referenceTimezoneOffset = reference.getTimezoneOffset();
const targetTimezoneOffset = target.getTimezoneOffset();
// 计算时区差异
const timezoneDifference = (referenceTimezoneOffset - targetTimezoneOffset) * 60 * 1000;
// 将 target 日期调整到 reference 时区
return new Date(target.getTime() + timezoneDifference);
},
getAutoRenewalEndDate(endDate, billingCycle) {
const expiration = new Date(endDate);
const current = this.getAdjustTimezone(new Date(endDate), new Date());
// 如果 expiration 无效,返回 null 并记录日志
if (isNaN(expiration.getTime())) {
console.error("getAutoRenewalEndDate: Invalid expiration format");
}
const result = {
flag: 1, // 1表示需要更新
check: 0, // 判断逻辑标记
count: 0 // 周期累计计数
};
// 如果当前时间还没到到期时间,直接返回到期时间
if (current < expiration) {
result.flag = 0;
result.date = expiration;
return result;
}
let nextExpiration = new Date(expiration); // 初始化为原到期时间
let newExpirationMonth = expiration.getMonth(); // 获取expiration初始月份
switch (billingCycle.toUpperCase()) {
case 'M': // 月度
nextExpiration.setFullYear(current.getFullYear());
const monthCheck = current < new Date(current.getFullYear(), current.getMonth(), expiration.getDate());
// 检查当前月是否在有效期内
if (monthCheck) {
nextExpiration.setMonth(current.getMonth());
result.check = 1;
} else {
nextExpiration.setMonth(current.getMonth() + 1);
result.check = 2;
}
break;
case 'Q': // 季度
nextExpiration.setFullYear(current.getFullYear());
// 每次增加 3 个月,直到新的月份大于当前月份
while (newExpirationMonth < current.getMonth()) {
newExpirationMonth += 3;
result.count += 1;
}
// nextExpiration设置获取到的新月份
nextExpiration.setMonth(newExpirationMonth);
const quarterlyCheck = current < nextExpiration;
// 检查新月份是否在有效期内
if (quarterlyCheck) {
result.check = 3;
} else {
nextExpiration.setMonth(current.getMonth() + 3);
result.check = 4;
result.count += 1;
}
break;
case 'H': // 半年
nextExpiration.setFullYear(current.getFullYear());
// 每次增加 6 个月,直到新的月份大于当前月份
while (newExpirationMonth < current.getMonth()) {
newExpirationMonth += 6;
result.count += 1;
}
// nextExpiration设置获取到的新月份
nextExpiration.setMonth(newExpirationMonth);
const halfCheck = current < nextExpiration;
// 检查新月份是否在有效期内
if (halfCheck) {
result.check = 5;
} else {
nextExpiration.setMonth(current.getMonth() + 6);
result.check = 6;
result.count += 1;
}
break;
case 'Y': // 年度
const yearCheck = current < new Date(current.getFullYear(), expiration.getMonth(), expiration.getDate());
// 如果当前时间比这一年有效期早,则到期为本年
if (yearCheck) {
nextExpiration.setFullYear(current.getFullYear());
result.check = 7;
} else {
// 否则推到下一年
nextExpiration.setFullYear(current.getFullYear() + 1);
result.check = 8;
}
break;
default:
throw new Error("Invalid billing cycle");
}
// 保持原到期时间的时分秒
nextExpiration.setHours(expiration.getHours());
nextExpiration.setMinutes(expiration.getMinutes());
nextExpiration.setSeconds(expiration.getSeconds());
result.date = nextExpiration;
return result;
},
getAutoRenewalStartDate(flag, startDate, check, count) {
//1.判断什么时候改变 2.如何改变
const start = new Date(startDate);
const current = this.getAdjustTimezone(start, new Date());
// 检查 startDate 格式是否有效,若无效返回 null
if (isNaN(start.getTime())) {
console.error("getAutoRenewalStartDate: Invalid startDate format");
}
// 如果 flag 为 0直接返回开始日期
if (flag === 0) {
return start;
}
// 初始化新的开始日期
const newStart = new Date(start);
switch (check) {
case 1: // 处理月份:设置为上个月
newStart.setFullYear(current.getFullYear());
newStart.setMonth(current.getMonth() - 1);
break;
case 2: // 处理月份:设置为当前月
newStart.setFullYear(current.getFullYear());
newStart.setMonth(current.getMonth());
break;
case 3:
case 4: // 处理季度
newStart.setFullYear(current.getFullYear());
newStart.setMonth(start.getMonth() + 3 * count);
break;
case 5:
case 6: // 处理半年
newStart.setFullYear(current.getFullYear());
newStart.setMonth(start.getMonth() + 6 * count);
break;
case 7: // 处理年份:设置为上一年
newStart.setFullYear(current.getFullYear() - 1);
break;
case 8: // 处理年份:保持当前年份
newStart.setFullYear(current.getFullYear());
break;
default: // 默认处理:直接返回
return;
}
return newStart;
},
getRemainingFormat(online, note) {
if (!note) return null;
const startDate = this.getNoteElementValue(note, "billingDataMod", "startDate");
const endDate = this.getNoteElementValue(note, "billingDataMod", "endDate");
// 检查 startDate 和 endDate 是否有效
if (!startDate || !endDate || typeof startDate !== 'string' || typeof endDate !== 'string') {
console.error("getRemainingFormat: Invalid startDate or endDate in note");
return null; // 如果无效,返回 null 或其他错误处理逻辑
}
//处理特殊时间格式
if (startDate.includes('0000-00-00') || endDate.includes("0000-00-00")) {
return this.formatPercents(online, this.toFixed2(100));
} else {
const percent = this.getRemainingPercent(startDate, endDate, note);
return this.formatPercents(online, this.toFixed2(percent));
}
},
getRemainingDays(endDate, note) {
// 检查 endDate 是否有效
if (!endDate || typeof endDate !== 'string') {
console.error("getRemainingDays: Invalid endDate format");
return null;
}
// 处理特殊时间格式
if (endDate.includes("0000-00-00")) return "lifetime";
// 获取当前时间,并调整时区
const currentTime = this.getAdjustTimezone(new Date(endDate), new Date());
// 获取计费周期和自动续订日期
const billingCycle = this.getNoteElementValue(note, "billingDataMod", "cycle") || "月";
const autoEndDate = this.getAutoRenewalEndDate(
this.getNoteElementValue(note, "billingDataMod", "endDate"),
this.getBillingCycle(billingCycle)
);
// 检查 autoRenewal 状态
const autoRenewal = this.getNoteElementValue(note, "billingDataMod", "autoRenewal") == 1;
// 确定到期时间
const end = autoRenewal ? autoEndDate.date : new Date(endDate);
// 计算剩余天数
const timeDiff = end - currentTime;
const daysDiff = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
return daysDiff;
},
getRemainingPercent(startDate, endDate, note) {
// 检查 startDate 和 endDate 是否为有效字符串并处理特殊格式
if (typeof startDate !== 'string' || typeof endDate !== 'string') {
console.error("getRemainingPercent: Invalid startDate or endDate format");
return null;
}
if (startDate.includes("0000-00-00") || endDate.includes("0000-00-00")) return 100;
// 获取当前时间并调整时区
const now = this.getAdjustTimezone(new Date(endDate), new Date());
// 获取计费周期
const billingCycle = this.getNoteElementValue(note, "billingDataMod", "cycle") || "月";
// 自动获取结束日期
const autoEndDate = this.getAutoRenewalEndDate(
this.getNoteElementValue(note, "billingDataMod", "endDate"),
this.getBillingCycle(billingCycle)
);
// 自动获取开始日期
const autoStartDate = autoEndDate.flag == 1
? this.getAutoRenewalStartDate(autoEndDate.flag, this.getNoteElementValue(note, "billingDataMod", "startDate"), autoEndDate.check, autoEndDate.count)
: new Date(startDate);
// 计算开始和结束时间
const autoRenewal = this.getNoteElementValue(note, "billingDataMod", "autoRenewal") == 1;
const start = autoRenewal ? autoStartDate : new Date(startDate);
const end = autoRenewal ? autoEndDate.date : new Date(endDate);
// 计算剩余百分比
if (now < start) {
return 0;
}
if (now >= end) {
return 100;
}
const totalDuration = end - start;
const elapsedDuration = now - start;
const percent = (elapsedDuration / totalDuration) * 100;
// 确保百分比在 0-100 之间
return Math.min(Math.max(percent, 0), 100);
}
}
})

View File

@ -15,20 +15,9 @@
</div>
<nav id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/"><i class="home icon"></i>{{tr "Home" }}</a></li>
<template v-if="isMobile">
<li class="dropdown">
<a data-toggle="dropdown"><i class="bi bi-gear-wide-connected" style="position:relative;top:1px;margin-right:3px;font-size:1.1rem;"></i>{{tr "Feature" }}<b class="caret"></b></a>
<ul class="dropdown-menu" style="min-width:100px;">
<li><a href="/service"><i class="rss icon"></i>{{tr "Services" }}</a></li>
<li><a href="/network"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a></li>
</ul>
</li>
</template>
<template v-else>
<li><a href="/service"><i class="rss icon"></i>{{tr "Services" }}</a></li>
<li><a href="/network"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a></li>
</template>
<li :class="isMenuActive('index')"><a href="/"><i class="home icon"></i>{{tr "Home" }}</a></li>
<li :class="isMenuActive('service')"><a href="/service"><i class="rss icon"></i>{{tr "Services" }}</a></li>
<li :class="isMenuActive('network')"><a href="/network"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a></li>
{{ if not .Conf.DisableSwitchTemplateInFrontend }}
<li class="dropdown">
<a data-toggle="dropdown"><i class="bi bi-incognito" style="position:relative;top:1px;margin-right:3px;font-size:1.2rem;vertical-align:top;"></i>{{tr "Template" }}<b class="caret"></b></a>