1184 lines
52 KiB
HTML
1184 lines
52 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="fa" dir="rtl">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>مدیریت محصولات</title>
|
||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap');
|
||
|
||
:root {
|
||
--primary: #1a365d;
|
||
--primary-light: #2c5282;
|
||
--primary-dark: #0f2341;
|
||
--accent: #c9a961;
|
||
--accent-dark: #a8893f;
|
||
--accent-light: #e0c789;
|
||
--bg: #f5f7fa;
|
||
--board-bg: #e2e8f0;
|
||
--card-bg: #ffffff;
|
||
--panel-bg: #ffffff;
|
||
--text: #1a202c;
|
||
--text-muted: #4a5568;
|
||
--text-light: #718096;
|
||
--border: #cbd5e0;
|
||
--border-light: #e2e8f0;
|
||
--success: #38a169;
|
||
--danger: #e53e3e;
|
||
--warning: #dd6b20;
|
||
--info: #3182ce;
|
||
--shadow: 0 4px 6px rgba(0, 0, 0, 0.08), 0 10px 20px rgba(26, 54, 93, 0.1);
|
||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.06);
|
||
--shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.15);
|
||
--radius: 10px;
|
||
--radius-sm: 6px;
|
||
--sidebar-width: 280px;
|
||
--header-height: 64px;
|
||
--tabs-height: 48px;
|
||
--footer-height: 36px;
|
||
}
|
||
|
||
[data-theme="dark"] {
|
||
--primary: #2c5282;
|
||
--primary-light: #3b6fb5;
|
||
--primary-dark: #1a365d;
|
||
--accent: #e0c789;
|
||
--accent-dark: #c9a961;
|
||
--accent-light: #f0d9a0;
|
||
--bg: #1a202c;
|
||
--board-bg: #2d3748;
|
||
--card-bg: #2d3748;
|
||
--panel-bg: #1f2937;
|
||
--text: #e2e8f0;
|
||
--text-muted: #cbd5e0;
|
||
--text-light: #a0aec0;
|
||
--border: #4a5568;
|
||
--border-light: #374151;
|
||
--shadow: 0 4px 6px rgba(0, 0, 0, 0.3), 0 10px 20px rgba(0, 0, 0, 0.4);
|
||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||
--shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
body {
|
||
font-family: 'Vazirmatn', Tahoma, sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
font-size: 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
transition: background 0.3s, color 0.3s;
|
||
}
|
||
|
||
.app-header {
|
||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
|
||
color: white;
|
||
padding: 0 20px;
|
||
height: var(--header-height);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 3px solid var(--accent);
|
||
box-shadow: var(--shadow);
|
||
z-index: 100;
|
||
flex-shrink: 0;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
.app-header::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: -50%;
|
||
left: -10%;
|
||
width: 300px;
|
||
height: 300px;
|
||
background: radial-gradient(circle, rgba(201, 169, 97, 0.15) 0%, transparent 70%);
|
||
border-radius: 50%;
|
||
pointer-events: none;
|
||
}
|
||
.app-brand { display: flex; align-items: center; gap: 14px; position: relative; z-index: 1; }
|
||
.app-logo {
|
||
width: 44px; height: 44px; background: var(--accent); border-radius: 10px;
|
||
display: flex; align-items: center; justify-content: center; font-size: 22px;
|
||
font-weight: 800; color: var(--primary-dark); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); flex-shrink: 0;
|
||
}
|
||
.app-brand-text { display: flex; flex-direction: column; gap: 2px; }
|
||
.app-title { font-size: 16px; font-weight: 800; letter-spacing: -0.3px; color: white; }
|
||
.app-subtitle { font-size: 11px; opacity: 0.85; color: rgba(255, 255, 255, 0.9); }
|
||
|
||
.btn {
|
||
background: linear-gradient(135deg, var(--primary-light), var(--primary));
|
||
color: white; border: none; padding: 8px 14px; border-radius: var(--radius-sm);
|
||
cursor: pointer; font-family: inherit; font-size: 13px; font-weight: 600;
|
||
display: inline-flex; align-items: center; gap: 6px; transition: all 0.2s;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); white-space: nowrap;
|
||
}
|
||
.btn:hover { transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); filter: brightness(1.1); }
|
||
.btn:active { transform: translateY(0); }
|
||
.btn-accent { background: linear-gradient(135deg, var(--accent), var(--accent-dark)); color: var(--primary-dark); font-weight: 700; }
|
||
.btn-ghost { background: rgba(26, 54, 93, 0.08); border: 1px solid var(--border); color: var(--primary); font-weight: 600; }
|
||
.btn-ghost:hover { background: rgba(26, 54, 93, 0.15); border-color: var(--primary-light); }
|
||
[data-theme="dark"] .btn-ghost { background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.25); color: white; }
|
||
[data-theme="dark"] .btn-ghost:hover { background: rgba(255, 255, 255, 0.2); }
|
||
.btn-danger { background: linear-gradient(135deg, var(--danger), #c53030); color: white; }
|
||
.btn-sm { padding: 4px 10px; font-size: 11px; }
|
||
.btn-xs { padding: 2px 6px; font-size: 10px; }
|
||
.btn-icon {
|
||
width: 32px; height: 32px; padding: 0;
|
||
display: inline-flex; align-items: center; justify-content: center;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.app-main { flex: 1; display: flex; overflow: hidden; position: relative; flex-direction: row; }
|
||
|
||
.sidebar {
|
||
order: -1; width: var(--sidebar-width); background: var(--panel-bg);
|
||
border-right: 1px solid var(--border); display: flex; flex-direction: column;
|
||
transition: width 0.3s ease; box-shadow: var(--shadow); overflow: hidden;
|
||
flex-shrink: 0; z-index: 50;
|
||
}
|
||
.sidebar.collapsed { width: 0; overflow: hidden; }
|
||
.sidebar-header {
|
||
background: linear-gradient(135deg, var(--primary), var(--primary-light)); color: white;
|
||
padding: 12px; display: flex; align-items: center; justify-content: space-between;
|
||
border-bottom: 2px solid var(--accent); min-height: 48px;
|
||
}
|
||
.sidebar-title { font-size: 13px; font-weight: 700; display: flex; align-items: center; gap: 8px; white-space: nowrap; overflow: hidden; }
|
||
.sidebar.collapsed .sidebar-title { display: none; }
|
||
.sidebar-body { flex: 1; overflow-y: auto; padding: 8px; }
|
||
.sidebar.collapsed .sidebar-body { display: none; }
|
||
.sidebar-group { margin-bottom: 6px; border: 1px solid var(--border-light); border-radius: var(--radius-sm); overflow: hidden; background: var(--card-bg); }
|
||
.sidebar-group-header {
|
||
background: linear-gradient(135deg, var(--primary), var(--primary-light)); color: white;
|
||
padding: 10px 12px; cursor: pointer; display: flex; align-items: center;
|
||
justify-content: space-between; font-size: 13px; font-weight: 700;
|
||
user-select: none; transition: all 0.2s; position: relative;
|
||
}
|
||
.sidebar-group-header:hover { filter: brightness(1.1); }
|
||
.sidebar-group-header .group-icon { display: flex; align-items: center; gap: 8px; flex: 1; overflow: hidden; }
|
||
.sidebar-group-header .group-arrow { transition: transform 0.3s; font-size: 12px; flex-shrink: 0; }
|
||
.sidebar-group.collapsed .group-arrow { transform: rotate(90deg); }
|
||
.sidebar-group.collapsed .sidebar-group-body { display: none; }
|
||
.sidebar-group-body { padding: 6px; display: flex; flex-direction: column; gap: 4px; }
|
||
.sidebar-btn {
|
||
background: var(--card-bg); border: 1px solid var(--border-light); color: var(--text);
|
||
padding: 8px 10px; border-radius: var(--radius-sm); cursor: pointer; font-family: inherit;
|
||
font-size: 12px; font-weight: 500; display: flex; align-items: center; gap: 8px;
|
||
transition: all 0.2s; text-align: right; width: 100%; position: relative;
|
||
}
|
||
.sidebar-btn:hover { background: linear-gradient(135deg, var(--primary), var(--primary-light)); color: white; border-color: var(--primary); transform: translateX(-2px); }
|
||
.sidebar-btn .btn-icon-emoji { font-size: 15px; width: 20px; text-align: center; flex-shrink: 0; }
|
||
.sidebar-btn .btn-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
|
||
[data-tooltip] { position: relative; }
|
||
[data-tooltip]::after {
|
||
content: attr(data-tooltip); position: absolute; right: calc(100% + 10px); top: 50%;
|
||
transform: translateY(-50%); background: var(--primary-dark); color: white;
|
||
padding: 8px 12px; border-radius: 6px; font-size: 11px; font-weight: 500;
|
||
white-space: normal; opacity: 0; pointer-events: none; transition: opacity 0.2s;
|
||
z-index: 1000; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); max-width: 250px; line-height: 1.5;
|
||
}
|
||
[data-tooltip]::before {
|
||
content: ''; position: absolute; right: calc(100% + 5px); top: 50%;
|
||
transform: translateY(-50%); border: 5px solid transparent;
|
||
border-left-color: var(--primary-dark); opacity: 0; pointer-events: none;
|
||
transition: opacity 0.2s; z-index: 1000;
|
||
}
|
||
[data-tooltip]:hover::after, [data-tooltip]:hover::before { opacity: 1; }
|
||
[data-theme="dark"] [data-tooltip]::after { background: var(--accent-dark); color: var(--primary-dark); }
|
||
[data-theme="dark"] [data-tooltip]::before { border-left-color: var(--accent-dark); }
|
||
|
||
.tabs-bar {
|
||
background: linear-gradient(135deg, #0f1724 0%, #1a2a44 100%);
|
||
border-bottom: 2px solid var(--accent);
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 12px;
|
||
min-height: var(--tabs-height);
|
||
box-shadow: var(--shadow-sm);
|
||
flex-shrink: 0;
|
||
gap: 10px;
|
||
position: relative;
|
||
flex-wrap: wrap;
|
||
}
|
||
.toolbar-controls { display: flex; gap: 6px; margin-left: 8px; }
|
||
.toolbar-icon-btn {
|
||
background: rgba(255, 255, 255, 0.08);
|
||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||
color: white;
|
||
width: 34px; height: 34px; border-radius: 6px; cursor: pointer;
|
||
font-size: 15px; display: flex; align-items: center; justify-content: center;
|
||
transition: all 0.2s; padding: 0;
|
||
}
|
||
.toolbar-icon-btn:hover { background: var(--accent); color: var(--primary-dark); border-color: var(--accent); transform: translateY(-1px); }
|
||
.toolbar-search { flex: 1; min-width: 180px; max-width: 320px; position: relative; display: flex; align-items: center; }
|
||
.search-icon { position: absolute; right: 10px; font-size: 13px; opacity: 0.7; pointer-events: none; }
|
||
.search-input {
|
||
width: 100%; padding: 7px 32px 7px 12px;
|
||
background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.15);
|
||
border-radius: 6px; color: white; font-family: inherit; font-size: 13px; transition: all 0.2s;
|
||
}
|
||
.search-input::placeholder { color: rgba(255, 255, 255, 0.5); }
|
||
.search-input:focus { outline: none; background: rgba(255, 255, 255, 0.12); border-color: var(--accent); box-shadow: 0 0 0 2px rgba(201, 169, 97, 0.2); }
|
||
.toolbar-filters { display: flex; gap: 8px; }
|
||
.filter-dropdown {
|
||
position: relative; display: flex; align-items: center; gap: 6px;
|
||
background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.15);
|
||
border-radius: 6px; padding: 0 10px; transition: all 0.2s;
|
||
}
|
||
.filter-dropdown:hover { border-color: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.12); }
|
||
.filter-icon { font-size: 13px; opacity: 0.9; }
|
||
.filter-select {
|
||
background: transparent; border: none; color: white; font-family: inherit;
|
||
font-size: 12px; font-weight: 600; padding: 7px 4px; cursor: pointer; outline: none; min-width: 110px;
|
||
}
|
||
.filter-select option { background: var(--primary-dark); color: white; }
|
||
.toolbar-stats { display: flex; gap: 12px; margin-right: auto; padding-right: 8px; border-right: 1px solid rgba(255, 255, 255, 0.1); }
|
||
.stat-item { display: flex; align-items: center; gap: 4px; font-size: 11px; color: rgba(255, 255, 255, 0.85); white-space: nowrap; }
|
||
.stat-icon { font-size: 13px; }
|
||
.stat-label { font-weight: 500; }
|
||
.stat-value {
|
||
font-weight: 800; color: var(--accent); background: rgba(201, 169, 97, 0.15);
|
||
padding: 2px 7px; border-radius: 10px; font-size: 11px; min-width: 22px; text-align: center;
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.tabs-bar { flex-wrap: wrap; padding: 8px; gap: 8px; }
|
||
.toolbar-stats { order: 3; width: 100%; margin-right: 0; padding-right: 0; border-right: none; border-top: 1px solid rgba(255, 255, 255, 0.1); padding-top: 8px; justify-content: center; }
|
||
.toolbar-search { order: 2; }
|
||
}
|
||
|
||
.board-area { flex: 1; overflow: auto; padding: 24px; background: var(--board-bg); transition: background 0.3s; }
|
||
|
||
.modal-overlay { position: fixed; inset: 0; background: rgba(26, 54, 93, 0.6); backdrop-filter: blur(3px); display: none; align-items: center; justify-content: center; z-index: 1000; padding: 20px; animation: fadeIn 0.2s; }
|
||
.modal-overlay.active { display: flex; }
|
||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||
.modal { background: var(--panel-bg); color: var(--text); border-radius: 12px; width: 100%; max-width: 600px; max-height: 90vh; overflow: hidden; display: flex; flex-direction: column; box-shadow: var(--shadow-lg); animation: slideUp 0.25s; }
|
||
.modal-lg { max-width: 800px; }
|
||
.modal-xl { max-width: 1000px; }
|
||
@keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
|
||
.modal-header { background: linear-gradient(135deg, var(--primary), var(--primary-light)); color: white; padding: 16px 20px; display: flex; align-items: center; justify-content: space-between; border-bottom: 3px solid var(--accent); }
|
||
.modal-title { font-size: 16px; font-weight: 700; display: flex; align-items: center; gap: 10px; }
|
||
.modal-close { background: rgba(255, 255, 255, 0.15); border: none; color: white; width: 30px; height: 30px; border-radius: 50%; cursor: pointer; font-size: 16px; transition: all 0.2s; }
|
||
.modal-close:hover { background: var(--danger); }
|
||
.modal-body { padding: 20px; overflow-y: auto; flex: 1; }
|
||
.modal-footer { padding: 14px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-start; gap: 8px; background: var(--border-light); }
|
||
|
||
.form-group { margin-bottom: 16px; }
|
||
.form-label { display: block; font-size: 13px; font-weight: 600; color: var(--primary); margin-bottom: 6px; }
|
||
[data-theme="dark"] .form-label { color: var(--accent-light); }
|
||
.form-label .required { color: var(--danger); }
|
||
.form-input, .form-textarea, .form-select { width: 100%; padding: 9px 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); font-family: inherit; font-size: 13px; background: var(--card-bg); color: var(--text); transition: all 0.2s; }
|
||
.form-input:focus, .form-textarea:focus, .form-select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(201, 169, 97, 0.15); }
|
||
.form-textarea { resize: vertical; min-height: 70px; line-height: 1.6; }
|
||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||
|
||
.webhook-section { background: var(--border-light); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 14px; margin-top: 12px; }
|
||
.webhook-section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
||
.webhook-section-title { font-size: 13px; font-weight: 700; color: var(--primary); display: flex; align-items: center; gap: 6px; }
|
||
[data-theme="dark"] .webhook-section-title { color: var(--accent-light); }
|
||
.webhook-toggle { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--text-muted); }
|
||
.switch { position: relative; display: inline-block; width: 36px; height: 20px; }
|
||
.switch input { opacity: 0; width: 0; height: 0; }
|
||
.slider { position: absolute; cursor: pointer; inset: 0; background: var(--border); transition: 0.3s; border-radius: 20px; }
|
||
.slider::before { position: absolute; content: ""; height: 14px; width: 14px; right: 3px; bottom: 3px; background: white; transition: 0.3s; border-radius: 50%; }
|
||
input:checked + .slider { background: var(--success); }
|
||
input:checked + .slider::before { transform: translateX(-16px); }
|
||
.webhook-status { display: flex; align-items: center; gap: 8px; margin-top: 10px; padding: 8px 10px; background: var(--card-bg); border-radius: 4px; font-size: 11px; color: var(--text-muted); }
|
||
.webhook-status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-muted); flex-shrink: 0; }
|
||
.webhook-status-dot.success { background: var(--success); }
|
||
.webhook-status-dot.error { background: var(--danger); }
|
||
.webhook-status-dot.pending { background: var(--warning); animation: pulse 1.5s infinite; }
|
||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||
|
||
.toast-container { position: fixed; bottom: 20px; left: 20px; z-index: 2000; display: flex; flex-direction: column; gap: 8px; }
|
||
.toast { background: var(--primary); color: white; padding: 12px 18px; border-radius: 8px; box-shadow: var(--shadow-lg); font-size: 13px; display: flex; align-items: center; gap: 10px; animation: slideInLeft 0.3s; min-width: 250px; }
|
||
.toast.success { background: var(--success); }
|
||
.toast.error { background: var(--danger); }
|
||
.toast.warning { background: var(--warning); }
|
||
@keyframes slideInLeft { from { opacity: 0; transform: translateX(-30px); } to { opacity: 1; transform: translateX(0); } }
|
||
|
||
.app-footer { background: linear-gradient(135deg, var(--primary), var(--primary-light)); color: white; padding: 8px 20px; font-size: 11px; display: flex; justify-content: space-between; align-items: center; border-top: 2px solid var(--accent); flex-shrink: 0; }
|
||
.footer-brand { color: var(--accent); font-weight: 700; }
|
||
|
||
.form-input[readonly], .form-textarea[readonly] { background: var(--border-light); color: var(--text-muted); cursor: not-allowed; opacity: 0.85; border-style: dashed; }
|
||
.readonly-badge { display: inline-flex; align-items: center; gap: 4px; font-size: 10px; font-weight: 600; color: var(--text-muted); background: var(--border-light); border: 1px solid var(--border); padding: 2px 8px; border-radius: 10px; margin-right: 6px; }
|
||
.readonly-badge::before { content: '🔒'; font-size: 10px; }
|
||
.readonly-info { background: linear-gradient(to left, #ebf8ff, #e6f6ff); border: 1px solid #90cdf4; border-right: 4px solid var(--info); padding: 10px 14px; border-radius: 6px; font-size: 11px; color: var(--text-muted); margin-bottom: 14px; display: flex; align-items: center; gap: 8px; }
|
||
[data-theme="dark"] .readonly-info { background: rgba(49, 130, 206, 0.12); border-color: rgba(49, 130, 206, 0.3); border-right-color: var(--info); }
|
||
|
||
/* Galaxy Chart Styles */
|
||
#galaxy-chart-container {
|
||
width: 100%;
|
||
height: 600px;
|
||
background: radial-gradient(ellipse at center, #0a0e27 0%, #000000 100%);
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
.galaxy-node {
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.galaxy-node:hover {
|
||
filter: brightness(1.3);
|
||
}
|
||
|
||
.galaxy-link {
|
||
stroke-opacity: 0.6;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.galaxy-link:hover {
|
||
stroke-opacity: 1;
|
||
stroke-width: 3;
|
||
}
|
||
|
||
.galaxy-label {
|
||
font-family: 'Vazirmatn', sans-serif;
|
||
font-size: 10px;
|
||
fill: #ffffff;
|
||
pointer-events: none;
|
||
text-anchor: middle;
|
||
text-shadow: 0 0 3px rgba(0,0,0,0.8);
|
||
}
|
||
|
||
.galaxy-tooltip {
|
||
position: absolute;
|
||
background: rgba(0, 0, 0, 0.9);
|
||
color: white;
|
||
padding: 10px 14px;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
pointer-events: none;
|
||
z-index: 1000;
|
||
max-width: 250px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
|
||
border: 1px solid rgba(255,255,255,0.2);
|
||
}
|
||
|
||
.galaxy-legend {
|
||
position: absolute;
|
||
top: 20px;
|
||
left: 20px;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
padding: 12px 16px;
|
||
border-radius: 8px;
|
||
color: white;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.galaxy-legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.galaxy-legend-color {
|
||
width: 14px;
|
||
height: 14px;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||
::-webkit-scrollbar-track { background: transparent; }
|
||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
||
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<header class="app-header">
|
||
<div class="app-brand">
|
||
<div class="app-logo"></div>
|
||
<div class="app-brand-text">
|
||
<div class="app-title" id="headerTitle">مدیریت محصولات</div>
|
||
<div class="app-subtitle" id="headerSubtitle">مدیریت نسخهها و نیازمندیهای محصولات</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="tabs-bar" id="advancedToolbar">
|
||
<div class="toolbar-controls">
|
||
<button class="toolbar-icon-btn" id="sidebarToggle" title="باز/بسته کردن سایدبار">
|
||
<span id="sidebarIcon"></span>
|
||
</button>
|
||
<button class="toolbar-icon-btn" id="themeToggle" title="تغییر تم">
|
||
<span id="themeIcon">🌙</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="toolbar-search">
|
||
<span class="search-icon">🔍</span>
|
||
<input type="text" class="search-input" id="toolbarSearch" placeholder="جستجو...">
|
||
</div>
|
||
|
||
<div class="toolbar-filters">
|
||
<div class="filter-dropdown">
|
||
<span class="filter-icon">🏷️</span>
|
||
<select class="filter-select" id="filterType">
|
||
<option value="">همه انواع</option>
|
||
<option value="backend">بکاند</option>
|
||
<option value="frontend">فرانتاند</option>
|
||
<option value="service">میکروسرویس</option>
|
||
<option value="database">پایگاه داده</option>
|
||
<option value="mobile">موبایل</option>
|
||
<option value="infrastructure">زیرساخت</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="filter-dropdown">
|
||
<span class="filter-icon">🌐</span>
|
||
<select class="filter-select" id="filterEnvironment">
|
||
<option value="">همه محیطها</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toolbar-stats">
|
||
<div class="stat-item">
|
||
<span class="stat-icon"></span>
|
||
<span class="stat-label">کل:</span>
|
||
<span class="stat-value" id="statTotal">0</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-icon"></span>
|
||
<span class="stat-label">محصول:</span>
|
||
<span class="stat-value" id="statProjects">0</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-icon"></span>
|
||
<span class="stat-label">پروژه:</span>
|
||
<span class="stat-value" id="statSubProjects">0</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-icon">🔗</span>
|
||
<span class="stat-label">وابستگی:</span>
|
||
<span class="stat-value" id="statDependencies">0</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="app-main">
|
||
<aside class="sidebar" id="sidebar">
|
||
<div class="sidebar-header">
|
||
<div class="sidebar-title">️ ابزارها</div>
|
||
</div>
|
||
<div class="sidebar-body" id="sidebarBody"></div>
|
||
</aside>
|
||
|
||
<main class="board-area" id="boardArea">
|
||
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-muted);">
|
||
<div style="font-size: 80px; opacity: 0.3; margin-bottom: 20px;"></div>
|
||
<h2 style="font-size: 20px; color: var(--primary); margin-bottom: 10px;">ناحیه اصلی</h2>
|
||
<p>محتوای اصلی برنامه در این ناحیه نمایش داده میشود</p>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<footer class="app-footer" id="appFooter">
|
||
<div>
|
||
<span class="footer-brand">طراح و توسعهدهنده:</span> هادی خزاعی اصل
|
||
<span style="margin: 0 10px;">|</span>
|
||
<span class="footer-brand">شرکت:</span> رایان پرداز کاوش
|
||
</div>
|
||
<div>
|
||
<span class="footer-brand">نسخه:</span> 1.4.21
|
||
</div>
|
||
</footer>
|
||
|
||
<!-- Settings Modal -->
|
||
<div class="modal-overlay" id="settingsModal">
|
||
<div class="modal modal-lg">
|
||
<div class="modal-header">
|
||
<div class="modal-title"><span>⚙️</span><span>تنظیمات محصول</span></div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="readonly-info">
|
||
<span>ℹ️</span>
|
||
<span>اطلاعات پایه محصول فقط قابل مشاهده هستند و از طریق فایل پیکربندی قابل تغییر میباشند.</span>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">عنوان محصول <span class="readonly-badge">فقط خواندنی</span></label>
|
||
<input type="text" class="form-input" id="settingProjectTitle" readonly>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">نسخه <span class="readonly-badge">فقط خواندنی</span></label>
|
||
<input type="text" class="form-input" id="settingVersion" readonly>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">توضیحات <span class="readonly-badge">فقط خواندنی</span></label>
|
||
<textarea class="form-textarea" id="settingDescription" readonly></textarea>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">توسعهدهنده <span class="readonly-badge">فقط خواندنی</span></label>
|
||
<input type="text" class="form-input" id="settingDeveloper" readonly>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">شرکت <span class="readonly-badge">فقط خواندنی</span></label>
|
||
<input type="text" class="form-input" id="settingCompany" readonly>
|
||
</div>
|
||
</div>
|
||
<div class="webhook-section">
|
||
<div class="webhook-section-header">
|
||
<div class="webhook-section-title"><span>🔗</span><span>تنظیمات وبهوک</span></div>
|
||
<div class="webhook-toggle">
|
||
<span>فعالسازی</span>
|
||
<label class="switch">
|
||
<input type="checkbox" id="settingWebhookEnabled">
|
||
<span class="slider"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">آدرس URL وبهوک <span class="required">*</span></label>
|
||
<input type="url" class="form-input" id="settingWebhookUrl" dir="ltr" placeholder="https://example.com/webhook">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">شناسه کلاینت (Client ID) <span class="required">*</span></label>
|
||
<input type="text" class="form-input" id="settingWebhookClientId" dir="ltr" placeholder="client-xxxx-xxxx">
|
||
</div>
|
||
<div class="webhook-status">
|
||
<span class="webhook-status-dot" id="webhookStatusDot"></span>
|
||
<span id="webhookStatusText">وضعیت: غیرفعال</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-accent" id="saveSettingsBtn">💾 ذخیره</button>
|
||
<button class="btn btn-ghost" data-close-modal>بستن</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Galaxy Chart Modal -->
|
||
<div class="modal-overlay" id="galaxyChartModal">
|
||
<div class="modal modal-xl">
|
||
<div class="modal-header">
|
||
<div class="modal-title"><span>🌌</span><span>نمودار کهکشانی محصولات و وابستگیها</span></div>
|
||
<button class="modal-close" data-close-modal></button>
|
||
</div>
|
||
<div class="modal-body" style="padding: 0;">
|
||
<div id="galaxy-chart-container"></div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-accent" onclick="resetGalaxyZoom()">🔍 بازنشانی نما</button>
|
||
<button class="btn btn-ghost" data-close-modal>بستن</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toast-container" id="toastContainer"></div>
|
||
|
||
<script>
|
||
const projectData = {
|
||
projectId: '550e8400-e29b-41d4-a716-446655440000',
|
||
projectName: 'xDependencies',
|
||
projectTitle: 'مدیریت محصولات',
|
||
description: 'مدیریت نسخهها و نیازمندیهای محصولات',
|
||
version: '1.4.21',
|
||
developer: 'هادی خزاعی اصل',
|
||
company: 'رایان پرداز کاوش',
|
||
metadata: {
|
||
createdAt: '2026-09-13T00:00:00Z',
|
||
updatedAt: '2026-09-14T00:00:00Z',
|
||
tags: ['release-management', 'architecture', 'product-management'],
|
||
customFields: {}
|
||
},
|
||
webhook: { url: '', clientId: '', enabled: false },
|
||
toolGroups: [
|
||
{
|
||
id: 'product-management', title: 'محصول', icon: '📁', description: 'مدیریت چرخه حیات محصول',
|
||
tools: [
|
||
{ id: 'create-product', title: 'ایجاد', icon: '➕', action: 'createProduct', shortcut: null, description: 'ایجاد یک محصول جدید' },
|
||
{ id: 'edit-product', title: 'ویرایش', icon: '✏️', action: 'editProduct', shortcut: null, description: 'ویرایش اطلاعات محصول' },
|
||
{ id: 'product-settings', title: 'تنظیمات', icon: '⚙️', action: 'openProductSettings', shortcut: null, description: 'پیکربندی تنظیمات' },
|
||
{ id: 'save-product', title: 'ذخیره', icon: '💾', action: 'saveProduct', shortcut: null, description: 'دانلود دادههای محصول' },
|
||
{ id: 'load-project', title: 'بارگذاری', icon: '📂', action: 'loadProduct', shortcut: null, description: 'بارگذاری از فایل JSON' },
|
||
],
|
||
},
|
||
{
|
||
id: 'management', title: 'مدیریت', icon: '⚙️', description: 'ابزارهای مدیریت پیکربندی',
|
||
tools: [
|
||
{ id: 'manage-envs', title: 'مدیریت محیطها', icon: '🌍', action: 'manageEnvironments', shortcut: null, description: 'مدیریت محیطهای عملیاتی' },
|
||
{ id: 'manage-products', title: 'مدیریت محصولات', icon: '📦', action: 'manageProducts', shortcut: null, description: 'مدیریت سلسلهمراتب' }
|
||
]
|
||
},
|
||
{
|
||
id: 'reports', title: 'گزارشها', icon: '📊', description: 'تحلیل و گزارشگیری',
|
||
tools: [
|
||
{ id: 'view-reports', title: 'نمایش گزارشها', icon: '📈', action: 'viewReports', shortcut: null, description: 'آمار کلی محصول' },
|
||
{ id: 'dependency-graph', title: 'نمودار وابستگی', icon: '🕸️', action: 'showDependencyGraph', shortcut: null, description: 'نمایش بصری وابستگیها' },
|
||
{ id: 'galaxy-graph', title: 'نمودار کهکشانی', icon: '🌌', action: 'showGalaxyGraph', shortcut: null, description: 'نمایش نمودار کهکشانی محصولات و روابط' },
|
||
],
|
||
},
|
||
],
|
||
appData: {
|
||
environments: [
|
||
{ id: 'env-prod-001', name: 'Production', description: 'محیط عملیاتی اصلی' },
|
||
{ id: 'env-stage-002', name: 'Staging', description: 'محیط پیش از تولید' },
|
||
{ id: 'env-dev-003', name: 'Development', description: 'محیط توسعه' },
|
||
],
|
||
products: [
|
||
{
|
||
id: 'prod-1001', title: 'سامانه فروشگاه آنلاین', description: 'پلتفرم جامع فروش محصولات',
|
||
metadata: { createdAt: '2026-09-01T00:00:00Z', owner: 'تیم توسعه محصول', customFields: {} },
|
||
projects: [
|
||
{
|
||
id: 'proj-1001-01', title: 'Backend API Service', description: 'سرویس بکاند اصلی',
|
||
type: 'backend', repositoryUrl: '',
|
||
metadata: { framework: '.NET 8', language: 'C#' },
|
||
dependencies: [],
|
||
lastDeployedVersions: [
|
||
{ environmentId: 'env-prod-001', version: '2.3.1', deploymentDate: '2026-09-10T14:30:00Z' },
|
||
{ environmentId: 'env-stage-002', version: '2.4.0-rc.1', deploymentDate: '2026-09-12T09:00:00Z' },
|
||
],
|
||
},
|
||
{
|
||
id: 'proj-1001-02', title: 'Frontend Web Application', description: 'اپلیکیشن فرانتاند',
|
||
type: 'frontend', repositoryUrl: '',
|
||
metadata: { framework: 'Angular 18', language: 'TypeScript' },
|
||
dependencies: ['proj-1001-01'],
|
||
lastDeployedVersions: [
|
||
{ environmentId: 'env-prod-001', version: '3.1.0', deploymentDate: '2026-09-11T10:00:00Z' },
|
||
],
|
||
},
|
||
{
|
||
id: 'proj-1001-03', title: 'Payment Gateway Service', description: 'سرویس درگاه پرداخت',
|
||
type: 'service', repositoryUrl: '',
|
||
metadata: { framework: '.NET 8', language: 'C#' },
|
||
dependencies: ['proj-1001-01'],
|
||
lastDeployedVersions: [
|
||
{ environmentId: 'env-prod-001', version: '1.2.0', deploymentDate: '2026-09-08T16:00:00Z' },
|
||
],
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
};
|
||
|
||
const VALID_PROJECT_TYPES = [
|
||
{ value: 'backend', label: 'بکاند (Backend)', icon: '🖥️' },
|
||
{ value: 'frontend', label: 'فرانتاند (Frontend)', icon: '🌐' },
|
||
{ value: 'service', label: 'میکروسرویس (Service)', icon: '⚙️' },
|
||
{ value: 'database', label: 'پایگاه داده (Database)', icon: '️' },
|
||
{ value: 'mobile', label: 'موبایل (Mobile)', icon: '📱' },
|
||
{ value: 'infrastructure', label: 'زیرساخت (Infrastructure)', icon: '🏗️' }
|
||
];
|
||
|
||
const TYPE_COLORS = {
|
||
backend: '#3182ce',
|
||
frontend: '#38a169',
|
||
service: '#dd6b20',
|
||
database: '#97266d',
|
||
mobile: '#553c9a',
|
||
infrastructure: '#9c4221'
|
||
};
|
||
|
||
function validateAppDataStructure(data) {
|
||
if (!data || typeof data !== 'object') return false;
|
||
if (!Array.isArray(data.environments)) return false;
|
||
if (!Array.isArray(data.products)) return false;
|
||
return true;
|
||
}
|
||
|
||
const ModalManager = {
|
||
open(id) { const m = document.getElementById(id); if (m) m.classList.add('active'); },
|
||
close(id) { const m = document.getElementById(id); if (m) m.classList.remove('active'); },
|
||
closeAll() { document.querySelectorAll('.modal-overlay.active').forEach(m => m.classList.remove('active')); },
|
||
init() {
|
||
document.querySelectorAll('[data-close-modal]').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const modal = e.target.closest('.modal-overlay');
|
||
if (modal) modal.classList.remove('active');
|
||
});
|
||
});
|
||
document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
||
overlay.addEventListener('click', (e) => {
|
||
if (e.target === overlay) overlay.classList.remove('active');
|
||
});
|
||
});
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') this.closeAll();
|
||
});
|
||
}
|
||
};
|
||
|
||
const ToolbarManager = {
|
||
currentFilters: { query: '', typeFilter: '', envFilter: '' },
|
||
init() {
|
||
this.populateEnvironmentFilter();
|
||
this.bindEvents();
|
||
this.updateStats();
|
||
},
|
||
populateEnvironmentFilter() {
|
||
const select = document.getElementById('filterEnvironment');
|
||
if (!select) return;
|
||
const envs = projectData.appData.environments || [];
|
||
select.innerHTML = '<option value="">همه محیطها</option>' +
|
||
envs.map(e => `<option value="${e.id}">${e.name}</option>`).join('');
|
||
},
|
||
bindEvents() {
|
||
const searchInput = document.getElementById('toolbarSearch');
|
||
const filterType = document.getElementById('filterType');
|
||
const filterEnv = document.getElementById('filterEnvironment');
|
||
if (searchInput) searchInput.addEventListener('input', () => { this.applyFilters(); });
|
||
if (filterType) filterType.addEventListener('change', () => { this.applyFilters(); });
|
||
if (filterEnv) filterEnv.addEventListener('change', () => { this.applyFilters(); });
|
||
},
|
||
applyFilters() {
|
||
this.currentFilters = {
|
||
query: (document.getElementById('toolbarSearch')?.value || '').trim().toLowerCase(),
|
||
typeFilter: document.getElementById('filterType')?.value || '',
|
||
envFilter: document.getElementById('filterEnvironment')?.value || ''
|
||
};
|
||
this.updateStats();
|
||
},
|
||
updateStats() {
|
||
const products = projectData.appData.products || [];
|
||
let totalProjects = 0;
|
||
let totalDeps = 0;
|
||
products.forEach(p => {
|
||
const projects = p.projects || [];
|
||
projects.forEach(pr => {
|
||
totalProjects++;
|
||
totalDeps += (pr.dependencies || []).length;
|
||
});
|
||
});
|
||
const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
|
||
set('statTotal', products.length + totalProjects);
|
||
set('statProjects', products.length);
|
||
set('statSubProjects', totalProjects);
|
||
set('statDependencies', totalDeps);
|
||
}
|
||
};
|
||
|
||
const ActionRegistry = {
|
||
openProductSettings() { openSettingsModal(); },
|
||
manageEnvironments() { showToast('مدیریت محیطها', 'info'); },
|
||
manageProducts() { showToast('مدیریت محصولات', 'info'); },
|
||
saveProduct() {
|
||
projectData.metadata.updatedAt = new Date().toISOString();
|
||
const jsonString = JSON.stringify(projectData.appData, null, 2);
|
||
const blob = new Blob([jsonString], { type: 'application/json;charset=utf-8' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||
a.download = `${projectData.projectName}_v${projectData.version}_${timestamp}.json`;
|
||
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
showToast(`💾 دادههای محصول دانلود شد`, 'success', 4000);
|
||
},
|
||
loadProduct() {
|
||
const input = document.createElement('input');
|
||
input.type = 'file'; input.accept = '.json,application/json';
|
||
input.addEventListener('change', (e) => {
|
||
const file = e.target.files[0];
|
||
if (!file) return;
|
||
const reader = new FileReader();
|
||
reader.onload = (ev) => {
|
||
try {
|
||
const loaded = JSON.parse(ev.target.result);
|
||
if (!validateAppDataStructure(loaded)) return showToast('❌ ساختار فایل نامعتبر است', 'error', 5000);
|
||
projectData.appData = loaded;
|
||
projectData.metadata.updatedAt = new Date().toISOString();
|
||
updateProductUI();
|
||
renderToolGroups();
|
||
ToolbarManager.populateEnvironmentFilter();
|
||
ToolbarManager.updateStats();
|
||
showToast(` محصول بارگذاری شد`, 'success', 4000);
|
||
} catch (err) { showToast('❌ خطا در خواندن فایل: ' + err.message, 'error', 5000); }
|
||
};
|
||
reader.readAsText(file);
|
||
});
|
||
input.click();
|
||
},
|
||
createProduct() { showToast('ایجاد محصول جدید', 'info'); },
|
||
editProduct() { showToast('ویرایش محصول', 'info'); },
|
||
viewReports() {
|
||
const p = projectData.appData.products.length;
|
||
const s = projectData.appData.products.reduce((x, p) => x + p.projects.length, 0);
|
||
const e = projectData.appData.environments.length;
|
||
showToast(`📊 محصولات: ${p} | پروژهها: ${s} | محیطها: ${e}`, 'info', 5000);
|
||
},
|
||
showDependencyGraph() { showToast('️ نمودار وابستگی', 'info', 3000); },
|
||
showGalaxyGraph() { showGalaxyChartModal(); },
|
||
defaultAction(tool) { showToast(`ابزار: ${tool.title}`, 'info', 3000); }
|
||
};
|
||
|
||
function showToast(message, type = 'info', duration = 3000) {
|
||
const container = document.getElementById('toastContainer');
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast ${type}`;
|
||
const icons = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' };
|
||
toast.innerHTML = `<span>${icons[type] || 'ℹ️'}</span><span>${message}</span>`;
|
||
container.appendChild(toast);
|
||
setTimeout(() => {
|
||
toast.style.transition = 'opacity 0.3s, transform 0.3s';
|
||
toast.style.opacity = '0';
|
||
toast.style.transform = 'translateX(-30px)';
|
||
setTimeout(() => toast.remove(), 300);
|
||
}, duration);
|
||
}
|
||
|
||
function openSettingsModal() {
|
||
document.getElementById('settingProjectTitle').value = projectData.projectTitle;
|
||
document.getElementById('settingVersion').value = projectData.version;
|
||
document.getElementById('settingDescription').value = projectData.description;
|
||
document.getElementById('settingDeveloper').value = projectData.developer;
|
||
document.getElementById('settingCompany').value = projectData.company;
|
||
document.getElementById('settingWebhookUrl').value = projectData.webhook.url;
|
||
document.getElementById('settingWebhookClientId').value = projectData.webhook.clientId;
|
||
document.getElementById('settingWebhookEnabled').checked = projectData.webhook.enabled;
|
||
ModalManager.open('settingsModal');
|
||
}
|
||
|
||
// Galaxy Chart Functions
|
||
let galaxySimulation = null;
|
||
|
||
function showGalaxyChartModal() {
|
||
ModalManager.open('galaxyChartModal');
|
||
setTimeout(() => renderGalaxyChart(), 100);
|
||
}
|
||
|
||
function resetGalaxyZoom() {
|
||
if (galaxySimulation) {
|
||
const svg = d3.select('#galaxy-chart-container svg');
|
||
svg.transition().duration(750).call(
|
||
d3.zoom().transform,
|
||
d3.zoomIdentity
|
||
);
|
||
}
|
||
}
|
||
|
||
function renderGalaxyChart() {
|
||
const container = document.getElementById('galaxy-chart-container');
|
||
if (!container) return;
|
||
|
||
// Clear previous chart
|
||
container.innerHTML = '';
|
||
|
||
const width = container.clientWidth;
|
||
const height = container.clientHeight;
|
||
|
||
// Prepare data
|
||
const nodes = [];
|
||
const links = [];
|
||
const nodeMap = new Map();
|
||
|
||
// Create nodes for products
|
||
projectData.appData.products.forEach((product, productIndex) => {
|
||
const productId = product.id;
|
||
|
||
const productNode = {
|
||
id: productId,
|
||
name: product.title,
|
||
type: 'product',
|
||
group: productIndex + 1,
|
||
size: 30,
|
||
color: '#c9a961'
|
||
};
|
||
|
||
nodes.push(productNode);
|
||
nodeMap.set(productId, productNode);
|
||
|
||
// Create nodes for projects within product
|
||
(product.projects || []).forEach((project, projectIndex2) => {
|
||
const projectNodeId = `${productId}-${project.id}`;
|
||
|
||
const projectNode = {
|
||
id: projectNodeId,
|
||
name: project.title,
|
||
type: project.type,
|
||
group: productIndex + 1,
|
||
size: 20,
|
||
color: TYPE_COLORS[project.type] || '#718096',
|
||
parent: productId
|
||
};
|
||
|
||
nodes.push(projectNode);
|
||
nodeMap.set(projectNodeId, projectNode);
|
||
|
||
// Create link from product to project
|
||
links.push({
|
||
source: productId,
|
||
target: projectNodeId,
|
||
value: 2
|
||
});
|
||
|
||
// Create links for dependencies
|
||
(project.dependencies || []).forEach(depId => {
|
||
const targetNodeId = `${productId}-${depId}`;
|
||
if (nodeMap.has(targetNodeId)) {
|
||
links.push({
|
||
source: projectNodeId,
|
||
target: targetNodeId,
|
||
value: 1
|
||
});
|
||
}
|
||
});
|
||
});
|
||
});
|
||
|
||
// Create SVG
|
||
const svg = d3.select('#galaxy-chart-container')
|
||
.append('svg')
|
||
.attr('width', width)
|
||
.attr('height', height)
|
||
.attr('viewBox', [0, 0, width, height]);
|
||
|
||
// Add zoom behavior
|
||
const g = svg.append('g');
|
||
|
||
const zoom = d3.zoom()
|
||
.scaleExtent([0.1, 4])
|
||
.on('zoom', (event) => {
|
||
g.attr('transform', event.transform);
|
||
});
|
||
|
||
svg.call(zoom);
|
||
|
||
// Add arrow markers
|
||
svg.append('defs').selectAll('marker')
|
||
.data(['end'])
|
||
.join('marker')
|
||
.attr('id', String)
|
||
.attr('viewBox', '0 -5 10 10')
|
||
.attr('refX', 25)
|
||
.attr('refY', 0)
|
||
.attr('markerWidth', 6)
|
||
.attr('markerHeight', 6)
|
||
.attr('orient', 'auto')
|
||
.append('path')
|
||
.attr('fill', 'rgba(255,255,255,0.6)')
|
||
.attr('d', 'M0,-5L10,0L0,5');
|
||
|
||
// Create force simulation
|
||
galaxySimulation = d3.forceSimulation(nodes)
|
||
.force('link', d3.forceLink(links).id(d => d.id).distance(100))
|
||
.force('charge', d3.forceManyBody().strength(-400))
|
||
.force('center', d3.forceCenter(width / 2, height / 2))
|
||
.force('collision', d3.forceCollide().radius(d => d.size + 10));
|
||
|
||
// Draw links
|
||
const link = g.append('g')
|
||
.attr('class', 'links')
|
||
.selectAll('line')
|
||
.data(links)
|
||
.join('line')
|
||
.attr('class', 'galaxy-link')
|
||
.attr('stroke', 'rgba(255,255,255,0.4)')
|
||
.attr('stroke-width', d => Math.sqrt(d.value))
|
||
.attr('marker-end', 'url(#end)');
|
||
|
||
// Draw nodes
|
||
const node = g.append('g')
|
||
.attr('class', 'nodes')
|
||
.selectAll('circle')
|
||
.data(nodes)
|
||
.join('circle')
|
||
.attr('class', 'galaxy-node')
|
||
.attr('r', d => d.size)
|
||
.attr('fill', d => d.color)
|
||
.attr('stroke', '#fff')
|
||
.attr('stroke-width', 2)
|
||
.call(drag(galaxySimulation));
|
||
|
||
// Add labels
|
||
const labels = g.append('g')
|
||
.attr('class', 'labels')
|
||
.selectAll('text')
|
||
.data(nodes)
|
||
.join('text')
|
||
.attr('class', 'galaxy-label')
|
||
.attr('dx', 0)
|
||
.attr('dy', d => d.size + 15)
|
||
.text(d => d.name);
|
||
|
||
// Add tooltip
|
||
const tooltip = d3.select('body').append('div')
|
||
.attr('class', 'galaxy-tooltip')
|
||
.style('opacity', 0);
|
||
|
||
node.on('mouseover', (event, d) => {
|
||
tooltip.transition().duration(200).style('opacity', .9);
|
||
tooltip.html(`
|
||
<strong>${d.name}</strong><br/>
|
||
نوع: ${d.type === 'product' ? 'محصول' : d.type}<br/>
|
||
${d.parent ? `محصول والد: ${projectData.appData.products.find(p => p.id === d.parent)?.title || ''}` : ''}
|
||
`)
|
||
.style('left', (event.pageX + 10) + 'px')
|
||
.style('top', (event.pageY - 28) + 'px');
|
||
})
|
||
.on('mouseout', () => {
|
||
tooltip.transition().duration(500).style('opacity', 0);
|
||
});
|
||
|
||
// Update positions on tick
|
||
galaxySimulation.on('tick', () => {
|
||
link
|
||
.attr('x1', d => d.source.x)
|
||
.attr('y1', d => d.source.y)
|
||
.attr('x2', d => d.target.x)
|
||
.attr('y2', d => d.target.y);
|
||
|
||
node
|
||
.attr('cx', d => d.x)
|
||
.attr('cy', d => d.y);
|
||
|
||
labels
|
||
.attr('x', d => d.x)
|
||
.attr('y', d => d.y);
|
||
});
|
||
|
||
// Add legend
|
||
addGalaxyLegend(svg, width, height);
|
||
}
|
||
|
||
function addGalaxyLegend(svg, width, height) {
|
||
const legendData = [
|
||
{ label: 'محصول', color: '#c9a961' },
|
||
{ label: 'بکاند', color: '#3182ce' },
|
||
{ label: 'فرانتاند', color: '#38a169' },
|
||
{ label: 'میکروسرویس', color: '#dd6b20' },
|
||
{ label: 'پایگاه داده', color: '#97266d' },
|
||
{ label: 'موبایل', color: '#553c9a' },
|
||
{ label: 'زیرساخت', color: '#9c4221' }
|
||
];
|
||
|
||
const legend = svg.append('g')
|
||
.attr('class', 'galaxy-legend')
|
||
.attr('transform', `translate(20, 20)`);
|
||
|
||
legend.append('rect')
|
||
.attr('width', 140)
|
||
.attr('height', legendData.length * 25 + 10)
|
||
.attr('fill', 'rgba(0,0,0,0.7)')
|
||
.attr('rx', 8);
|
||
|
||
legendData.forEach((item, i) => {
|
||
const itemGroup = legend.append('g')
|
||
.attr('transform', `translate(10, ${i * 25 + 10})`);
|
||
|
||
itemGroup.append('circle')
|
||
.attr('r', 7)
|
||
.attr('fill', item.color);
|
||
|
||
itemGroup.append('text')
|
||
.attr('x', 15)
|
||
.attr('y', 4)
|
||
.attr('fill', 'white')
|
||
.attr('font-size', '11px')
|
||
.text(item.label);
|
||
});
|
||
}
|
||
|
||
function drag(simulation) {
|
||
function dragstarted(event, d) {
|
||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
||
d.fx = d.x;
|
||
d.fy = d.y;
|
||
}
|
||
|
||
function dragged(event, d) {
|
||
d.fx = event.x;
|
||
d.fy = event.y;
|
||
}
|
||
|
||
function dragended(event, d) {
|
||
if (!event.active) simulation.alphaTarget(0);
|
||
d.fx = null;
|
||
d.fy = null;
|
||
}
|
||
|
||
return d3.drag()
|
||
.on('start', dragstarted)
|
||
.on('drag', dragged)
|
||
.on('end', dragended);
|
||
}
|
||
|
||
function handleToolSelect(tool, group) {
|
||
const actionHandler = ActionRegistry[tool.action] || ActionRegistry.defaultAction;
|
||
actionHandler(tool, group);
|
||
}
|
||
|
||
const themeToggle = document.getElementById('themeToggle');
|
||
const themeIcon = document.getElementById('themeIcon');
|
||
const htmlEl = document.documentElement;
|
||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||
htmlEl.setAttribute('data-theme', savedTheme);
|
||
if (themeIcon) themeIcon.textContent = savedTheme === 'dark' ? '☀️' : '🌙';
|
||
if (themeToggle) {
|
||
themeToggle.addEventListener('click', () => {
|
||
const current = htmlEl.getAttribute('data-theme');
|
||
const next = current === 'dark' ? 'light' : 'dark';
|
||
htmlEl.setAttribute('data-theme', next);
|
||
localStorage.setItem('theme', next);
|
||
if (themeIcon) themeIcon.textContent = next === 'dark' ? '☀️' : '';
|
||
});
|
||
}
|
||
|
||
const sidebarToggle = document.getElementById('sidebarToggle');
|
||
const sidebar = document.getElementById('sidebar');
|
||
const sidebarIcon = document.getElementById('sidebarIcon');
|
||
function updateSidebarIcon() { if (!sidebarIcon) return; sidebarIcon.textContent = sidebar.classList.contains('collapsed') ? '☰' : '✕'; }
|
||
if (sidebarToggle) { sidebarToggle.addEventListener('click', () => { sidebar.classList.toggle('collapsed'); updateSidebarIcon(); }); }
|
||
updateSidebarIcon();
|
||
|
||
const sidebarBody = document.getElementById('sidebarBody');
|
||
function renderToolGroups() {
|
||
sidebarBody.innerHTML = '';
|
||
projectData.toolGroups.forEach(group => {
|
||
const groupEl = document.createElement('div');
|
||
groupEl.className = 'sidebar-group';
|
||
const headerEl = document.createElement('div');
|
||
headerEl.className = 'sidebar-group-header';
|
||
if (group.description) headerEl.setAttribute('data-tooltip', group.description);
|
||
headerEl.innerHTML = `<div class="group-icon"><span>${group.icon || ''}</span><span>${group.title}</span></div><span class="group-arrow">▼</span>`;
|
||
const bodyEl = document.createElement('div');
|
||
bodyEl.className = 'sidebar-group-body';
|
||
group.tools.forEach(tool => {
|
||
const btn = document.createElement('button');
|
||
btn.className = 'sidebar-btn';
|
||
btn.title = tool.description;
|
||
btn.innerHTML = `<span class="btn-icon-emoji">${tool.icon || '🔧'}</span><span class="btn-title">${tool.title}</span>`;
|
||
btn.addEventListener('click', () => handleToolSelect(tool, group));
|
||
bodyEl.appendChild(btn);
|
||
});
|
||
headerEl.addEventListener('click', () => groupEl.classList.toggle('collapsed'));
|
||
groupEl.appendChild(headerEl);
|
||
groupEl.appendChild(bodyEl);
|
||
sidebarBody.appendChild(groupEl);
|
||
});
|
||
}
|
||
|
||
function updateProductUI() {
|
||
document.title = projectData.projectTitle;
|
||
document.getElementById('headerTitle').textContent = projectData.projectTitle;
|
||
document.getElementById('headerSubtitle').textContent = projectData.description;
|
||
document.getElementById('appFooter').innerHTML = `<div><span class="footer-brand">طراح و توسعهدهنده:</span> ${projectData.developer}<span style="margin: 0 10px;">|</span><span class="footer-brand">شرکت:</span> ${projectData.company}</div><div><span class="footer-brand">نسخه:</span> ${projectData.version}</div>`;
|
||
}
|
||
|
||
ModalManager.init();
|
||
renderToolGroups();
|
||
updateProductUI();
|
||
ToolbarManager.init();
|
||
setTimeout(() => showToast(`${projectData.projectTitle} (نسخه ${projectData.version}) آماده است`, 'success'), 500);
|
||
</script>
|
||
</body>
|
||
</html> |