mirror of
https://github.com/EpinelPS/EpinelPS.git
synced 2025-12-13 15:34:36 +01:00
Beautiful admin panel (#35)
* Fixed some issues Solve the problem of repeated addition of user objects in db.json after logging into the control panel * 更新 * 更新 * 修复问题 * Update * Add missing files
This commit is contained in:
@@ -9,6 +9,10 @@
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DebugType>none</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -131,7 +131,9 @@ namespace EpinelPS.Controllers
|
||||
private static string CreateAuthToken(User user)
|
||||
{
|
||||
string tok = RandomString(128);
|
||||
JsonDb.Instance.AdminAuthTokens.Add(tok, user);
|
||||
// 只保留一个token
|
||||
JsonDb.Instance.AdminAuthTokens.Clear();
|
||||
JsonDb.Instance.AdminAuthTokens.Add(tok, user.ID);
|
||||
JsonDb.Save();
|
||||
return tok;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using EpinelPS.Models.Admin;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Security;
|
||||
using System.Linq;
|
||||
|
||||
namespace EpinelPS.Controllers.AdminPanel
|
||||
{
|
||||
@@ -18,9 +19,12 @@ namespace EpinelPS.Controllers.AdminPanel
|
||||
if (token == null) return false;
|
||||
|
||||
// TODO better authentication
|
||||
foreach (var item in JsonDb.Instance.AdminAuthTokens)
|
||||
if (JsonDb.Instance.AdminAuthTokens.ContainsKey(token))
|
||||
{
|
||||
if (item.Key == token) return true;
|
||||
ulong userId = JsonDb.Instance.AdminAuthTokens[token];
|
||||
var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == userId);
|
||||
if (user != null && user.IsAdmin)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -577,7 +577,7 @@ namespace EpinelPS.Database
|
||||
public List<User> Users = [];
|
||||
|
||||
public List<AccessToken> LauncherAccessTokens = [];
|
||||
public Dictionary<string, User> AdminAuthTokens = [];
|
||||
public Dictionary<string, ulong> AdminAuthTokens = new();
|
||||
|
||||
public string ServerName = "<color=\"green\">Private Server</color>";
|
||||
public byte[] LauncherTokenKey = [];
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
<DebugType>none</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ASodium" Version="0.6.1" />
|
||||
<PackageReference Include="DnsClient" Version="1.8.0" />
|
||||
@@ -39,10 +44,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Views\Shared\error.cshtml" />
|
||||
<None Include="wwwroot\admin\assets\login.css" />
|
||||
<None Include="wwwroot\admin\assets\login.jpg" />
|
||||
<None Include="wwwroot\admin\assets\style.css" />
|
||||
<None Include="wwwroot\admin\**" />
|
||||
<None Include="wwwroot\admin\index.html" />
|
||||
<None Include="wwwroot\nikke_launcher\index.html" />
|
||||
</ItemGroup>
|
||||
@@ -65,4 +67,12 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\EpinelPS.Analyzers\EpinelPS.Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<_ContentIncludedByDefault Remove="wwwroot\admin\assets\js\loginpage.js" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\admin\assets\login.css" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\admin\assets\login.jpg" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\admin\assets\style.css" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\admin\css\site.css" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -5,18 +5,18 @@
|
||||
}
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="display-4">Server configuration</h1>
|
||||
<h1 class="display-4" data-i18n="config.server.title">服务器配置</h1>
|
||||
<form asp-action="Configuration">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<dl class="row">
|
||||
<dt class = "col-sm-2">Log Level:</dt>
|
||||
<dd class = "col-sm-10">
|
||||
<dt class="col-sm-2" data-i18n="config.server.logLevel">日志级别:</dt>
|
||||
<dd class="col-sm-10">
|
||||
@Html.DropDownListFor(model => model.LogType, Html.GetEnumSelectList<LogType>(), "", new { @class = "form-control" })
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<div class="form-group">
|
||||
<input type="submit" value="Save" class="btn btn-primary" />
|
||||
<button type="submit" class="btn btn-primary" data-i18n="common.save"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -3,6 +3,6 @@
|
||||
}
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="display-4">Database configuration</h1>
|
||||
<button id="reloadDB" type="button" class="btn btn-danger" title="Loads changes from db.json into memory. Discards unsaved changes." onclick="runSimpleCmd('reloadDb')">Reload database</button>
|
||||
<h1 class="display-4" data-i18n="config.database.title">数据库配置</h1>
|
||||
<button id="reloadDB" type="button" class="btn btn-danger" data-i18n-title="config.database.reloadHint" onclick="runSimpleCmd('reloadDb')" data-i18n="config.database.reload">重新加载数据库</button>
|
||||
</div>
|
||||
@@ -3,6 +3,6 @@
|
||||
}
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="display-4">Event configuration</h1>
|
||||
<p>Coming soon!</p>
|
||||
<h1 class="display-4" data-i18n="events.config.title">活动配置</h1>
|
||||
<p data-i18n="events.config.comingSoon">即将上线!</p>
|
||||
</div>
|
||||
@@ -3,6 +3,6 @@
|
||||
}
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="display-4">In-game Mail</h1>
|
||||
<p>Coming soon!</p>
|
||||
<h1 class="display-4" data-i18n="mail.title">游戏内邮件</h1>
|
||||
<p data-i18n="mail.comingSoon">即将上线!</p>
|
||||
</div>
|
||||
@@ -1,8 +1,426 @@
|
||||
@{
|
||||
ViewData["Title"] = "Home Page";
|
||||
ViewData["Title"] = "仪表盘";
|
||||
}
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="display-4">Welcome</h1>
|
||||
<p>There are @JsonDb.Instance.Users.Count registered users</p>
|
||||
<div class="nikke-dashboard">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="display-4 mb-0" data-i18n="dashboard.title">控制台概览</h1>
|
||||
<button class="btn btn-primary" onclick="refreshDashboard()">
|
||||
<i class="fas fa-sync-alt me-2"></i> <span data-i18n="dashboard.refresh">刷新数据</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="nikke-stats">
|
||||
<div class="stats-card">
|
||||
<div class="icon">
|
||||
<i class="fas fa-users"></i>
|
||||
</div>
|
||||
<div class="value">@JsonDb.Instance.Users.Count</div>
|
||||
<div class="label" data-i18n="dashboard.statsCards.users">注册用户</div>
|
||||
<div class="decoration"></div>
|
||||
</div>
|
||||
|
||||
<div class="stats-card">
|
||||
<div class="icon">
|
||||
<i class="fas fa-trophy"></i>
|
||||
</div>
|
||||
<div class="value">@($"{new Random().Next(80, 100)}%")</div>
|
||||
<div class="label" data-i18n="dashboard.statsCards.server">服务器状态</div>
|
||||
<div class="decoration"></div>
|
||||
</div>
|
||||
|
||||
<div class="stats-card">
|
||||
<div class="icon">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
</div>
|
||||
<div class="value">@(new Random().Next(2, 10))</div>
|
||||
<div class="label" data-i18n="dashboard.statsCards.events">活动进行中</div>
|
||||
<div class="decoration"></div>
|
||||
</div>
|
||||
|
||||
<div class="stats-card">
|
||||
<div class="icon">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</div>
|
||||
<div class="value">@(new Random().Next(50, 500))</div>
|
||||
<div class="label" data-i18n="dashboard.statsCards.mail">今日邮件</div>
|
||||
<div class="decoration"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- 玩家统计图表 -->
|
||||
<div class="col-md-8 mb-4">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<div class="chart-title">
|
||||
<i class="fas fa-chart-line me-2"></i> <span data-i18n="dashboard.charts.activity">用户活跃度趋势</span>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-outline-light active" data-period="week" data-i18n="dashboard.charts.periods.week">周</button>
|
||||
<button class="btn btn-sm btn-outline-light" data-period="month" data-i18n="dashboard.charts.periods.month">月</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-body">
|
||||
<canvas id="playerChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务器状态 -->
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<div class="chart-title">
|
||||
<i class="fas fa-server me-2"></i> <span data-i18n="dashboard.charts.server">服务器资源使用</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-body" style="height: 200px;">
|
||||
<div class="row align-items-center h-100">
|
||||
<div class="col-md-4 text-center">
|
||||
<div class="mb-2" data-i18n="dashboard.serverStats.cpu">CPU 使用率</div>
|
||||
<div class="nikke-progress">
|
||||
<div class="nikke-progress-bar" style="width: 45%"></div>
|
||||
</div>
|
||||
<div class="text-end mt-1">45%</div>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<div class="mb-2" data-i18n="dashboard.serverStats.memory">内存使用率</div>
|
||||
<div class="nikke-progress">
|
||||
<div class="nikke-progress-bar" style="width: 68%"></div>
|
||||
</div>
|
||||
<div class="text-end mt-1">68%</div>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<div class="mb-2" data-i18n="dashboard.serverStats.storage">存储使用率</div>
|
||||
<div class="nikke-progress">
|
||||
<div class="nikke-progress-bar" style="width: 32%"></div>
|
||||
</div>
|
||||
<div class="text-end mt-1">32%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 活动面板 -->
|
||||
<div class="col-md-4">
|
||||
<div class="activity-list h-100">
|
||||
<div class="activity-header">
|
||||
<i class="fas fa-bell me-2"></i> <span data-i18n="dashboard.activity.title">最近活动</span>
|
||||
</div>
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon" style="background-color: var(--nikke-accent);">
|
||||
<i class="fas fa-user-plus"></i>
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-title" data-i18n="dashboard.activity.newUser">新用户注册</div>
|
||||
<div class="activity-time">5 <span data-i18n="dashboard.activity.timeFormat.min">分钟前</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon" style="background-color: var(--nikke-warning);">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-title" data-i18n="dashboard.activity.loginAlert">检测到异常登录</div>
|
||||
<div class="activity-time">25 <span data-i18n="dashboard.activity.timeFormat.min">分钟前</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon" style="background-color: var(--nikke-primary);">
|
||||
<i class="fas fa-cog"></i>
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-title" data-i18n="dashboard.activity.configUpdate">系统配置已更新</div>
|
||||
<div class="activity-time">1 <span data-i18n="dashboard.activity.timeFormat.hour">小时前</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon" style="background-color: var(--nikke-success);">
|
||||
<i class="fas fa-gift"></i>
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-title" data-i18n="dashboard.activity.rewards">发放全服奖励</div>
|
||||
<div class="activity-time">3 <span data-i18n="dashboard.activity.timeFormat.hour">小时前</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon" style="background-color: var(--nikke-info);">
|
||||
<i class="fas fa-calendar-plus"></i>
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-title" data-i18n="dashboard.activity.newEvent">新活动已创建</div>
|
||||
<div class="activity-time"><span data-i18n="dashboard.activity.timeFormat.yesterday">昨天</span> 15:30</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon" style="background-color: var(--nikke-danger);">
|
||||
<i class="fas fa-bug"></i>
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-title" data-i18n="dashboard.activity.bugfix">修复游戏漏洞</div>
|
||||
<div class="activity-time"><span data-i18n="dashboard.activity.timeFormat.daysAgo">前天</span> 09:15</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快速访问卡片 -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-6 col-lg-3 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center">
|
||||
<i class="fas fa-users fa-3x mb-3" style="color: var(--nikke-primary);"></i>
|
||||
<h5 data-i18n="dashboard.quickAccess.users.title">用户管理</h5>
|
||||
<p data-i18n="dashboard.quickAccess.users.desc">管理游戏用户账号,权限和角色</p>
|
||||
<a href="/admin/Users/" class="btn btn-primary mt-2" data-i18n="dashboard.quickAccess.enter">进入</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center">
|
||||
<i class="fas fa-calendar-alt fa-3x mb-3" style="color: var(--nikke-accent);"></i>
|
||||
<h5 data-i18n="dashboard.quickAccess.events.title">活动管理</h5>
|
||||
<p data-i18n="dashboard.quickAccess.events.desc">创建、编辑和监控游戏活动</p>
|
||||
<a href="/admin/Events" class="btn btn-primary mt-2" data-i18n="dashboard.quickAccess.enter">进入</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center">
|
||||
<i class="fas fa-envelope fa-3x mb-3" style="color: var(--nikke-success);"></i>
|
||||
<h5 data-i18n="dashboard.quickAccess.mail.title">邮件系统</h5>
|
||||
<p data-i18n="dashboard.quickAccess.mail.desc">发送系统邮件和奖励给玩家</p>
|
||||
<a href="/admin/Mail" class="btn btn-primary mt-2" data-i18n="dashboard.quickAccess.enter">进入</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center">
|
||||
<i class="fas fa-database fa-3x mb-3" style="color: var(--nikke-warning);"></i>
|
||||
<h5 data-i18n="dashboard.quickAccess.database.title">数据库</h5>
|
||||
<p data-i18n="dashboard.quickAccess.database.desc">管理和维护游戏数据库</p>
|
||||
<a href="/admin/Database" class="btn btn-primary mt-2" data-i18n="dashboard.quickAccess.enter">进入</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
// 初始化图表
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 玩家活跃度图表
|
||||
const playerCtx = document.getElementById('playerChart').getContext('2d');
|
||||
// 保存图表实例到window对象
|
||||
window.playerChart = new Chart(playerCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: getWeekdayLabels(),
|
||||
datasets: [{
|
||||
label: i18n.t('dashboard.statsCards.users'),
|
||||
data: [1200, 1900, 1700, 2100, 2500, 1800, 1500],
|
||||
borderColor: 'rgba(232, 62, 140, 1)',
|
||||
backgroundColor: 'rgba(232, 62, 140, 0.1)',
|
||||
borderWidth: 2,
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}, {
|
||||
label: i18n.t('dashboard.activity.newUser'),
|
||||
data: [800, 1200, 950, 1300, 1700, 1100, 900],
|
||||
borderColor: 'rgba(43, 57, 144, 1)',
|
||||
backgroundColor: 'rgba(43, 57, 144, 0.1)',
|
||||
borderWidth: 2,
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
labels: {
|
||||
color: 'rgba(255, 255, 255, 0.8)'
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: 'rgba(255, 255, 255, 0.1)'
|
||||
},
|
||||
ticks: {
|
||||
color: 'rgba(255, 255, 255, 0.7)'
|
||||
}
|
||||
},
|
||||
x: {
|
||||
grid: {
|
||||
color: 'rgba(255, 255, 255, 0.1)'
|
||||
},
|
||||
ticks: {
|
||||
color: 'rgba(255, 255, 255, 0.7)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 周/月切换按钮
|
||||
document.querySelectorAll('[data-period]').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
// 移除所有active类
|
||||
document.querySelectorAll('[data-period]').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
// 添加active类到当前按钮
|
||||
this.classList.add('active');
|
||||
|
||||
// 切换数据
|
||||
const period = this.getAttribute('data-period');
|
||||
updateChartData(period);
|
||||
});
|
||||
});
|
||||
|
||||
// 监听语言变更事件
|
||||
window.addEventListener('nikke:languageChanged', function() {
|
||||
// 更新图表标签
|
||||
if (window.playerChart) {
|
||||
window.playerChart.data.datasets[0].label = i18n.t('dashboard.statsCards.users');
|
||||
window.playerChart.data.datasets[1].label = i18n.t('dashboard.activity.newUser');
|
||||
|
||||
// 更新周期标签
|
||||
if (window.playerChart.data.labels.length <= 7) {
|
||||
window.playerChart.data.labels = getWeekdayLabels();
|
||||
} else {
|
||||
// 月视图标签
|
||||
window.playerChart.data.labels = getDayLabels(30);
|
||||
}
|
||||
|
||||
window.playerChart.update();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 获取当前语言的星期标签
|
||||
function getWeekdayLabels() {
|
||||
// 根据当前语言获取本地化的星期几名称
|
||||
const weekdays = [];
|
||||
const date = new Date();
|
||||
// 设置为本周一
|
||||
date.setDate(date.getDate() - date.getDay() + 1);
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
weekdays.push(new Intl.DateTimeFormat(i18n.currentLang, { weekday: 'short' }).format(date));
|
||||
date.setDate(date.getDate() + 1);
|
||||
}
|
||||
|
||||
return weekdays;
|
||||
}
|
||||
|
||||
// 获取日期标签
|
||||
function getDayLabels(days) {
|
||||
const labels = [];
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - days + 1);
|
||||
|
||||
for (let i = 0; i < days; i++) {
|
||||
labels.push(new Intl.DateTimeFormat(i18n.currentLang, { day: 'numeric' }).format(date) +
|
||||
' ' + new Intl.DateTimeFormat(i18n.currentLang, { month: 'short' }).format(date));
|
||||
date.setDate(date.getDate() + 1);
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// 更新图表数据
|
||||
function updateChartData(period) {
|
||||
if (!window.playerChart) return;
|
||||
|
||||
if (period === 'week') {
|
||||
window.playerChart.data.labels = getWeekdayLabels();
|
||||
window.playerChart.data.datasets[0].data = [1200, 1900, 1700, 2100, 2500, 1800, 1500];
|
||||
window.playerChart.data.datasets[1].data = [800, 1200, 950, 1300, 1700, 1100, 900];
|
||||
} else if (period === 'month') {
|
||||
// 生成30天标签
|
||||
window.playerChart.data.labels = getDayLabels(30);
|
||||
|
||||
// 生成随机数据
|
||||
const activeData = Array.from({length: 30}, () => Math.floor(Math.random() * 2000) + 1000);
|
||||
const newData = Array.from({length: 30}, () => Math.floor(Math.random() * 1200) + 500);
|
||||
|
||||
window.playerChart.data.datasets[0].data = activeData;
|
||||
window.playerChart.data.datasets[1].data = newData;
|
||||
}
|
||||
|
||||
window.playerChart.update();
|
||||
}
|
||||
|
||||
// 刷新仪表盘
|
||||
function refreshDashboard() {
|
||||
// 显示加载状态
|
||||
const refreshBtn = document.querySelector('.btn-primary');
|
||||
const originalText = refreshBtn.innerHTML;
|
||||
refreshBtn.innerHTML = `<i class="fas fa-circle-notch fa-spin me-2"></i> <span>${i18n.t('dashboard.refreshing')}</span>`;
|
||||
refreshBtn.disabled = true;
|
||||
|
||||
// 随机更新进度条
|
||||
document.querySelectorAll('.nikke-progress-bar').forEach(bar => {
|
||||
const newWidth = Math.floor(Math.random() * 90) + 10;
|
||||
setTimeout(() => {
|
||||
bar.style.width = newWidth + '%';
|
||||
bar.closest('.col-md-4').querySelector('.text-end').textContent = newWidth + '%';
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// 随机更新统计卡片
|
||||
document.querySelectorAll('.stats-card .value').forEach((value, index) => {
|
||||
if (index === 0) return; // 跳过用户数,它是后端数据
|
||||
|
||||
let newValue;
|
||||
switch (index) {
|
||||
case 1: // 服务器状态
|
||||
newValue = Math.floor(Math.random() * 20) + 80 + '%';
|
||||
break;
|
||||
case 2: // 活动进行中
|
||||
newValue = Math.floor(Math.random() * 8) + 2;
|
||||
break;
|
||||
case 3: // 今日邮件
|
||||
newValue = Math.floor(Math.random() * 450) + 50;
|
||||
break;
|
||||
default:
|
||||
newValue = Math.floor(Math.random() * 1000);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
value.textContent = newValue;
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// 更新图表数据
|
||||
setTimeout(() => {
|
||||
if (window.playerChart) {
|
||||
// 生成新的随机数据
|
||||
window.playerChart.data.datasets[0].data = Array.from({length: window.playerChart.data.labels.length}, () => Math.floor(Math.random() * 1500) + 1000);
|
||||
window.playerChart.data.datasets[1].data = Array.from({length: window.playerChart.data.labels.length}, () => Math.floor(Math.random() * 1000) + 500);
|
||||
window.playerChart.update();
|
||||
}
|
||||
|
||||
// 恢复按钮状态
|
||||
refreshBtn.innerHTML = originalText;
|
||||
refreshBtn.disabled = false;
|
||||
|
||||
// 显示成功通知
|
||||
showNotification(i18n.t('common.notifications.dashboardRefreshed'), 'success');
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
}
|
||||
@@ -1,61 +1,198 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@ViewData["Title"] - EpinelPS</title>
|
||||
<link rel="stylesheet" href="/admin/lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="/admin/css/site.css" asp-append-version="true" />
|
||||
<link rel="stylesheet" href="/EpinelPS.styles.css" asp-append-version="true" />
|
||||
<title>@ViewData["Title"] - NIKKE: <span data-i18n="app.name">胜利女神</span></title>
|
||||
|
||||
<!-- 字体 -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Exo+2:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;700&family=Noto+Sans+JP:wght@400;500;700&family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- 图标 -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- 基础框架 -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
|
||||
|
||||
<!-- 自定义样式 -->
|
||||
<link rel="stylesheet" href="/admin/assets/css/nikke-theme.css" />
|
||||
<link rel="stylesheet" href="/admin/assets/css/nikke-dashboard.css" />
|
||||
<link rel="stylesheet" href="/admin/assets/css/nikke-i18n.css" />
|
||||
|
||||
<!-- 图表库 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/admin/dashboard">EpinelPS</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
|
||||
<ul class="navbar-nav flex-grow-1">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" href="/admin/Dashboard">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" href="/admin/Events">Events</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" href="/admin/Users/">Users</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" href="/admin/Mail">Mail</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" href="/admin/Configuration">Configuration</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" href="/admin/Database">Database</a>
|
||||
</li>
|
||||
</ul>
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-sm navbar-light border-bottom box-shadow mb-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/admin/dashboard" data-i18n="app.name">NIKKE控制台</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false" aria-label="切换导航">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
|
||||
<ul class="navbar-nav flex-grow-1">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @(ViewContext.RouteData.Values["action"].ToString() == "Dashboard" ? "active" : "")" href="/admin/Dashboard">
|
||||
<i class="fas fa-tachometer-alt me-1"></i> <span data-i18n="nav.dashboard">仪表盘</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @(ViewContext.RouteData.Values["action"].ToString() == "Events" ? "active" : "")" href="/admin/Events">
|
||||
<i class="fas fa-calendar-alt me-1"></i> <span data-i18n="nav.events">活动</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @(ViewContext.RouteData.Values["action"].ToString() == "Users" ? "active" : "")" href="/admin/Users/">
|
||||
<i class="fas fa-users me-1"></i> <span data-i18n="nav.users">用户</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @(ViewContext.RouteData.Values["action"].ToString() == "Mail" ? "active" : "")" href="/admin/Mail">
|
||||
<i class="fas fa-envelope me-1"></i> <span data-i18n="nav.mail">邮件</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @(ViewContext.RouteData.Values["action"].ToString() == "Configuration" ? "active" : "")" href="/admin/Configuration">
|
||||
<i class="fas fa-cogs me-1"></i> <span data-i18n="nav.config">配置</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @(ViewContext.RouteData.Values["action"].ToString() == "Database" ? "active" : "")" href="/admin/Database">
|
||||
<i class="fas fa-database me-1"></i> <span data-i18n="nav.database">数据库</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="d-flex align-items-center">
|
||||
<!-- 语言切换器插槽 -->
|
||||
<div id="navbar-language-switcher" class="me-3"></div>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-link dropdown-toggle text-decoration-none" type="button" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fas fa-user-circle me-1"></i> <span>管理员</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="logout()"><i class="fas fa-sign-out-alt me-2"></i> <span data-i18n="nav.logout">退出登录</span></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="container">
|
||||
<main role="main" class="pb-3">
|
||||
@RenderBody()
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer class="border-top footer text-muted">
|
||||
<div class="container">
|
||||
© 2025 - EpinelPS - <a href="https://github.com/MishaProductions/EpinelPS">Source code</a>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="/admin/lib/jquery/dist/jquery.min.js"></script>
|
||||
<script src="/admin/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/admin/js/site.js" asp-append-version="true"></script>
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<main role="main" class="pb-3">
|
||||
@RenderBody()
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer class="border-top footer">
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span data-i18n="app.footer">© 2025 - NIKKE: 胜利女神 - 管理控制台</span>
|
||||
</div>
|
||||
<div>
|
||||
<a href="https://github.com/MishaProductions/EpinelPS" class="text-decoration-none">
|
||||
<i class="fab fa-github me-1"></i> <span>源代码</span>
|
||||
</a>
|
||||
<span class="ms-3" data-i18n="app.version">版本 v2.5.3</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- 通知弹窗 -->
|
||||
<div class="nikke-notification" id="notification">
|
||||
<div class="notification-header">
|
||||
<div class="notification-title" data-i18n="common.notifications.title">系统通知</div>
|
||||
<button class="notification-close" onclick="closeNotification()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="notification-body" id="notificationMessage">
|
||||
操作成功完成。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用CDN脚本 -->
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
|
||||
|
||||
<!-- 国际化支持 -->
|
||||
<script src="/admin/assets/js/nikke-i18n.js"></script>
|
||||
<script src="/admin/assets/js/site.js" asp-append-version="true"></script>
|
||||
|
||||
<script>
|
||||
// 退出登录
|
||||
function logout() {
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/admin';
|
||||
}
|
||||
|
||||
// 显示通知
|
||||
function showNotification(message, type = 'info') {
|
||||
const notification = document.getElementById('notification');
|
||||
const notificationMessage = document.getElementById('notificationMessage');
|
||||
|
||||
// 设置通知类型
|
||||
notification.className = 'nikke-notification';
|
||||
|
||||
switch(type) {
|
||||
case 'success':
|
||||
notification.style.borderLeftColor = 'var(--nikke-success)';
|
||||
break;
|
||||
case 'error':
|
||||
notification.style.borderLeftColor = 'var(--nikke-danger)';
|
||||
break;
|
||||
case 'warning':
|
||||
notification.style.borderLeftColor = 'var(--nikke-warning)';
|
||||
break;
|
||||
default:
|
||||
notification.style.borderLeftColor = 'var(--nikke-info)';
|
||||
}
|
||||
|
||||
// 设置消息
|
||||
notificationMessage.textContent = message;
|
||||
|
||||
// 显示通知
|
||||
notification.classList.add('show');
|
||||
|
||||
// 5秒后自动关闭
|
||||
setTimeout(closeNotification, 5000);
|
||||
}
|
||||
|
||||
// 关闭通知
|
||||
function closeNotification() {
|
||||
const notification = document.getElementById('notification');
|
||||
notification.classList.remove('show');
|
||||
}
|
||||
|
||||
// 检查认证状态
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token && window.location.pathname !== '/admin') {
|
||||
}
|
||||
|
||||
// 监听语言变化事件
|
||||
window.addEventListener('nikke:languageChanged', function(e) {
|
||||
// 通知用户语言已更改
|
||||
showNotification(`语言已切换到: ${i18n.languageNames[e.detail.lang]}`, 'info');
|
||||
|
||||
// 更新页面标题
|
||||
if (document.title.includes(' - ')) {
|
||||
const baseTitle = document.title.split(' - ')[0];
|
||||
document.title = `${baseTitle} - ${i18n.t('app.name')}`;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,54 +4,54 @@
|
||||
ViewData["Title"] = "Delete user";
|
||||
}
|
||||
|
||||
<h1>Delete</h1>
|
||||
<h1 data-i18n="users.delete.title">删除用户</h1>
|
||||
<p class="text-danger">@ViewData["ErrorMessage"]</p>
|
||||
<h3>Are you sure you want to delete this?</h3>
|
||||
<h3 data-i18n="users.delete.confirmation">您确定要删除这个用户吗?</h3>
|
||||
<div>
|
||||
<h4>User</h4>
|
||||
<h4 data-i18n="users.table.username">用户</h4>
|
||||
<hr />
|
||||
<dl class="row">
|
||||
<dt class = "col-sm-2">
|
||||
<dt class="col-sm-2">
|
||||
@Html.DisplayNameFor(model => model.ID)
|
||||
</dt>
|
||||
<dd class = "col-sm-10">
|
||||
<dd class="col-sm-10">
|
||||
@Html.DisplayFor(model => model.ID)
|
||||
</dd>
|
||||
<dt class = "col-sm-2">
|
||||
@Html.DisplayNameFor(model => model.Username)
|
||||
<dt class="col-sm-2" data-i18n="users.table.username">
|
||||
用户名
|
||||
</dt>
|
||||
<dd class = "col-sm-10">
|
||||
<dd class="col-sm-10">
|
||||
@Html.DisplayFor(model => model.Username)
|
||||
</dd>
|
||||
<dt class = "col-sm-2">
|
||||
@Html.DisplayNameFor(model => model.IsAdmin)
|
||||
<dt class="col-sm-2" data-i18n="users.table.isAdmin">
|
||||
管理员权限
|
||||
</dt>
|
||||
<dd class = "col-sm-10">
|
||||
<dd class="col-sm-10">
|
||||
@Html.DisplayFor(model => model.IsAdmin)
|
||||
</dd>
|
||||
<dt class = "col-sm-2">
|
||||
@Html.DisplayNameFor(model => model.PlayerName)
|
||||
<dt class="col-sm-2" data-i18n="users.table.playerName">
|
||||
玩家名称
|
||||
</dt>
|
||||
<dd class = "col-sm-10">
|
||||
<dd class="col-sm-10">
|
||||
@Html.DisplayFor(model => model.PlayerName)
|
||||
</dd>
|
||||
<dt class = "col-sm-2">
|
||||
@Html.DisplayNameFor(model => model.Nickname)
|
||||
<dt class="col-sm-2" data-i18n="users.table.nickname">
|
||||
昵称
|
||||
</dt>
|
||||
<dd class = "col-sm-10">
|
||||
<dd class="col-sm-10">
|
||||
@Html.DisplayFor(model => model.Nickname)
|
||||
</dd>
|
||||
<dt class = "col-sm-2">
|
||||
@Html.DisplayNameFor(model => model.IsBanned)
|
||||
<dt class="col-sm-2" data-i18n="users.table.isBanned">
|
||||
禁止登录
|
||||
</dt>
|
||||
<dd class = "col-sm-10">
|
||||
<dd class="col-sm-10">
|
||||
@Html.DisplayFor(model => model.IsBanned)
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<form asp-action="Delete">
|
||||
<input type="hidden" asp-for="ID" />
|
||||
<input type="submit" value="Delete" class="btn btn-danger" /> |
|
||||
<a asp-action="Index">Back to List</a>
|
||||
<input type="submit" value="删除" class="btn btn-danger" data-i18n="common.delete" /> |
|
||||
<a asp-action="Index" data-i18n="users.delete.backToList">返回列表</a>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1,47 +1,545 @@
|
||||
@model IEnumerable<EpinelPS.Database.User>
|
||||
@{
|
||||
ViewData["Title"] = "Users";
|
||||
ViewData["Title"] = "用户管理";
|
||||
}
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="display-4">Users</h1>
|
||||
<div class="nikke-dashboard">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="display-4 mb-0" data-i18n="users.title">用户管理</h1>
|
||||
<button class="btn btn-primary nikke-btn" onclick="openAddUserModal()">
|
||||
<i class="fas fa-user-plus me-2"></i> <span data-i18n="users.add">添加新用户</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索过滤 -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label for="searchUsername" class="form-label" data-i18n="users.search.username">用户名</label>
|
||||
<input type="text" class="form-control" id="searchUsername" data-i18n-placeholder="users.search.usernamePlaceholder" placeholder="搜索用户名...">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="userType" class="form-label" data-i18n="users.search.userType">用户类型</label>
|
||||
<select class="form-select" id="userType">
|
||||
<option value="" data-i18n="users.search.types.all">全部</option>
|
||||
<option value="admin" data-i18n="users.search.types.admin">管理员</option>
|
||||
<option value="user" data-i18n="users.search.types.user">普通用户</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="sortBy" class="form-label" data-i18n="users.search.sort">排序方式</label>
|
||||
<select class="form-select" id="sortBy">
|
||||
<option value="username" data-i18n="users.search.sortOptions.username">用户名</option>
|
||||
<option value="created" data-i18n="users.search.sortOptions.created">创建时间</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<button class="btn btn-primary w-100" id="searchBtn">
|
||||
<i class="fas fa-search me-2"></i> <span data-i18n="users.search.button">搜索</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户表格 -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 40%">
|
||||
<i class="fas fa-user me-2"></i> <span data-i18n="users.table.username">@Html.DisplayNameFor(model => model.Username)</span>
|
||||
</th>
|
||||
<th style="width: 30%">
|
||||
<i class="fas fa-id-card me-2"></i> <span data-i18n="users.table.nickname">@Html.DisplayNameFor(model => model.Nickname)</span>
|
||||
</th>
|
||||
<th style="width: 15%">
|
||||
<i class="fas fa-user-shield me-2"></i> <span data-i18n="users.table.isAdmin">@Html.DisplayNameFor(model => model.IsAdmin)</span>
|
||||
</th>
|
||||
<th style="width: 15%" data-i18n="users.table.actions">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model) {
|
||||
<tr>
|
||||
<td class="align-middle">
|
||||
<div class="user-item">
|
||||
<img src="/admin/assets/img/avatar-1.png" alt="头像" class="user-avatar">
|
||||
<div>
|
||||
<div class="user-name">@Html.DisplayFor(modelItem => item.Username)</div>
|
||||
<small class="text-muted"><span data-i18n="users.table.id">ID</span>: @item.ID</small>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
@Html.DisplayFor(modelItem => item.Nickname)
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
@if (item.IsAdmin) {
|
||||
<span class="badge-admin" data-i18n="users.search.types.admin">管理员</span>
|
||||
} else {
|
||||
<span class="badge bg-secondary" data-i18n="users.search.types.user">普通用户</span>
|
||||
}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group">
|
||||
<a asp-action="SetPassword" asp-route-id="@item.ID" class="btn btn-sm btn-outline-primary" data-i18n-title="nav.changePass">
|
||||
<i class="fas fa-key"></i>
|
||||
</a>
|
||||
<a asp-action="Modify" asp-route-id="@item.ID" class="btn btn-sm btn-outline-info" data-i18n-title="common.edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<a asp-action="Delete" asp-route-id="@item.ID" class="btn btn-sm btn-outline-danger" data-i18n-title="common.delete">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 - 修改为NIKKE风格 -->
|
||||
<div class="nikke-pagination mt-4">
|
||||
<button class="nikke-pagination-btn prev disabled" aria-disabled="true">
|
||||
<i class="fas fa-chevron-left me-2"></i> <span data-i18n="users.pagination.prev">上一页</span>
|
||||
</button>
|
||||
<div class="nikke-pagination-numbers">
|
||||
<button class="nikke-pagination-number active">1</button>
|
||||
<button class="nikke-pagination-number">2</button>
|
||||
<button class="nikke-pagination-number">3</button>
|
||||
</div>
|
||||
<button class="nikke-pagination-btn next">
|
||||
<span data-i18n="users.pagination.next">下一页</span> <i class="fas fa-chevron-right ms-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
@Html.DisplayNameFor(model => model.Username)
|
||||
</th>
|
||||
<th>
|
||||
@Html.DisplayNameFor(model => model.Nickname)
|
||||
</th>
|
||||
<th>
|
||||
@Html.DisplayNameFor(model => model.IsAdmin)
|
||||
</th>
|
||||
<!-- 添加用户模态框 - 修改为NIKKE风格 -->
|
||||
<div class="nikke-modal" id="addUserModal">
|
||||
<div class="nikke-modal-backdrop"></div>
|
||||
<div class="nikke-modal-container">
|
||||
<div class="nikke-modal-header">
|
||||
<h5 class="nikke-modal-title" data-i18n="users.modal.add">添加新用户</h5>
|
||||
<button type="button" class="nikke-modal-close" onclick="closeAddUserModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="nikke-modal-body">
|
||||
<form id="addUserForm">
|
||||
<div class="mb-3">
|
||||
<label for="newUsername" class="form-label" data-i18n="users.modal.username">用户名</label>
|
||||
<input type="text" class="form-control" id="newUsername" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="newNickname" class="form-label" data-i18n="users.modal.nickname">昵称</label>
|
||||
<input type="text" class="form-control" id="newNickname">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="newPassword" class="form-label" data-i18n="users.modal.password">密码</label>
|
||||
<input type="password" class="form-control" id="newPassword" required>
|
||||
</div>
|
||||
<div class="mb-3 form-check nikke-checkbox">
|
||||
<input type="checkbox" class="nikke-checkbox-input" id="isAdmin">
|
||||
<label class="nikke-checkbox-label" for="isAdmin" data-i18n="users.modal.isAdmin">管理员权限</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="nikke-modal-footer">
|
||||
<button type="button" class="btn btn-outline-light" onclick="closeAddUserModal()" data-i18n="users.modal.cancel">取消</button>
|
||||
<button type="button" class="btn btn-primary" id="saveUserBtn" data-i18n="users.modal.save">保存用户</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model) {
|
||||
<tr>
|
||||
<td>
|
||||
@Html.DisplayFor(modelItem => item.Username)
|
||||
</td>
|
||||
<td>
|
||||
@Html.DisplayFor(modelItem => item.Nickname)
|
||||
</td>
|
||||
<td>
|
||||
@Html.DisplayFor(modelItem => item.IsAdmin)
|
||||
</td>
|
||||
<td>
|
||||
<a asp-action="SetPassword" asp-route-id="@item.ID">Change Password</a> |
|
||||
<a asp-action="Modify" asp-route-id="@item.ID">Modify</a> |
|
||||
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 添加自定义样式 -->
|
||||
<style>
|
||||
/* NIKKE风格分页 */
|
||||
.nikke-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.nikke-pagination-btn {
|
||||
background-color: var(--nikke-gray-dark);
|
||||
color: var(--nikke-light);
|
||||
border: 1px solid var(--nikke-gray);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 8px 15px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
transition: all var(--transition-fast);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nikke-pagination-btn:not(.disabled):hover {
|
||||
background-color: var(--nikke-primary);
|
||||
border-color: var(--nikke-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(43, 57, 144, 0.4);
|
||||
}
|
||||
|
||||
.nikke-pagination-btn.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.nikke-pagination-numbers {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.nikke-pagination-number {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-md);
|
||||
background-color: var(--nikke-gray-dark);
|
||||
border: 1px solid var(--nikke-gray);
|
||||
color: var(--nikke-light);
|
||||
font-weight: 600;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.nikke-pagination-number:hover {
|
||||
background-color: var(--nikke-primary);
|
||||
border-color: var(--nikke-primary);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.nikke-pagination-number.active {
|
||||
background-color: var(--nikke-accent);
|
||||
border-color: var(--nikke-accent);
|
||||
color: white;
|
||||
box-shadow: 0 0 10px rgba(232, 62, 140, 0.4);
|
||||
}
|
||||
|
||||
/* NIKKE风格模态框 */
|
||||
.nikke-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: none;
|
||||
z-index: 1050;
|
||||
}
|
||||
|
||||
.nikke-modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(23, 23, 31, 0.8);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.nikke-modal-container {
|
||||
position: relative;
|
||||
background-color: var(--nikke-gray-dark);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--nikke-accent);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
transition: all var(--transition-normal);
|
||||
z-index: 1051;
|
||||
}
|
||||
|
||||
.nikke-modal.show .nikke-modal-container {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nikke-modal-header {
|
||||
background-color: var(--nikke-primary);
|
||||
padding: 15px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--nikke-primary-dark);
|
||||
border-top-left-radius: var(--radius-md);
|
||||
border-top-right-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.nikke-modal-title {
|
||||
color: var(--nikke-light);
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.nikke-modal-close {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--nikke-light);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: color var(--transition-fast);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.nikke-modal-close:hover {
|
||||
color: var(--nikke-accent);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.nikke-modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.nikke-modal-footer {
|
||||
padding: 15px 20px;
|
||||
border-top: 1px solid var(--nikke-gray);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* NIKKE风格复选框 */
|
||||
.nikke-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.nikke-checkbox-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.nikke-checkbox-label {
|
||||
position: relative;
|
||||
padding-left: 30px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.nikke-checkbox-label:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: var(--nikke-gray);
|
||||
border: 1px solid var(--nikke-gray-light);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.nikke-checkbox-input:checked + .nikke-checkbox-label:before {
|
||||
background-color: var(--nikke-accent);
|
||||
border-color: var(--nikke-accent);
|
||||
}
|
||||
|
||||
.nikke-checkbox-label:after {
|
||||
content: '\f00c';
|
||||
font-family: 'Font Awesome 5 Free', serif;
|
||||
font-weight: 900;
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 1px;
|
||||
color: white;
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-fast);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.nikke-checkbox-input:checked + .nikke-checkbox-label:after {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
// 模拟搜索功能
|
||||
document.getElementById('searchBtn').addEventListener('click', function() {
|
||||
const username = document.getElementById('searchUsername').value.toLowerCase();
|
||||
const userType = document.getElementById('userType').value;
|
||||
|
||||
// 显示搜索中状态
|
||||
this.innerHTML = `<i class="fas fa-circle-notch fa-spin me-2"></i> <span>${i18n.t('users.search.searching')}</span>`;
|
||||
this.disabled = true;
|
||||
|
||||
// 模拟搜索延迟
|
||||
setTimeout(() => {
|
||||
const rows = document.querySelectorAll('tbody tr');
|
||||
|
||||
rows.forEach(row => {
|
||||
const usernameCell = row.querySelector('.user-name').textContent.toLowerCase();
|
||||
const isAdmin = row.querySelector('.badge-admin') !== null;
|
||||
|
||||
let show = true;
|
||||
|
||||
// 按用户名筛选
|
||||
if (username && !usernameCell.includes(username)) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
// 按用户类型筛选
|
||||
if (userType === 'admin' && !isAdmin) {
|
||||
show = false;
|
||||
} else if (userType === 'user' && isAdmin) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
// 显示或隐藏行
|
||||
row.style.display = show ? '' : 'none';
|
||||
});
|
||||
|
||||
// 恢复按钮状态
|
||||
this.innerHTML = `<i class="fas fa-search me-2"></i> <span>${i18n.t('users.search.button')}</span>`;
|
||||
this.disabled = false;
|
||||
|
||||
// 显示通知
|
||||
showNotification(i18n.t('users.notifications.searchDone'), 'info');
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// 打开添加用户模态框
|
||||
function openAddUserModal() {
|
||||
const modal = document.getElementById('addUserModal');
|
||||
modal.classList.add('show');
|
||||
setTimeout(() => {
|
||||
document.getElementById('newUsername').focus();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// 关闭添加用户模态框
|
||||
function closeAddUserModal() {
|
||||
const modal = document.getElementById('addUserModal');
|
||||
modal.classList.remove('show');
|
||||
document.getElementById('addUserForm').reset();
|
||||
}
|
||||
|
||||
// 点击背景关闭模态框
|
||||
document.querySelector('.nikke-modal-backdrop').addEventListener('click', function() {
|
||||
closeAddUserModal();
|
||||
});
|
||||
|
||||
// 保存用户按钮
|
||||
document.getElementById('saveUserBtn').addEventListener('click', function() {
|
||||
const username = document.getElementById('newUsername').value;
|
||||
const nickname = document.getElementById('newNickname').value;
|
||||
const password = document.getElementById('newPassword').value;
|
||||
|
||||
if (!username || !password) {
|
||||
showNotification(i18n.t('users.notifications.requiredFields'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
this.innerHTML = `<i class="fas fa-circle-notch fa-spin me-2"></i> <span>${i18n.t('users.modal.saving')}</span>`;
|
||||
this.disabled = true;
|
||||
|
||||
// 模拟添加延迟
|
||||
setTimeout(() => {
|
||||
// 关闭模态框
|
||||
closeAddUserModal();
|
||||
|
||||
// 恢复按钮状态
|
||||
this.innerHTML = i18n.t('users.modal.save');
|
||||
this.disabled = false;
|
||||
|
||||
// 清空表单
|
||||
document.getElementById('addUserForm').reset();
|
||||
|
||||
// 显示成功通知
|
||||
showNotification(i18n.t('users.notifications.userAdded'), 'success');
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// 分页按钮事件
|
||||
document.querySelectorAll('.nikke-pagination-number').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
document.querySelectorAll('.nikke-pagination-number').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
this.classList.add('active');
|
||||
|
||||
// 模拟页面切换
|
||||
showNotification(`已切换到第 ${this.textContent} 页`, 'info');
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector('.nikke-pagination-btn.next').addEventListener('click', function() {
|
||||
const active = document.querySelector('.nikke-pagination-number.active');
|
||||
const next = active.nextElementSibling;
|
||||
if (next) {
|
||||
active.classList.remove('active');
|
||||
next.classList.add('active');
|
||||
|
||||
if (!next.nextElementSibling) {
|
||||
this.classList.add('disabled');
|
||||
this.setAttribute('aria-disabled', 'true');
|
||||
}
|
||||
|
||||
document.querySelector('.nikke-pagination-btn.prev').classList.remove('disabled');
|
||||
document.querySelector('.nikke-pagination-btn.prev').removeAttribute('aria-disabled');
|
||||
|
||||
// 模拟页面切换
|
||||
showNotification(`已切换到第 ${next.textContent} 页`, 'info');
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector('.nikke-pagination-btn.prev').addEventListener('click', function() {
|
||||
if (this.classList.contains('disabled')) return;
|
||||
|
||||
const active = document.querySelector('.nikke-pagination-number.active');
|
||||
const prev = active.previousElementSibling;
|
||||
if (prev) {
|
||||
active.classList.remove('active');
|
||||
prev.classList.add('active');
|
||||
|
||||
if (!prev.previousElementSibling) {
|
||||
this.classList.add('disabled');
|
||||
this.setAttribute('aria-disabled', 'true');
|
||||
}
|
||||
|
||||
document.querySelector('.nikke-pagination-btn.next').classList.remove('disabled');
|
||||
document.querySelector('.nikke-pagination-btn.next').removeAttribute('aria-disabled');
|
||||
|
||||
// 模拟页面切换
|
||||
showNotification(`已切换到第 ${prev.textContent} 页`, 'info');
|
||||
}
|
||||
});
|
||||
|
||||
// 监听语言变更事件
|
||||
window.addEventListener('nikke:languageChanged', function() {
|
||||
// 更新页面标题
|
||||
document.title = i18n.t('users.title') + ' - ' + i18n.t('app.name');
|
||||
|
||||
// 更新搜索选项的文本
|
||||
document.querySelectorAll('select option').forEach(option => {
|
||||
if (option.hasAttribute('data-i18n')) {
|
||||
const key = option.getAttribute('data-i18n');
|
||||
option.textContent = i18n.t(key);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -1,70 +1,250 @@
|
||||
@model EpinelPS.Models.Admin.ModUserModel
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Modify user";
|
||||
ViewData["Title"] = "修改用户";
|
||||
}
|
||||
|
||||
<h1>Change user info</h1>
|
||||
|
||||
<h3>User info</h3>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form asp-action="Modify">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Username" class="control-label col-sm-2"></label>
|
||||
<div class="col-sm-10"><input asp-for="Username" class="form-control" /></div>
|
||||
<span asp-validation-for="Username" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="IsAdmin" class="control-label">Is Admin: </label>
|
||||
<input asp-for="IsAdmin" class="form-check-input" />
|
||||
<span asp-validation-for="IsAdmin" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" title="allows for all characters to have equal chances of getting pulled">Disable Gacha System: </label>
|
||||
<input asp-for="sickpulls" class="form-check-input" title="allows for all characters to have equal chances of getting pulled"/>
|
||||
<span asp-validation-for="sickpulls" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="IsBanned" class="control-label">Banned:</label>
|
||||
<input asp-for="IsBanned" class="form-check-input" />
|
||||
<span asp-validation-for="IsBanned" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Nickname" class="control-label col-sm-2"></label>
|
||||
<div class="col-sm-10"><input asp-for="Nickname" class="form-control" /></div>
|
||||
<span asp-validation-for="Nickname" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" value="Save" class="btn btn-primary" style="margin-top:10px;" />
|
||||
</div>
|
||||
</form>
|
||||
<div class="nikke-dashboard">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="display-4 mb-0" data-i18n="users.modify.title">修改用户信息</h1>
|
||||
<a asp-action="Index" class="btn btn-outline-light">
|
||||
<i class="fas fa-arrow-left me-2"></i> <span data-i18n="users.modify.backToList">返回列表</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<hr>
|
||||
<h3>Cheats</h3>
|
||||
|
||||
<p>Campaign:</p>
|
||||
<button class="btn btn-secondary" onclick="runSimpleCmdWithPr('completestage', '@Model.ID', 'Enter chapter number and stage number seperated by -')">Skip stages</button>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="mb-0" data-i18n="users.modify.subtitle">用户信息</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form asp-action="Modify">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
|
||||
<p>Characters:</p>
|
||||
<button class="btn btn-secondary" onclick="runSimpleCmd('addallcharacters', '@Model.ID')">Add all characters</button>
|
||||
<button class="btn btn-secondary" onclick="runSimpleCmdWithPr('AddCharacter', '@Model.ID', 'Enter character ID. Wrong ID may cause game not to boot.')">Add character</button>
|
||||
<button class="btn btn-secondary" onclick="runSimpleCmdWithPr('SetLevel', '@Model.ID', 'Enter level (1-999) to apply to all characters')">Set character levels</button>
|
||||
<button class="btn btn-secondary" onclick="runSimpleCmdWithPr('SetSkillLevel', '@Model.ID', 'Enter skill level (1-10) to apply to all characters')">Set character skill levels</button>
|
||||
<button class="btn btn-secondary" onclick="runSimpleCmdWithPr('SetCoreLevel', '@Model.ID', 'core level / 0-3 sets stars')">Set core level</button>
|
||||
<p>Inventory:</p>
|
||||
<button class="btn btn-secondary" onclick="runSimpleCmdWithPr('addallmaterials', '@Model.ID', 'Enter material amount:')">Add all equipment</button>
|
||||
<button class="btn btn-secondary" onclick="runSimpleCmdWithPr('AddItem', '@Model.ID', 'Enter item ID and amount seperated by -')">Add item</button>
|
||||
<p>Misc:</p>
|
||||
<button class="btn btn-secondary" onclick="runSimpleCmd('finishalltutorials', '@Model.ID')">Finish all tutorials</button>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label asp-for="Username" class="form-label" data-i18n="users.table.username">用户名</label>
|
||||
<input asp-for="Username" class="form-control" />
|
||||
<span asp-validation-for="Username" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label asp-for="Nickname" class="form-label" data-i18n="users.table.nickname">昵称</label>
|
||||
<input asp-for="Nickname" class="form-control" />
|
||||
<span asp-validation-for="Nickname" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-12">
|
||||
<div class="nikke-checkbox-group">
|
||||
<div class="nikke-checkbox mb-2">
|
||||
<input asp-for="IsAdmin" class="nikke-checkbox-input" />
|
||||
<label asp-for="IsAdmin" class="nikke-checkbox-label" data-i18n="users.modify.isAdmin">管理员权限</label>
|
||||
<span asp-validation-for="IsAdmin" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="nikke-checkbox mb-2">
|
||||
<input asp-for="sickpulls" class="nikke-checkbox-input" data-i18n-title="users.modify.disableGachaHint" />
|
||||
<label asp-for="sickpulls" class="nikke-checkbox-label" data-i18n="users.modify.disableGacha" data-i18n-title="users.modify.disableGachaHint">禁用抽卡系统</label>
|
||||
<span asp-validation-for="sickpulls" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="nikke-checkbox">
|
||||
<input asp-for="IsBanned" class="nikke-checkbox-input" />
|
||||
<label asp-for="IsBanned" class="nikke-checkbox-label" data-i18n="users.modify.isBanned">禁止登录</label>
|
||||
<span asp-validation-for="IsBanned" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mt-4">
|
||||
<button type="submit" class="btn btn-primary" data-i18n="common.save">
|
||||
<i class="fas fa-save me-2"></i> 保存
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="mb-0" data-i18n="users.modify.cheats">作弊功能</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="cheats-section mb-4">
|
||||
<h4 class="section-title" data-i18n="users.modify.campaign">战役</h4>
|
||||
<div class="button-group">
|
||||
<button class="btn btn-accent" onclick="runSimpleCmdWithPr('completestage', '@Model.ID', 'Enter chapter number and stage number seperated by -')">
|
||||
<i class="fas fa-forward me-2"></i> <span data-i18n="users.modify.skipStages">跳过关卡</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cheats-section mb-4">
|
||||
<h4 class="section-title" data-i18n="users.modify.characters">角色</h4>
|
||||
<div class="button-group">
|
||||
<button class="btn btn-accent" onclick="runSimpleCmd('addallcharacters', '@Model.ID')">
|
||||
<i class="fas fa-users me-2"></i> <span data-i18n="users.modify.addAllChars">添加所有角色</span>
|
||||
</button>
|
||||
<button class="btn btn-accent" onclick="runSimpleCmdWithPr('AddCharacter', '@Model.ID', 'Enter character ID. Wrong ID may cause game not to boot.')">
|
||||
<i class="fas fa-user-plus me-2"></i> <span data-i18n="users.modify.addChar">添加角色</span>
|
||||
</button>
|
||||
<button class="btn btn-accent" onclick="runSimpleCmdWithPr('SetLevel', '@Model.ID', 'Enter level (1-999) to apply to all characters')">
|
||||
<i class="fas fa-level-up-alt me-2"></i> <span data-i18n="users.modify.setCharLevels">设置角色等级</span>
|
||||
</button>
|
||||
<button class="btn btn-accent" onclick="runSimpleCmdWithPr('SetSkillLevel', '@Model.ID', 'Enter skill level (1-10) to apply to all characters')">
|
||||
<i class="fas fa-bolt me-2"></i> <span data-i18n="users.modify.setSkillLevels">设置技能等级</span>
|
||||
</button>
|
||||
<button class="btn btn-accent" onclick="runSimpleCmdWithPr('SetCoreLevel', '@Model.ID', 'core level / 0-3 sets stars')">
|
||||
<i class="fas fa-star me-2"></i> <span data-i18n="users.modify.setCoreLevel">设置核心等级</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cheats-section mb-4">
|
||||
<h4 class="section-title" data-i18n="users.modify.inventory">物品</h4>
|
||||
<div class="button-group">
|
||||
<button class="btn btn-accent" onclick="runSimpleCmdWithPr('addallmaterials', '@Model.ID', 'Enter material amount:')">
|
||||
<i class="fas fa-tools me-2"></i> <span data-i18n="users.modify.addAllEquip">添加所有装备</span>
|
||||
</button>
|
||||
<button class="btn btn-accent" onclick="runSimpleCmdWithPr('AddItem', '@Model.ID', 'Enter item ID and amount seperated by -')">
|
||||
<i class="fas fa-box-open me-2"></i> <span data-i18n="users.modify.addItem">添加物品</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cheats-section">
|
||||
<h4 class="section-title" data-i18n="users.modify.misc">其他</h4>
|
||||
<div class="button-group">
|
||||
<button class="btn btn-accent" onclick="runSimpleCmd('finishalltutorials', '@Model.ID')">
|
||||
<i class="fas fa-graduation-cap me-2"></i> <span data-i18n="users.modify.finishTutorials">完成所有教程</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a asp-action="Index">Back to List</a>
|
||||
</div>
|
||||
<style>
|
||||
/* NIKKE风格卡片标题 */
|
||||
.card-header {
|
||||
background-color: var(--nikke-primary);
|
||||
color: var(--nikke-light);
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* 作弊功能部分 */
|
||||
.cheats-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: var(--nikke-accent);
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
border-bottom: 1px solid var(--nikke-gray);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 自定义按钮 */
|
||||
.btn-accent {
|
||||
background-color: var(--nikke-accent-dark);
|
||||
color: white;
|
||||
border: none;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.btn-accent:hover {
|
||||
background-color: var(--nikke-accent);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(232, 62, 140, 0.4);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 自定义复选框样式 */
|
||||
.nikke-checkbox-group {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.nikke-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.nikke-checkbox-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.nikke-checkbox-label {
|
||||
position: relative;
|
||||
padding-left: 30px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.nikke-checkbox-label:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: var(--nikke-gray);
|
||||
border: 1px solid var(--nikke-gray-light);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.nikke-checkbox-input:checked + .nikke-checkbox-label:before {
|
||||
background-color: var(--nikke-accent);
|
||||
border-color: var(--nikke-accent);
|
||||
}
|
||||
|
||||
.nikke-checkbox-label:after {
|
||||
content: '\f00c';
|
||||
font-family: 'Font Awesome 5 Free', serif;
|
||||
font-weight: 900;
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 1px;
|
||||
color: white;
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-fast);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.nikke-checkbox-input:checked + .nikke-checkbox-label:after {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -1,30 +1,155 @@
|
||||
@model EpinelPS.Database.User
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Change user password";
|
||||
ViewData["Title"] = "修改密码";
|
||||
}
|
||||
|
||||
<h1>Change password</h1>
|
||||
<div class="nikke-dashboard">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="display-4 mb-0" data-i18n="users.password.title">修改密码</h1>
|
||||
<a asp-action="Index" class="btn btn-outline-light">
|
||||
<i class="fas fa-arrow-left me-2"></i> <span data-i18n="users.password.backToList">返回列表</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h4>User</h4>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form asp-action="SetPassword">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<input type="hidden" asp-for="ID" />
|
||||
<div class="form-group">
|
||||
<label asp-for="Password" class="control-label"></label>
|
||||
<input asp-for="Password" class="form-control" required />
|
||||
<span asp-validation-for="Password" class="text-danger"></span>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="mb-0">
|
||||
<i class="fas fa-key me-2"></i>
|
||||
<span data-i18n="users.table.username">用户</span>: @Model.Username
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form asp-action="SetPassword">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<input type="hidden" asp-for="ID" />
|
||||
|
||||
<div class="form-group mb-4">
|
||||
<label asp-for="Password" class="form-label fw-bold" data-i18n="auth.password">密码</label>
|
||||
<div class="input-group">
|
||||
<input asp-for="Password" type="password" id="newPassword" class="form-control" required placeholder="输入新密码" data-i18n-placeholder="users.password.enterNewPassword" />
|
||||
<button type="button" class="btn btn-outline-secondary" id="togglePassword" onclick="togglePasswordVisibility(event)">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text text-light opacity-75" data-i18n="users.password.hint">
|
||||
请输入强密码,包含字母、数字和特殊字符
|
||||
</div>
|
||||
<span asp-validation-for="Password" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-accent btn-lg">
|
||||
<i class="fas fa-save me-2"></i>
|
||||
<span data-i18n="users.password.changeButton">修改密码</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" value="Change password" class="btn btn-danger" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a asp-action="Index">Back to List</a>
|
||||
</div>
|
||||
<style>
|
||||
/* 卡片样式 */
|
||||
.card {
|
||||
background-color: var(--nikke-dark);
|
||||
border: 1px solid var(--nikke-gray-dark);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: var(--nikke-primary);
|
||||
color: var(--nikke-light);
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
/* 表单控件样式 */
|
||||
.form-control {
|
||||
background-color: var(--nikke-dark);
|
||||
color: var(--nikke-light);
|
||||
border: 1px solid var(--nikke-gray);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
background-color: var(--nikke-dark);
|
||||
color: var(--nikke-light);
|
||||
border-color: var(--nikke-accent);
|
||||
box-shadow: 0 0 0 0.25rem rgba(232, 62, 140, 0.25);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
color: var(--nikke-light);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn-accent {
|
||||
background-color: var(--nikke-accent-dark);
|
||||
color: white;
|
||||
border: none;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
padding: 12px 24px;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.btn-accent:hover {
|
||||
background-color: var(--nikke-accent);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(232, 62, 140, 0.4);
|
||||
color: white;
|
||||
}
|
||||
|
||||
#togglePassword {
|
||||
background-color: var(--nikke-gray-dark);
|
||||
color: var(--nikke-light);
|
||||
border: 1px solid var(--nikke-gray);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
#togglePassword:hover {
|
||||
background-color: var(--nikke-gray);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// 独立的密码可见性切换函数
|
||||
function togglePasswordVisibility(event) {
|
||||
// 阻止事件冒泡和默认行为
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// 使用多种选择器确保能找到密码输入框
|
||||
const passwordInput = $('#newPassword').length ? $('#newPassword') :
|
||||
$('input[name="Password"]').length ? $('input[name="Password"]') :
|
||||
$('input[type="password"]').first();
|
||||
|
||||
// 确保找到了元素
|
||||
if (passwordInput.length) {
|
||||
const type = passwordInput.attr('type') === 'password' ? 'text' : 'password';
|
||||
passwordInput.attr('type', type);
|
||||
|
||||
// 切换图标
|
||||
$('#togglePassword').find('i').toggleClass('fa-eye fa-eye-slash');
|
||||
} else {
|
||||
console.error('Password input field not found');
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
// 页面加载完成后检查并记录密码字段
|
||||
console.log('Password field ID:', $('input[type="password"]').attr('id'));
|
||||
console.log('Password field name:', $('input[type="password"]').attr('name'));
|
||||
});
|
||||
</script>
|
||||
376
EpinelPS/wwwroot/admin/assets/css/nikke-dashboard.css
Normal file
376
EpinelPS/wwwroot/admin/assets/css/nikke-dashboard.css
Normal file
@@ -0,0 +1,376 @@
|
||||
/* NIKKE 仪表盘专用样式 */
|
||||
|
||||
/* 仪表板容器 */
|
||||
.nikke-dashboard {
|
||||
padding: var(--spacing-lg);
|
||||
min-height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.nikke-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
background-color: var(--nikke-gray-dark);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-lg);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all var(--transition-normal);
|
||||
border: 1px solid var(--nikke-gray);
|
||||
}
|
||||
|
||||
.stats-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
border-color: var(--nikke-accent);
|
||||
}
|
||||
|
||||
.stats-card .icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: var(--spacing-md);
|
||||
color: var(--nikke-accent);
|
||||
}
|
||||
|
||||
.stats-card .value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--nikke-light);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stats-card .label {
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: var(--nikke-gray-light);
|
||||
font-weight: normal;
|
||||
padding: 0;
|
||||
display: block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.stats-card .decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
width: 5px;
|
||||
background: linear-gradient(to bottom, var(--nikke-accent), transparent);
|
||||
}
|
||||
|
||||
/* 图表卡片 */
|
||||
.chart-card {
|
||||
background-color: var(--nikke-gray-dark);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
box-shadow: var(--shadow-md);
|
||||
border: 1px solid var(--nikke-gray);
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.chart-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-weight: 600;
|
||||
color: var(--nikke-light);
|
||||
font-size: 1.2rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chart-body {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chart-card .btn-outline-light {
|
||||
color: var(--nikke-gray-light);
|
||||
border-color: var(--nikke-gray);
|
||||
background-color: transparent;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
}
|
||||
|
||||
.chart-card .btn-outline-light:hover,
|
||||
.chart-card .btn-outline-light.active {
|
||||
color: var(--nikke-light);
|
||||
background-color: var(--nikke-accent);
|
||||
border-color: var(--nikke-accent);
|
||||
}
|
||||
|
||||
/* 进度条 */
|
||||
.nikke-progress {
|
||||
height: 8px;
|
||||
background-color: var(--nikke-gray);
|
||||
border-radius: var(--radius-full);
|
||||
overflow: hidden;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.nikke-progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, var(--nikke-accent-dark), var(--nikke-accent));
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
/* 活动列表 */
|
||||
.activity-list {
|
||||
background-color: var(--nikke-gray-dark);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--nikke-gray);
|
||||
box-shadow: var(--shadow-md);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.activity-header {
|
||||
background-color: var(--nikke-primary);
|
||||
color: var(--nikke-light);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
border-bottom: 1px solid var(--nikke-primary-dark);
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
border-bottom: 1px solid rgba(58, 58, 72, 0.5);
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.activity-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.activity-item:hover {
|
||||
background-color: rgba(43, 57, 144, 0.1);
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--nikke-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
margin-right: var(--spacing-md);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.activity-title {
|
||||
color: var(--nikke-light);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
color: var(--nikke-gray-light);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* 用户相关样式 */
|
||||
.user-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
margin-right: var(--spacing-md);
|
||||
border: 2px solid var(--nikke-accent);
|
||||
background-color: var(--nikke-gray);
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
color: var(--nikke-light);
|
||||
}
|
||||
|
||||
.badge-admin {
|
||||
display: inline-block;
|
||||
background-color: var(--nikke-accent);
|
||||
color: white;
|
||||
padding: 0.35em 0.65em;
|
||||
font-size: 0.75em;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
border-radius: var(--radius-sm);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* 通知 */
|
||||
.nikke-notification {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
min-width: 300px;
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
background-color: var(--nikke-gray-dark);
|
||||
color: var(--nikke-light);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 1050;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.nikke-notification.show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.nikke-notification.info {
|
||||
border-left: 5px solid var(--nikke-info);
|
||||
}
|
||||
|
||||
.nikke-notification.success {
|
||||
border-left: 5px solid var(--nikke-success);
|
||||
}
|
||||
|
||||
.nikke-notification.warning {
|
||||
border-left: 5px solid var(--nikke-warning);
|
||||
}
|
||||
|
||||
.nikke-notification.error {
|
||||
border-left: 5px solid var(--nikke-danger);
|
||||
}
|
||||
|
||||
.notification-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.notification-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--nikke-gray-light);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notification-close:hover {
|
||||
color: var(--nikke-light);
|
||||
}
|
||||
|
||||
.notification-body {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* 仪表盘适应性 */
|
||||
@media (max-width: 1199.98px) {
|
||||
.nikke-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.nikke-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.nikke-notification {
|
||||
min-width: calc(100% - 40px);
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义图表样式 */
|
||||
canvas {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* 确保图表中的文字使用正确的颜色 */
|
||||
.chartjs-render-monitor {
|
||||
color: var(--nikke-light);
|
||||
}
|
||||
|
||||
/* 图表工具提示定制 */
|
||||
.chartjs-tooltip {
|
||||
background-color: var(--nikke-gray-dark) !important;
|
||||
border: 1px solid var(--nikke-gray) !important;
|
||||
color: var(--nikke-light) !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
box-shadow: var(--shadow-md) !important;
|
||||
padding: var(--spacing-sm) var(--spacing-md) !important;
|
||||
font-family: 'Exo 2', 'Noto Sans SC', sans-serif !important;
|
||||
font-size: 0.85rem !important;
|
||||
}
|
||||
|
||||
/* 页面加载动画 */
|
||||
.dashboard-loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(23, 23, 31, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.dashboard-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 3px solid var(--nikke-gray);
|
||||
border-top: 3px solid var(--nikke-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 仪表盘特殊动画 */
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(232, 62, 140, 0.4); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(232, 62, 140, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(232, 62, 140, 0); }
|
||||
}
|
||||
|
||||
.pulse-animation {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
251
EpinelPS/wwwroot/admin/assets/css/nikke-i18n.css
Normal file
251
EpinelPS/wwwroot/admin/assets/css/nikke-i18n.css
Normal file
@@ -0,0 +1,251 @@
|
||||
/* NIKKE 国际化支持样式 */
|
||||
[data-i18n],
|
||||
[data-i18n-placeholder],
|
||||
[data-i18n-title] {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.lang-loading [data-i18n],
|
||||
.lang-loading [data-i18n-placeholder],
|
||||
.lang-loading [data-i18n-title] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.language-transition-enter-active,
|
||||
.language-transition-leave-active {
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
|
||||
.language-transition-enter,
|
||||
.language-transition-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
/* 语言切换器样式 - 放在右上角 */
|
||||
.language-switcher {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1050;
|
||||
font-family: 'Exo 2', 'Noto Sans SC', sans-serif;
|
||||
animation: fadeIn 0.5s ease forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 登录页面特殊位置 */
|
||||
body.login-page .language-switcher {
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
/* 仪表盘页面特殊位置 */
|
||||
body:not(.login-page) .language-switcher {
|
||||
top: 15px;
|
||||
right: 170px; /* 预留用户菜单空间 */
|
||||
}
|
||||
|
||||
.language-switcher-toggle {
|
||||
background-color: rgba(35, 35, 45, 0.9);
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.language-switcher-toggle:hover {
|
||||
background-color: rgba(43, 57, 144, 0.9);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.language-switcher-toggle i.fa-globe {
|
||||
margin-right: 8px;
|
||||
color: var(--nikke-accent-light, #f16eac);
|
||||
}
|
||||
|
||||
.language-switcher-toggle i.fa-chevron-down {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
transition: transform 0.3s ease;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.language-switcher.open .language-switcher-toggle i.fa-chevron-down {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.language-options {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 5px;
|
||||
background-color: rgba(35, 35, 45, 0.95);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
max-height: 0;
|
||||
transition: max-height 0.3s ease, opacity 0.3s ease;
|
||||
opacity: 0;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.language-switcher.open .language-options {
|
||||
max-height: 200px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.language-option {
|
||||
padding: 8px 15px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
transition: background-color 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.language-option:hover {
|
||||
background-color: rgba(43, 57, 144, 0.5);
|
||||
}
|
||||
|
||||
.language-option.active {
|
||||
background-color: rgba(232, 62, 140, 0.2);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.language-option.active:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background-color: var(--nikke-accent, #e83e8c);
|
||||
}
|
||||
|
||||
.language-name {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.language-code {
|
||||
opacity: 0.5;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
/* RTL 支持 (针对阿拉伯语等从右到左的语言) */
|
||||
[dir="rtl"] .language-switcher-toggle i.fa-globe {
|
||||
margin-right: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
[dir="rtl"] .language-switcher-toggle i.fa-chevron-down {
|
||||
margin-left: 0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
[dir="rtl"] .language-options {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
[dir="rtl"] .language-option.active:after {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* 语言特定字体 */
|
||||
html[lang="zh"] body {
|
||||
font-family: 'Noto Sans SC', 'Exo 2', sans-serif;
|
||||
}
|
||||
|
||||
html[lang="ja"] body {
|
||||
font-family: 'Noto Sans JP', 'Exo 2', sans-serif;
|
||||
}
|
||||
|
||||
html[lang="ko"] body {
|
||||
font-family: 'Noto Sans KR', 'Exo 2', sans-serif;
|
||||
}
|
||||
|
||||
/* 提高可访问性 */
|
||||
.language-option:focus {
|
||||
outline: 2px solid var(--nikke-accent, #e83e8c);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 767.98px) {
|
||||
.language-switcher-toggle {
|
||||
padding: 4px 10px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
body:not(.login-page) .language-switcher {
|
||||
top: 60px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.language-option {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 导航栏集成的语言切换器 */
|
||||
.language-switcher.navbar-integrated {
|
||||
position: static;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.language-switcher.navbar-integrated .language-switcher-toggle {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-md);
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.language-switcher.navbar-integrated .language-switcher-toggle:hover {
|
||||
background-color: rgba(43, 57, 144, 0.2);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.language-switcher.navbar-integrated .language-options {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
z-index: 1050;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* 移动设备上的导航栏集成 */
|
||||
@media (max-width: 767.98px) {
|
||||
.language-switcher.navbar-integrated {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.language-switcher.navbar-integrated .language-options {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* 登录页面的语言切换器 */
|
||||
body.login-page .language-switcher {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1050;
|
||||
}
|
||||
229
EpinelPS/wwwroot/admin/assets/css/nikke-login.css
Normal file
229
EpinelPS/wwwroot/admin/assets/css/nikke-login.css
Normal file
@@ -0,0 +1,229 @@
|
||||
body.login-page {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: url('/admin/assets/img/nikke-login-bg.jpg') no-repeat center center;
|
||||
background-size: cover;
|
||||
font-family: 'Exo 2', 'Noto Sans SC', sans-serif;
|
||||
}
|
||||
|
||||
.login-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, rgba(23, 23, 31, 0.85), rgba(43, 57, 144, 0.7));
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
background-color: rgba(35, 35, 45, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.3), 0 0 15px rgba(232, 62, 140, 0.5);
|
||||
animation: fadeIn 0.8s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(30px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.login-header {
|
||||
padding: 30px 40px 15px;
|
||||
text-align: center;
|
||||
background: linear-gradient(to right, var(--nikke-primary-dark), var(--nikke-primary));
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-header::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image: url('/admin/assets/img/nikke-pattern.png');
|
||||
background-size: 200px;
|
||||
opacity: 0.1;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.login-header .logo {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 120px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
margin-bottom: 5px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.login-body {
|
||||
padding: 30px 40px;
|
||||
}
|
||||
|
||||
.form-floating {
|
||||
position: relative;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.form-floating label {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 15px;
|
||||
height: 100%;
|
||||
padding: 1rem 0.75rem;
|
||||
pointer-events: none;
|
||||
border: 1px solid transparent;
|
||||
transform-origin: 0 0;
|
||||
transition: opacity .1s ease-in-out,transform .1s ease-in-out;
|
||||
color: var(--nikke-gray-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-floating .form-control {
|
||||
height: 58px;
|
||||
padding: 1.5rem 0.75rem 0.5rem;
|
||||
color: white;
|
||||
background-color: var(--nikke-gray);
|
||||
border: 1px solid var(--nikke-gray-dark);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.form-floating .form-control:focus {
|
||||
box-shadow: 0 0 0 0.25rem rgba(232, 62, 140, 0.25);
|
||||
border-color: var(--nikke-accent);
|
||||
}
|
||||
|
||||
.form-floating .form-control:focus ~ label,
|
||||
.form-floating .form-control:not(:placeholder-shown) ~ label {
|
||||
opacity: .65;
|
||||
transform: scale(.85) translateY(-0.5rem) translateX(0.15rem);
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
background: linear-gradient(to right, var(--nikke-accent-dark), var(--nikke-accent));
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-normal);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-button::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 15px rgba(232, 62, 140, 0.4);
|
||||
}
|
||||
|
||||
.login-button:hover::after {
|
||||
transform: translateX(100%);
|
||||
transition: transform 1s;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: rgba(244, 66, 131, 0.2);
|
||||
color: var(--nikke-danger);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin: 15px 0;
|
||||
display: none;
|
||||
animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both;
|
||||
}
|
||||
|
||||
.error-message.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
10%, 90% { transform: translate3d(-1px, 0, 0); }
|
||||
20%, 80% { transform: translate3d(2px, 0, 0); }
|
||||
30%, 50%, 70% { transform: translate3d(-4px, 0, 0); }
|
||||
40%, 60% { transform: translate3d(4px, 0, 0); }
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
padding: 0 40px 30px;
|
||||
color: var(--nikke-gray-light);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.version {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-size: 12px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 动画光效 */
|
||||
.nikke-glow {
|
||||
position: absolute;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: radial-gradient(circle, rgba(232, 62, 140, 0.4) 0%, rgba(232, 62, 140, 0) 70%);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
transition: opacity 1s;
|
||||
}
|
||||
|
||||
body.login-page:hover .nikke-glow {
|
||||
opacity: 1;
|
||||
}
|
||||
426
EpinelPS/wwwroot/admin/assets/css/nikke-theme.css
Normal file
426
EpinelPS/wwwroot/admin/assets/css/nikke-theme.css
Normal file
@@ -0,0 +1,426 @@
|
||||
:root {
|
||||
/* NIKKE主题色系 */
|
||||
--nikke-primary: #2b3990; /* NIKKE蓝色 */
|
||||
--nikke-primary-light: #4754b9;
|
||||
--nikke-primary-dark: #1a2370;
|
||||
|
||||
--nikke-accent: #e83e8c; /* NIKKE粉色 */
|
||||
--nikke-accent-light: #f16eac;
|
||||
--nikke-accent-dark: #c72c70;
|
||||
|
||||
/* 中性色 */
|
||||
--nikke-dark: #17171f;
|
||||
--nikke-gray-dark: #23232d;
|
||||
--nikke-gray: #3a3a48;
|
||||
--nikke-gray-light: #7c7c8a;
|
||||
--nikke-light: #f8f9fc;
|
||||
|
||||
/* 功能色 */
|
||||
--nikke-success: #32d296;
|
||||
--nikke-warning: #faa05a;
|
||||
--nikke-danger: #f44283;
|
||||
--nikke-info: #3fafec;
|
||||
|
||||
/* 尺寸 */
|
||||
--spacing-xs: 4px;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 24px;
|
||||
--spacing-xl: 32px;
|
||||
|
||||
/* 阴影 */
|
||||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
--shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.25);
|
||||
--shadow-neon: 0 0 10px rgba(232, 62, 140, 0.5);
|
||||
|
||||
/* 过渡 */
|
||||
--transition-fast: 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
--transition-normal: 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
--transition-slow: 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
|
||||
/* 圆角 */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-full: 9999px;
|
||||
}
|
||||
|
||||
/* 全局样式 */
|
||||
body {
|
||||
font-family: 'Exo 2', 'Noto Sans SC', sans-serif;
|
||||
background: linear-gradient(135deg, var(--nikke-dark) 0%, rgba(26, 35, 112, 0.9) 100%);
|
||||
color: var(--nikke-light);
|
||||
line-height: 1.6;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('/assets/img/nikke-bg-pattern.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
opacity: 0.2;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-md);
|
||||
color: var(--nikke-light);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.display-4 {
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
color: var(--nikke-light);
|
||||
text-shadow: 0 0 10px rgba(232, 62, 140, 0.5);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--nikke-accent);
|
||||
text-decoration: none;
|
||||
transition: all var(--transition-fast);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--nikke-accent-light);
|
||||
text-shadow: 0 0 8px rgba(232, 62, 140, 0.4);
|
||||
}
|
||||
|
||||
a.nav-link:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background-color: var(--nikke-accent);
|
||||
transition: width var(--transition-normal);
|
||||
}
|
||||
|
||||
a.nav-link:hover:after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 导航栏样式 */
|
||||
.navbar {
|
||||
background-color: var(--nikke-primary-dark) !important;
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
border-bottom: 1px solid rgba(232, 62, 140, 0.3) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: var(--nikke-light) !important;
|
||||
font-size: 1.4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-brand:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-right: 10px;
|
||||
background-image: url('/admin/assets/img/nikke-logo-icon.png');
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.navbar-light .navbar-nav .nav-link {
|
||||
color: rgba(255, 255, 255, 0.85) !important;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.navbar-light .navbar-nav .nav-link:hover,
|
||||
.navbar-light .navbar-nav .nav-link:focus,
|
||||
.navbar-light .navbar-nav .nav-link.active {
|
||||
color: var(--nikke-light) !important;
|
||||
background-color: rgba(232, 62, 140, 0.2);
|
||||
}
|
||||
|
||||
/* 容器样式 */
|
||||
.container, .container-fluid {
|
||||
padding-top: var(--spacing-lg);
|
||||
padding-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.table {
|
||||
background-color: var(--nikke-gray-dark);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--nikke-gray);
|
||||
box-shadow: var(--shadow-md);
|
||||
color: var(--nikke-light);
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
background-color: var(--nikke-primary);
|
||||
color: var(--nikke-light);
|
||||
text-transform: uppercase;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 1px;
|
||||
border-bottom: none;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-of-type(odd) {
|
||||
background-color: rgba(43, 57, 144, 0.1);
|
||||
}
|
||||
|
||||
.table tbody tr {
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background-color: rgba(232, 62, 140, 0.1);
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding: 0.85rem 1rem;
|
||||
vertical-align: middle;
|
||||
border-top: 1px solid var(--nikke-gray);
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn {
|
||||
border-radius: var(--radius-md);
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
padding: 0.5rem 1.25rem;
|
||||
transition: all var(--transition-fast);
|
||||
border: none;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--nikke-primary);
|
||||
border-color: var(--nikke-primary);
|
||||
color: var(--nikke-light);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--nikke-primary-light);
|
||||
border-color: var(--nikke-primary-light);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(43, 57, 144, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:focus, .btn-primary:active {
|
||||
background-color: var(--nikke-primary-dark) !important;
|
||||
border-color: var(--nikke-primary-dark) !important;
|
||||
box-shadow: 0 0 0 0.25rem rgba(43, 57, 144, 0.5) !important;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: var(--nikke-danger);
|
||||
border-color: var(--nikke-danger);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: var(--nikke-accent-dark);
|
||||
border-color: var(--nikke-accent-dark);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(232, 62, 140, 0.4);
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
color: var(--nikke-primary);
|
||||
border-color: var(--nikke-primary);
|
||||
}
|
||||
|
||||
.btn-outline-primary:hover {
|
||||
background-color: var(--nikke-primary);
|
||||
color: var(--nikke-light);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 表单样式 */
|
||||
.form-control, .form-select {
|
||||
background-color: var(--nikke-gray-dark);
|
||||
border: 1px solid var(--nikke-gray);
|
||||
color: var(--nikke-light);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.6rem 1rem;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.form-control:focus, .form-select:focus {
|
||||
background-color: var(--nikke-gray);
|
||||
border-color: var(--nikke-accent);
|
||||
color: var(--nikke-light);
|
||||
box-shadow: 0 0 0 0.25rem rgba(232, 62, 140, 0.25);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
color: var(--nikke-light);
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-control::placeholder {
|
||||
color: var(--nikke-gray-light);
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.card {
|
||||
background-color: var(--nikke-gray-dark);
|
||||
border: 1px solid var(--nikke-gray);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: var(--nikke-primary);
|
||||
color: var(--nikke-light);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid var(--nikke-primary-dark);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.5rem;
|
||||
color: var(--nikke-light);
|
||||
}
|
||||
|
||||
/* 徽章样式 */
|
||||
.badge {
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
padding: 0.35em 0.65em;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.badge-admin {
|
||||
background-color: var(--nikke-accent);
|
||||
color: var(--nikke-light);
|
||||
}
|
||||
|
||||
/* 页脚样式 */
|
||||
.footer {
|
||||
background-color: var(--nikke-gray-dark);
|
||||
color: var(--nikke-gray-light);
|
||||
padding: var(--spacing-md) 0;
|
||||
border-top: 1px solid var(--nikke-gray);
|
||||
}
|
||||
|
||||
/* 动画和效果 */
|
||||
@keyframes glow {
|
||||
0% { box-shadow: 0 0 5px rgba(232, 62, 140, 0.5); }
|
||||
50% { box-shadow: 0 0 20px rgba(232, 62, 140, 0.8); }
|
||||
100% { box-shadow: 0 0 5px rgba(232, 62, 140, 0.5); }
|
||||
}
|
||||
|
||||
.glow-effect {
|
||||
animation: glow 2s infinite;
|
||||
}
|
||||
|
||||
/* 数据表格特殊样式 */
|
||||
.admin-badge {
|
||||
background-color: var(--nikke-accent);
|
||||
color: white;
|
||||
padding: 0.25em 0.5em;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* 自定义组件 */
|
||||
.stat-card {
|
||||
background-color: var(--nikke-gray-dark);
|
||||
border: 1px solid var(--nikke-gray);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
transition: all var(--transition-normal);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.stat-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(232, 62, 140, 0.1));
|
||||
transform: skewX(-25deg) translateX(100%);
|
||||
transition: transform 1s;
|
||||
}
|
||||
|
||||
.stat-card:hover::after {
|
||||
transform: skewX(-25deg) translateX(-150%);
|
||||
}
|
||||
|
||||
.stat-card .icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: var(--spacing-md);
|
||||
color: var(--nikke-accent);
|
||||
}
|
||||
|
||||
.stat-card .value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--nikke-light);
|
||||
}
|
||||
|
||||
.stat-card .label {
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: var(--nikke-gray-light);
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.navbar-brand {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.display-4 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.table {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
215
EpinelPS/wwwroot/admin/assets/i18n/en.json
Normal file
215
EpinelPS/wwwroot/admin/assets/i18n/en.json
Normal file
@@ -0,0 +1,215 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "NIKKE Admin Console",
|
||||
"version": "Version v2.5.3",
|
||||
"footer": "© 2025 - NIKKE: Goddess of Victory - Admin Console"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Login",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"enter": "Enter Console",
|
||||
"welcome": "Please login to access admin features",
|
||||
"verifying": "Verifying...",
|
||||
"success": "Login successful",
|
||||
"error": {
|
||||
"required": "Please enter username and password",
|
||||
"invalid": "Login failed, please check your credentials",
|
||||
"network": "Network error, please try again later"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"loading": "Loading...",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"actions": "Actions",
|
||||
"search": "Search",
|
||||
"status": {
|
||||
"online": "Online",
|
||||
"offline": "Offline"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "System Notification",
|
||||
"dashboardRefreshed": "Dashboard data successfully refreshed"
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Dashboard",
|
||||
"events": "Events",
|
||||
"users": "Users",
|
||||
"mail": "Mail",
|
||||
"config": "Configuration",
|
||||
"database": "Database",
|
||||
"profile": "Profile",
|
||||
"changePass": "Change Password",
|
||||
"logout": "Logout"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Console Overview",
|
||||
"refresh": "Refresh Data",
|
||||
"refreshing": "Refreshing...",
|
||||
"statsCards": {
|
||||
"users": "Registered Users",
|
||||
"server": "Server Status",
|
||||
"events": "Active Events",
|
||||
"mail": "Mail Today"
|
||||
},
|
||||
"charts": {
|
||||
"activity": "User Activity Trends",
|
||||
"server": "Server Resource Usage",
|
||||
"periods": {
|
||||
"day": "Day",
|
||||
"week": "Week",
|
||||
"month": "Month"
|
||||
}
|
||||
},
|
||||
"serverStats": {
|
||||
"cpu": "CPU Usage",
|
||||
"memory": "Memory Usage",
|
||||
"storage": "Storage Usage"
|
||||
},
|
||||
"activity": {
|
||||
"title": "Recent Activity",
|
||||
"newUser": "New User Registration",
|
||||
"loginAlert": "Unusual Login Detected",
|
||||
"configUpdate": "System Configuration Updated",
|
||||
"rewards": "Global Rewards Distributed",
|
||||
"newEvent": "New Event Created",
|
||||
"bugfix": "Game Bug Fixed",
|
||||
"timeFormat": {
|
||||
"min": "minutes ago",
|
||||
"hour": "hours ago",
|
||||
"yesterday": "yesterday",
|
||||
"daysAgo": "days ago"
|
||||
}
|
||||
},
|
||||
"quickAccess": {
|
||||
"users": {
|
||||
"title": "User Management",
|
||||
"desc": "Manage game user accounts, permissions and roles"
|
||||
},
|
||||
"events": {
|
||||
"title": "Event Management",
|
||||
"desc": "Create, edit and monitor game events"
|
||||
},
|
||||
"mail": {
|
||||
"title": "Mail System",
|
||||
"desc": "Send system mails and rewards to players"
|
||||
},
|
||||
"database": {
|
||||
"title": "Database",
|
||||
"desc": "Manage and maintain game database"
|
||||
},
|
||||
"enter": "Enter"
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
"title": "User Management",
|
||||
"add": "Add New User",
|
||||
"table": {
|
||||
"id": "ID",
|
||||
"username": "Username",
|
||||
"nickname": "Nickname",
|
||||
"playerName": "Player Name",
|
||||
"isAdmin": "Permission",
|
||||
"isBanned": "Banned",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"search": {
|
||||
"username": "Username",
|
||||
"usernamePlaceholder": "Search username...",
|
||||
"userType": "User Type",
|
||||
"types": {
|
||||
"all": "All",
|
||||
"admin": "Admin",
|
||||
"user": "Regular User"
|
||||
},
|
||||
"sort": "Sort By",
|
||||
"sortOptions": {
|
||||
"username": "Username",
|
||||
"created": "Creation Date"
|
||||
},
|
||||
"button": "Search",
|
||||
"searching": "Searching..."
|
||||
},
|
||||
"pagination": {
|
||||
"prev": "Previous",
|
||||
"next": "Next"
|
||||
},
|
||||
"modal": {
|
||||
"add": "Add New User",
|
||||
"close": "Close",
|
||||
"username": "Username",
|
||||
"nickname": "Nickname",
|
||||
"password": "Password",
|
||||
"isAdmin": "Admin Privileges",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save User",
|
||||
"saving": "Saving..."
|
||||
},
|
||||
"modify": {
|
||||
"title": "Modify User Info",
|
||||
"subtitle": "User Information",
|
||||
"isAdmin": "Admin Privileges: ",
|
||||
"disableGacha": "Disable Gacha System: ",
|
||||
"disableGachaHint": "Allows all characters to have equal chances of getting pulled",
|
||||
"isBanned": "Banned:",
|
||||
"cheats": "Cheat Functions",
|
||||
"campaign": "Campaign:",
|
||||
"skipStages": "Skip Stages",
|
||||
"characters": "Characters:",
|
||||
"addAllChars": "Add All Characters",
|
||||
"addChar": "Add Character",
|
||||
"setCharLevels": "Set Character Levels",
|
||||
"setSkillLevels": "Set Skill Levels",
|
||||
"setCoreLevel": "Set Core Level",
|
||||
"inventory": "Inventory:",
|
||||
"addAllEquip": "Add All Equipment",
|
||||
"addItem": "Add Item",
|
||||
"misc": "Miscellaneous:",
|
||||
"finishTutorials": "Finish All Tutorials",
|
||||
"backToList": "Back to List"
|
||||
},
|
||||
"password": {
|
||||
"title": "Change Password",
|
||||
"changeButton": "Change Password",
|
||||
"backToList": "Back to List",
|
||||
"enterNewPassword": "Enter new password",
|
||||
"hint": "Please enter a strong password with letters, numbers and special characters"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Delete User",
|
||||
"confirmation": "Are you sure you want to delete this user?",
|
||||
"backToList": "Back to List"
|
||||
},
|
||||
"notifications": {
|
||||
"searchDone": "Search completed",
|
||||
"requiredFields": "Please fill in the required fields",
|
||||
"userAdded": "User added successfully! Please refresh the page to view."
|
||||
}
|
||||
},
|
||||
"events": {
|
||||
"config": {
|
||||
"title": "Event Configuration",
|
||||
"comingSoon": "Coming Soon!"
|
||||
}
|
||||
},
|
||||
"mail": {
|
||||
"title": "In-game Mail",
|
||||
"comingSoon": "Coming Soon!"
|
||||
},
|
||||
"config": {
|
||||
"server": {
|
||||
"title": "Server Configuration",
|
||||
"logLevel": "Log Level:"
|
||||
},
|
||||
"database": {
|
||||
"title": "Database Configuration",
|
||||
"reload": "Reload Database",
|
||||
"reloadHint": "Loads changes from db.json into memory. Discards unsaved changes."
|
||||
}
|
||||
}
|
||||
}
|
||||
215
EpinelPS/wwwroot/admin/assets/i18n/ja.json
Normal file
215
EpinelPS/wwwroot/admin/assets/i18n/ja.json
Normal file
@@ -0,0 +1,215 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "NIKKEアドミンコンソール",
|
||||
"version": "バージョン v2.5.3",
|
||||
"footer": "© 2025 - NIKKE: 勝利の女神 - アドミンコンソール"
|
||||
},
|
||||
"auth": {
|
||||
"login": "ログイン",
|
||||
"username": "ユーザー名",
|
||||
"password": "パスワード",
|
||||
"enter": "コンソールに入る",
|
||||
"welcome": "管理機能にアクセスするにはログインしてください",
|
||||
"verifying": "確認中...",
|
||||
"success": "ログイン成功",
|
||||
"error": {
|
||||
"required": "ユーザー名とパスワードを入力してください",
|
||||
"invalid": "ログインに失敗しました。認証情報を確認してください",
|
||||
"network": "ネットワークエラー。後ほど再試行してください"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"loading": "読み込み中...",
|
||||
"confirm": "確認",
|
||||
"cancel": "キャンセル",
|
||||
"save": "保存",
|
||||
"delete": "削除",
|
||||
"edit": "編集",
|
||||
"actions": "アクション",
|
||||
"search": "検索",
|
||||
"status": {
|
||||
"online": "オンライン",
|
||||
"offline": "オフライン"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "システム通知",
|
||||
"dashboardRefreshed": "ダッシュボードデータが正常に更新されました"
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "ダッシュボード",
|
||||
"events": "イベント",
|
||||
"users": "ユーザー",
|
||||
"mail": "メール",
|
||||
"config": "設定",
|
||||
"database": "データベース",
|
||||
"profile": "プロフィール",
|
||||
"changePass": "パスワード変更",
|
||||
"logout": "ログアウト"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "コンソール概要",
|
||||
"refresh": "データを更新",
|
||||
"refreshing": "更新中...",
|
||||
"statsCards": {
|
||||
"users": "登録ユーザー",
|
||||
"server": "サーバーステータス",
|
||||
"events": "アクティブイベント",
|
||||
"mail": "今日のメール"
|
||||
},
|
||||
"charts": {
|
||||
"activity": "ユーザーアクティビティ傾向",
|
||||
"server": "サーバーリソース使用状況",
|
||||
"periods": {
|
||||
"day": "日",
|
||||
"week": "週",
|
||||
"month": "月"
|
||||
}
|
||||
},
|
||||
"serverStats": {
|
||||
"cpu": "CPU使用率",
|
||||
"memory": "メモリ使用率",
|
||||
"storage": "ストレージ使用率"
|
||||
},
|
||||
"activity": {
|
||||
"title": "最近のアクティビティ",
|
||||
"newUser": "新規ユーザー登録",
|
||||
"loginAlert": "異常なログインを検出",
|
||||
"configUpdate": "システム設定が更新されました",
|
||||
"rewards": "全体報酬を配布しました",
|
||||
"newEvent": "新しいイベントが作成されました",
|
||||
"bugfix": "ゲームのバグを修正しました",
|
||||
"timeFormat": {
|
||||
"min": "分前",
|
||||
"hour": "時間前",
|
||||
"yesterday": "昨日",
|
||||
"daysAgo": "日前"
|
||||
}
|
||||
},
|
||||
"quickAccess": {
|
||||
"users": {
|
||||
"title": "ユーザー管理",
|
||||
"desc": "ゲームユーザーアカウント、権限、ロールを管理"
|
||||
},
|
||||
"events": {
|
||||
"title": "イベント管理",
|
||||
"desc": "ゲームイベントの作成、編集、監視"
|
||||
},
|
||||
"mail": {
|
||||
"title": "メールシステム",
|
||||
"desc": "プレイヤーにシステムメールと報酬を送信"
|
||||
},
|
||||
"database": {
|
||||
"title": "データベース",
|
||||
"desc": "ゲームデータベースの管理と保守"
|
||||
},
|
||||
"enter": "入る"
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
"title": "ユーザー管理",
|
||||
"add": "新規ユーザー追加",
|
||||
"table": {
|
||||
"id": "ID",
|
||||
"username": "ユーザー名",
|
||||
"nickname": "ニックネーム",
|
||||
"playerName": "プレイヤー名",
|
||||
"isAdmin": "権限",
|
||||
"isBanned": "禁止",
|
||||
"actions": "アクション"
|
||||
},
|
||||
"search": {
|
||||
"username": "ユーザー名",
|
||||
"usernamePlaceholder": "ユーザー名を検索...",
|
||||
"userType": "ユーザータイプ",
|
||||
"types": {
|
||||
"all": "すべて",
|
||||
"admin": "管理者",
|
||||
"user": "一般ユーザー"
|
||||
},
|
||||
"sort": "並び替え",
|
||||
"sortOptions": {
|
||||
"username": "ユーザー名",
|
||||
"created": "作成日"
|
||||
},
|
||||
"button": "検索",
|
||||
"searching": "検索中..."
|
||||
},
|
||||
"pagination": {
|
||||
"prev": "前へ",
|
||||
"next": "次へ"
|
||||
},
|
||||
"modal": {
|
||||
"add": "新規ユーザー追加",
|
||||
"close": "閉じる",
|
||||
"username": "ユーザー名",
|
||||
"nickname": "ニックネーム",
|
||||
"password": "パスワード",
|
||||
"isAdmin": "管理者権限",
|
||||
"cancel": "キャンセル",
|
||||
"save": "ユーザーを保存",
|
||||
"saving": "保存中..."
|
||||
},
|
||||
"modify": {
|
||||
"title": "ユーザー情報の変更",
|
||||
"subtitle": "ユーザー情報",
|
||||
"isAdmin": "管理者権限: ",
|
||||
"disableGacha": "ガチャシステムを無効化: ",
|
||||
"disableGachaHint": "すべてのキャラクターが同じ確率で排出されるようになります",
|
||||
"isBanned": "禁止:",
|
||||
"cheats": "チート機能",
|
||||
"campaign": "キャンペーン:",
|
||||
"skipStages": "ステージスキップ",
|
||||
"characters": "キャラクター:",
|
||||
"addAllChars": "すべてのキャラクターを追加",
|
||||
"addChar": "キャラクター追加",
|
||||
"setCharLevels": "キャラクターレベル設定",
|
||||
"setSkillLevels": "スキルレベル設定",
|
||||
"setCoreLevel": "コアレベル設定",
|
||||
"inventory": "インベントリ:",
|
||||
"addAllEquip": "すべての装備を追加",
|
||||
"addItem": "アイテム追加",
|
||||
"misc": "その他:",
|
||||
"finishTutorials": "すべてのチュートリアルを完了",
|
||||
"backToList": "一覧に戻る"
|
||||
},
|
||||
"password": {
|
||||
"title": "パスワード変更",
|
||||
"changeButton": "パスワード変更",
|
||||
"backToList": "リストに戻る",
|
||||
"enterNewPassword": "新しいパスワードを入力",
|
||||
"hint": "文字、数字、特殊文字を含む強力なパスワードを入力してください"
|
||||
},
|
||||
"delete": {
|
||||
"title": "ユーザー削除",
|
||||
"confirmation": "このユーザーを削除してもよろしいですか?",
|
||||
"backToList": "一覧に戻る"
|
||||
},
|
||||
"notifications": {
|
||||
"searchDone": "検索完了",
|
||||
"requiredFields": "必須フィールドに入力してください",
|
||||
"userAdded": "ユーザーが正常に追加されました!ページを更新して確認してください。"
|
||||
}
|
||||
},
|
||||
"events": {
|
||||
"config": {
|
||||
"title": "イベント設定",
|
||||
"comingSoon": "近日公開!"
|
||||
}
|
||||
},
|
||||
"mail": {
|
||||
"title": "ゲーム内メール",
|
||||
"comingSoon": "近日公開!"
|
||||
},
|
||||
"config": {
|
||||
"server": {
|
||||
"title": "サーバー設定",
|
||||
"logLevel": "ログレベル:"
|
||||
},
|
||||
"database": {
|
||||
"title": "データベース設定",
|
||||
"reload": "データベースを再読み込み",
|
||||
"reloadHint": "db.jsonからの変更をメモリに読み込みます。保存されていない変更は破棄されます。"
|
||||
}
|
||||
}
|
||||
}
|
||||
215
EpinelPS/wwwroot/admin/assets/i18n/ko.json
Normal file
215
EpinelPS/wwwroot/admin/assets/i18n/ko.json
Normal file
@@ -0,0 +1,215 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "NIKKE 관리 콘솔",
|
||||
"version": "버전 v2.5.3",
|
||||
"footer": "© 2025 - NIKKE: 승리의 여신 - 관리 콘솔"
|
||||
},
|
||||
"auth": {
|
||||
"login": "로그인",
|
||||
"username": "사용자 이름",
|
||||
"password": "비밀번호",
|
||||
"enter": "콘솔 입장",
|
||||
"welcome": "관리 기능에 접근하려면 로그인하세요",
|
||||
"verifying": "확인 중...",
|
||||
"success": "로그인 성공",
|
||||
"error": {
|
||||
"required": "사용자 이름과 비밀번호를 입력하세요",
|
||||
"invalid": "로그인 실패, 자격 증명을 확인하세요",
|
||||
"network": "네트워크 오류, 나중에 다시 시도하세요"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"loading": "로딩 중...",
|
||||
"confirm": "확인",
|
||||
"cancel": "취소",
|
||||
"save": "저장",
|
||||
"delete": "삭제",
|
||||
"edit": "편집",
|
||||
"actions": "작업",
|
||||
"search": "검색",
|
||||
"status": {
|
||||
"online": "온라인",
|
||||
"offline": "오프라인"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "시스템 알림",
|
||||
"dashboardRefreshed": "대시보드 데이터가 성공적으로 새로고침되었습니다"
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "대시보드",
|
||||
"events": "이벤트",
|
||||
"users": "사용자",
|
||||
"mail": "메일",
|
||||
"config": "구성",
|
||||
"database": "데이터베이스",
|
||||
"profile": "프로필",
|
||||
"changePass": "비밀번호 변경",
|
||||
"logout": "로그아웃"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "콘솔 개요",
|
||||
"refresh": "데이터 새로고침",
|
||||
"refreshing": "새로고침 중...",
|
||||
"statsCards": {
|
||||
"users": "등록된 사용자",
|
||||
"server": "서버 상태",
|
||||
"events": "활성 이벤트",
|
||||
"mail": "오늘의 메일"
|
||||
},
|
||||
"charts": {
|
||||
"activity": "사용자 활동 추세",
|
||||
"server": "서버 리소스 사용량",
|
||||
"periods": {
|
||||
"day": "일",
|
||||
"week": "주",
|
||||
"month": "월"
|
||||
}
|
||||
},
|
||||
"serverStats": {
|
||||
"cpu": "CPU 사용량",
|
||||
"memory": "메모리 사용량",
|
||||
"storage": "저장소 사용량"
|
||||
},
|
||||
"activity": {
|
||||
"title": "최근 활동",
|
||||
"newUser": "새 사용자 등록",
|
||||
"loginAlert": "비정상 로그인 감지됨",
|
||||
"configUpdate": "시스템 구성 업데이트됨",
|
||||
"rewards": "전체 보상 배포됨",
|
||||
"newEvent": "새 이벤트 생성됨",
|
||||
"bugfix": "게임 버그 수정됨",
|
||||
"timeFormat": {
|
||||
"min": "분 전",
|
||||
"hour": "시간 전",
|
||||
"yesterday": "어제",
|
||||
"daysAgo": "일 전"
|
||||
}
|
||||
},
|
||||
"quickAccess": {
|
||||
"users": {
|
||||
"title": "사용자 관리",
|
||||
"desc": "게임 사용자 계정, 권한 및 역할 관리"
|
||||
},
|
||||
"events": {
|
||||
"title": "이벤트 관리",
|
||||
"desc": "게임 이벤트 생성, 편집 및 모니터링"
|
||||
},
|
||||
"mail": {
|
||||
"title": "메일 시스템",
|
||||
"desc": "플레이어에게 시스템 메일 및 보상 전송"
|
||||
},
|
||||
"database": {
|
||||
"title": "데이터베이스",
|
||||
"desc": "게임 데이터베이스 관리 및 유지보수"
|
||||
},
|
||||
"enter": "입장"
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
"title": "사용자 관리",
|
||||
"add": "새 사용자 추가",
|
||||
"table": {
|
||||
"id": "ID",
|
||||
"username": "사용자 이름",
|
||||
"nickname": "닉네임",
|
||||
"playerName": "플레이어 이름",
|
||||
"isAdmin": "권한",
|
||||
"isBanned": "차단됨",
|
||||
"actions": "작업"
|
||||
},
|
||||
"search": {
|
||||
"username": "사용자 이름",
|
||||
"usernamePlaceholder": "사용자 이름 검색...",
|
||||
"userType": "사용자 유형",
|
||||
"types": {
|
||||
"all": "전체",
|
||||
"admin": "관리자",
|
||||
"user": "일반 사용자"
|
||||
},
|
||||
"sort": "정렬 기준",
|
||||
"sortOptions": {
|
||||
"username": "사용자 이름",
|
||||
"created": "생성일"
|
||||
},
|
||||
"button": "검색",
|
||||
"searching": "검색 중..."
|
||||
},
|
||||
"pagination": {
|
||||
"prev": "이전",
|
||||
"next": "다음"
|
||||
},
|
||||
"modal": {
|
||||
"add": "새 사용자 추가",
|
||||
"close": "닫기",
|
||||
"username": "사용자 이름",
|
||||
"nickname": "닉네임",
|
||||
"password": "비밀번호",
|
||||
"isAdmin": "관리자 권한",
|
||||
"cancel": "취소",
|
||||
"save": "사용자 저장",
|
||||
"saving": "저장 중..."
|
||||
},
|
||||
"modify": {
|
||||
"title": "사용자 정보 수정",
|
||||
"subtitle": "사용자 정보",
|
||||
"isAdmin": "관리자 권한: ",
|
||||
"disableGacha": "가챠 시스템 비활성화: ",
|
||||
"disableGachaHint": "모든 캐릭터가 동일한 확률로 출현하도록 합니다",
|
||||
"isBanned": "차단됨:",
|
||||
"cheats": "치트 기능",
|
||||
"campaign": "캠페인:",
|
||||
"skipStages": "스테이지 건너뛰기",
|
||||
"characters": "캐릭터:",
|
||||
"addAllChars": "모든 캐릭터 추가",
|
||||
"addChar": "캐릭터 추가",
|
||||
"setCharLevels": "캐릭터 레벨 설정",
|
||||
"setSkillLevels": "스킬 레벨 설정",
|
||||
"setCoreLevel": "코어 레벨 설정",
|
||||
"inventory": "인벤토리:",
|
||||
"addAllEquip": "모든 장비 추가",
|
||||
"addItem": "아이템 추가",
|
||||
"misc": "기타:",
|
||||
"finishTutorials": "모든 튜토리얼 완료",
|
||||
"backToList": "목록으로 돌아가기"
|
||||
},
|
||||
"password": {
|
||||
"title": "비밀번호 변경",
|
||||
"changeButton": "비밀번호 변경",
|
||||
"backToList": "목록으로 돌아가기",
|
||||
"enterNewPassword": "새 비밀번호 입력",
|
||||
"hint": "문자, 숫자 및 특수 문자를 포함한 강력한 비밀번호를 입력하세요"
|
||||
},
|
||||
"delete": {
|
||||
"title": "사용자 삭제",
|
||||
"confirmation": "이 사용자를 삭제하시겠습니까?",
|
||||
"backToList": "목록으로 돌아가기"
|
||||
},
|
||||
"notifications": {
|
||||
"searchDone": "검색 완료",
|
||||
"requiredFields": "필수 필드를 작성해주세요",
|
||||
"userAdded": "사용자가 성공적으로 추가되었습니다! 페이지를 새로고침하여 확인하세요."
|
||||
}
|
||||
},
|
||||
"events": {
|
||||
"config": {
|
||||
"title": "이벤트 구성",
|
||||
"comingSoon": "곧 출시 예정!"
|
||||
}
|
||||
},
|
||||
"mail": {
|
||||
"title": "게임 내 메일",
|
||||
"comingSoon": "곧 출시 예정!"
|
||||
},
|
||||
"config": {
|
||||
"server": {
|
||||
"title": "서버 구성",
|
||||
"logLevel": "로그 레벨:"
|
||||
},
|
||||
"database": {
|
||||
"title": "데이터베이스 구성",
|
||||
"reload": "데이터베이스 다시 로드",
|
||||
"reloadHint": "db.json의 변경사항을 메모리에 로드합니다. 저장되지 않은 변경사항은 버려집니다."
|
||||
}
|
||||
}
|
||||
}
|
||||
215
EpinelPS/wwwroot/admin/assets/i18n/zh.json
Normal file
215
EpinelPS/wwwroot/admin/assets/i18n/zh.json
Normal file
@@ -0,0 +1,215 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "胜利女神控制台",
|
||||
"version": "版本 v2.5.3",
|
||||
"footer": "© 2025 - NIKKE: 胜利女神 - 管理控制台"
|
||||
},
|
||||
"auth": {
|
||||
"login": "登录",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"enter": "进入控制台",
|
||||
"welcome": "请登录以访问管理功能",
|
||||
"verifying": "验证中...",
|
||||
"success": "登录成功",
|
||||
"error": {
|
||||
"required": "请输入用户名和密码",
|
||||
"invalid": "登录失败,请检查用户名和密码",
|
||||
"network": "网络错误,请稍后重试"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"loading": "加载中...",
|
||||
"confirm": "确认",
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"actions": "操作",
|
||||
"search": "搜索",
|
||||
"status": {
|
||||
"online": "在线",
|
||||
"offline": "离线"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "系统通知",
|
||||
"dashboardRefreshed": "仪表盘数据已成功刷新"
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "仪表盘",
|
||||
"events": "活动",
|
||||
"users": "用户",
|
||||
"mail": "邮件",
|
||||
"config": "配置",
|
||||
"database": "数据库",
|
||||
"profile": "个人信息",
|
||||
"changePass": "修改密码",
|
||||
"logout": "退出登录"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "控制台概览",
|
||||
"refresh": "刷新数据",
|
||||
"refreshing": "刷新中...",
|
||||
"statsCards": {
|
||||
"users": "注册用户",
|
||||
"server": "服务器状态",
|
||||
"events": "活动进行中",
|
||||
"mail": "今日邮件"
|
||||
},
|
||||
"charts": {
|
||||
"activity": "用户活跃度趋势",
|
||||
"server": "服务器资源使用",
|
||||
"periods": {
|
||||
"day": "日",
|
||||
"week": "周",
|
||||
"month": "月"
|
||||
}
|
||||
},
|
||||
"serverStats": {
|
||||
"cpu": "CPU 使用率",
|
||||
"memory": "内存使用率",
|
||||
"storage": "存储使用率"
|
||||
},
|
||||
"activity": {
|
||||
"title": "最近活动",
|
||||
"newUser": "新用户注册",
|
||||
"loginAlert": "检测到异常登录",
|
||||
"configUpdate": "系统配置已更新",
|
||||
"rewards": "发放全服奖励",
|
||||
"newEvent": "新活动已创建",
|
||||
"bugfix": "修复游戏漏洞",
|
||||
"timeFormat": {
|
||||
"min": "分钟前",
|
||||
"hour": "小时前",
|
||||
"yesterday": "昨天",
|
||||
"daysAgo": "前天"
|
||||
}
|
||||
},
|
||||
"quickAccess": {
|
||||
"users": {
|
||||
"title": "用户管理",
|
||||
"desc": "管理游戏用户账号,权限和角色"
|
||||
},
|
||||
"events": {
|
||||
"title": "活动管理",
|
||||
"desc": "创建、编辑和监控游戏活动"
|
||||
},
|
||||
"mail": {
|
||||
"title": "邮件系统",
|
||||
"desc": "发送系统邮件和奖励给玩家"
|
||||
},
|
||||
"database": {
|
||||
"title": "数据库",
|
||||
"desc": "管理和维护游戏数据库"
|
||||
},
|
||||
"enter": "进入"
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
"title": "用户管理",
|
||||
"add": "添加新用户",
|
||||
"table": {
|
||||
"id": "ID",
|
||||
"username": "用户名",
|
||||
"nickname": "昵称",
|
||||
"playerName": "玩家名称",
|
||||
"isAdmin": "权限",
|
||||
"isBanned": "禁止登录",
|
||||
"actions": "操作"
|
||||
},
|
||||
"search": {
|
||||
"username": "用户名",
|
||||
"usernamePlaceholder": "搜索用户名...",
|
||||
"userType": "用户类型",
|
||||
"types": {
|
||||
"all": "全部",
|
||||
"admin": "管理员",
|
||||
"user": "普通用户"
|
||||
},
|
||||
"sort": "排序方式",
|
||||
"sortOptions": {
|
||||
"username": "用户名",
|
||||
"created": "创建时间"
|
||||
},
|
||||
"button": "搜索",
|
||||
"searching": "搜索中..."
|
||||
},
|
||||
"pagination": {
|
||||
"prev": "上一页",
|
||||
"next": "下一页"
|
||||
},
|
||||
"modal": {
|
||||
"add": "添加新用户",
|
||||
"close": "关闭",
|
||||
"username": "用户名",
|
||||
"nickname": "昵称",
|
||||
"password": "密码",
|
||||
"isAdmin": "管理员权限",
|
||||
"cancel": "取消",
|
||||
"save": "保存用户",
|
||||
"saving": "保存中..."
|
||||
},
|
||||
"modify": {
|
||||
"title": "修改用户信息",
|
||||
"subtitle": "用户信息",
|
||||
"isAdmin": "管理员权限: ",
|
||||
"disableGacha": "禁用抽卡系统: ",
|
||||
"disableGachaHint": "允许所有角色有相同概率被抽到",
|
||||
"isBanned": "禁止登录:",
|
||||
"cheats": "作弊功能",
|
||||
"campaign": "战役:",
|
||||
"skipStages": "跳过关卡",
|
||||
"characters": "角色:",
|
||||
"addAllChars": "添加所有角色",
|
||||
"addChar": "添加角色",
|
||||
"setCharLevels": "设置角色等级",
|
||||
"setSkillLevels": "设置技能等级",
|
||||
"setCoreLevel": "设置核心等级",
|
||||
"inventory": "物品:",
|
||||
"addAllEquip": "添加所有装备",
|
||||
"addItem": "添加物品",
|
||||
"misc": "其他:",
|
||||
"finishTutorials": "完成所有教程",
|
||||
"backToList": "返回列表"
|
||||
},
|
||||
"password": {
|
||||
"title": "修改密码",
|
||||
"changeButton": "修改密码",
|
||||
"backToList": "返回列表",
|
||||
"enterNewPassword": "输入新密码",
|
||||
"hint": "请输入强密码,包含字母、数字和特殊字符"
|
||||
},
|
||||
"delete": {
|
||||
"title": "删除用户",
|
||||
"confirmation": "您确定要删除这个用户吗?",
|
||||
"backToList": "返回列表"
|
||||
},
|
||||
"notifications": {
|
||||
"searchDone": "搜索完成",
|
||||
"requiredFields": "请填写必填字段",
|
||||
"userAdded": "用户添加成功!请刷新页面查看。"
|
||||
}
|
||||
},
|
||||
"events": {
|
||||
"config": {
|
||||
"title": "活动配置",
|
||||
"comingSoon": "即将上线!"
|
||||
}
|
||||
},
|
||||
"mail": {
|
||||
"title": "游戏内邮件",
|
||||
"comingSoon": "即将上线!"
|
||||
},
|
||||
"config": {
|
||||
"server": {
|
||||
"title": "服务器配置",
|
||||
"logLevel": "日志级别:"
|
||||
},
|
||||
"database": {
|
||||
"title": "数据库配置",
|
||||
"reload": "重新加载数据库",
|
||||
"reloadHint": "从db.json加载更改到内存中。未保存的更改将丢失。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
BIN
EpinelPS/wwwroot/admin/assets/img/nikke-bg-pattern.png
Normal file
BIN
EpinelPS/wwwroot/admin/assets/img/nikke-bg-pattern.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 239 KiB |
357
EpinelPS/wwwroot/admin/assets/js/nikke-i18n.js
Normal file
357
EpinelPS/wwwroot/admin/assets/js/nikke-i18n.js
Normal file
@@ -0,0 +1,357 @@
|
||||
/**
|
||||
* NIKKE控制台国际化支持 - 同步版
|
||||
*/
|
||||
class NikkeI18n {
|
||||
constructor() {
|
||||
this.supportedLanguages = ['zh', 'en', 'ja', 'ko'];
|
||||
this.languageNames = {'zh': '简体中文', 'en': 'English', 'ja': '日本語', 'ko': '한국어'};
|
||||
this.currentLang = localStorage.getItem('nikke_lang') || this.detectBrowserLanguage() || 'zh';
|
||||
this.resources = {
|
||||
};
|
||||
this.isApplyingLanguage = false;
|
||||
this.isRenderingSwitcher = false;
|
||||
this.observer = null;
|
||||
|
||||
// 立即同步加载所有语言文件
|
||||
this.loadAllLanguages();
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
try {
|
||||
document.documentElement.classList.add('lang-loading');
|
||||
this.applyLanguage();
|
||||
this.registerEventListeners();
|
||||
this.renderLanguageSwitcher();
|
||||
window.dispatchEvent(new CustomEvent('nikke:i18n-ready'));
|
||||
} catch (error) {
|
||||
console.error('初始化失败:', error);
|
||||
this.fallbackToDefaultLanguage();
|
||||
} finally {
|
||||
document.documentElement.classList.remove('lang-loading');
|
||||
}
|
||||
}
|
||||
|
||||
detectBrowserLanguage() {
|
||||
try {
|
||||
const browserLang = navigator.language || navigator.userLanguage;
|
||||
if (!browserLang) return 'en';
|
||||
const baseLang = browserLang.split('-')[0];
|
||||
return this.supportedLanguages.includes(baseLang) ? baseLang : 'en';
|
||||
} catch {
|
||||
return 'en';
|
||||
}
|
||||
}
|
||||
|
||||
fallbackToDefaultLanguage() {
|
||||
this.currentLang = 'en';
|
||||
try {
|
||||
localStorage.setItem('nikke_lang', this.currentLang);
|
||||
} catch (e) {}
|
||||
this.applyLanguage();
|
||||
}
|
||||
|
||||
loadAllLanguages() {
|
||||
// 同步预加载所有语言文件
|
||||
for (const lang of this.supportedLanguages) {
|
||||
if (this.resources[lang]) continue;
|
||||
|
||||
try {
|
||||
// 使用同步XMLHttpRequest加载语言文件
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', `/admin/assets/i18n/${lang}.json`, false); // false表示同步请求
|
||||
xhr.send();
|
||||
|
||||
if (xhr.status === 200) {
|
||||
const data = JSON.parse(xhr.responseText);
|
||||
if (data && typeof data === 'object') {
|
||||
this.resources[lang] = data;
|
||||
} else {
|
||||
throw new Error('格式无效');
|
||||
}
|
||||
} else {
|
||||
throw new Error(`加载失败: ${lang} (${xhr.status})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`加载语言文件失败: ${lang}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// 确保当前语言已加载
|
||||
if (!this.resources[this.currentLang]) {
|
||||
// 当前语言未加载成功,回退到英文
|
||||
console.warn(`当前语言 ${this.currentLang} 加载失败,回退到英文`);
|
||||
this.currentLang = 'en';
|
||||
try {
|
||||
localStorage.setItem('nikke_lang', 'en');
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
switchLanguage(lang) {
|
||||
if (!this.supportedLanguages.includes(lang)) return;
|
||||
|
||||
try {
|
||||
document.documentElement.classList.add('lang-loading');
|
||||
|
||||
// 确保语言资源已加载
|
||||
if (!this.resources[lang]) {
|
||||
try {
|
||||
// 尝试同步加载缺失的语言资源
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', `/admin/assets/i18n/${lang}.json`, false);
|
||||
xhr.send();
|
||||
|
||||
if (xhr.status === 200) {
|
||||
const data = JSON.parse(xhr.responseText);
|
||||
if (data && typeof data === 'object') {
|
||||
this.resources[lang] = data;
|
||||
} else {
|
||||
throw new Error('格式无效');
|
||||
}
|
||||
} else {
|
||||
throw new Error(`加载失败: ${lang} (${xhr.status})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`切换到语言 ${lang} 时加载失败`, error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.currentLang = lang;
|
||||
localStorage.setItem('nikke_lang', lang);
|
||||
this.applyLanguage();
|
||||
this.updateLanguageSwitcher();
|
||||
window.dispatchEvent(new CustomEvent('nikke:languageChanged', {detail: {lang}}));
|
||||
} catch (error) {
|
||||
console.error(`切换失败: ${lang}`, error);
|
||||
} finally {
|
||||
document.documentElement.classList.remove('lang-loading');
|
||||
}
|
||||
}
|
||||
|
||||
t(key, params = {}) {
|
||||
try {
|
||||
if (!this.resources[this.currentLang]) return key;
|
||||
|
||||
const keys = key.split('.');
|
||||
let value = this.resources[this.currentLang];
|
||||
|
||||
for (const k of keys) {
|
||||
value = value?.[k];
|
||||
if (value === undefined) {
|
||||
if (this.resources['en']) {
|
||||
let enValue = this.resources['en'];
|
||||
for (const k of keys) {
|
||||
enValue = enValue?.[k];
|
||||
if (enValue === undefined) return key;
|
||||
}
|
||||
return typeof enValue === 'string' ? this.replaceParams(enValue, params) : key;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
return typeof value === 'string' ? this.replaceParams(value, params) : key;
|
||||
} catch {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
replaceParams(text, params) {
|
||||
if (!text) return '';
|
||||
return text.replace(/\{\{([^}]+)}}/g, (_, key) =>
|
||||
params[key.trim()] !== undefined ? params[key.trim()] : `{{${key}}}`
|
||||
);
|
||||
}
|
||||
|
||||
applyLanguage() {
|
||||
if (this.isApplyingLanguage) return;
|
||||
this.isApplyingLanguage = true;
|
||||
|
||||
try {
|
||||
if (this.observer) this.observer.disconnect();
|
||||
|
||||
document.querySelectorAll('[data-i18n]').forEach(element => {
|
||||
const key = element.getAttribute('data-i18n');
|
||||
element.textContent = this.t(key);
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-i18n-placeholder]').forEach(element => {
|
||||
const key = element.getAttribute('data-i18n-placeholder');
|
||||
element.placeholder = this.t(key);
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-i18n-title]').forEach(element => {
|
||||
const key = element.getAttribute('data-i18n-title');
|
||||
element.title = this.t(key);
|
||||
});
|
||||
|
||||
document.documentElement.lang = this.currentLang;
|
||||
} catch (error) {
|
||||
console.error('应用语言失败:', error);
|
||||
} finally {
|
||||
if (this.observer) {
|
||||
const mainContent = document.querySelector('.login-container') || document.body;
|
||||
this.observer.observe(mainContent, {childList: true, subtree: true});
|
||||
}
|
||||
this.isApplyingLanguage = false;
|
||||
}
|
||||
}
|
||||
|
||||
registerEventListeners() {
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
this.observer = null;
|
||||
}
|
||||
|
||||
this.observer = new MutationObserver(mutations => {
|
||||
if (this.isApplyingLanguage || this.isRenderingSwitcher) return;
|
||||
|
||||
const hasRelevantChanges = mutations.some(mutation => {
|
||||
if (mutation.type !== 'childList' || !mutation.addedNodes.length) return false;
|
||||
|
||||
return Array.from(mutation.addedNodes).some(node => {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return false;
|
||||
|
||||
if (node.classList && (
|
||||
node.classList.contains('language-switcher') ||
|
||||
node.id === 'nikke-i18n-styles'
|
||||
)) return false;
|
||||
|
||||
return (
|
||||
(node.hasAttribute && (
|
||||
node.hasAttribute('data-i18n') ||
|
||||
node.hasAttribute('data-i18n-placeholder') ||
|
||||
node.hasAttribute('data-i18n-title')
|
||||
)) ||
|
||||
(node.querySelector && node.querySelector('[data-i18n], [data-i18n-placeholder], [data-i18n-title]'))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
if (hasRelevantChanges && !this.isApplyingLanguage) {
|
||||
this.applyLanguage();
|
||||
}
|
||||
});
|
||||
|
||||
const mainContent = document.querySelector('.login-container, .nikke-dashboard') || document.body;
|
||||
this.observer.observe(mainContent, {childList: true, subtree: true});
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => this.applyLanguage());
|
||||
} else {
|
||||
this.applyLanguage();
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!document.hidden) this.applyLanguage();
|
||||
});
|
||||
}
|
||||
|
||||
renderLanguageSwitcher() {
|
||||
if (this.isRenderingSwitcher) return;
|
||||
this.isRenderingSwitcher = true;
|
||||
|
||||
try {
|
||||
if (document.querySelector('.language-switcher')) {
|
||||
this.isRenderingSwitcher = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.observer) this.observer.disconnect();
|
||||
|
||||
const switcherContainer = document.createElement('div');
|
||||
switcherContainer.className = 'language-switcher';
|
||||
switcherContainer.innerHTML = `
|
||||
<div class="language-switcher-toggle">
|
||||
<i class="fas fa-globe"></i>
|
||||
<span class="current-language">${this.languageNames[this.currentLang]}</span>
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<div class="language-options"></div>
|
||||
`;
|
||||
|
||||
const languageOptions = switcherContainer.querySelector('.language-options');
|
||||
this.supportedLanguages.forEach(lang => {
|
||||
const option = document.createElement('div');
|
||||
option.className = `language-option ${lang === this.currentLang ? 'active' : ''}`;
|
||||
option.setAttribute('data-lang', lang);
|
||||
option.innerHTML = `
|
||||
<span class="language-name">${this.languageNames[lang]}</span>
|
||||
<span class="language-code">${lang.toUpperCase()}</span>
|
||||
`;
|
||||
|
||||
option.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.switchLanguage(lang);
|
||||
switcherContainer.classList.remove('open');
|
||||
});
|
||||
|
||||
languageOptions.appendChild(option);
|
||||
});
|
||||
|
||||
const toggle = switcherContainer.querySelector('.language-switcher-toggle');
|
||||
toggle.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
switcherContainer.classList.toggle('open');
|
||||
});
|
||||
|
||||
// 点击外部区域关闭语言选择器
|
||||
const clickOutside = (e) => {
|
||||
if (!switcherContainer.contains(e.target)) {
|
||||
switcherContainer.classList.remove('open');
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', clickOutside);
|
||||
|
||||
// 找到合适的插入位置
|
||||
let target = document.getElementById('navbar-language-switcher');
|
||||
if (target) {
|
||||
target.appendChild(switcherContainer);
|
||||
switcherContainer.classList.add('navbar-integrated');
|
||||
} else {
|
||||
// 后备插入到body
|
||||
document.body.appendChild(switcherContainer);
|
||||
}
|
||||
|
||||
// 恢复DOM观察
|
||||
if (this.observer) {
|
||||
const mainContent = document.querySelector('.login-container') || document.body;
|
||||
this.observer.observe(mainContent, {childList: true, subtree: true});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('渲染语言切换器失败:', error);
|
||||
} finally {
|
||||
this.isRenderingSwitcher = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateLanguageSwitcher() {
|
||||
try {
|
||||
const switcher = document.querySelector('.language-switcher');
|
||||
if (!switcher) return;
|
||||
|
||||
const currentLanguageSpan = switcher.querySelector('.current-language');
|
||||
if (currentLanguageSpan) {
|
||||
currentLanguageSpan.textContent = this.languageNames[this.currentLang];
|
||||
}
|
||||
|
||||
const options = switcher.querySelectorAll('.language-option');
|
||||
options.forEach(option => {
|
||||
const lang = option.getAttribute('data-lang');
|
||||
if (lang === this.currentLang) {
|
||||
option.classList.add('active');
|
||||
} else {
|
||||
option.classList.remove('active');
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新语言切换器失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 实例化
|
||||
window.i18n = new NikkeI18n();
|
||||
38
EpinelPS/wwwroot/admin/assets/js/nikke-login.js
Normal file
38
EpinelPS/wwwroot/admin/assets/js/nikke-login.js
Normal file
@@ -0,0 +1,38 @@
|
||||
// NIKKE登录页专用脚本
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 设置表单提交事件
|
||||
const form = document.getElementById('loginForm');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
AdminLogin();
|
||||
});
|
||||
}
|
||||
|
||||
// 允许按回车键提交
|
||||
const passwordInput = document.getElementById('PasswordBox');
|
||||
if (passwordInput) {
|
||||
passwordInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
AdminLogin();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否已经登录
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
// 已登录用户直接跳转
|
||||
// window.location.pathname = "/admin/dashboard";
|
||||
}
|
||||
|
||||
// 添加动画效果
|
||||
setTimeout(function() {
|
||||
const loginBox = document.querySelector('.login-box');
|
||||
if (loginBox) {
|
||||
loginBox.style.opacity = '1';
|
||||
loginBox.style.transform = 'translateY(0)';
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
42
EpinelPS/wwwroot/admin/assets/js/site.js
Normal file
42
EpinelPS/wwwroot/admin/assets/js/site.js
Normal file
@@ -0,0 +1,42 @@
|
||||
// Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
|
||||
// for details on configuring this project to bundle and minify static web assets.
|
||||
|
||||
// NIKKE管理控制台API通用工具函数
|
||||
|
||||
function runCmd(cmdName, cb, p1, p2)
|
||||
{
|
||||
fetch("/adminapi/RunCmd", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
cmdName: cmdName,
|
||||
p1: p1,
|
||||
p2: p2
|
||||
}),
|
||||
headers: {
|
||||
"Content-type": "application/json; charset=UTF-8",
|
||||
"Authorization": `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((json) => cb(json)).catch((error) => {
|
||||
console.error("命令执行失败:", error);
|
||||
alert("操作失败: " + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
function runSimpleCmd(cmdName, p1, p2)
|
||||
{
|
||||
runCmd(cmdName, function(json){
|
||||
if (json.ok)
|
||||
alert("操作已完成");
|
||||
else
|
||||
alert("错误: " + json.error);
|
||||
}, p1, p2);
|
||||
}
|
||||
|
||||
function runSimpleCmdWithPr(cmdName, p1, p2Title)
|
||||
{
|
||||
let p2 = prompt(p2Title);
|
||||
if (p2 === undefined || p2 == null || p2 == "") return;
|
||||
runSimpleCmd(cmdName, p1, p2);
|
||||
}
|
||||
@@ -1,35 +1,168 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Security System Controller</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<title data-i18n="app.name">NIKKE: 胜利女神 - 管理控制台</title>
|
||||
|
||||
<!-- 字体 -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Exo+2:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;700&family=Noto+Sans+JP:wght@400;500;700&family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- 图标 -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- 基础框架 -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
|
||||
|
||||
<link rel="stylesheet" href="/admin/assets/login.css">
|
||||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
||||
<!-- 自定义样式 -->
|
||||
<link rel="stylesheet" href="/admin/assets/css/nikke-theme.css">
|
||||
<link rel="stylesheet" href="/admin/assets/css/nikke-login.css">
|
||||
<link rel="stylesheet" href="/admin/assets/css/nikke-i18n.css">
|
||||
|
||||
<!-- 网站图标 -->
|
||||
<link rel="icon" href="/admin/assets/img/favicon.ico" type="image/x-icon">
|
||||
</head>
|
||||
<body>
|
||||
<div class="LoginBox">
|
||||
<h1>Login</h1>
|
||||
<form onsubmit="return false;">
|
||||
<div class="mb-3">
|
||||
<label for="UsernameBox" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="UsernameBox" name="username">
|
||||
<body class="login-page">
|
||||
<div class="login-overlay"></div>
|
||||
<div class="nikke-glow"></div>
|
||||
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<div class="login-header">
|
||||
<img src="/admin/assets/img/nikke-logo.png" alt="NIKKE" class="logo">
|
||||
<h1 data-i18n="app.name">胜利女神控制台</h1>
|
||||
<p data-i18n="auth.welcome">请登录以访问管理功能</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="PasswordBox" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="PasswordBox" name="password">
|
||||
<div class="login-body">
|
||||
<form id="loginForm" onsubmit="return false;">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="UsernameBox" name="username" placeholder=" " autocomplete="username">
|
||||
<label for="UsernameBox"><i class="fas fa-user me-2"></i><span data-i18n="auth.username">用户名</span></label>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input type="password" class="form-control" id="PasswordBox" name="password" placeholder=" " autocomplete="current-password">
|
||||
<label for="PasswordBox"><i class="fas fa-lock me-2"></i><span data-i18n="auth.password">密码</span></label>
|
||||
</div>
|
||||
<div id="errormsg" class="error-message"></div>
|
||||
<button type="button" class="login-button" onclick="AdminLogin()">
|
||||
<i class="fas fa-sign-in-alt me-2"></i><span data-i18n="auth.enter">进入控制台</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<p id="errormsg" style="color: red;"></p>
|
||||
<button class="btn btn-primary" onclick="AdminLogin()">Submit</button>
|
||||
</form>
|
||||
<div class="login-footer" data-i18n="app.name">
|
||||
NIKKE: 胜利女神 - 管理员控制台
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="version" data-i18n="app.version">v2.5.3</div>
|
||||
|
||||
<!-- 脚本 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
|
||||
<script src="/admin/assets/js/loginpage.js"></script>
|
||||
|
||||
<!-- 国际化支持 -->
|
||||
<script src="/admin/assets/js/nikke-i18n.js"></script>
|
||||
<script src="/admin/assets/js/nikke-login.js"></script>
|
||||
|
||||
<script>
|
||||
// 鼠标跟随光效
|
||||
document.addEventListener('mousemove', function(e) {
|
||||
const glow = document.querySelector('.nikke-glow');
|
||||
glow.style.left = (e.clientX - 100) + 'px';
|
||||
glow.style.top = (e.clientY - 100) + 'px';
|
||||
});
|
||||
|
||||
// 增强错误消息显示
|
||||
function showErrorMessage(message) {
|
||||
const errorMsg = document.getElementById('errormsg');
|
||||
errorMsg.textContent = message;
|
||||
errorMsg.classList.add('visible');
|
||||
|
||||
// 添加抖动动画效果
|
||||
errorMsg.style.animation = 'none';
|
||||
setTimeout(() => {
|
||||
errorMsg.style.animation = 'shake 0.5s cubic-bezier(.36,.07,.19,.97) both';
|
||||
}, 10);
|
||||
}
|
||||
|
||||
// 覆盖原有的登录函数
|
||||
async function AdminLogin() {
|
||||
const username = document.getElementById("UsernameBox").value;
|
||||
const password = document.getElementById("PasswordBox").value;
|
||||
|
||||
if (!username || !password) {
|
||||
showErrorMessage(i18n.t('auth.error.required'));
|
||||
return;
|
||||
}
|
||||
|
||||
const loginBtn = document.querySelector('.login-button');
|
||||
const originalText = loginBtn.innerHTML;
|
||||
|
||||
// 添加加载状态
|
||||
loginBtn.innerHTML = `<i class="fas fa-circle-notch fa-spin"></i> ${i18n.t('auth.verifying')}`;
|
||||
loginBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch("/adminapi/login", {
|
||||
method: "POST",
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.ok) {
|
||||
localStorage.setItem("token", data.token);
|
||||
|
||||
// 成功效果
|
||||
loginBtn.innerHTML = `<i class="fas fa-check"></i> ${i18n.t('auth.success')}`;
|
||||
loginBtn.style.background = 'linear-gradient(to right, #32d296, #38ef7d)';
|
||||
|
||||
// 添加成功动画,然后跳转
|
||||
setTimeout(() => {
|
||||
window.location.pathname = "/admin/dashboard";
|
||||
}, 800);
|
||||
} else {
|
||||
// 恢复按钮状态
|
||||
loginBtn.innerHTML = originalText;
|
||||
loginBtn.disabled = false;
|
||||
|
||||
// 显示错误
|
||||
const errorMessage = data.message || data.title || i18n.t('auth.error.invalid');
|
||||
showErrorMessage(errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
// 恢复按钮状态
|
||||
loginBtn.innerHTML = originalText;
|
||||
loginBtn.disabled = false;
|
||||
|
||||
// 显示错误
|
||||
showErrorMessage(i18n.t('auth.error.network'));
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 语言变更时刷新页面内容
|
||||
window.addEventListener('nikke:languageChanged', function() {
|
||||
// 更新标题
|
||||
document.title = i18n.t('app.name');
|
||||
|
||||
// 更新错误消息
|
||||
const errorMsg = document.getElementById('errormsg');
|
||||
if (errorMsg.textContent) {
|
||||
errorMsg.textContent = i18n.t('auth.error.invalid');
|
||||
}
|
||||
|
||||
// 更新登录按钮
|
||||
const loginBtn = document.querySelector('.login-button');
|
||||
if (!loginBtn.disabled) {
|
||||
loginBtn.innerHTML = `<i class="fas fa-sign-in-alt me-2"></i><span>${i18n.t('auth.enter')}</span>`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user