Files
resume/templates/admin/base.html
T

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>