1764 lines
59 KiB
HTML
1764 lines
59 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{{.PageTitle}} - {{.Config.WebsiteTitle}}</title>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
<script>
|
|
function showCustomAlert(type, title, message) {
|
|
const modal = document.getElementById('customAlertModal');
|
|
const icon = document.getElementById('alertIcon');
|
|
const alertTitle = document.getElementById('alertTitle');
|
|
const alertMessage = document.getElementById('alertMessage');
|
|
|
|
alertTitle.textContent = title;
|
|
alertMessage.textContent = message;
|
|
|
|
if (type === 'success') {
|
|
icon.className = 'w-16 h-16 mx-auto mb-4 rounded-full flex items-center justify-center text-3xl alert-icon-success';
|
|
icon.innerHTML = '<i class="fas fa-check-circle"></i>';
|
|
} else if (type === 'error') {
|
|
icon.className = 'w-16 h-16 mx-auto mb-4 rounded-full flex items-center justify-center text-3xl alert-icon-error';
|
|
icon.innerHTML = '<i class="fas fa-times-circle"></i>';
|
|
} else {
|
|
icon.className = 'w-16 h-16 mx-auto mb-4 rounded-full flex items-center justify-center text-3xl alert-icon-info';
|
|
icon.innerHTML = '<i class="fas fa-info-circle"></i>';
|
|
}
|
|
|
|
modal.classList.add('active');
|
|
}
|
|
|
|
function closeCustomAlert() {
|
|
const modal = document.getElementById('customAlertModal');
|
|
modal.classList.remove('active');
|
|
}
|
|
|
|
function showToast(message, type) {
|
|
let title = '提示';
|
|
if (type === 'success') {
|
|
title = '成功';
|
|
} else if (type === 'error') {
|
|
title = '错误';
|
|
} else if (type === 'info') {
|
|
title = '信息';
|
|
}
|
|
showCustomAlert(type || 'info', title, message);
|
|
}
|
|
|
|
let confirmCallback = null;
|
|
function showCustomConfirm(title, message, callback) {
|
|
const modal = document.getElementById('customConfirmModal');
|
|
const icon = document.getElementById('confirmIcon');
|
|
const confirmTitle = document.getElementById('confirmTitle');
|
|
const confirmMessage = document.getElementById('confirmMessage');
|
|
|
|
confirmCallback = callback;
|
|
confirmTitle.textContent = title;
|
|
confirmMessage.textContent = message;
|
|
icon.className = 'w-16 h-16 mx-auto mb-4 rounded-full flex items-center justify-center text-3xl alert-icon-info';
|
|
icon.innerHTML = '<i class="fas fa-exclamation-triangle"></i>';
|
|
|
|
modal.classList.add('active');
|
|
}
|
|
|
|
function closeCustomConfirm(result) {
|
|
const modal = document.getElementById('customConfirmModal');
|
|
modal.classList.remove('active');
|
|
if (confirmCallback) {
|
|
confirmCallback(result);
|
|
confirmCallback = null;
|
|
}
|
|
}
|
|
|
|
function confirmLogout(event) {
|
|
event.preventDefault();
|
|
showCustomConfirm('确认退出', '确定要退出登录吗?退出后将需要重新登录。', function(confirmed) {
|
|
if (confirmed) {
|
|
window.location.href = '{{$.AdminPath}}/logout';
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
|
|
|
body {
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background-color: #f8fafc;
|
|
}
|
|
|
|
.modal {
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.modal.active {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
}
|
|
|
|
:root {
|
|
--primary: #6366f1;
|
|
--primary-hover: #4f46e5;
|
|
--primary-light: #e0e7ff;
|
|
--success: #10b981;
|
|
--success-hover: #059669;
|
|
--warning: #f59e0b;
|
|
--warning-hover: #d97706;
|
|
--danger: #ef4444;
|
|
--danger-hover: #dc2626;
|
|
--info: #06b6d4;
|
|
--info-hover: #0891b2;
|
|
--sidebar-bg: #0f172a;
|
|
--sidebar-hover: #1e293b;
|
|
--sidebar-active: #6366f1;
|
|
}
|
|
|
|
.sidebar {
|
|
position: fixed;
|
|
left: 0;
|
|
top: 0;
|
|
height: 100vh;
|
|
width: 15.62rem;
|
|
background: var(--sidebar-bg);
|
|
color: #cbd5e1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
z-index: 100;
|
|
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
|
box-shadow: 0.25rem 0 1.50rem rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
.sidebar.collapsed {
|
|
width: 4.50rem;
|
|
}
|
|
|
|
.sidebar.collapsed ~ .main-area {
|
|
margin-left: 4.50rem;
|
|
}
|
|
|
|
.sidebar-header {
|
|
padding: 1.50rem;
|
|
border-bottom: 0.06rem solid rgba(255, 255, 255, 0.06);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.88rem;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.sidebar.collapsed .sidebar-header {
|
|
padding: 1.25rem 0;
|
|
justify-content: center;
|
|
}
|
|
|
|
.sidebar-logo {
|
|
width: 3rem;
|
|
height: 3rem;
|
|
border-radius: 0.88rem;
|
|
background: linear-gradient(135deg, var(--primary) 0%, #8b5cf6 100%);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
color: white;
|
|
box-shadow: 0 0.38rem 1.25rem rgba(99, 102, 241, 0.4);
|
|
flex-shrink: 0;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.sidebar.collapsed .sidebar-logo {
|
|
width: 2.62rem;
|
|
height: 2.62rem;
|
|
}
|
|
|
|
.sidebar-header-text h1 {
|
|
font-size: 1.12rem;
|
|
font-weight: 700;
|
|
color: #ffffff;
|
|
margin: 0;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.sidebar-header-text p {
|
|
font-size: 0.69rem;
|
|
color: #818cf8;
|
|
margin: 0.12rem 0 0 0;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.sidebar.collapsed .sidebar-header-text {
|
|
display: none;
|
|
}
|
|
|
|
.sidebar-nav {
|
|
padding: 1rem;
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.nav-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.81rem 1rem;
|
|
border-radius: 0.75rem;
|
|
text-decoration: none;
|
|
font-size: 0.88rem;
|
|
font-weight: 500;
|
|
color: #94a3b8;
|
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
position: relative;
|
|
}
|
|
|
|
.nav-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 0.19rem;
|
|
background: var(--sidebar-active);
|
|
opacity: 0;
|
|
transition: opacity 0.25s ease;
|
|
border-radius: 0 0.19rem 0.19rem 0;
|
|
}
|
|
|
|
.nav-item:hover {
|
|
background: var(--sidebar-hover);
|
|
color: #f1f5f9;
|
|
transform: translateX(0.25rem);
|
|
}
|
|
|
|
.nav-item:hover::before {
|
|
opacity: 1;
|
|
}
|
|
|
|
.nav-item.active {
|
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.15) 0%, rgba(139, 92, 246, 0.1) 100%);
|
|
color: #a5b4fc;
|
|
box-shadow: 0 0.25rem 0.94rem rgba(99, 102, 241, 0.2);
|
|
}
|
|
|
|
.nav-item.active::before {
|
|
opacity: 1;
|
|
}
|
|
|
|
.nav-item i {
|
|
width: 1.25rem;
|
|
font-size: 1.12rem;
|
|
text-align: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sidebar.collapsed .nav-item {
|
|
padding: 0.88rem 0;
|
|
justify-content: center;
|
|
border-radius: 0.75rem;
|
|
margin: 0.25rem auto;
|
|
width: 3rem;
|
|
}
|
|
|
|
.sidebar.collapsed .nav-item span {
|
|
display: none;
|
|
}
|
|
|
|
.sidebar.collapsed .nav-item:hover {
|
|
transform: none;
|
|
}
|
|
|
|
.sidebar-footer {
|
|
padding: 1rem;
|
|
border-top: 0.06rem solid rgba(255, 255, 255, 0.06);
|
|
}
|
|
|
|
.sidebar-footer a {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.75rem 1rem;
|
|
border-radius: 0.75rem;
|
|
text-decoration: none;
|
|
font-size: 0.81rem;
|
|
color: #64748b;
|
|
transition: all 0.25s ease;
|
|
}
|
|
|
|
.sidebar-footer a:hover {
|
|
background: var(--sidebar-hover);
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.sidebar.collapsed .sidebar-footer {
|
|
padding: 1rem 0;
|
|
}
|
|
|
|
.sidebar.collapsed .sidebar-footer a {
|
|
padding: 0.88rem 0;
|
|
justify-content: center;
|
|
border-radius: 0.75rem;
|
|
margin: 0.25rem auto;
|
|
width: 3rem;
|
|
}
|
|
|
|
.sidebar.collapsed .sidebar-footer span {
|
|
display: none;
|
|
}
|
|
|
|
.sidebar-toggle {
|
|
position: absolute;
|
|
right: -1.50rem;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 3rem;
|
|
height: 3rem;
|
|
border-radius: 50%;
|
|
background: #ffffff;
|
|
border: 0.12rem solid #e2e8f0;
|
|
color: var(--primary);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
box-shadow: 0 0.25rem 0.94rem rgba(0, 0, 0, 0.1);
|
|
z-index: 101;
|
|
}
|
|
|
|
.sidebar-toggle:hover {
|
|
background: var(--primary-light);
|
|
transform: translateY(-50%) scale(1.1);
|
|
box-shadow: 0 0.38rem 1.25rem rgba(99, 102, 241, 0.3);
|
|
}
|
|
|
|
.sidebar.collapsed .sidebar-toggle {
|
|
right: -1.50rem;
|
|
}
|
|
|
|
.nav-item[data-title]:hover::after {
|
|
content: attr(data-title);
|
|
position: absolute;
|
|
left: calc(100% + 1rem);
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
background: #1e293b;
|
|
color: #f8fafc;
|
|
padding: 0.62rem 1rem;
|
|
border-radius: 0.62rem;
|
|
font-size: 0.81rem;
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
box-shadow: 0 0.50rem 1.56rem rgba(0, 0, 0, 0.4);
|
|
z-index: 200;
|
|
opacity: 0;
|
|
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
pointer-events: none;
|
|
border: 0.06rem solid rgba(255, 255, 255, 0.08);
|
|
}
|
|
|
|
.nav-item[data-title]:hover::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 100%;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
border: 0.38rem solid transparent;
|
|
border-left-color: #1e293b;
|
|
z-index: 200;
|
|
opacity: 0;
|
|
transition: opacity 0.2s ease;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.sidebar.collapsed .nav-item:hover::after {
|
|
opacity: 1;
|
|
transform: translateY(-50%) translateX(0.50rem);
|
|
}
|
|
|
|
.sidebar.collapsed .nav-item:hover::before {
|
|
opacity: 1;
|
|
}
|
|
|
|
.main-area {
|
|
margin-left: 15.62rem;
|
|
transition: margin-left 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: calc(100% - 15.62rem);
|
|
}
|
|
|
|
.sidebar.collapsed ~ .main-area {
|
|
margin-left: 4.50rem;
|
|
width: calc(100% - 4.50rem);
|
|
}
|
|
|
|
.header {
|
|
background: #ffffff;
|
|
border-bottom: 0.06rem solid #e2e8f0;
|
|
padding: 0 1.50rem;
|
|
height: 3.50rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 50;
|
|
box-shadow: 0 0.06rem 0.19rem rgba(0, 0, 0, 0.05);
|
|
width: 100%;
|
|
}
|
|
|
|
.header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.header-title {
|
|
h2 {
|
|
font-size: 1.12rem;
|
|
font-weight: 600;
|
|
color: #1e293b;
|
|
margin: 0.12rem 0 0 0;
|
|
}
|
|
p {
|
|
font-size: 0.81rem;
|
|
color: #64748b;
|
|
margin: 0.12rem 0 0 0;
|
|
}
|
|
}
|
|
.breadcrumb {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.50rem;
|
|
font-size: 0.75rem;
|
|
color: #94a3b8;
|
|
}
|
|
.breadcrumb a {
|
|
text-decoration: none;
|
|
color: #94a3b8;
|
|
transition: color 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
}
|
|
.breadcrumb a:hover {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.header-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.user-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.50rem 0.75rem;
|
|
border-radius: 0.75rem;
|
|
transition: all 0.25s ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.user-info:hover {
|
|
background: #f1f5f9;
|
|
}
|
|
|
|
.user-avatar {
|
|
width: 2.50rem;
|
|
height: 2.50rem;
|
|
border-radius: 0.75rem;
|
|
background: linear-gradient(135deg, var(--primary-light) 0%, #ddd6fe 100%);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.88rem;
|
|
font-weight: 600;
|
|
color: var(--primary);
|
|
box-shadow: 0 0.12rem 0.50rem rgba(99, 102, 241, 0.15);
|
|
}
|
|
|
|
.user-details {
|
|
text-align: right;
|
|
p {
|
|
margin: 0;
|
|
font-size: 0.88rem;
|
|
font-weight: 600;
|
|
color: #334155;
|
|
}
|
|
span {
|
|
font-size: 0.75rem;
|
|
color: #64748b;
|
|
}
|
|
}
|
|
|
|
.content {
|
|
padding: 1.50rem;
|
|
flex: 1;
|
|
width: 100%;
|
|
max-width: 120rem;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.card {
|
|
background: #ffffff;
|
|
border-radius: 1rem;
|
|
border: 0.06rem solid #e2e8f0;
|
|
box-shadow: 0 0.06rem 0.19rem rgba(0, 0, 0, 0.05);
|
|
overflow: hidden;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.card:hover {
|
|
box-shadow: 0 0.25rem 1.25rem rgba(0, 0, 0, 0.08);
|
|
}
|
|
|
|
.card-header {
|
|
padding: 1.25rem 1.50rem;
|
|
border-bottom: 0.06rem solid #f1f5f9;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.card-title {
|
|
h3 {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: #1e293b;
|
|
margin: 0;
|
|
}
|
|
p {
|
|
font-size: 0.81rem;
|
|
color: #64748b;
|
|
margin: 0.12rem 0 0 0;
|
|
}
|
|
}
|
|
|
|
.card-body {
|
|
padding: 1.50rem;
|
|
}
|
|
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.31rem;
|
|
padding: 0.44rem 1rem;
|
|
border-radius: 0.50rem;
|
|
font-size: 0.81rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
border: none;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: var(--primary);
|
|
color: white;
|
|
box-shadow: 0 0.12rem 0.50rem rgba(99, 102, 241, 0.3);
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: var(--primary-hover);
|
|
box-shadow: 0 0.25rem 1rem rgba(99, 102, 241, 0.4);
|
|
transform: translateY(-0.06rem);
|
|
}
|
|
|
|
.btn-primary:active {
|
|
transform: translateY(0);
|
|
box-shadow: 0 0.12rem 0.50rem rgba(99, 102, 241, 0.3);
|
|
}
|
|
|
|
.btn-success {
|
|
background: var(--success);
|
|
color: white;
|
|
box-shadow: 0 0.12rem 0.50rem rgba(16, 185, 129, 0.3);
|
|
}
|
|
|
|
.btn-success:hover {
|
|
background: var(--success-hover);
|
|
box-shadow: 0 0.25rem 1rem rgba(16, 185, 129, 0.4);
|
|
transform: translateY(-0.06rem);
|
|
}
|
|
|
|
.btn-warning {
|
|
background: var(--warning);
|
|
color: white;
|
|
box-shadow: 0 0.12rem 0.50rem rgba(245, 158, 11, 0.3);
|
|
}
|
|
|
|
.btn-warning:hover {
|
|
background: var(--warning-hover);
|
|
box-shadow: 0 0.25rem 1rem rgba(245, 158, 11, 0.4);
|
|
transform: translateY(-0.06rem);
|
|
}
|
|
|
|
.btn-danger {
|
|
background: var(--danger);
|
|
color: white;
|
|
box-shadow: 0 0.12rem 0.50rem rgba(239, 68, 68, 0.3);
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background: var(--danger-hover);
|
|
box-shadow: 0 0.25rem 1rem rgba(239, 68, 68, 0.4);
|
|
transform: translateY(-0.06rem);
|
|
}
|
|
|
|
.btn-info {
|
|
background: var(--info);
|
|
color: white;
|
|
box-shadow: 0 0.12rem 0.50rem rgba(6, 182, 212, 0.3);
|
|
}
|
|
|
|
.btn-info:hover {
|
|
background: var(--info-hover);
|
|
box-shadow: 0 0.25rem 1rem rgba(6, 182, 212, 0.4);
|
|
transform: translateY(-0.06rem);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #f1f5f9;
|
|
color: #475569;
|
|
border: 0.06rem solid #e2e8f0;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: #e2e8f0;
|
|
border-color: #cbd5e1;
|
|
transform: translateY(-0.06rem);
|
|
}
|
|
|
|
.btn-sm {
|
|
padding: 0.38rem 0.88rem;
|
|
font-size: 0.81rem;
|
|
border-radius: 0.50rem;
|
|
}
|
|
|
|
.btn-xs {
|
|
padding: 0.25rem 0.62rem;
|
|
font-size: 0.75rem;
|
|
border-radius: 0.38rem;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.btn-group {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.50rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.modal {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
backdrop-filter: blur(0.25rem);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 500;
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.modal.active {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
}
|
|
|
|
.modal-content {
|
|
background: #ffffff;
|
|
border-radius: 0.62rem;
|
|
box-shadow: 0 1.56rem 3.12rem -0.75rem rgba(0, 0, 0, 0.25);
|
|
max-width: 31.25rem;
|
|
width: 90%;
|
|
overflow: hidden;
|
|
transform: scale(0.9) translateY(1.25rem);
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.modal.active .modal-content {
|
|
transform: scale(1) translateY(0);
|
|
}
|
|
|
|
.modal-header {
|
|
padding: 1.25rem 1.50rem;
|
|
border-bottom: 0.06rem solid #f1f5f9;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.modal-title {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: #1e293b;
|
|
margin: 0;
|
|
}
|
|
|
|
.modal-close {
|
|
width: 2rem;
|
|
height: 2rem;
|
|
border-radius: 0.50rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #94a3b8;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.modal-close:hover {
|
|
background: #f1f5f9;
|
|
color: #64748b;
|
|
}
|
|
|
|
.modal-body {
|
|
padding: 1.50rem;
|
|
}
|
|
|
|
.modal-footer {
|
|
padding: 1rem 1.50rem;
|
|
border-top: 0.06rem solid #f1f5f9;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
|
|
.form-label {
|
|
display: block;
|
|
font-size: 0.81rem;
|
|
font-weight: 600;
|
|
color: #475569;
|
|
margin-bottom: 0.50rem;
|
|
}
|
|
|
|
.form-input {
|
|
width: 100%;
|
|
padding: 0.75rem 1rem;
|
|
border: 0.12rem solid #e2e8f0;
|
|
border-radius: 0.75rem;
|
|
font-size: 0.88rem;
|
|
font-weight: 500;
|
|
color: #334155;
|
|
background: #fafafa;
|
|
transition: all 0.25s ease;
|
|
outline: none;
|
|
}
|
|
|
|
.form-input:focus {
|
|
border-color: var(--primary);
|
|
background: #ffffff;
|
|
box-shadow: 0 0 0 0.19rem rgba(99, 102, 241, 0.1);
|
|
}
|
|
|
|
.form-input::placeholder {
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
table-layout: auto;
|
|
}
|
|
|
|
.table th {
|
|
padding: 1rem 1.25rem;
|
|
text-align: left;
|
|
font-size: 0.81rem;
|
|
font-weight: 600;
|
|
color: #64748b;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03rem;
|
|
border-bottom: 0.12rem solid #f1f5f9;
|
|
background: #fafafa;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.table td {
|
|
padding: 1rem 1.25rem;
|
|
font-size: 0.88rem;
|
|
color: #475569;
|
|
border-bottom: 0.06rem solid #f1f5f9;
|
|
transition: background 0.2s ease;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
max-width: 12.50rem;
|
|
}
|
|
|
|
.table td.wrap,
|
|
.table td.actions {
|
|
white-space: normal;
|
|
max-width: none;
|
|
}
|
|
|
|
.table tbody tr:hover td {
|
|
background: #f8fafc;
|
|
}
|
|
|
|
.table tbody tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.table th.actions-col,
|
|
.table td.actions-col {
|
|
width: 15rem;
|
|
min-width: 12.50rem;
|
|
}
|
|
.table td.actions-col .btn-group {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.page-header {
|
|
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
|
border-bottom: 0.06rem solid #e2e8f0;
|
|
padding: 1.25rem 1.50rem;
|
|
margin: -1.50rem -1.50rem 1.50rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
.page-header h3 {
|
|
font-size: 1.12rem;
|
|
font-weight: 600;
|
|
color: #1e293b;
|
|
margin: 0;
|
|
}
|
|
.page-header p {
|
|
font-size: 0.81rem;
|
|
color: #64748b;
|
|
margin: 0.25rem 0 0 0;
|
|
}
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 3rem 1.50rem;
|
|
}
|
|
.empty-state-icon {
|
|
width: 4rem;
|
|
height: 4rem;
|
|
margin: 0 auto 1rem;
|
|
border-radius: 50%;
|
|
background: #f1f5f9;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.empty-state-icon i {
|
|
font-size: 1.50rem;
|
|
color: #94a3b8;
|
|
}
|
|
.empty-state p {
|
|
color: #64748b;
|
|
font-size: 0.88rem;
|
|
margin: 0;
|
|
}
|
|
|
|
.badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 1.25rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.badge-success {
|
|
background: #d1fae5;
|
|
color: #065f46;
|
|
}
|
|
|
|
.badge-warning {
|
|
background: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.badge-danger {
|
|
background: #fee2e2;
|
|
color: #991b1b;
|
|
}
|
|
|
|
.badge-info {
|
|
background: #cffafe;
|
|
color: #0e7490;
|
|
}
|
|
|
|
.badge-slate {
|
|
background: #f1f5f9;
|
|
color: #475569;
|
|
}
|
|
|
|
.search-box {
|
|
position: relative;
|
|
width: 18.75rem;
|
|
}
|
|
|
|
.search-box input {
|
|
width: 100%;
|
|
padding: 0.62rem 2.50rem 0.62rem 1rem;
|
|
border: 0.12rem solid #e2e8f0;
|
|
border-radius: 0.75rem;
|
|
font-size: 0.88rem;
|
|
background: #fafafa;
|
|
transition: all 0.25s ease;
|
|
outline: none;
|
|
}
|
|
|
|
.search-box input:focus {
|
|
border-color: var(--primary);
|
|
background: #ffffff;
|
|
box-shadow: 0 0 0 0.19rem rgba(99, 102, 241, 0.1);
|
|
}
|
|
|
|
.search-box i {
|
|
position: absolute;
|
|
right: 0.88rem;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: #94a3b8;
|
|
font-size: 0.88rem;
|
|
}
|
|
|
|
.alert-icon-success {
|
|
background: #d1fae5;
|
|
color: #065f46;
|
|
}
|
|
|
|
.alert-icon-error {
|
|
background: #fee2e2;
|
|
color: #991b1b;
|
|
}
|
|
|
|
.alert-icon-info {
|
|
background: #cffafe;
|
|
color: #0e7490;
|
|
}
|
|
|
|
.back-to-top {
|
|
position: fixed;
|
|
bottom: 2rem;
|
|
right: 0.38rem;
|
|
width: 1.75rem;
|
|
height: 1.75rem;
|
|
border-radius: 0.62rem;
|
|
background: var(--primary);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.62rem;
|
|
cursor: pointer;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
box-shadow: 0 0.25rem 0.94rem rgba(99, 102, 241, 0.4);
|
|
z-index: 400;
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transform: translateY(1.25rem);
|
|
}
|
|
|
|
.fixed-top-right {
|
|
position: fixed;
|
|
top: 6.88rem;
|
|
right: 1.50rem;
|
|
z-index: 100;
|
|
box-shadow: 0 0.25rem 0.94rem rgba(99, 102, 241, 0.3);
|
|
}
|
|
|
|
.back-to-top.show {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.back-to-top:hover {
|
|
background: var(--primary-hover);
|
|
box-shadow: 0 0.38rem 1.25rem rgba(99, 102, 241, 0.5);
|
|
transform: translateY(-0.12rem);
|
|
}
|
|
|
|
.back-to-top:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.markdown-body {
|
|
background-color: #eff6ff !important;
|
|
font-size: 0.88rem;
|
|
}
|
|
|
|
.ai-chat-toggle {
|
|
position: fixed;
|
|
bottom: 5rem;
|
|
right: 0.38rem;
|
|
width: 1.75rem;
|
|
height: 1.75rem;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, var(--primary) 0%, #8b5cf6 100%);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.62rem;
|
|
cursor: pointer;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
box-shadow: 0 0.38rem 1.25rem rgba(99, 102, 241, 0.5);
|
|
z-index: 450;
|
|
}
|
|
|
|
.ai-chat-toggle:hover {
|
|
transform: scale(1.1);
|
|
box-shadow: 0 0.50rem 1.56rem rgba(99, 102, 241, 0.6);
|
|
}
|
|
|
|
.ai-chat-panel {
|
|
position: fixed;
|
|
bottom: 7.50rem;
|
|
right: 1.50rem;
|
|
width: 22.50rem;
|
|
min-height: 6.25rem;
|
|
background: #ffffff;
|
|
border-radius: 0.62rem;
|
|
box-shadow: 0 1.25rem 3.75rem rgba(0, 0, 0, 0.15);
|
|
border: 0.06rem solid #e2e8f0;
|
|
z-index: 550;
|
|
overflow: hidden;
|
|
opacity: 0;
|
|
transform: translateY(1.25rem);
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.ai-chat-panel.show {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.ai-chat-header {
|
|
padding: 0.75rem 1.12rem;
|
|
background: linear-gradient(135deg, var(--primary) 0%, #8b5cf6 100%);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.ai-chat-header h4 {
|
|
margin: 0;
|
|
font-size: 0.88rem;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.50rem;
|
|
}
|
|
|
|
.ai-chat-close {
|
|
width: 1.75rem;
|
|
height: 1.75rem;
|
|
border-radius: 0.50rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
transition: background 0.2s ease;
|
|
}
|
|
|
|
.ai-chat-close:hover {
|
|
background: rgba(255, 255, 255, 0.2);
|
|
}
|
|
|
|
.ai-chat-messages {
|
|
padding: 1rem;
|
|
max-height: 25rem;
|
|
overflow-y: auto;
|
|
background: #f8fafc;
|
|
word-break: break-all;
|
|
/*white-space: pre-wrap;*/
|
|
}
|
|
pre,code {
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
.ai-chat-input-area {
|
|
padding: 0.75rem 1rem;
|
|
border-top: 0.06rem solid #e2e8f0;
|
|
display: flex;
|
|
gap: 0.62rem;
|
|
background: #ffffff;
|
|
}
|
|
|
|
.ai-chat-input {
|
|
flex: 1;
|
|
padding: 0.62rem 0.88rem;
|
|
border: 0.12rem solid #e2e8f0;
|
|
border-radius: 0.75rem;
|
|
font-size: 0.81rem;
|
|
outline: none;
|
|
transition: all 0.25s ease;
|
|
}
|
|
|
|
.ai-chat-input:focus {
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 0.19rem rgba(99, 102, 241, 0.1);
|
|
}
|
|
|
|
.ai-chat-send-btn {
|
|
padding: 0.62rem 1rem;
|
|
border-radius: 0.75rem;
|
|
background: var(--primary);
|
|
color: white;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: all 0.25s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.ai-chat-send-btn:hover {
|
|
background: var(--primary-hover);
|
|
transform: translateY(-0.06rem);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="flex min-h-screen">
|
|
<aside id="sidebar" class="sidebar">
|
|
<button onclick="toggleSidebar()" class="sidebar-toggle">
|
|
<i class="fas fa-chevron-left" id="sidebarToggleIcon"></i>
|
|
</button>
|
|
<div class="sidebar-header">
|
|
{{ if .Config.LogoPath }}
|
|
<img src="{{.Config.LogoPath}}" class="sidebar-logo object-cover" alt="Logo">
|
|
{{ else }}
|
|
<div class="sidebar-logo">
|
|
R
|
|
</div>
|
|
{{ end }}
|
|
<div class="sidebar-header-text">
|
|
<h2>{{.Config.WebsiteTitle}}管理后台</h2>
|
|
</div>
|
|
</div>
|
|
<nav class="sidebar-nav">
|
|
{{ range .Menus }}
|
|
<a href="{{$.AdminPath}}/{{.Path}}" class="nav-item{{ if eq $.CurrentMenu .Path }} active{{ end }}" data-title="{{.Name}}">
|
|
<i class="{{.Icon}}"></i> <span>{{.Name}}</span>
|
|
</a>
|
|
{{ end }}
|
|
</nav>
|
|
<div class="flex-1"></div>
|
|
<div class="sidebar-footer">
|
|
<a href="#" onclick="confirmLogout(event)" data-title="退出登录">
|
|
<i class="fas fa-sign-out-alt"></i> <span>退出登录</span>
|
|
</a>
|
|
</div>
|
|
</aside>
|
|
<div class="main-area">
|
|
<header class="header">
|
|
<div class="header-left">
|
|
<div class="header-title">
|
|
<div class="breadcrumb">
|
|
<a href="{{.AdminPath}}/" class="text-slate-400 hover:text-primary-500 transition-colors">
|
|
<i class="fas fa-home"></i>
|
|
</a>
|
|
<i class="fas fa-chevron-right text-slate-300 text-xs"></i>
|
|
<span class="text-slate-700">{{.PageTitle}}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="header-right">
|
|
<button class="relative p-3 text-slate-500 hover:text-slate-700 hover:bg-slate-100 rounded-xl transition-all cursor-pointer" onclick="toggleNotifications()">
|
|
<i class="fas fa-bell"></i>
|
|
<span class="absolute top-2 right-2 w-2 h-2 bg-rose-500 rounded-full hidden" id="notificationBadge"></span>
|
|
</button>
|
|
<div class="user-info">
|
|
<div class="user-details">
|
|
<p>{{.Username}}</p>
|
|
<span>管理员</span>
|
|
</div>
|
|
<div class="user-avatar">
|
|
{{ substr .Username 0 1 }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<main class="content">
|
|
{{.Content}}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
<div class="modal" id="customAlertModal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title" id="alertTitle">提示</h3>
|
|
<div class="modal-close" onclick="closeCustomAlert()">
|
|
<i class="fas fa-times"></i>
|
|
</div>
|
|
</div>
|
|
<div class="modal-body text-center">
|
|
<div id="alertIcon" class="w-16 h-16 mx-auto mb-4 rounded-full flex items-center justify-center text-3xl"></div>
|
|
<p id="alertMessage" class="text-slate-600"></p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button onclick="closeCustomAlert()" class="btn btn-primary">确定</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal" id="customConfirmModal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title" id="confirmTitle">确认操作</h3>
|
|
<div class="modal-close" onclick="closeCustomConfirm(false)">
|
|
<i class="fas fa-times"></i>
|
|
</div>
|
|
</div>
|
|
<div class="modal-body text-center">
|
|
<div id="confirmIcon" class="w-16 h-16 mx-auto mb-4 rounded-full flex items-center justify-center text-3xl"></div>
|
|
<p id="confirmMessage" class="text-slate-600"></p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button onclick="closeCustomConfirm(false)" class="btn btn-secondary">取消</button>
|
|
<button onclick="closeCustomConfirm(true)" class="btn btn-danger">确定</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button onclick="scrollToTop()" class="back-to-top" id="backToTop">
|
|
<i class="fas fa-chevron-up"></i>
|
|
</button>
|
|
|
|
<div onclick="toggleChat()" class="ai-chat-toggle">
|
|
<i class="fas fa-robot"></i>
|
|
</div>
|
|
|
|
<div class="ai-chat-panel hidden" id="aiChatPanel">
|
|
<div class="ai-chat-header">
|
|
<h4><i class="fas fa-robot"></i> AI 助手</h4>
|
|
<div class="ai-chat-close" onclick="event.stopPropagation(); toggleChat();">
|
|
<i class="fas fa-times"></i>
|
|
</div>
|
|
</div>
|
|
<div class="ai-chat-messages" id="chatMessages">
|
|
<div class="flex items-start gap-3 mb-3">
|
|
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-purple-400 to-purple-600 flex items-center justify-center flex-shrink-0">
|
|
<i class="fas fa-robot text-white text-xs"></i>
|
|
</div>
|
|
<div class="bg-slate-100 rounded-xl rounded-tl-none p-3 text-sm text-slate-600 max-w-[80%]">你好!我是您的 AI 助手,有什么可以帮您的?</div>
|
|
</div>
|
|
</div>
|
|
<div class="ai-chat-input-area">
|
|
<input type="text" class="ai-chat-input" id="chatInput" placeholder="输入您的问题..." onkeypress="if(event.key === 'Enter') sendChatMessage()">
|
|
<button class="ai-chat-send-btn" onclick="sendChatMessage()">
|
|
<i class="fas fa-paper-plane"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="fixed top-20 right-4 w-80 bg-white rounded-xl shadow-lg border border-slate-200 z-50 hidden" id="notificationDropdown">
|
|
<div class="p-4 border-b border-slate-200 flex items-center justify-between">
|
|
<h3 class="font-semibold text-slate-800">通知</h3>
|
|
<div class="flex items-center gap-3">
|
|
<button onclick="markAllAsRead()" class="text-xs text-blue-500 hover:text-blue-700" id="markAllReadBtn">全部已读</button>
|
|
<button onclick="clearNotifications()" class="text-xs text-slate-500 hover:text-slate-700">清空</button>
|
|
</div>
|
|
</div>
|
|
<div class="max-h-80 overflow-y-auto p-2" id="notificationList">
|
|
<div class="text-center py-8 text-slate-500">
|
|
<i class="fas fa-bell text-3xl mb-2 text-slate-300"></i>
|
|
<p class="text-sm">暂无通知</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
function toggleSidebar() {
|
|
const sidebar = document.getElementById('sidebar');
|
|
const icon = document.getElementById('sidebarToggleIcon');
|
|
const mainArea = document.querySelector('.main-area');
|
|
|
|
if (sidebar.classList.contains('collapsed')) {
|
|
sidebar.classList.remove('collapsed');
|
|
sidebar.style.width = '15.62rem';
|
|
mainArea.style.marginLeft = '15.62rem';
|
|
icon.className = 'fas fa-chevron-left';
|
|
} else {
|
|
sidebar.classList.add('collapsed');
|
|
sidebar.style.width = '4.50rem';
|
|
mainArea.style.marginLeft = '4.50rem';
|
|
icon.className = 'fas fa-chevron-right';
|
|
}
|
|
}
|
|
|
|
document.addEventListener('click', function(e) {
|
|
const alertModal = document.getElementById('customAlertModal');
|
|
const confirmModal = document.getElementById('customConfirmModal');
|
|
|
|
if (alertModal && e.target === alertModal) {
|
|
closeCustomAlert();
|
|
}
|
|
if (confirmModal && e.target === confirmModal) {
|
|
closeCustomConfirm(false);
|
|
}
|
|
});
|
|
|
|
function scrollToTop() {
|
|
window.scrollTo({
|
|
top: 0,
|
|
behavior: 'smooth'
|
|
});
|
|
}
|
|
|
|
window.addEventListener('scroll', function() {
|
|
const backToTop = document.getElementById('backToTop');
|
|
if (backToTop) {
|
|
if (window.scrollY > 200) {
|
|
backToTop.classList.add('show');
|
|
} else {
|
|
backToTop.classList.remove('show');
|
|
}
|
|
}
|
|
});
|
|
|
|
let notificationOpen = false;
|
|
let notifications = [];
|
|
let searchOpen = false;
|
|
let searchFilter = 'all';
|
|
let searchTimeout = null;
|
|
|
|
// 页面加载时获取未读通知数量,决定是否显示小红点
|
|
(function initNotificationBadge() {
|
|
fetch('/dashboard/api/notifications/unread-count')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
const count = (data.data && data.data.count) || 0;
|
|
const badge = document.getElementById('notificationBadge');
|
|
if (badge) {
|
|
badge.classList.toggle('hidden', count === 0);
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
})();
|
|
|
|
function toggleNotifications() {
|
|
const dropdown = document.getElementById('notificationDropdown');
|
|
notificationOpen = !notificationOpen;
|
|
if (notificationOpen) {
|
|
dropdown.classList.remove('hidden');
|
|
loadNotifications();
|
|
} else {
|
|
dropdown.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
function loadNotifications() {
|
|
fetch('/dashboard/api/notifications')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
notifications = data.data || [];
|
|
renderNotifications();
|
|
})
|
|
.catch(() => {
|
|
renderNotifications();
|
|
});
|
|
}
|
|
|
|
function renderNotifications() {
|
|
const list = document.getElementById('notificationList');
|
|
const badge = document.getElementById('notificationBadge');
|
|
const markAllBtn = document.getElementById('markAllReadBtn');
|
|
|
|
if (notifications.length === 0) {
|
|
list.innerHTML = '<div class="text-center py-8 text-slate-500"><i class="fas fa-bell text-3xl mb-2 text-slate-300"></i><p class="text-sm">暂无通知</p></div>';
|
|
badge.classList.add('hidden');
|
|
if (markAllBtn) markAllBtn.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
const unreadCount = notifications.filter(n => !n.read).length;
|
|
badge.classList.toggle('hidden', unreadCount === 0);
|
|
if (markAllBtn) markAllBtn.style.display = unreadCount > 0 ? '' : 'none';
|
|
|
|
list.innerHTML = notifications.map(n => `
|
|
<div class="notification-item p-3 mb-2 rounded-lg hover:bg-slate-50 cursor-pointer transition-colors ${n.read ? '' : 'bg-blue-50'}" data-id="${n.id}">
|
|
<div class="flex items-start gap-3">
|
|
<div class="w-8 h-8 rounded-full ${getNotificationIconBg(n.type)} flex items-center justify-center flex-shrink-0">
|
|
<i class="fas ${getNotificationIcon(n.type)} text-xs text-white"></i>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm font-medium text-slate-800 truncate">${n.title}</p>
|
|
<p class="text-xs text-slate-500 mt-1 line-clamp-2">${n.message}</p>
|
|
<p class="text-xs text-slate-400 mt-1">${n.time}</p>
|
|
</div>
|
|
${!n.read ? '<div class="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0 mt-2"></div>' : ''}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
document.querySelectorAll('.notification-item').forEach((item, index) => {
|
|
item.onclick = () => handleNotificationClick(notifications[index]);
|
|
});
|
|
}
|
|
|
|
function getNotificationIcon(type) {
|
|
switch(type) {
|
|
case 'user': return 'fa-user';
|
|
case 'resume': return 'fa-file-alt';
|
|
case 'portfolio': return 'fa-folder-open';
|
|
case 'quiz': return 'fa-clipboard-list';
|
|
case 'system': return 'fa-cog';
|
|
default: return 'fa-bell';
|
|
}
|
|
}
|
|
|
|
function getNotificationIconBg(type) {
|
|
switch(type) {
|
|
case 'user': return 'bg-blue-500';
|
|
case 'resume': return 'bg-green-500';
|
|
case 'portfolio': return 'bg-purple-500';
|
|
case 'quiz': return 'bg-orange-500';
|
|
case 'system': return 'bg-gray-500';
|
|
default: return 'bg-primary';
|
|
}
|
|
}
|
|
|
|
function handleNotificationClick(notification) {
|
|
if (!notification.read) {
|
|
markNotificationAsRead(notification.id);
|
|
}
|
|
if (notification.url) {
|
|
window.location.href = notification.url;
|
|
}
|
|
}
|
|
|
|
function markNotificationAsRead(id) {
|
|
fetch('/dashboard/api/notifications/' + id + '/read', { method: 'POST' })
|
|
.then(() => {
|
|
const n = notifications.find(item => item.id === id);
|
|
if (n) n.read = true;
|
|
renderNotifications();
|
|
});
|
|
}
|
|
|
|
function markAllAsRead() {
|
|
fetch('/dashboard/api/notifications/read-all', { method: 'POST' })
|
|
.then(() => {
|
|
notifications.forEach(n => n.read = true);
|
|
renderNotifications();
|
|
});
|
|
}
|
|
|
|
function clearNotifications() {
|
|
fetch('/dashboard/api/notifications', { method: 'DELETE' })
|
|
.then(() => {
|
|
notifications = [];
|
|
renderNotifications();
|
|
});
|
|
}
|
|
|
|
function handleSearchInput(value) {
|
|
const query = value.trim();
|
|
const results = document.getElementById('searchResults');
|
|
|
|
if (query.length >= 2) {
|
|
searchOpen = true;
|
|
results.classList.remove('hidden');
|
|
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => {
|
|
performSearch(query);
|
|
}, 300);
|
|
} else {
|
|
searchOpen = false;
|
|
results.classList.add('hidden');
|
|
document.getElementById('searchResultsList').innerHTML = '<div class="text-center py-4 text-slate-500 text-sm">输入关键词开始搜索</div>';
|
|
}
|
|
}
|
|
|
|
function performSearch(query) {
|
|
fetch('/dashboard/api/search?q=' + encodeURIComponent(query) + '&type=' + searchFilter)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
renderSearchResults(data.results || []);
|
|
})
|
|
.catch(() => {
|
|
renderSearchResults([]);
|
|
});
|
|
}
|
|
|
|
function filterSearch(type) {
|
|
searchFilter = type;
|
|
document.querySelectorAll('.search-filter-btn').forEach(btn => {
|
|
btn.classList.toggle('active', btn.textContent === getFilterLabel(type));
|
|
btn.classList.toggle('bg-primary-50', btn.textContent === getFilterLabel(type));
|
|
btn.classList.toggle('text-primary-600', btn.textContent === getFilterLabel(type));
|
|
});
|
|
const query = document.getElementById('globalSearchInput').value.trim();
|
|
if (query.length >= 2) {
|
|
performSearch(query);
|
|
}
|
|
}
|
|
|
|
function getFilterLabel(type) {
|
|
switch(type) {
|
|
case 'user': return '用户';
|
|
case 'resume': return '简历';
|
|
case 'portfolio': return '作品集';
|
|
default: return '全部';
|
|
}
|
|
}
|
|
|
|
function renderSearchResults(results) {
|
|
const list = document.getElementById('searchResultsList');
|
|
|
|
if (results.length === 0) {
|
|
list.innerHTML = '<div class="text-center py-4 text-slate-500 text-sm">未找到匹配结果</div>';
|
|
return;
|
|
}
|
|
|
|
list.innerHTML = results.map(r => `
|
|
<a href="${r.url}" class="flex items-center gap-3 p-3 hover:bg-slate-50 rounded-lg transition-colors">
|
|
<div class="w-10 h-10 rounded-full ${getSearchIconBg(r.type)} flex items-center justify-center">
|
|
<i class="fas ${getSearchIcon(r.type)} text-sm text-white"></i>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm font-medium text-slate-800 truncate">${r.title}</p>
|
|
<p class="text-xs text-slate-500">${r.subtitle}</p>
|
|
</div>
|
|
<span class="text-xs px-2 py-1 bg-slate-100 text-slate-600 rounded-full">${getSearchTypeName(r.type)}</span>
|
|
</a>
|
|
`).join('');
|
|
}
|
|
|
|
function getSearchIcon(type) {
|
|
switch(type) {
|
|
case 'user': return 'fa-user';
|
|
case 'resume': return 'fa-file-alt';
|
|
case 'portfolio': return 'fa-folder-open';
|
|
default: return 'fa-search';
|
|
}
|
|
}
|
|
|
|
function getSearchIconBg(type) {
|
|
switch(type) {
|
|
case 'user': return 'bg-blue-100';
|
|
case 'resume': return 'bg-green-100';
|
|
case 'portfolio': return 'bg-purple-100';
|
|
default: return 'bg-slate-100';
|
|
}
|
|
}
|
|
|
|
function getSearchTypeName(type) {
|
|
switch(type) {
|
|
case 'user': return '用户';
|
|
case 'resume': return '简历';
|
|
case 'portfolio': return '作品集';
|
|
default: return '其他';
|
|
}
|
|
}
|
|
|
|
function handleGlobalSearch(e) {
|
|
if (e.key === 'Enter') {
|
|
const query = e.target.value.trim();
|
|
if (query) {
|
|
window.location.href = '/dashboard/users?search=' + encodeURIComponent(query);
|
|
}
|
|
}
|
|
}
|
|
|
|
function renderMarkdown(text) {
|
|
if (!text) return '';
|
|
return marked.parse(text);
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
document.querySelectorAll('.markdown-content').forEach(function(el) {
|
|
el.innerHTML = renderMarkdown(el.textContent);
|
|
});
|
|
});
|
|
|
|
let chatOpen = false;
|
|
let chatMessages = [];
|
|
let ws = null;
|
|
let wsReconnectAttempts = 0;
|
|
|
|
function initWebSocket() {
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const wsUrl = `${protocol}//${window.location.host}/api/im/ws`;
|
|
|
|
ws = new WebSocket(wsUrl);
|
|
|
|
ws.onopen = function() {
|
|
console.log('WebSocket connected');
|
|
wsReconnectAttempts = 0;
|
|
};
|
|
|
|
ws.onmessage = function(event) {
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
if (data.type === 'message' && data.content) {
|
|
const loadingIndicator = document.querySelector('.ai-chat-messages .animate-bounce');
|
|
if (loadingIndicator) {
|
|
const loadingDiv = loadingIndicator.closest('.flex.items-start');
|
|
if (loadingDiv) loadingDiv.remove();
|
|
}
|
|
addChatMessage('ai', data.content);
|
|
} else if (data.type === 'error') {
|
|
const loadingIndicator = document.querySelector('.ai-chat-messages .animate-bounce');
|
|
if (loadingIndicator) {
|
|
const loadingDiv = loadingIndicator.closest('.flex.items-start');
|
|
if (loadingDiv) loadingDiv.remove();
|
|
}
|
|
addChatMessage('ai', '服务端错误: ' + data.message);
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to parse WebSocket message:', e);
|
|
}
|
|
};
|
|
|
|
ws.onerror = function(error) {
|
|
console.error('WebSocket error:', error);
|
|
};
|
|
|
|
ws.onclose = function(event) {
|
|
console.log('WebSocket closed:', event.code);
|
|
if (event.code !== 1000 && wsReconnectAttempts < 5) {
|
|
setTimeout(() => {
|
|
wsReconnectAttempts++;
|
|
initWebSocket();
|
|
}, 2000);
|
|
}
|
|
};
|
|
}
|
|
|
|
function toggleChat() {
|
|
const chatPanel = document.getElementById('aiChatPanel');
|
|
chatOpen = !chatOpen;
|
|
if (chatOpen) {
|
|
chatPanel.classList.remove('hidden');
|
|
chatPanel.classList.add('show');
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
initWebSocket();
|
|
}
|
|
setTimeout(() => {
|
|
document.getElementById('chatInput').focus();
|
|
}, 300);
|
|
} else {
|
|
chatPanel.classList.remove('show');
|
|
setTimeout(() => {
|
|
chatPanel.classList.add('hidden');
|
|
}, 300);
|
|
}
|
|
}
|
|
|
|
function sendChatMessage() {
|
|
const input = document.getElementById('chatInput');
|
|
const message = input.value.trim();
|
|
if (!message) return;
|
|
|
|
addChatMessage('user', message);
|
|
input.value = '';
|
|
|
|
const messagesContainer = document.getElementById('chatMessages');
|
|
const loadingIndicator = document.createElement('div');
|
|
loadingIndicator.className = 'flex items-start gap-3 mb-3';
|
|
loadingIndicator.innerHTML = `
|
|
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-purple-400 to-purple-600 flex items-center justify-center flex-shrink-0">
|
|
<i class="fas fa-robot text-white text-xs"></i>
|
|
</div>
|
|
<div class="bg-slate-100 rounded-xl rounded-tl-none p-3 text-sm text-slate-600 max-w-[80%]">
|
|
<div class="flex gap-1">
|
|
<span class="w-2 h-2 bg-slate-400 rounded-full animate-bounce" style="animation-delay: 0ms;"></span>
|
|
<span class="w-2 h-2 bg-slate-400 rounded-full animate-bounce" style="animation-delay: 150ms;"></span>
|
|
<span class="w-2 h-2 bg-slate-400 rounded-full animate-bounce" style="animation-delay: 300ms;"></span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
messagesContainer.appendChild(loadingIndicator);
|
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify({ message: message }));
|
|
} else {
|
|
initWebSocket();
|
|
setTimeout(() => {
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify({ message: message }));
|
|
} else {
|
|
loadingIndicator.remove();
|
|
addChatMessage('ai', 'WebSocket连接失败,请检查网络。');
|
|
}
|
|
}, 1000);
|
|
}
|
|
}
|
|
|
|
function addChatMessage(sender, content) {
|
|
const messagesContainer = document.getElementById('chatMessages');
|
|
const messageDiv = document.createElement('div');
|
|
messageDiv.className = 'flex items-start gap-3 mb-3';
|
|
|
|
if (sender === 'user') {
|
|
messageDiv.classList.add('flex-row-reverse');
|
|
messageDiv.innerHTML = `
|
|
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-green-400 to-green-600 flex items-center justify-center flex-shrink-0">
|
|
<i class="fas fa-user text-white text-xs"></i>
|
|
</div>
|
|
<div class="bg-gradient-to-br from-green-400 to-green-600 text-white rounded-xl rounded-tr-none p-3 text-sm max-w-[80%]">
|
|
${content}
|
|
</div>
|
|
`;
|
|
} else {
|
|
messageDiv.innerHTML = `
|
|
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-purple-400 to-purple-600 flex items-center justify-center flex-shrink-0">
|
|
<i class="fas fa-robot text-white text-xs"></i>
|
|
</div>
|
|
<div class="bg-slate-100 rounded-xl rounded-tl-none p-3 text-sm text-slate-600 max-w-[80%]">
|
|
${renderMarkdown(content)}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
messagesContainer.appendChild(messageDiv);
|
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
}
|
|
|
|
document.addEventListener('click', function(e) {
|
|
const searchBox = document.getElementById('globalSearchBox');
|
|
const searchResults = document.getElementById('searchResults');
|
|
|
|
if (searchResults && !searchResults.classList.contains('hidden')) {
|
|
if (!searchBox.contains(e.target)) {
|
|
searchResults.classList.add('hidden');
|
|
searchOpen = false;
|
|
}
|
|
}
|
|
|
|
const dropdown = document.getElementById('notificationDropdown');
|
|
if (dropdown && !dropdown.classList.contains('hidden')) {
|
|
const bellBtn = document.querySelector('.fa-bell').closest('button');
|
|
if (!dropdown.contains(e.target) && !bellBtn.contains(e.target)) {
|
|
dropdown.classList.add('hidden');
|
|
notificationOpen = false;
|
|
}
|
|
}
|
|
});
|
|
|
|
</script>
|
|
</body>
|
|
</html> |