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:
阁主
2025-05-25 21:32:34 +08:00
committed by GitHub
parent 03aca327b0
commit 059821deb7
29 changed files with 4332 additions and 242 deletions

View File

@@ -9,6 +9,10 @@
<IncludeBuildOutput>false</IncludeBuildOutput> <IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugType>none</DebugType>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0"> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -131,7 +131,9 @@ namespace EpinelPS.Controllers
private static string CreateAuthToken(User user) private static string CreateAuthToken(User user)
{ {
string tok = RandomString(128); string tok = RandomString(128);
JsonDb.Instance.AdminAuthTokens.Add(tok, user); // 只保留一个token
JsonDb.Instance.AdminAuthTokens.Clear();
JsonDb.Instance.AdminAuthTokens.Add(tok, user.ID);
JsonDb.Save(); JsonDb.Save();
return tok; return tok;
} }

View File

@@ -4,6 +4,7 @@ using EpinelPS.Models.Admin;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Diagnostics; using System.Diagnostics;
using System.Net.Security; using System.Net.Security;
using System.Linq;
namespace EpinelPS.Controllers.AdminPanel namespace EpinelPS.Controllers.AdminPanel
{ {
@@ -18,9 +19,12 @@ namespace EpinelPS.Controllers.AdminPanel
if (token == null) return false; if (token == null) return false;
// TODO better authentication // 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; return false;
} }

View File

@@ -577,7 +577,7 @@ namespace EpinelPS.Database
public List<User> Users = []; public List<User> Users = [];
public List<AccessToken> LauncherAccessTokens = []; public List<AccessToken> LauncherAccessTokens = [];
public Dictionary<string, User> AdminAuthTokens = []; public Dictionary<string, ulong> AdminAuthTokens = new();
public string ServerName = "<color=\"green\">Private Server</color>"; public string ServerName = "<color=\"green\">Private Server</color>";
public byte[] LauncherTokenKey = []; public byte[] LauncherTokenKey = [];

View File

@@ -16,6 +16,11 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>false</DebugSymbols>
<DebugType>none</DebugType>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ASodium" Version="0.6.1" /> <PackageReference Include="ASodium" Version="0.6.1" />
<PackageReference Include="DnsClient" Version="1.8.0" /> <PackageReference Include="DnsClient" Version="1.8.0" />
@@ -39,10 +44,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="Views\Shared\error.cshtml" /> <None Include="wwwroot\admin\**" />
<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\index.html" /> <None Include="wwwroot\admin\index.html" />
<None Include="wwwroot\nikke_launcher\index.html" /> <None Include="wwwroot\nikke_launcher\index.html" />
</ItemGroup> </ItemGroup>
@@ -65,4 +67,12 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\EpinelPS.Analyzers\EpinelPS.Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/> <ProjectReference Include="..\EpinelPS.Analyzers\EpinelPS.Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup> </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> </Project>

View File

@@ -5,18 +5,18 @@
} }
<div class="text-center"> <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"> <form asp-action="Configuration">
<div asp-validation-summary="ModelOnly" class="text-danger"></div> <div asp-validation-summary="ModelOnly" class="text-danger"></div>
<dl class="row"> <dl class="row">
<dt class = "col-sm-2">Log Level:</dt> <dt class="col-sm-2" data-i18n="config.server.logLevel">日志级别:</dt>
<dd class = "col-sm-10"> <dd class="col-sm-10">
@Html.DropDownListFor(model => model.LogType, Html.GetEnumSelectList<LogType>(), "", new { @class = "form-control" }) @Html.DropDownListFor(model => model.LogType, Html.GetEnumSelectList<LogType>(), "", new { @class = "form-control" })
</dd> </dd>
</dl> </dl>
<div class="form-group"> <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> </div>
</form> </form>
</div> </div>

View File

@@ -3,6 +3,6 @@
} }
<div class="text-center"> <div class="text-center">
<h1 class="display-4">Database configuration</h1> <h1 class="display-4" data-i18n="config.database.title">数据库配置</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> <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> </div>

View File

@@ -3,6 +3,6 @@
} }
<div class="text-center"> <div class="text-center">
<h1 class="display-4">Event configuration</h1> <h1 class="display-4" data-i18n="events.config.title">活动配置</h1>
<p>Coming soon!</p> <p data-i18n="events.config.comingSoon">即将上线!</p>
</div> </div>

View File

@@ -3,6 +3,6 @@
} }
<div class="text-center"> <div class="text-center">
<h1 class="display-4">In-game Mail</h1> <h1 class="display-4" data-i18n="mail.title">游戏内邮件</h1>
<p>Coming soon!</p> <p data-i18n="mail.comingSoon">即将上线!</p>
</div> </div>

View File

@@ -1,8 +1,426 @@
@{ @{
ViewData["Title"] = "Home Page"; ViewData["Title"] = "仪表盘";
} }
<div class="text-center"> <div class="nikke-dashboard">
<h1 class="display-4">Welcome</h1> <div class="d-flex justify-content-between align-items-center mb-4">
<p>There are @JsonDb.Instance.Users.Count registered users</p> <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> </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>
}

View File

@@ -1,61 +1,198 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="zh-CN">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - EpinelPS</title> <title>@ViewData["Title"] - NIKKE: <span data-i18n="app.name">胜利女神</span></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" /> <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> </head>
<body> <body>
<header> <header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3"> <nav class="navbar navbar-expand-sm navbar-light border-bottom box-shadow mb-3">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="/admin/dashboard">EpinelPS</a> <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" <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"> aria-expanded="false" aria-label="切换导航">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between"> <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1"> <ul class="navbar-nav flex-grow-1">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" href="/admin/Dashboard">Dashboard</a> <a class="nav-link @(ViewContext.RouteData.Values["action"].ToString() == "Dashboard" ? "active" : "")" href="/admin/Dashboard">
</li> <i class="fas fa-tachometer-alt me-1"></i> <span data-i18n="nav.dashboard">仪表盘</span>
<li class="nav-item"> </a>
<a class="nav-link text-dark" href="/admin/Events">Events</a> </li>
</li> <li class="nav-item">
<li class="nav-item"> <a class="nav-link @(ViewContext.RouteData.Values["action"].ToString() == "Events" ? "active" : "")" href="/admin/Events">
<a class="nav-link text-dark" href="/admin/Users/">Users</a> <i class="fas fa-calendar-alt me-1"></i> <span data-i18n="nav.events">活动</span>
</li> </a>
<li class="nav-item"> </li>
<a class="nav-link text-dark" href="/admin/Mail">Mail</a> <li class="nav-item">
</li> <a class="nav-link @(ViewContext.RouteData.Values["action"].ToString() == "Users" ? "active" : "")" href="/admin/Users/">
<li class="nav-item"> <i class="fas fa-users me-1"></i> <span data-i18n="nav.users">用户</span>
<a class="nav-link text-dark" href="/admin/Configuration">Configuration</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" href="/admin/Database">Database</a> <a class="nav-link @(ViewContext.RouteData.Values["action"].ToString() == "Mail" ? "active" : "")" href="/admin/Mail">
</li> <i class="fas fa-envelope me-1"></i> <span data-i18n="nav.mail">邮件</span>
</ul> </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>
</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">
&copy; 2025 - EpinelPS - <a href="https://github.com/MishaProductions/EpinelPS">Source code</a>
</div> </div>
</footer> </nav>
<script src="/admin/lib/jquery/dist/jquery.min.js"></script> </header>
<script src="/admin/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="/admin/js/site.js" asp-append-version="true"></script> <div class="container">
@await RenderSectionAsync("Scripts", required: false) <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">&copy; 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> </body>
</html> </html>

View File

@@ -4,54 +4,54 @@
ViewData["Title"] = "Delete user"; ViewData["Title"] = "Delete user";
} }
<h1>Delete</h1> <h1 data-i18n="users.delete.title">删除用户</h1>
<p class="text-danger">@ViewData["ErrorMessage"]</p> <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> <div>
<h4>User</h4> <h4 data-i18n="users.table.username">用户</h4>
<hr /> <hr />
<dl class="row"> <dl class="row">
<dt class = "col-sm-2"> <dt class="col-sm-2">
@Html.DisplayNameFor(model => model.ID) @Html.DisplayNameFor(model => model.ID)
</dt> </dt>
<dd class = "col-sm-10"> <dd class="col-sm-10">
@Html.DisplayFor(model => model.ID) @Html.DisplayFor(model => model.ID)
</dd> </dd>
<dt class = "col-sm-2"> <dt class="col-sm-2" data-i18n="users.table.username">
@Html.DisplayNameFor(model => model.Username) 用户名
</dt> </dt>
<dd class = "col-sm-10"> <dd class="col-sm-10">
@Html.DisplayFor(model => model.Username) @Html.DisplayFor(model => model.Username)
</dd> </dd>
<dt class = "col-sm-2"> <dt class="col-sm-2" data-i18n="users.table.isAdmin">
@Html.DisplayNameFor(model => model.IsAdmin) 管理员权限
</dt> </dt>
<dd class = "col-sm-10"> <dd class="col-sm-10">
@Html.DisplayFor(model => model.IsAdmin) @Html.DisplayFor(model => model.IsAdmin)
</dd> </dd>
<dt class = "col-sm-2"> <dt class="col-sm-2" data-i18n="users.table.playerName">
@Html.DisplayNameFor(model => model.PlayerName) 玩家名称
</dt> </dt>
<dd class = "col-sm-10"> <dd class="col-sm-10">
@Html.DisplayFor(model => model.PlayerName) @Html.DisplayFor(model => model.PlayerName)
</dd> </dd>
<dt class = "col-sm-2"> <dt class="col-sm-2" data-i18n="users.table.nickname">
@Html.DisplayNameFor(model => model.Nickname) 昵称
</dt> </dt>
<dd class = "col-sm-10"> <dd class="col-sm-10">
@Html.DisplayFor(model => model.Nickname) @Html.DisplayFor(model => model.Nickname)
</dd> </dd>
<dt class = "col-sm-2"> <dt class="col-sm-2" data-i18n="users.table.isBanned">
@Html.DisplayNameFor(model => model.IsBanned) 禁止登录
</dt> </dt>
<dd class = "col-sm-10"> <dd class="col-sm-10">
@Html.DisplayFor(model => model.IsBanned) @Html.DisplayFor(model => model.IsBanned)
</dd> </dd>
</dl> </dl>
<form asp-action="Delete"> <form asp-action="Delete">
<input type="hidden" asp-for="ID" /> <input type="hidden" asp-for="ID" />
<input type="submit" value="Delete" class="btn btn-danger" /> | <input type="submit" value="删除" class="btn btn-danger" data-i18n="common.delete" /> |
<a asp-action="Index">Back to List</a> <a asp-action="Index" data-i18n="users.delete.backToList">返回列表</a>
</form> </form>
</div> </div>

View File

@@ -1,47 +1,545 @@
@model IEnumerable<EpinelPS.Database.User> @model IEnumerable<EpinelPS.Database.User>
@{ @{
ViewData["Title"] = "Users"; ViewData["Title"] = "用户管理";
} }
<div class="text-center"> <div class="nikke-dashboard">
<h1 class="display-4">Users</h1> <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> </div>
<table class="table"> <!-- 添加用户模态框 - 修改为NIKKE风格 -->
<thead> <div class="nikke-modal" id="addUserModal">
<tr> <div class="nikke-modal-backdrop"></div>
<th> <div class="nikke-modal-container">
@Html.DisplayNameFor(model => model.Username) <div class="nikke-modal-header">
</th> <h5 class="nikke-modal-title" data-i18n="users.modal.add">添加新用户</h5>
<th> <button type="button" class="nikke-modal-close" onclick="closeAddUserModal()">
@Html.DisplayNameFor(model => model.Nickname) <i class="fas fa-times"></i>
</th> </button>
<th> </div>
@Html.DisplayNameFor(model => model.IsAdmin) <div class="nikke-modal-body">
</th> <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> <style>
</thead> /* NIKKE风格分页 */
<tbody> .nikke-pagination {
@foreach (var item in Model) { display: flex;
<tr> justify-content: center;
<td> align-items: center;
@Html.DisplayFor(modelItem => item.Username) gap: 10px;
</td> }
<td>
@Html.DisplayFor(modelItem => item.Nickname) .nikke-pagination-btn {
</td> background-color: var(--nikke-gray-dark);
<td> color: var(--nikke-light);
@Html.DisplayFor(modelItem => item.IsAdmin) border: 1px solid var(--nikke-gray);
</td> border-radius: var(--radius-md);
<td> padding: 8px 15px;
<a asp-action="SetPassword" asp-route-id="@item.ID">Change Password</a> | font-size: 0.9rem;
<a asp-action="Modify" asp-route-id="@item.ID">Modify</a> | font-weight: 600;
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a> transition: all var(--transition-fast);
</td> display: flex;
</tr> align-items: center;
} }
</tbody>
</table> .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>
}

View File

@@ -1,70 +1,250 @@
@model EpinelPS.Models.Admin.ModUserModel @model EpinelPS.Models.Admin.ModUserModel
@{ @{
ViewData["Title"] = "Modify user"; ViewData["Title"] = "修改用户";
} }
<h1>Change user info</h1> <div class="nikke-dashboard">
<div class="d-flex justify-content-between align-items-center mb-4">
<h3>User info</h3> <h1 class="display-4 mb-0" data-i18n="users.modify.title">修改用户信息</h1>
<hr /> <a asp-action="Index" class="btn btn-outline-light">
<div class="row"> <i class="fas fa-arrow-left me-2"></i> <span data-i18n="users.modify.backToList">返回列表</span>
<div class="col-md-4"> </a>
<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> </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>
<p>Characters:</p> <div class="row">
<button class="btn btn-secondary" onclick="runSimpleCmd('addallcharacters', '@Model.ID')">Add all characters</button> <div class="col-md-6 mb-4">
<button class="btn btn-secondary" onclick="runSimpleCmdWithPr('AddCharacter', '@Model.ID', 'Enter character ID. Wrong ID may cause game not to boot.')">Add character</button> <div class="card">
<button class="btn btn-secondary" onclick="runSimpleCmdWithPr('SetLevel', '@Model.ID', 'Enter level (1-999) to apply to all characters')">Set character levels</button> <div class="card-header">
<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> <h3 class="mb-0" data-i18n="users.modify.subtitle">用户信息</h3>
<button class="btn btn-secondary" onclick="runSimpleCmdWithPr('SetCoreLevel', '@Model.ID', 'core level / 0-3 sets stars')">Set core level</button> </div>
<p>Inventory:</p> <div class="card-body">
<button class="btn btn-secondary" onclick="runSimpleCmdWithPr('addallmaterials', '@Model.ID', 'Enter material amount:')">Add all equipment</button> <form asp-action="Modify">
<button class="btn btn-secondary" onclick="runSimpleCmdWithPr('AddItem', '@Model.ID', 'Enter item ID and amount seperated by -')">Add item</button> <div asp-validation-summary="ModelOnly" class="text-danger"></div>
<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> </div>
<div> <style>
<a asp-action="Index">Back to List</a> /* NIKKE风格卡片标题 */
</div> .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>

View File

@@ -1,30 +1,155 @@
@model EpinelPS.Database.User @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> <div class="row justify-content-center">
<hr /> <div class="col-md-6">
<div class="row"> <div class="card">
<div class="col-md-4"> <div class="card-header">
<form asp-action="SetPassword"> <h3 class="mb-0">
<div asp-validation-summary="ModelOnly" class="text-danger"></div> <i class="fas fa-key me-2"></i>
<input type="hidden" asp-for="ID" /> <span data-i18n="users.table.username">用户</span>: @Model.Username
<div class="form-group"> </h3>
<label asp-for="Password" class="control-label"></label> </div>
<input asp-for="Password" class="form-control" required /> <div class="card-body">
<span asp-validation-for="Password" class="text-danger"></span> <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>
<div class="form-group"> </div>
<input type="submit" value="Change password" class="btn btn-danger" />
</div>
</form>
</div> </div>
</div> </div>
<div> <style>
<a asp-action="Index">Back to List</a> /* 卡片样式 */
</div> .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>

View 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;
}

View 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;
}

View 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;
}

View 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;
}
}

View 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."
}
}
}

View 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からの変更をメモリに読み込みます。保存されていない変更は破棄されます。"
}
}
}

View 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의 변경사항을 메모리에 로드합니다. 저장되지 않은 변경사항은 버려집니다."
}
}
}

View 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加载更改到内存中。未保存的更改将丢失。"
}
}
}

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

View 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();

View 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);
});

View 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);
}

View File

@@ -1,35 +1,168 @@
 <!DOCTYPE html>
<!DOCTYPE html> <html lang="zh-CN">
<html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Security System Controller</title> <title data-i18n="app.name">NIKKE: 胜利女神 - 管理控制台</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<!-- 字体 -->
<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 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> </head>
<body> <body class="login-page">
<div class="LoginBox"> <div class="login-overlay"></div>
<h1>Login</h1> <div class="nikke-glow"></div>
<form onsubmit="return false;">
<div class="mb-3"> <div class="login-container">
<label for="UsernameBox" class="form-label">Username</label> <div class="login-box">
<input type="text" class="form-control" id="UsernameBox" name="username"> <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>
<div class="mb-3"> <div class="login-body">
<label for="PasswordBox" class="form-label">Password</label> <form id="loginForm" onsubmit="return false;">
<input type="password" class="form-control" id="PasswordBox" name="password"> <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> </div>
<p id="errormsg" style="color: red;"></p> <div class="login-footer" data-i18n="app.name">
<button class="btn btn-primary" onclick="AdminLogin()">Submit</button> NIKKE: 胜利女神 - 管理员控制台
</form> </div>
</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://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="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> </body>
</html> </html>