- 创建品牌资源目录 brand/ - Logo SVG(主Logo、深色模式、图标、favicon) - 各子系统专用 Logo(AI-Proj、Gitea、Jenkins、Metabase、DBeaver) - CSS 变量文件 pipexerp-variables.css - 品牌条样式 pipexerp-brand-bar.css - 品牌 Header 模板 - 更新导航页 nav-home.html - 企业区域配色改为企业绿 #52c41a - 更新标题和 Logo 为 PipexERP - 添加 favicon 引用 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1281 lines
40 KiB
HTML
1281 lines
40 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>PipexERP | Command Center</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Orbitron:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
|
<link rel="icon" type="image/svg+xml" href="brand/logo/favicon.svg">
|
|
<style>
|
|
:root {
|
|
--bg-deep: #0a0a0f;
|
|
--bg-surface: #12121a;
|
|
--bg-elevated: #1a1a25;
|
|
--bg-card: #16161f;
|
|
--border-dim: #2a2a3a;
|
|
--border-glow: #3a3a4a;
|
|
|
|
--text-primary: #e8e8f0;
|
|
--text-secondary: #8888a0;
|
|
--text-dim: #555570;
|
|
|
|
--accent-home: #00ff9d;
|
|
--accent-home-dim: #00ff9d33;
|
|
--accent-home-glow: #00ff9d55;
|
|
|
|
--accent-enterprise: #52c41a;
|
|
--accent-enterprise-dim: #52c41a33;
|
|
--accent-enterprise-glow: #52c41a55;
|
|
|
|
--status-online: #00ff9d;
|
|
--status-offline: #ff4466;
|
|
--status-unknown: #ffaa00;
|
|
|
|
--font-display: 'Orbitron', monospace;
|
|
--font-mono: 'JetBrains Mono', monospace;
|
|
|
|
--radius-sm: 4px;
|
|
--radius-md: 8px;
|
|
--radius-lg: 12px;
|
|
|
|
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
--transition-smooth: 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html {
|
|
font-size: 16px;
|
|
scroll-behavior: smooth;
|
|
}
|
|
|
|
body {
|
|
font-family: var(--font-mono);
|
|
background: var(--bg-deep);
|
|
color: var(--text-primary);
|
|
min-height: 100vh;
|
|
overflow-x: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
/* Animated Grid Background */
|
|
.grid-bg {
|
|
position: fixed;
|
|
inset: 0;
|
|
background-image:
|
|
linear-gradient(var(--border-dim) 1px, transparent 1px),
|
|
linear-gradient(90deg, var(--border-dim) 1px, transparent 1px);
|
|
background-size: 60px 60px;
|
|
opacity: 0.3;
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
}
|
|
|
|
.grid-bg::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
background: radial-gradient(ellipse at 50% 0%, var(--accent-home-dim) 0%, transparent 60%);
|
|
opacity: 0.5;
|
|
transition: opacity var(--transition-smooth);
|
|
}
|
|
|
|
body.enterprise .grid-bg::before {
|
|
background: radial-gradient(ellipse at 50% 0%, var(--accent-enterprise-dim) 0%, transparent 60%);
|
|
}
|
|
|
|
/* Scanline Effect */
|
|
.scanlines {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: repeating-linear-gradient(
|
|
0deg,
|
|
transparent,
|
|
transparent 2px,
|
|
rgba(0, 0, 0, 0.03) 2px,
|
|
rgba(0, 0, 0, 0.03) 4px
|
|
);
|
|
pointer-events: none;
|
|
z-index: 1000;
|
|
}
|
|
|
|
/* Main Container */
|
|
.container {
|
|
position: relative;
|
|
z-index: 1;
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
}
|
|
|
|
/* Header */
|
|
.header {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
margin-bottom: 3rem;
|
|
animation: fadeInDown 0.6s ease-out;
|
|
}
|
|
|
|
@keyframes fadeInDown {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.logo {
|
|
font-family: var(--font-display);
|
|
font-size: 2.5rem;
|
|
font-weight: 800;
|
|
letter-spacing: 0.15em;
|
|
margin-bottom: 0.5rem;
|
|
background: linear-gradient(135deg, var(--accent-home) 0%, var(--accent-enterprise) 100%);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
text-shadow: 0 0 40px var(--accent-home-glow);
|
|
}
|
|
|
|
.subtitle {
|
|
font-size: 0.75rem;
|
|
color: var(--text-dim);
|
|
letter-spacing: 0.3em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
/* Time Display */
|
|
.time-display {
|
|
position: fixed;
|
|
top: 1.5rem;
|
|
right: 2rem;
|
|
font-family: var(--font-display);
|
|
font-size: 1.25rem;
|
|
color: var(--text-secondary);
|
|
letter-spacing: 0.1em;
|
|
z-index: 100;
|
|
}
|
|
|
|
.time-display .seconds {
|
|
color: var(--accent-home);
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
body.enterprise .time-display .seconds {
|
|
color: var(--accent-enterprise);
|
|
}
|
|
|
|
/* Tab Switcher */
|
|
.tab-switcher {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 0;
|
|
margin-bottom: 2.5rem;
|
|
animation: fadeIn 0.6s ease-out 0.2s both;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
.tab-btn {
|
|
font-family: var(--font-display);
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.15em;
|
|
text-transform: uppercase;
|
|
padding: 1rem 2.5rem;
|
|
border: 1px solid var(--border-dim);
|
|
background: var(--bg-surface);
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
transition: all var(--transition-smooth);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.tab-btn:first-child {
|
|
border-radius: var(--radius-md) 0 0 var(--radius-md);
|
|
border-right: none;
|
|
}
|
|
|
|
.tab-btn:last-child {
|
|
border-radius: 0 var(--radius-md) var(--radius-md) 0;
|
|
}
|
|
|
|
.tab-btn::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
background: linear-gradient(180deg, transparent 0%, currentColor 100%);
|
|
opacity: 0;
|
|
transition: opacity var(--transition-smooth);
|
|
}
|
|
|
|
.tab-btn:hover {
|
|
color: var(--text-primary);
|
|
background: var(--bg-elevated);
|
|
}
|
|
|
|
.tab-btn.active[data-tab="home"] {
|
|
color: var(--accent-home);
|
|
border-color: var(--accent-home);
|
|
background: var(--accent-home-dim);
|
|
box-shadow: 0 0 20px var(--accent-home-glow), inset 0 0 20px var(--accent-home-dim);
|
|
}
|
|
|
|
.tab-btn.active[data-tab="enterprise"] {
|
|
color: var(--accent-enterprise);
|
|
border-color: var(--accent-enterprise);
|
|
background: var(--accent-enterprise-dim);
|
|
box-shadow: 0 0 20px var(--accent-enterprise-glow), inset 0 0 20px var(--accent-enterprise-dim);
|
|
}
|
|
|
|
.tab-icon {
|
|
margin-right: 0.5rem;
|
|
}
|
|
|
|
/* Search Bar */
|
|
.search-container {
|
|
max-width: 500px;
|
|
margin: 0 auto 2.5rem;
|
|
animation: fadeIn 0.6s ease-out 0.3s both;
|
|
}
|
|
|
|
.search-wrapper {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.search-icon {
|
|
position: absolute;
|
|
left: 1rem;
|
|
color: var(--text-dim);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.search-input {
|
|
width: 100%;
|
|
padding: 0.875rem 1rem 0.875rem 2.75rem;
|
|
font-family: var(--font-mono);
|
|
font-size: 0.875rem;
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border-dim);
|
|
border-radius: var(--radius-md);
|
|
color: var(--text-primary);
|
|
outline: none;
|
|
transition: all var(--transition-smooth);
|
|
}
|
|
|
|
.search-input::placeholder {
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.search-input:focus {
|
|
border-color: var(--accent-home);
|
|
box-shadow: 0 0 0 3px var(--accent-home-dim);
|
|
}
|
|
|
|
body.enterprise .search-input:focus {
|
|
border-color: var(--accent-enterprise);
|
|
box-shadow: 0 0 0 3px var(--accent-enterprise-dim);
|
|
}
|
|
|
|
/* Section */
|
|
.section {
|
|
display: none;
|
|
animation: fadeInUp 0.4s ease-out;
|
|
}
|
|
|
|
.section.active {
|
|
display: block;
|
|
}
|
|
|
|
@keyframes fadeInUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
/* Cards Grid */
|
|
.cards-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 1.25rem;
|
|
}
|
|
|
|
/* Service Card */
|
|
.service-card {
|
|
position: relative;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border-dim);
|
|
border-radius: var(--radius-lg);
|
|
padding: 1.5rem;
|
|
cursor: pointer;
|
|
transition: all var(--transition-smooth);
|
|
text-decoration: none;
|
|
color: inherit;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.service-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 2px;
|
|
background: linear-gradient(90deg, transparent, var(--card-accent, var(--accent-home)), transparent);
|
|
opacity: 0;
|
|
transition: opacity var(--transition-smooth);
|
|
}
|
|
|
|
.service-card:hover {
|
|
border-color: var(--card-accent, var(--accent-home));
|
|
transform: translateY(-4px);
|
|
box-shadow:
|
|
0 10px 40px -10px var(--card-accent-glow, var(--accent-home-glow)),
|
|
0 0 0 1px var(--card-accent, var(--accent-home));
|
|
}
|
|
|
|
.service-card:hover::before {
|
|
opacity: 1;
|
|
}
|
|
|
|
body.enterprise .service-card {
|
|
--card-accent: var(--accent-enterprise);
|
|
--card-accent-glow: var(--accent-enterprise-glow);
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.card-icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--bg-elevated);
|
|
border: 1px solid var(--border-dim);
|
|
border-radius: var(--radius-md);
|
|
font-size: 1.5rem;
|
|
transition: all var(--transition-smooth);
|
|
}
|
|
|
|
.service-card:hover .card-icon {
|
|
border-color: var(--card-accent, var(--accent-home));
|
|
box-shadow: 0 0 15px var(--card-accent-glow, var(--accent-home-glow));
|
|
}
|
|
|
|
.status-indicator {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.375rem;
|
|
font-size: 0.625rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.status-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: var(--status-unknown);
|
|
box-shadow: 0 0 10px currentColor;
|
|
animation: pulse 2s ease-in-out infinite;
|
|
}
|
|
|
|
.status-dot.online {
|
|
background: var(--status-online);
|
|
box-shadow: 0 0 10px var(--status-online);
|
|
}
|
|
|
|
.status-dot.offline {
|
|
background: var(--status-offline);
|
|
box-shadow: 0 0 10px var(--status-offline);
|
|
animation: none;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.5; }
|
|
}
|
|
|
|
.card-title {
|
|
font-family: var(--font-display);
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.05em;
|
|
margin-bottom: 0.375rem;
|
|
}
|
|
|
|
.card-description {
|
|
font-size: 0.75rem;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 1rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.card-url {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-size: 0.6875rem;
|
|
color: var(--text-dim);
|
|
padding: 0.5rem 0.75rem;
|
|
background: var(--bg-deep);
|
|
border-radius: var(--radius-sm);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.card-url code {
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.card-url .protocol {
|
|
color: var(--card-accent, var(--accent-home));
|
|
}
|
|
|
|
/* Add Card Button */
|
|
.add-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 180px;
|
|
border: 2px dashed var(--border-dim);
|
|
background: transparent;
|
|
border-radius: var(--radius-lg);
|
|
cursor: pointer;
|
|
transition: all var(--transition-smooth);
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.add-card:hover {
|
|
border-color: var(--accent-home);
|
|
color: var(--accent-home);
|
|
background: var(--accent-home-dim);
|
|
}
|
|
|
|
body.enterprise .add-card:hover {
|
|
border-color: var(--accent-enterprise);
|
|
color: var(--accent-enterprise);
|
|
background: var(--accent-enterprise-dim);
|
|
}
|
|
|
|
.add-card-icon {
|
|
font-size: 2rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.add-card-text {
|
|
font-size: 0.75rem;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
/* Modal */
|
|
.modal-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
backdrop-filter: blur(4px);
|
|
z-index: 1000;
|
|
display: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
animation: fadeIn 0.2s ease-out;
|
|
}
|
|
|
|
.modal-overlay.active {
|
|
display: flex;
|
|
}
|
|
|
|
.modal {
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border-glow);
|
|
border-radius: var(--radius-lg);
|
|
padding: 2rem;
|
|
width: 90%;
|
|
max-width: 450px;
|
|
animation: modalIn 0.3s ease-out;
|
|
}
|
|
|
|
@keyframes modalIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: scale(0.95) translateY(10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: scale(1) translateY(0);
|
|
}
|
|
}
|
|
|
|
.modal-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.modal-title {
|
|
font-family: var(--font-display);
|
|
font-size: 1.125rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.modal-close {
|
|
width: 32px;
|
|
height: 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--bg-elevated);
|
|
border: 1px solid var(--border-dim);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
.modal-close:hover {
|
|
border-color: var(--status-offline);
|
|
color: var(--status-offline);
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
|
|
.form-label {
|
|
display: block;
|
|
font-size: 0.6875rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.15em;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.form-input, .form-select {
|
|
width: 100%;
|
|
padding: 0.75rem 1rem;
|
|
font-family: var(--font-mono);
|
|
font-size: 0.875rem;
|
|
background: var(--bg-deep);
|
|
border: 1px solid var(--border-dim);
|
|
border-radius: var(--radius-md);
|
|
color: var(--text-primary);
|
|
outline: none;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
.form-input:focus, .form-select:focus {
|
|
border-color: var(--accent-home);
|
|
box-shadow: 0 0 0 3px var(--accent-home-dim);
|
|
}
|
|
|
|
body.enterprise .form-input:focus,
|
|
body.enterprise .form-select:focus {
|
|
border-color: var(--accent-enterprise);
|
|
box-shadow: 0 0 0 3px var(--accent-enterprise-dim);
|
|
}
|
|
|
|
.form-select {
|
|
cursor: pointer;
|
|
appearance: none;
|
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238888a0' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
|
|
background-repeat: no-repeat;
|
|
background-position: right 1rem center;
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
margin-top: 1.5rem;
|
|
}
|
|
|
|
.btn {
|
|
flex: 1;
|
|
padding: 0.875rem 1.5rem;
|
|
font-family: var(--font-mono);
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
border: 1px solid var(--border-dim);
|
|
border-radius: var(--radius-md);
|
|
cursor: pointer;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: var(--bg-elevated);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: var(--bg-card);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.btn-primary {
|
|
background: var(--accent-home);
|
|
border-color: var(--accent-home);
|
|
color: var(--bg-deep);
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
box-shadow: 0 0 20px var(--accent-home-glow);
|
|
}
|
|
|
|
body.enterprise .btn-primary {
|
|
background: var(--accent-enterprise);
|
|
border-color: var(--accent-enterprise);
|
|
}
|
|
|
|
body.enterprise .btn-primary:hover {
|
|
box-shadow: 0 0 20px var(--accent-enterprise-glow);
|
|
}
|
|
|
|
/* Context Menu */
|
|
.context-menu {
|
|
position: fixed;
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border-glow);
|
|
border-radius: var(--radius-md);
|
|
padding: 0.5rem;
|
|
min-width: 150px;
|
|
z-index: 1001;
|
|
display: none;
|
|
animation: fadeIn 0.15s ease-out;
|
|
}
|
|
|
|
.context-menu.active {
|
|
display: block;
|
|
}
|
|
|
|
.context-menu-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.625rem 0.875rem;
|
|
font-size: 0.75rem;
|
|
color: var(--text-secondary);
|
|
border-radius: var(--radius-sm);
|
|
cursor: pointer;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
.context-menu-item:hover {
|
|
background: var(--bg-elevated);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.context-menu-item.danger:hover {
|
|
background: rgba(255, 68, 102, 0.15);
|
|
color: var(--status-offline);
|
|
}
|
|
|
|
/* Stats Footer */
|
|
.stats-footer {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 3rem;
|
|
margin-top: 3rem;
|
|
padding-top: 2rem;
|
|
border-top: 1px solid var(--border-dim);
|
|
animation: fadeIn 0.6s ease-out 0.5s both;
|
|
}
|
|
|
|
.stat-item {
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-value {
|
|
font-family: var(--font-display);
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
color: var(--accent-home);
|
|
}
|
|
|
|
body.enterprise .stat-value {
|
|
color: var(--accent-enterprise);
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.625rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.15em;
|
|
color: var(--text-dim);
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.container {
|
|
padding: 1rem;
|
|
}
|
|
|
|
.logo {
|
|
font-size: 1.75rem;
|
|
}
|
|
|
|
.time-display {
|
|
display: none;
|
|
}
|
|
|
|
.tab-btn {
|
|
padding: 0.75rem 1.5rem;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.cards-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.stats-footer {
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.25rem;
|
|
}
|
|
}
|
|
|
|
/* Emoji Icons (fallback) */
|
|
.emoji-icon {
|
|
font-style: normal;
|
|
}
|
|
|
|
/* Hidden utility */
|
|
.hidden {
|
|
display: none !important;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="grid-bg"></div>
|
|
<div class="scanlines"></div>
|
|
|
|
<div class="time-display">
|
|
<span class="time-value">00:00</span><span class="seconds">:00</span>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<header class="header">
|
|
<h1 class="logo">PIPEXERP</h1>
|
|
<p class="subtitle">Command Center // Tailscale Network</p>
|
|
</header>
|
|
|
|
<div class="tab-switcher">
|
|
<button class="tab-btn active" data-tab="home">
|
|
<span class="tab-icon">🏠</span>家庭
|
|
</button>
|
|
<button class="tab-btn" data-tab="enterprise">
|
|
<span class="tab-icon">🏢</span>企业
|
|
</button>
|
|
</div>
|
|
|
|
<div class="search-container">
|
|
<div class="search-wrapper">
|
|
<span class="search-icon">🔍</span>
|
|
<input type="text" class="search-input" placeholder="搜索服务..." id="searchInput">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Home Section -->
|
|
<section class="section active" id="home-section">
|
|
<div class="cards-grid" id="home-cards">
|
|
<!-- Cards will be rendered by JS -->
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Enterprise Section -->
|
|
<section class="section" id="enterprise-section">
|
|
<div class="cards-grid" id="enterprise-cards">
|
|
<!-- Cards will be rendered by JS -->
|
|
</div>
|
|
</section>
|
|
|
|
<footer class="stats-footer">
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="total-services">0</div>
|
|
<div class="stat-label">Total Services</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="online-services">0</div>
|
|
<div class="stat-label">Online</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="offline-services">0</div>
|
|
<div class="stat-label">Offline</div>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
|
|
<!-- Add Service Modal -->
|
|
<div class="modal-overlay" id="addModal">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<h2 class="modal-title">添加服务</h2>
|
|
<button class="modal-close" id="closeModal">✕</button>
|
|
</div>
|
|
<form id="addForm">
|
|
<div class="form-group">
|
|
<label class="form-label">服务名称</label>
|
|
<input type="text" class="form-input" id="serviceName" placeholder="例如: Nginx" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">服务描述</label>
|
|
<input type="text" class="form-input" id="serviceDesc" placeholder="例如: Web服务器">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">服务地址</label>
|
|
<input type="url" class="form-input" id="serviceUrl" placeholder="http://100.x.x.x:port" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">图标 (Emoji)</label>
|
|
<input type="text" class="form-input" id="serviceIcon" placeholder="🌐" maxlength="2">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">分类</label>
|
|
<select class="form-select" id="serviceCategory">
|
|
<option value="home">家庭</option>
|
|
<option value="enterprise">企业</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" id="cancelAdd">取消</button>
|
|
<button type="submit" class="btn btn-primary">添加</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Context Menu -->
|
|
<div class="context-menu" id="contextMenu">
|
|
<div class="context-menu-item" data-action="edit">
|
|
<span>✏️</span> 编辑
|
|
</div>
|
|
<div class="context-menu-item danger" data-action="delete">
|
|
<span>🗑️</span> 删除
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Default Services Data
|
|
const defaultServices = {
|
|
home: [
|
|
{
|
|
id: 'fnos',
|
|
name: '飞牛NAS',
|
|
description: 'NAS存储 · 文件管理',
|
|
url: 'http://100.118.62.18:5000',
|
|
icon: '💾',
|
|
isCustom: false
|
|
},
|
|
{
|
|
id: 'siyuan',
|
|
name: '思源笔记',
|
|
description: 'SiYuan · 知识管理',
|
|
url: 'http://100.118.62.18:6806',
|
|
icon: '📝',
|
|
isCustom: false
|
|
},
|
|
{
|
|
id: 'pve',
|
|
name: 'PVE',
|
|
description: 'Proxmox · 虚拟化管理',
|
|
url: 'https://100.122.244.18:8006',
|
|
icon: '🖥️',
|
|
isCustom: false
|
|
},
|
|
{
|
|
id: 'ollama',
|
|
name: 'Ollama',
|
|
description: 'AI服务 · 本地大模型',
|
|
url: 'http://100.99.17.32:11434',
|
|
icon: '🤖',
|
|
isCustom: false
|
|
},
|
|
{
|
|
id: 'fusionwrt',
|
|
name: 'FusionWRT',
|
|
description: '路由管理 · OpenWRT',
|
|
url: 'http://100.91.188.114',
|
|
icon: '📡',
|
|
isCustom: false
|
|
},
|
|
{
|
|
id: 'cherrypi5',
|
|
name: 'CherryPi5',
|
|
description: '树莓派5 · IoT控制',
|
|
url: 'http://100.90.98.71',
|
|
icon: '🍒',
|
|
isCustom: false
|
|
}
|
|
],
|
|
enterprise: [
|
|
{
|
|
id: 'aiproj',
|
|
name: 'AI-Proj',
|
|
description: '项目管理 · 任务追踪',
|
|
url: 'https://ai.pipexerp.com',
|
|
icon: '📊',
|
|
isCustom: false
|
|
},
|
|
{
|
|
id: 'gitea',
|
|
name: 'Gitea',
|
|
description: '代码仓库 · Git托管',
|
|
url: 'https://gitea.pipexerp.com',
|
|
icon: '🐙',
|
|
isCustom: false
|
|
},
|
|
{
|
|
id: 'jenkins',
|
|
name: 'Jenkins',
|
|
description: 'CI/CD · 持续集成',
|
|
url: 'https://jenkins.pipexerp.com',
|
|
icon: '🔧',
|
|
isCustom: false
|
|
},
|
|
{
|
|
id: 'pipexerp',
|
|
name: 'PipexERP',
|
|
description: '官网 · 企业门户',
|
|
url: 'https://www.pipexerp.com',
|
|
icon: '🌐',
|
|
isCustom: false
|
|
},
|
|
{
|
|
id: 'metabase',
|
|
name: 'Metabase',
|
|
description: 'BI分析 · 数据可视化',
|
|
url: 'https://meta.pipexerp.com',
|
|
icon: '📈',
|
|
isCustom: false
|
|
},
|
|
{
|
|
id: 'dbeaver',
|
|
name: 'DBeaver',
|
|
description: '数据库管理 · CloudBeaver',
|
|
url: 'https://dbeaver.pipexerp.com',
|
|
icon: '🗄️',
|
|
isCustom: false
|
|
}
|
|
]
|
|
};
|
|
|
|
// State
|
|
let services = JSON.parse(localStorage.getItem('nav-services')) || defaultServices;
|
|
let currentTab = 'home';
|
|
let selectedCard = null;
|
|
|
|
// DOM Elements
|
|
const tabBtns = document.querySelectorAll('.tab-btn');
|
|
const sections = document.querySelectorAll('.section');
|
|
const homeCards = document.getElementById('home-cards');
|
|
const enterpriseCards = document.getElementById('enterprise-cards');
|
|
const searchInput = document.getElementById('searchInput');
|
|
const addModal = document.getElementById('addModal');
|
|
const addForm = document.getElementById('addForm');
|
|
const contextMenu = document.getElementById('contextMenu');
|
|
const timeDisplay = document.querySelector('.time-value');
|
|
const secondsDisplay = document.querySelector('.seconds');
|
|
|
|
// Time Update
|
|
function updateTime() {
|
|
const now = new Date();
|
|
const hours = String(now.getHours()).padStart(2, '0');
|
|
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
timeDisplay.textContent = `${hours}:${minutes}`;
|
|
secondsDisplay.textContent = `:${seconds}`;
|
|
}
|
|
setInterval(updateTime, 1000);
|
|
updateTime();
|
|
|
|
// Parse URL for display
|
|
function parseUrl(url) {
|
|
try {
|
|
const urlObj = new URL(url);
|
|
return {
|
|
protocol: urlObj.protocol.replace(':', ''),
|
|
host: urlObj.host
|
|
};
|
|
} catch {
|
|
return { protocol: 'http', host: url };
|
|
}
|
|
}
|
|
|
|
// Check service status
|
|
async function checkStatus(url) {
|
|
try {
|
|
// Try to fetch with a timeout
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
|
|
await fetch(url, {
|
|
mode: 'no-cors',
|
|
signal: controller.signal
|
|
});
|
|
clearTimeout(timeout);
|
|
return 'online';
|
|
} catch {
|
|
return 'unknown';
|
|
}
|
|
}
|
|
|
|
// Render Cards
|
|
async function renderCards(category, filter = '') {
|
|
const container = category === 'home' ? homeCards : enterpriseCards;
|
|
const categoryServices = services[category] || [];
|
|
|
|
const filteredServices = categoryServices.filter(s =>
|
|
s.name.toLowerCase().includes(filter.toLowerCase()) ||
|
|
s.description.toLowerCase().includes(filter.toLowerCase())
|
|
);
|
|
|
|
let html = '';
|
|
|
|
for (const service of filteredServices) {
|
|
const urlParts = parseUrl(service.url);
|
|
html += `
|
|
<a href="${service.url}" target="_blank" class="service-card"
|
|
data-id="${service.id}" data-category="${category}" data-custom="${service.isCustom}">
|
|
<div class="card-header">
|
|
<div class="card-icon">${service.icon}</div>
|
|
<div class="status-indicator">
|
|
<span class="status-dot" data-url="${service.url}"></span>
|
|
<span class="status-text">检测中</span>
|
|
</div>
|
|
</div>
|
|
<h3 class="card-title">${service.name}</h3>
|
|
<p class="card-description">${service.description}</p>
|
|
<div class="card-url">
|
|
<span class="protocol">${urlParts.protocol}://</span>
|
|
<code>${urlParts.host}</code>
|
|
</div>
|
|
</a>
|
|
`;
|
|
}
|
|
|
|
// Add button
|
|
html += `
|
|
<div class="add-card" data-category="${category}">
|
|
<div class="add-card-icon">+</div>
|
|
<div class="add-card-text">添加服务</div>
|
|
</div>
|
|
`;
|
|
|
|
container.innerHTML = html;
|
|
|
|
// Check status for each service
|
|
filteredServices.forEach(async (service) => {
|
|
const status = await checkStatus(service.url);
|
|
const dot = container.querySelector(`[data-url="${service.url}"]`);
|
|
const text = dot?.nextElementSibling;
|
|
if (dot) {
|
|
dot.classList.remove('online', 'offline');
|
|
if (status === 'online') {
|
|
dot.classList.add('online');
|
|
text.textContent = '在线';
|
|
} else {
|
|
text.textContent = '未知';
|
|
}
|
|
}
|
|
});
|
|
|
|
// Add event listeners
|
|
container.querySelectorAll('.add-card').forEach(btn => {
|
|
btn.addEventListener('click', () => openAddModal(category));
|
|
});
|
|
|
|
container.querySelectorAll('.service-card').forEach(card => {
|
|
card.addEventListener('contextmenu', (e) => {
|
|
if (card.dataset.custom === 'true') {
|
|
e.preventDefault();
|
|
showContextMenu(e, card);
|
|
}
|
|
});
|
|
});
|
|
|
|
updateStats();
|
|
}
|
|
|
|
// Update Stats
|
|
function updateStats() {
|
|
const total = (services.home?.length || 0) + (services.enterprise?.length || 0);
|
|
const onlineCount = document.querySelectorAll('.status-dot.online').length;
|
|
|
|
document.getElementById('total-services').textContent = total;
|
|
document.getElementById('online-services').textContent = onlineCount;
|
|
document.getElementById('offline-services').textContent = total - onlineCount;
|
|
}
|
|
|
|
// Tab Switching
|
|
tabBtns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const tab = btn.dataset.tab;
|
|
|
|
tabBtns.forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
|
|
sections.forEach(s => s.classList.remove('active'));
|
|
document.getElementById(`${tab}-section`).classList.add('active');
|
|
|
|
document.body.classList.toggle('enterprise', tab === 'enterprise');
|
|
currentTab = tab;
|
|
|
|
searchInput.value = '';
|
|
});
|
|
});
|
|
|
|
// Search
|
|
searchInput.addEventListener('input', (e) => {
|
|
const filter = e.target.value;
|
|
renderCards('home', filter);
|
|
renderCards('enterprise', filter);
|
|
});
|
|
|
|
// Modal Functions
|
|
function openAddModal(category) {
|
|
document.getElementById('serviceCategory').value = category;
|
|
addModal.classList.add('active');
|
|
}
|
|
|
|
function closeAddModal() {
|
|
addModal.classList.remove('active');
|
|
addForm.reset();
|
|
}
|
|
|
|
document.getElementById('closeModal').addEventListener('click', closeAddModal);
|
|
document.getElementById('cancelAdd').addEventListener('click', closeAddModal);
|
|
|
|
addModal.addEventListener('click', (e) => {
|
|
if (e.target === addModal) closeAddModal();
|
|
});
|
|
|
|
// Add Form Submit
|
|
addForm.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
|
|
const category = document.getElementById('serviceCategory').value;
|
|
const newService = {
|
|
id: 'custom-' + Date.now(),
|
|
name: document.getElementById('serviceName').value,
|
|
description: document.getElementById('serviceDesc').value,
|
|
url: document.getElementById('serviceUrl').value,
|
|
icon: document.getElementById('serviceIcon').value || '🌐',
|
|
isCustom: true
|
|
};
|
|
|
|
if (!services[category]) services[category] = [];
|
|
services[category].push(newService);
|
|
|
|
localStorage.setItem('nav-services', JSON.stringify(services));
|
|
renderCards(category);
|
|
closeAddModal();
|
|
});
|
|
|
|
// Context Menu
|
|
function showContextMenu(e, card) {
|
|
selectedCard = {
|
|
id: card.dataset.id,
|
|
category: card.dataset.category
|
|
};
|
|
|
|
contextMenu.style.left = e.clientX + 'px';
|
|
contextMenu.style.top = e.clientY + 'px';
|
|
contextMenu.classList.add('active');
|
|
}
|
|
|
|
function hideContextMenu() {
|
|
contextMenu.classList.remove('active');
|
|
selectedCard = null;
|
|
}
|
|
|
|
document.addEventListener('click', hideContextMenu);
|
|
document.addEventListener('contextmenu', (e) => {
|
|
if (!e.target.closest('.service-card[data-custom="true"]')) {
|
|
hideContextMenu();
|
|
}
|
|
});
|
|
|
|
contextMenu.querySelectorAll('.context-menu-item').forEach(item => {
|
|
item.addEventListener('click', () => {
|
|
const action = item.dataset.action;
|
|
|
|
if (action === 'delete' && selectedCard) {
|
|
const { id, category } = selectedCard;
|
|
services[category] = services[category].filter(s => s.id !== id);
|
|
localStorage.setItem('nav-services', JSON.stringify(services));
|
|
renderCards(category);
|
|
}
|
|
|
|
hideContextMenu();
|
|
});
|
|
});
|
|
|
|
// Keyboard Shortcuts
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
closeAddModal();
|
|
hideContextMenu();
|
|
}
|
|
|
|
if (e.key === '/' && !e.target.matches('input, textarea')) {
|
|
e.preventDefault();
|
|
searchInput.focus();
|
|
}
|
|
|
|
if (e.key === '1' && e.altKey) {
|
|
tabBtns[0].click();
|
|
}
|
|
|
|
if (e.key === '2' && e.altKey) {
|
|
tabBtns[1].click();
|
|
}
|
|
});
|
|
|
|
// Initial Render
|
|
renderCards('home');
|
|
renderCards('enterprise');
|
|
|
|
// Periodic status check
|
|
setInterval(() => {
|
|
renderCards('home', searchInput.value);
|
|
renderCards('enterprise', searchInput.value);
|
|
}, 60000);
|
|
</script>
|
|
</body>
|
|
</html>
|