Улучшение фронта
This commit is contained in:
70
front_vue/src/Core/composables/useConfirm.js
Normal file
70
front_vue/src/Core/composables/useConfirm.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
const isOpen = ref(false)
|
||||||
|
const config = ref({
|
||||||
|
icon: 'fas fa-question-circle',
|
||||||
|
iconColor: 'var(--accent-red)',
|
||||||
|
title: '',
|
||||||
|
message: '',
|
||||||
|
buttons: [],
|
||||||
|
})
|
||||||
|
let resolvePromise = null
|
||||||
|
|
||||||
|
export function useConfirm() {
|
||||||
|
const open = (options = {}) => {
|
||||||
|
config.value = {
|
||||||
|
icon: options.icon || 'fas fa-exclamation-triangle',
|
||||||
|
iconColor: options.iconColor || 'var(--accent-red)',
|
||||||
|
title: options.title || '',
|
||||||
|
message: options.message || '',
|
||||||
|
buttons: options.buttons || [],
|
||||||
|
}
|
||||||
|
isOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
isOpen.value = false
|
||||||
|
if (resolvePromise) {
|
||||||
|
resolvePromise(false)
|
||||||
|
resolvePromise = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirm = (options = {}) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolvePromise = resolve
|
||||||
|
open({
|
||||||
|
...options,
|
||||||
|
buttons: (options.buttons || []).map(btn => ({
|
||||||
|
...btn,
|
||||||
|
action: () => {
|
||||||
|
resolvePromise = null
|
||||||
|
isOpen.value = false
|
||||||
|
resolve(btn.value !== undefined ? btn.value : btn.label)
|
||||||
|
if (btn.action) btn.action()
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDelete = (options = {}) => {
|
||||||
|
return confirm({
|
||||||
|
icon: options.icon || 'fas fa-trash-alt',
|
||||||
|
iconColor: options.iconColor || 'var(--accent-red)',
|
||||||
|
title: options.title || '',
|
||||||
|
message: options.message || '',
|
||||||
|
buttons: [
|
||||||
|
{ label: options.cancelText || 'Отмена', variant: 'default', value: false },
|
||||||
|
{ label: options.confirmText || 'Удалить', variant: 'danger', icon: 'fas fa-trash', value: true },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isOpen: readonly(isOpen),
|
||||||
|
config: readonly(config),
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
confirm,
|
||||||
|
confirmDelete,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,3 +43,5 @@ export const STORAGE_KEYS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AUTO_REFRESH_INTERVAL = 5000
|
export const AUTO_REFRESH_INTERVAL = 5000
|
||||||
|
|
||||||
|
export const APP_VERSION = __APP_VERSION__
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"app": {
|
"app": {
|
||||||
"title": "vServer Admin Panel",
|
"title": "vServer Admin Panel",
|
||||||
"logo": "vServer",
|
"logo": "vServer v1.0",
|
||||||
"footerAuthor": "Author: Sumaneev Roman",
|
"footerAuthor": "Author: Sumaneev Roman",
|
||||||
"loading": "Starting vServer..."
|
"loading": "Starting vServer..."
|
||||||
},
|
},
|
||||||
@@ -21,7 +21,9 @@
|
|||||||
"stop": "Stop",
|
"stop": "Stop",
|
||||||
"starting": "Starting...",
|
"starting": "Starting...",
|
||||||
"stopping": "Stopping...",
|
"stopping": "Stopping...",
|
||||||
"wait": "Please wait..."
|
"wait": "Please wait...",
|
||||||
|
"stopConfirmTitle": "Stop server?",
|
||||||
|
"stopConfirmMessage": "All services will be stopped"
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"title": "Services Status",
|
"title": "Services Status",
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
"stop": "Остановить",
|
"stop": "Остановить",
|
||||||
"starting": "Запускается...",
|
"starting": "Запускается...",
|
||||||
"stopping": "Выключается...",
|
"stopping": "Выключается...",
|
||||||
"wait": "Ожидайте..."
|
"wait": "Ожидайте...",
|
||||||
|
"stopConfirmTitle": "Остановить сервер?",
|
||||||
|
"stopConfirmMessage": "Все сервисы будут остановлены"
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"title": "Статус сервисов",
|
"title": "Статус сервисов",
|
||||||
@@ -176,7 +178,6 @@
|
|||||||
"errorPrefix": "Ошибка"
|
"errorPrefix": "Ошибка"
|
||||||
},
|
},
|
||||||
"notify": {
|
"notify": {
|
||||||
"settingsSaved": "Настройки сохранены и сервисы перезапущены!",
|
|
||||||
"settingsSaved": "Настройки сохранены",
|
"settingsSaved": "Настройки сохранены",
|
||||||
"dataSaved": "Данные сохранены",
|
"dataSaved": "Данные сохранены",
|
||||||
"siteCreated": "Сайт успешно создан!",
|
"siteCreated": "Сайт успешно создан!",
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { api } from '@core/api/index.js'
|
|
||||||
|
|
||||||
export const useCertsStore = defineStore('certs', {
|
export const useCertsStore = defineStore('certs', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { api } from '@core/api/index.js'
|
|
||||||
|
|
||||||
export const useConfigStore = defineStore('config', {
|
export const useConfigStore = defineStore('config', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { api } from '@core/api/index.js'
|
|
||||||
|
|
||||||
export const useProxiesStore = defineStore('proxies', {
|
export const useProxiesStore = defineStore('proxies', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { api } from '@core/api/index.js'
|
|
||||||
|
|
||||||
export const useServicesStore = defineStore('services', {
|
export const useServicesStore = defineStore('services', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { api } from '@core/api/index.js'
|
|
||||||
|
|
||||||
export const useSitesStore = defineStore('sites', {
|
export const useSitesStore = defineStore('sites', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
|
|||||||
@@ -1,100 +1,91 @@
|
|||||||
/* ============================================
|
/* ============================================
|
||||||
Общие стили таблиц
|
Общие стили таблиц — Modern Clean
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
.section-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--space-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: var(--text-md);
|
|
||||||
font-weight: var(--font-bold);
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin: 0;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1.5px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title::before {
|
|
||||||
content: '';
|
|
||||||
width: 4px;
|
|
||||||
height: 16px;
|
|
||||||
background: linear-gradient(180deg, var(--accent-purple), var(--accent-purple-light));
|
|
||||||
border-radius: 2px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/* Table Container */
|
||||||
.table-container {
|
.table-container {
|
||||||
background: var(--glass-bg-light);
|
|
||||||
backdrop-filter: var(--backdrop-blur);
|
|
||||||
border: 1px solid var(--glass-border);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Table */
|
||||||
.data-table {
|
.data-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: separate;
|
||||||
}
|
border-spacing: 0 8px;
|
||||||
|
|
||||||
.data-table thead {
|
|
||||||
background: var(--table-header-bg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
.data-table thead tr {
|
.data-table thead tr {
|
||||||
border-bottom: 1px solid var(--table-border);
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table th {
|
.data-table th {
|
||||||
padding: 18px 20px;
|
padding: 8px 20px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: var(--text-sm);
|
font-size: 11px;
|
||||||
font-weight: var(--font-bold);
|
font-weight: var(--font-semibold);
|
||||||
color: var(--text-secondary);
|
color: var(--text-muted);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 1.2px;
|
letter-spacing: 1.5px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table th:last-child {
|
.th-icon {
|
||||||
width: 160px;
|
opacity: 0.4;
|
||||||
text-align: center;
|
font-size: 10px;
|
||||||
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.th-actions {
|
||||||
|
text-align: right !important;
|
||||||
|
padding-right: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rows */
|
||||||
.data-table tbody tr {
|
.data-table tbody tr {
|
||||||
border-bottom: 1px solid var(--table-border);
|
|
||||||
transition: all var(--transition-base);
|
transition: all var(--transition-base);
|
||||||
|
background: var(--glass-bg-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table tbody tr:hover {
|
.data-table tbody tr:hover {
|
||||||
background: var(--table-hover-bg);
|
background: rgba(var(--accent-rgb), 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table td {
|
.data-table td {
|
||||||
padding: 16px 20px;
|
padding: 14px 20px;
|
||||||
font-size: var(--text-base);
|
font-size: var(--text-base);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
border-top: 1px solid var(--glass-border);
|
||||||
|
border-bottom: 1px solid var(--glass-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table td:first-child {
|
||||||
|
border-left: 1px solid var(--glass-border);
|
||||||
|
border-radius: var(--radius) 0 0 var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table td:last-child {
|
.data-table td:last-child {
|
||||||
text-align: center;
|
border-right: 1px solid var(--glass-border);
|
||||||
|
border-radius: 0 var(--radius) var(--radius) 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-xs);
|
gap: var(--space-xs);
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.data-table tbody tr:hover td {
|
||||||
|
border-color: rgba(var(--accent-rgb), 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code */
|
||||||
.data-table code {
|
.data-table code {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Links */
|
||||||
.clickable-link {
|
.clickable-link {
|
||||||
color: var(--link-color);
|
color: var(--link-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -117,59 +108,82 @@
|
|||||||
.cert-icon {
|
.cert-icon {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
margin-right: 6px;
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
margin-right: 8px;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cert-valid {
|
.cert-valid {
|
||||||
background: rgba(var(--success-rgb), 0.2);
|
|
||||||
color: var(--accent-green);
|
color: var(--accent-green);
|
||||||
border: 1px solid rgba(var(--success-rgb), 0.4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cert-expired {
|
.cert-expired {
|
||||||
background: rgba(var(--danger-rgb), 0.2);
|
|
||||||
color: var(--accent-red);
|
color: var(--accent-red);
|
||||||
border: 1px solid rgba(var(--danger-rgb), 0.4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cert-none {
|
.cert-none {
|
||||||
background: rgba(var(--muted-rgb), 0.15);
|
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
border: 1px solid rgba(var(--muted-rgb), 0.3);
|
opacity: 0.25;
|
||||||
opacity: 0.5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Icon buttons */
|
/* Icon buttons */
|
||||||
.icon-btn {
|
.icon-btn {
|
||||||
width: 32px;
|
width: 30px;
|
||||||
height: 32px;
|
height: 30px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: var(--btn-icon-bg);
|
background: transparent;
|
||||||
border: 1px solid var(--btn-icon-border);
|
border: none;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
color: var(--btn-icon-color);
|
color: var(--text-muted);
|
||||||
font-size: var(--text-md);
|
font-size: var(--text-sm);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all var(--transition-base);
|
transition: all var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-btn:hover {
|
.icon-btn:hover {
|
||||||
background: var(--btn-icon-hover-bg);
|
background: rgba(var(--accent-rgb), 0.1);
|
||||||
border-color: var(--btn-icon-hover-border);
|
color: var(--accent-purple-light);
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-btn:disabled {
|
.icon-btn:disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
transform: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-toggle {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-toggle:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drag and drop */
|
||||||
|
.drag-grip {
|
||||||
|
color: var(--text-muted);
|
||||||
|
opacity: 0.15;
|
||||||
|
cursor: grab;
|
||||||
|
font-size: 11px;
|
||||||
|
margin-right: 8px;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table tbody tr:hover .drag-grip {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-grip:hover {
|
||||||
|
opacity: 1 !important;
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table tbody tr.dragging {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table tbody tr.drag-over td {
|
||||||
|
border-top-color: var(--accent-purple) !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { api } from '@core/api/index.js'
|
|
||||||
|
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
@@ -7,6 +6,7 @@ const servicesStore = useServicesStore()
|
|||||||
const { isDark, toggleTheme } = useTheme()
|
const { isDark, toggleTheme } = useTheme()
|
||||||
|
|
||||||
const isWails = typeof window !== 'undefined' && window?.runtime
|
const isWails = typeof window !== 'undefined' && window?.runtime
|
||||||
|
const { confirm } = useConfirm()
|
||||||
const operating = ref(false)
|
const operating = ref(false)
|
||||||
const statusLabel = ref('')
|
const statusLabel = ref('')
|
||||||
|
|
||||||
@@ -22,6 +22,18 @@ const serverToggle = async () => {
|
|||||||
if (operating.value) return
|
if (operating.value) return
|
||||||
|
|
||||||
if (appStore.serverRunning) {
|
if (appStore.serverRunning) {
|
||||||
|
const result = await confirm({
|
||||||
|
icon: 'fas fa-power-off',
|
||||||
|
iconColor: 'var(--accent-red)',
|
||||||
|
title: t('server.stopConfirmTitle'),
|
||||||
|
message: t('server.stopConfirmMessage'),
|
||||||
|
buttons: [
|
||||||
|
{ label: t('common.cancel'), variant: 'default', value: false },
|
||||||
|
{ label: t('server.stop'), variant: 'danger', icon: 'fas fa-power-off', value: true },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
if (!result) return
|
||||||
|
|
||||||
operating.value = true
|
operating.value = true
|
||||||
statusLabel.value = t('server.stopping')
|
statusLabel.value = t('server.stopping')
|
||||||
servicesStore.setPending(t('server.stopping'))
|
servicesStore.setPending(t('server.stopping'))
|
||||||
@@ -63,6 +75,7 @@ const windowClose = () => { if (isWails) window.runtime.Quit() }
|
|||||||
<div class="app-logo">
|
<div class="app-logo">
|
||||||
<span class="logo-icon">🚀</span>
|
<span class="logo-icon">🚀</span>
|
||||||
<span class="logo-text">{{ t('app.logo') }}</span>
|
<span class="logo-text">{{ t('app.logo') }}</span>
|
||||||
|
<span class="logo-version">v{{ APP_VERSION }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="server-status">
|
<div class="server-status">
|
||||||
<span class="status-indicator" :class="[operating ? 'status-pending' : (appStore.serverRunning ? 'status-online' : 'status-offline')]"></span>
|
<span class="status-indicator" :class="[operating ? 'status-pending' : (appStore.serverRunning ? 'status-online' : 'status-offline')]"></span>
|
||||||
@@ -126,6 +139,12 @@ const windowClose = () => { if (isWails) window.runtime.Quit() }
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logo-version {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
.server-status {
|
.server-status {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,9 +1,45 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const proxiesStore = useProxiesStore()
|
const proxiesStore = useProxiesStore()
|
||||||
const certsStore = useCertsStore()
|
const certsStore = useCertsStore()
|
||||||
|
|
||||||
|
const togglingStatus = ref('')
|
||||||
|
const dragIdx = ref(null)
|
||||||
|
const overIdx = ref(null)
|
||||||
|
|
||||||
|
const onDragStart = (i, e) => { dragIdx.value = i; e.dataTransfer.effectAllowed = 'move' }
|
||||||
|
const onDragOver = (i, e) => { e.preventDefault(); overIdx.value = i }
|
||||||
|
const onDragLeave = () => { overIdx.value = null }
|
||||||
|
const onDragEnd = () => { dragIdx.value = null; overIdx.value = null }
|
||||||
|
|
||||||
|
const onDrop = async (i) => {
|
||||||
|
if (dragIdx.value === null || dragIdx.value === i) { dragIdx.value = null; overIdx.value = null; return }
|
||||||
|
const items = [...proxiesStore.list]
|
||||||
|
const [moved] = items.splice(dragIdx.value, 1)
|
||||||
|
items.splice(i, 0, moved)
|
||||||
|
proxiesStore.list = items
|
||||||
|
dragIdx.value = null
|
||||||
|
overIdx.value = null
|
||||||
|
const config = await api.getConfig()
|
||||||
|
config.Proxy_Service = items.map(p => config.Proxy_Service.find(c => c.ExternalDomain === p.ExternalDomain)).filter(Boolean)
|
||||||
|
await api.saveConfig(JSON.stringify(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleStatus = async (proxy) => {
|
||||||
|
togglingStatus.value = proxy.ExternalDomain
|
||||||
|
const config = await api.getConfig()
|
||||||
|
const idx = config.Proxy_Service.findIndex(p => p.ExternalDomain === proxy.ExternalDomain)
|
||||||
|
if (idx >= 0) {
|
||||||
|
config.Proxy_Service[idx].Enable = !proxy.Enable
|
||||||
|
await api.saveConfig(JSON.stringify(config))
|
||||||
|
await api.reloadConfig()
|
||||||
|
await proxiesStore.load()
|
||||||
|
}
|
||||||
|
togglingStatus.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
const openUrl = (host) => {
|
const openUrl = (host) => {
|
||||||
if (window.runtime?.BrowserOpenURL) {
|
if (window.runtime?.BrowserOpenURL) {
|
||||||
window.runtime.BrowserOpenURL('http://' + host)
|
window.runtime.BrowserOpenURL('http://' + host)
|
||||||
@@ -41,27 +77,33 @@ const findCertForDomain = (domain) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="section-header">
|
<VSectionHeader :title="t('proxies.title')" addable @add="router.push('/proxies/create')" />
|
||||||
<h2 class="section-title">{{ t('proxies.title') }}</h2>
|
|
||||||
<VButton variant="primary" icon="fas fa-plus" @click="router.push('/proxies/create')">
|
|
||||||
{{ t('proxies.add') }}
|
|
||||||
</VButton>
|
|
||||||
</div>
|
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ t('sites.name') }}</th>
|
<th><i class="fas fa-tag th-icon"></i> {{ t('sites.name') }}</th>
|
||||||
<th>{{ t('proxies.externalDomain') }}</th>
|
<th><i class="fas fa-globe th-icon"></i> {{ t('proxies.externalDomain') }}</th>
|
||||||
<th>{{ t('proxies.localAddress') }}</th>
|
<th><i class="fas fa-server th-icon"></i> {{ t('proxies.localAddress') }}</th>
|
||||||
<th>HTTPS</th>
|
<th><i class="fas fa-lock th-icon"></i> HTTPS</th>
|
||||||
<th>{{ t('proxies.status') }}</th>
|
<th><i class="fas fa-circle-check th-icon"></i> {{ t('proxies.status') }}</th>
|
||||||
<th>{{ t('proxies.actions') }}</th>
|
<th class="th-actions">{{ t('proxies.actions') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="proxy in proxiesStore.list" :key="proxy.ExternalDomain">
|
<tr
|
||||||
|
v-for="(proxy, i) in proxiesStore.list"
|
||||||
|
:key="proxy.ExternalDomain"
|
||||||
|
draggable="true"
|
||||||
|
:class="{ 'drag-over': overIdx === i, 'dragging': dragIdx === i }"
|
||||||
|
@dragstart="onDragStart(i, $event)"
|
||||||
|
@dragover="onDragOver(i, $event)"
|
||||||
|
@dragleave="onDragLeave"
|
||||||
|
@drop="onDrop(i)"
|
||||||
|
@dragend="onDragEnd"
|
||||||
|
>
|
||||||
<td>
|
<td>
|
||||||
|
<i class="fas fa-grip-vertical drag-grip"></i>
|
||||||
<span class="cert-icon" :class="findCertForDomain(proxy.ExternalDomain) ? (findCertForDomain(proxy.ExternalDomain).is_expired ? 'cert-expired' : 'cert-valid') : 'cert-none'">
|
<span class="cert-icon" :class="findCertForDomain(proxy.ExternalDomain) ? (findCertForDomain(proxy.ExternalDomain).is_expired ? 'cert-expired' : 'cert-valid') : 'cert-none'">
|
||||||
<i class="fas fa-shield-alt"></i>
|
<i class="fas fa-shield-alt"></i>
|
||||||
</span>
|
</span>
|
||||||
@@ -77,8 +119,12 @@ const findCertForDomain = (domain) => {
|
|||||||
</VBadge>
|
</VBadge>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<VBadge :variant="proxy.Enable ? 'online' : 'offline'">
|
<VBadge
|
||||||
{{ proxy.Enable ? 'active' : 'disabled' }}
|
class="status-toggle"
|
||||||
|
:variant="togglingStatus === proxy.ExternalDomain ? 'pending' : (proxy.Enable ? 'online' : 'offline')"
|
||||||
|
@click="toggleStatus(proxy)"
|
||||||
|
>
|
||||||
|
{{ togglingStatus === proxy.ExternalDomain ? '...' : (proxy.Enable ? 'active' : 'disabled') }}
|
||||||
</VBadge>
|
</VBadge>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -12,117 +12,80 @@ const serviceIcons = {
|
|||||||
PHP: 'fab fa-php',
|
PHP: 'fab fa-php',
|
||||||
Proxy: 'fas fa-exchange-alt',
|
Proxy: 'fas fa-exchange-alt',
|
||||||
}
|
}
|
||||||
|
|
||||||
const serviceInfoLabel = {
|
|
||||||
HTTP: 'services.port',
|
|
||||||
HTTPS: 'services.port',
|
|
||||||
MySQL: 'services.port',
|
|
||||||
PHP: 'services.ports',
|
|
||||||
Proxy: 'services.rules',
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="service-card">
|
<div class="service-item" :class="{ 'service-active': service.status, 'service-pending': service.pending }">
|
||||||
<div class="service-header">
|
<span class="service-dot"></span>
|
||||||
<h3 class="service-name">
|
<i :class="serviceIcons[service.name] || 'fas fa-server'" class="service-icon"></i>
|
||||||
<i :class="serviceIcons[service.name] || 'fas fa-server'"></i>
|
<span class="service-label">{{ service.name }}</span>
|
||||||
{{ service.name }}
|
<span v-if="service.pending" class="service-status-text pending">{{ service.pending }}</span>
|
||||||
</h3>
|
|
||||||
<VBadge :variant="service.pending ? 'pending' : (service.status ? 'online' : 'offline')">
|
|
||||||
{{ service.pending || (service.status ? t('common.enabled') : t('common.disabled')) }}
|
|
||||||
</VBadge>
|
|
||||||
</div>
|
|
||||||
<div class="service-info">
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="info-label">{{ t(serviceInfoLabel[service.name] || 'services.port') }}:</span>
|
|
||||||
<span class="info-value">{{ service.port || service.info || '—' }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="service.info && service.port" class="info-row">
|
|
||||||
<span class="info-label">{{ t('services.rules') }}:</span>
|
|
||||||
<span class="info-value">{{ service.info }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.service-card {
|
.service-item {
|
||||||
background: var(--glass-bg-light);
|
display: flex;
|
||||||
backdrop-filter: var(--backdrop-blur);
|
align-items: center;
|
||||||
border: 1px solid var(--glass-border);
|
gap: 10px;
|
||||||
|
padding: 12px 18px;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: var(--space-lg);
|
border: 1px solid var(--glass-border);
|
||||||
transition: all var(--transition-bounce);
|
background: var(--glass-bg-light);
|
||||||
position: relative;
|
transition: all var(--transition-base);
|
||||||
overflow: hidden;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-card::before {
|
.service-item:hover {
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 3px;
|
|
||||||
background: var(--service-card-gradient);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity var(--transition-slow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-card:hover {
|
|
||||||
transform: translateY(-4px);
|
|
||||||
box-shadow: var(--card-hover-shadow);
|
|
||||||
border-color: var(--card-border-hover);
|
border-color: var(--card-border-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-card:hover::before {
|
.service-dot {
|
||||||
opacity: 1;
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
background: var(--accent-red);
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-header {
|
.service-active .service-dot {
|
||||||
display: flex;
|
background: var(--accent-green);
|
||||||
justify-content: space-between;
|
box-shadow: 0 0 8px var(--accent-green);
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 18px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-name {
|
.service-pending .service-dot {
|
||||||
|
background: var(--accent-yellow, #f0ad4e);
|
||||||
|
box-shadow: 0 0 8px var(--accent-yellow, #f0ad4e);
|
||||||
|
animation: dot-blink 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-icon {
|
||||||
|
font-size: var(--text-md);
|
||||||
|
color: var(--text-muted);
|
||||||
|
width: 18px;
|
||||||
|
text-align: center;
|
||||||
|
transition: color var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-active .service-icon {
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-label {
|
||||||
font-size: var(--text-md);
|
font-size: var(--text-md);
|
||||||
font-weight: var(--font-semibold);
|
font-weight: var(--font-semibold);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-sm);
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-name i {
|
.service-status-text.pending {
|
||||||
color: var(--accent-purple-light);
|
margin-left: auto;
|
||||||
font-size: var(--text-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-label {
|
|
||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
color: var(--text-secondary);
|
color: var(--accent-yellow, #f0ad4e);
|
||||||
font-weight: var(--font-medium);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-value {
|
@keyframes dot-blink {
|
||||||
font-size: 12px;
|
0%, 100% { opacity: 1; }
|
||||||
color: var(--text-primary);
|
50% { opacity: 0.3; }
|
||||||
font-weight: var(--font-semibold);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const servicesStore = useServicesStore()
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="services-grid">
|
<div class="services-row">
|
||||||
<ServiceCard
|
<ServiceCard
|
||||||
v-for="service in servicesStore.list"
|
v-for="service in servicesStore.list"
|
||||||
:key="service.name"
|
:key="service.name"
|
||||||
@@ -16,21 +16,14 @@ const servicesStore = useServicesStore()
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.services-grid {
|
.services-row {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(5, 1fr);
|
gap: 10px;
|
||||||
gap: var(--space-lg);
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
.services-row > * {
|
||||||
.services-grid { grid-template-columns: repeat(3, 1fr); }
|
flex: 1;
|
||||||
}
|
min-width: 140px;
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.services-grid { grid-template-columns: repeat(2, 1fr); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.services-grid { grid-template-columns: 1fr; }
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,12 +1,49 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const sitesStore = useSitesStore()
|
const sitesStore = useSitesStore()
|
||||||
const certsStore = useCertsStore()
|
const certsStore = useCertsStore()
|
||||||
const { success, error } = useNotification()
|
const { success, error } = useNotification()
|
||||||
const modal = useModal()
|
const { confirmDelete: showConfirm } = useConfirm()
|
||||||
|
|
||||||
|
|
||||||
|
const dragIdx = ref(null)
|
||||||
|
const overIdx = ref(null)
|
||||||
|
|
||||||
|
const onDragStart = (i, e) => { dragIdx.value = i; e.dataTransfer.effectAllowed = 'move' }
|
||||||
|
const onDragOver = (i, e) => { e.preventDefault(); overIdx.value = i }
|
||||||
|
const onDragLeave = () => { overIdx.value = null }
|
||||||
|
const onDragEnd = () => { dragIdx.value = null; overIdx.value = null }
|
||||||
|
|
||||||
|
const onDrop = async (i) => {
|
||||||
|
if (dragIdx.value === null || dragIdx.value === i) { dragIdx.value = null; overIdx.value = null; return }
|
||||||
|
const items = [...sitesStore.list]
|
||||||
|
const [moved] = items.splice(dragIdx.value, 1)
|
||||||
|
items.splice(i, 0, moved)
|
||||||
|
sitesStore.list = items
|
||||||
|
dragIdx.value = null
|
||||||
|
overIdx.value = null
|
||||||
|
const config = await api.getConfig()
|
||||||
|
config.Site_www = items.map(s => config.Site_www.find(c => c.host === s.host)).filter(Boolean)
|
||||||
|
await api.saveConfig(JSON.stringify(config))
|
||||||
|
}
|
||||||
|
|
||||||
const openingFolder = ref('')
|
const openingFolder = ref('')
|
||||||
|
const togglingStatus = ref('')
|
||||||
|
|
||||||
|
const toggleStatus = async (site) => {
|
||||||
|
togglingStatus.value = site.host
|
||||||
|
const newStatus = site.status === 'active' ? 'inactive' : 'active'
|
||||||
|
const config = await api.getConfig()
|
||||||
|
const idx = config.Site_www.findIndex(s => s.host === site.host)
|
||||||
|
if (idx >= 0) {
|
||||||
|
config.Site_www[idx].status = newStatus
|
||||||
|
await api.saveConfig(JSON.stringify(config))
|
||||||
|
await api.updateSiteCache()
|
||||||
|
await sitesStore.load()
|
||||||
|
}
|
||||||
|
togglingStatus.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
const openUrl = (host) => {
|
const openUrl = (host) => {
|
||||||
if (window.runtime?.BrowserOpenURL) {
|
if (window.runtime?.BrowserOpenURL) {
|
||||||
@@ -51,44 +88,48 @@ const findCertForDomain = (domain, aliases = []) => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmDelete = (site) => {
|
const confirmDelete = async (site) => {
|
||||||
modal.open({
|
const result = await showConfirm({
|
||||||
title: t('sites.deleteTitle'),
|
title: t('sites.deleteTitle'),
|
||||||
message: t('sites.deleteConfirm', { name: site.name, host: site.host }),
|
message: `${site.name} (${site.host})`,
|
||||||
warning: t('sites.deleteWarning'),
|
|
||||||
onConfirm: async () => {
|
|
||||||
const result = await sitesStore.remove(site.host)
|
|
||||||
if (result && !String(result).startsWith('Error')) success(t('notify.siteDeleted'))
|
|
||||||
else error(String(result))
|
|
||||||
modal.close()
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
if (result) {
|
||||||
|
const res = await sitesStore.remove(site.host)
|
||||||
|
if (res && !String(res).startsWith('Error')) success(t('notify.siteDeleted'))
|
||||||
|
else error(String(res))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="section-header">
|
<VSectionHeader :title="t('sites.title')" addable @add="router.push('/sites/create')" />
|
||||||
<h2 class="section-title">{{ t('sites.title') }}</h2>
|
|
||||||
<VButton variant="primary" icon="fas fa-plus" @click="router.push('/sites/create')">
|
|
||||||
{{ t('sites.add') }}
|
|
||||||
</VButton>
|
|
||||||
</div>
|
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ t('sites.name') }}</th>
|
<th><i class="fas fa-tag th-icon"></i> {{ t('sites.name') }}</th>
|
||||||
<th>{{ t('sites.host') }}</th>
|
<th><i class="fas fa-globe th-icon"></i> {{ t('sites.host') }}</th>
|
||||||
<th>{{ t('sites.alias') }}</th>
|
<th><i class="fas fa-link th-icon"></i> {{ t('sites.alias') }}</th>
|
||||||
<th>{{ t('sites.status') }}</th>
|
<th><i class="fas fa-circle-check th-icon"></i> {{ t('sites.status') }}</th>
|
||||||
<th>{{ t('sites.rootFile') }}</th>
|
<th><i class="fas fa-file th-icon"></i> {{ t('sites.rootFile') }}</th>
|
||||||
<th>{{ t('sites.actions') }}</th>
|
<th class="th-actions">{{ t('sites.actions') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="site in sitesStore.list" :key="site.host">
|
<tr
|
||||||
|
v-for="(site, i) in sitesStore.list"
|
||||||
|
:key="site.host"
|
||||||
|
draggable="true"
|
||||||
|
:class="{ 'drag-over': overIdx === i, 'dragging': dragIdx === i }"
|
||||||
|
@dragstart="onDragStart(i, $event)"
|
||||||
|
@dragover="onDragOver(i, $event)"
|
||||||
|
@dragleave="onDragLeave"
|
||||||
|
@drop="onDrop(i)"
|
||||||
|
@dragend="onDragEnd"
|
||||||
|
>
|
||||||
<td>
|
<td>
|
||||||
|
<i class="fas fa-grip-vertical drag-grip"></i>
|
||||||
<span class="cert-icon" :class="findCertForDomain(site.host, site.alias) ? (findCertForDomain(site.host, site.alias).is_expired ? 'cert-expired' : 'cert-valid') : 'cert-none'" :title="findCertForDomain(site.host, site.alias) ? (findCertForDomain(site.host, site.alias).is_expired ? 'SSL истёк' : `SSL (${findCertForDomain(site.host, site.alias).days_left} дн.)`) : 'Нет сертификата'">
|
<span class="cert-icon" :class="findCertForDomain(site.host, site.alias) ? (findCertForDomain(site.host, site.alias).is_expired ? 'cert-expired' : 'cert-valid') : 'cert-none'" :title="findCertForDomain(site.host, site.alias) ? (findCertForDomain(site.host, site.alias).is_expired ? 'SSL истёк' : `SSL (${findCertForDomain(site.host, site.alias).days_left} дн.)`) : 'Нет сертификата'">
|
||||||
<i class="fas fa-shield-alt"></i>
|
<i class="fas fa-shield-alt"></i>
|
||||||
</span>
|
</span>
|
||||||
@@ -99,8 +140,12 @@ const confirmDelete = (site) => {
|
|||||||
</td>
|
</td>
|
||||||
<td><code>{{ site.alias?.join(', ') || '—' }}</code></td>
|
<td><code>{{ site.alias?.join(', ') || '—' }}</code></td>
|
||||||
<td>
|
<td>
|
||||||
<VBadge :variant="site.status === 'active' ? 'online' : 'offline'">
|
<VBadge
|
||||||
{{ site.status }}
|
class="status-toggle"
|
||||||
|
:variant="togglingStatus === site.host ? 'pending' : (site.status === 'active' ? 'online' : 'offline')"
|
||||||
|
@click="toggleStatus(site)"
|
||||||
|
>
|
||||||
|
{{ togglingStatus === site.host ? '...' : site.status }}
|
||||||
</VBadge>
|
</VBadge>
|
||||||
</td>
|
</td>
|
||||||
<td><code>{{ site.root_file }}</code></td>
|
<td><code>{{ site.root_file }}</code></td>
|
||||||
|
|||||||
142
front_vue/src/Design/components/ui/VConfirmDialog.vue
Normal file
142
front_vue/src/Design/components/ui/VConfirmDialog.vue
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
const { isOpen, config, close } = useConfirm()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<transition name="fade">
|
||||||
|
<div v-if="isOpen" class="confirm-overlay" @click.self="close()">
|
||||||
|
<transition name="scale">
|
||||||
|
<div v-if="isOpen" class="confirm-dialog">
|
||||||
|
<div class="confirm-icon" :style="{ color: config.iconColor }">
|
||||||
|
<i :class="config.icon"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="confirm-title">{{ config.title }}</h3>
|
||||||
|
<p v-if="config.message" class="confirm-message">{{ config.message }}</p>
|
||||||
|
<div class="confirm-buttons">
|
||||||
|
<button
|
||||||
|
v-for="(btn, i) in config.buttons"
|
||||||
|
:key="i"
|
||||||
|
class="confirm-btn"
|
||||||
|
:class="`confirm-btn--${btn.variant || 'default'}`"
|
||||||
|
@click="btn.action?.()"
|
||||||
|
>
|
||||||
|
<i v-if="btn.icon" :class="btn.icon"></i>
|
||||||
|
{{ btn.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.confirm-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: var(--z-modal);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-dialog {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 40px;
|
||||||
|
max-width: 420px;
|
||||||
|
width: 90%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-icon {
|
||||||
|
font-size: 42px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
filter: drop-shadow(0 0 16px currentColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-title {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-message {
|
||||||
|
font-size: var(--text-md);
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin: 0 0 28px 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: var(--text-md);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn--default {
|
||||||
|
background: rgba(var(--muted-rgb), 0.15);
|
||||||
|
border-color: rgba(var(--muted-rgb), 0.3);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn--default:hover {
|
||||||
|
background: rgba(var(--muted-rgb), 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn--primary {
|
||||||
|
background: rgba(var(--accent-rgb), 0.2);
|
||||||
|
border-color: rgba(var(--accent-rgb), 0.4);
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn--primary:hover {
|
||||||
|
background: rgba(var(--accent-rgb), 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn--danger {
|
||||||
|
background: rgba(var(--danger-rgb), 0.2);
|
||||||
|
border-color: rgba(var(--danger-rgb), 0.4);
|
||||||
|
color: var(--accent-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn--danger:hover {
|
||||||
|
background: rgba(var(--danger-rgb), 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn--success {
|
||||||
|
background: rgba(var(--success-rgb), 0.2);
|
||||||
|
border-color: rgba(var(--success-rgb), 0.4);
|
||||||
|
color: var(--accent-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn--success:hover {
|
||||||
|
background: rgba(var(--success-rgb), 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scale-enter-active { transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); }
|
||||||
|
.scale-leave-active { transition: all 0.15s ease-in; }
|
||||||
|
.scale-enter-from { opacity: 0; transform: scale(0.9); }
|
||||||
|
.scale-leave-to { opacity: 0; transform: scale(0.95); }
|
||||||
|
</style>
|
||||||
72
front_vue/src/Design/components/ui/VSectionHeader.vue
Normal file
72
front_vue/src/Design/components/ui/VSectionHeader.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
title: { type: String, required: true },
|
||||||
|
addable: { type: Boolean, default: false },
|
||||||
|
})
|
||||||
|
|
||||||
|
defineEmits(['add'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="v-section-header">
|
||||||
|
<span class="v-section-bar"></span>
|
||||||
|
<h2 class="v-section-title">{{ title }}</h2>
|
||||||
|
<button v-if="addable" class="v-section-add" @click="$emit('add')">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||||
|
<line x1="5" y1="0" x2="5" y2="10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
<line x1="0" y1="5" x2="10" y2="5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-section-bar {
|
||||||
|
width: 3px;
|
||||||
|
height: 14px;
|
||||||
|
background: var(--accent-purple);
|
||||||
|
border-radius: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-section-title {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin: 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-section-add {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: rgba(var(--accent-rgb), 0.1);
|
||||||
|
border: 1px solid rgba(var(--accent-rgb), 0.2);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-section-add:hover {
|
||||||
|
background: rgba(var(--accent-rgb), 0.2);
|
||||||
|
border-color: rgba(var(--accent-rgb), 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-section-add svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -47,6 +47,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<VNotification />
|
<VNotification />
|
||||||
<VModal />
|
<VModal />
|
||||||
|
<VConfirmDialog />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -56,7 +57,15 @@ onUnmounted(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
background: var(--bg-primary);
|
|
||||||
|
/* Красивый грандиент */
|
||||||
|
background:
|
||||||
|
/*
|
||||||
|
radial-gradient(circle at 100% 30%, rgba(53, 55, 126, 0.089) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 45% 100%, rgba(126, 53, 98, 0.082) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 0% 100%, rgba(53, 55, 126, 0.089) 0%, transparent 50%),
|
||||||
|
*/
|
||||||
|
var(--bg-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-body {
|
.app-body {
|
||||||
@@ -73,8 +82,9 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
|
--page-padding: 50px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 40px var(--space-3xl);
|
padding: var(--page-padding) var(--page-padding) var(--page-padding) var(--page-padding);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="dashboard-view">
|
<div class="dashboard-view">
|
||||||
<ServicesGrid />
|
<ServicesGrid />
|
||||||
|
<div class="dashboard-tables">
|
||||||
<SitesTable />
|
<SitesTable />
|
||||||
<ProxiesTable />
|
<ProxiesTable />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -30,4 +32,22 @@ onMounted(async () => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-xl);
|
gap: var(--space-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-tables {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 2200px) {
|
||||||
|
.dashboard-tables {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-tables > * {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const proxiesStore = useProxiesStore()
|
const proxiesStore = useProxiesStore()
|
||||||
const { success, error } = useNotification()
|
const { success, error } = useNotification()
|
||||||
const modal = useModal()
|
const { confirmDelete: showConfirm } = useConfirm()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
domain: { type: String, required: true },
|
domain: { type: String, required: true },
|
||||||
@@ -20,7 +20,6 @@ const form = reactive({
|
|||||||
autoSSL: false,
|
autoSSL: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
import { api } from '@core/api/index.js'
|
|
||||||
|
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
|
|
||||||
@@ -66,21 +65,20 @@ const saveProxy = async () => {
|
|||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmDelete = () => {
|
const confirmDelete = async () => {
|
||||||
modal.open({
|
const result = await showConfirm({
|
||||||
title: t('proxies.deleteTitle'),
|
title: t('proxies.deleteTitle'),
|
||||||
message: t('proxies.deleteConfirm', { domain: form.domain }),
|
message: form.domain,
|
||||||
onConfirm: async () => {
|
})
|
||||||
const result = await proxiesStore.remove(form.domain)
|
if (result) {
|
||||||
if (result && !String(result).startsWith('Error')) {
|
const res = await proxiesStore.remove(form.domain)
|
||||||
|
if (res && !String(res).startsWith('Error')) {
|
||||||
success(t('notify.proxyDeleted'))
|
success(t('notify.proxyDeleted'))
|
||||||
router.push('/')
|
router.push('/')
|
||||||
} else {
|
} else {
|
||||||
error(String(result))
|
error(String(res))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
modal.close()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const toggleAcme = async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="settings-view">
|
<div class="settings-view">
|
||||||
<div class="settings-header">
|
<div class="settings-header">
|
||||||
<h2 class="section-title">{{ t('settings.title') }}</h2>
|
<VSectionHeader :title="t('settings.title')" />
|
||||||
<VButton variant="success" icon="fas fa-save" :loading="saving" @click="saveSettings">
|
<VButton variant="success" icon="fas fa-save" :loading="saving" @click="saveSettings">
|
||||||
{{ saving ? t('settings.saving') : t('settings.save') }}
|
{{ saving ? t('settings.saving') : t('settings.save') }}
|
||||||
</VButton>
|
</VButton>
|
||||||
@@ -127,20 +127,25 @@ const toggleAcme = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-card {
|
.settings-card {
|
||||||
background: var(--glass-bg-light);
|
background: rgba(var(--accent-rgb), 0.02);
|
||||||
backdrop-filter: var(--backdrop-blur);
|
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: var(--space-lg);
|
padding: var(--space-lg);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card:hover {
|
||||||
|
background: rgba(var(--accent-rgb), 0.04);
|
||||||
|
border-color: rgba(var(--accent-rgb), 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-card-title {
|
.settings-card-title {
|
||||||
font-size: var(--text-lg);
|
font-size: var(--text-md);
|
||||||
font-weight: var(--font-semibold);
|
font-weight: var(--font-semibold);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
margin: 0 0 20px 0;
|
margin: 0 0 20px 0;
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
border-bottom: 1px solid var(--divider-subtle);
|
border-bottom: 1px solid rgba(var(--accent-rgb), 0.1);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const sitesStore = useSitesStore()
|
const sitesStore = useSitesStore()
|
||||||
const { success, error } = useNotification()
|
const { success, error } = useNotification()
|
||||||
const modal = useModal()
|
const { confirmDelete: showConfirm } = useConfirm()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
host: { type: String, required: true },
|
host: { type: String, required: true },
|
||||||
@@ -19,7 +19,6 @@ const form = reactive({
|
|||||||
autoSSL: false,
|
autoSSL: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
import { api } from '@core/api/index.js'
|
|
||||||
|
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const aliasInput = ref('')
|
const aliasInput = ref('')
|
||||||
@@ -81,22 +80,20 @@ const saveSite = async () => {
|
|||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmDelete = () => {
|
const confirmDelete = async () => {
|
||||||
modal.open({
|
const result = await showConfirm({
|
||||||
title: t('sites.deleteTitle'),
|
title: t('sites.deleteTitle'),
|
||||||
message: t('sites.deleteConfirm', { name: form.name, host: form.host }),
|
message: `${form.name} (${form.host})`,
|
||||||
warning: t('sites.deleteWarning'),
|
})
|
||||||
onConfirm: async () => {
|
if (result) {
|
||||||
const result = await sitesStore.remove(form.host)
|
const res = await sitesStore.remove(form.host)
|
||||||
if (result && !String(result).startsWith('Error')) {
|
if (res && !String(res).startsWith('Error')) {
|
||||||
success(t('notify.siteDeleted'))
|
success(t('notify.siteDeleted'))
|
||||||
router.push('/')
|
router.push('/')
|
||||||
} else {
|
} else {
|
||||||
error(String(result))
|
error(String(res))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
modal.close()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { api } from '@core/api/index.js'
|
|
||||||
import { useDraggable } from '@core/composables/useDraggable.js'
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import vue from '@vitejs/plugin-vue'
|
|||||||
import AutoImport from 'unplugin-auto-import/vite'
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
import Components from 'unplugin-vue-components/vite'
|
import Components from 'unplugin-vue-components/vite'
|
||||||
import { resolve } from 'path'
|
import { resolve } from 'path'
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
|
||||||
|
const wailsConfig = JSON.parse(readFileSync(resolve(__dirname, '../wails.json'), 'utf-8'))
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -14,6 +17,13 @@ export default defineConfig({
|
|||||||
'vue-router',
|
'vue-router',
|
||||||
'vue-i18n',
|
'vue-i18n',
|
||||||
'pinia',
|
'pinia',
|
||||||
|
{
|
||||||
|
'@core/api/index.js': ['api'],
|
||||||
|
'@core/constants.js': [
|
||||||
|
'SERVICE_NAMES', 'SITE_STATUS', 'PROXY_STATUS', 'VACCESS_TYPE',
|
||||||
|
'CERT_MODE', 'THEME', 'LOCALE', 'STORAGE_KEYS', 'AUTO_REFRESH_INTERVAL', 'APP_VERSION',
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
dirs: [
|
dirs: [
|
||||||
'src/Core/composables',
|
'src/Core/composables',
|
||||||
@@ -41,6 +51,10 @@ export default defineConfig({
|
|||||||
port: 4444,
|
port: 4444,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
define: {
|
||||||
|
__APP_VERSION__: JSON.stringify(wailsConfig.info.productVersion),
|
||||||
|
},
|
||||||
|
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': resolve(__dirname, 'src'),
|
'@': resolve(__dirname, 'src'),
|
||||||
|
|||||||
959
test/index.html
Normal file
959
test/index.html
Normal file
@@ -0,0 +1,959 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="vServer - Мощный функциональный веб-сервер на Go с поддержкой HTTP/HTTPS, MySQL, PHP и веб-админкой">
|
||||||
|
<title>vServer - Функциональный веб-сервер на Go</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<!-- Yandex.Metrika counter -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
(function(m,e,t,r,i,k,a){
|
||||||
|
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
||||||
|
m[i].l=1*new Date();
|
||||||
|
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
|
||||||
|
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
|
||||||
|
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=104391783', 'ym');
|
||||||
|
|
||||||
|
ym(104391783, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", accurateTrackBounce:true, trackLinks:true});
|
||||||
|
</script>
|
||||||
|
<noscript><div><img src="https://mc.yandex.ru/watch/104391783" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
||||||
|
<!-- /Yandex.Metrika counter -->
|
||||||
|
<body>
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="header">
|
||||||
|
<div class="container">
|
||||||
|
<div class="nav">
|
||||||
|
<div class="logo">
|
||||||
|
<span class="logo-icon">🚀</span>
|
||||||
|
<span class="logo-text">vServer</span>
|
||||||
|
</div>
|
||||||
|
<nav class="menu">
|
||||||
|
<a href="#features">Возможности</a>
|
||||||
|
<a href="#vaccess">vAccess</a>
|
||||||
|
<!-- <a href="#benefits">Преимущества</a> -->
|
||||||
|
<a href="#install">Установка</a>
|
||||||
|
<a href="#ssl">SSL</a>
|
||||||
|
</nav>
|
||||||
|
<a href="https://github.com/Falknat/vServer" class="btn-github" target="_blank">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
||||||
|
</svg>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<section class="hero">
|
||||||
|
<div class="hero-bg"></div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="hero-content">
|
||||||
|
<div class="badge">Open Source</div>
|
||||||
|
<h1 class="hero-title">
|
||||||
|
vSerf веб-сервер <br>
|
||||||
|
<span class="gradient-text">нового поколения</span>
|
||||||
|
</h1>
|
||||||
|
<p class="hero-description">
|
||||||
|
vServer - функциональный веб-сервер на Go с поддержкой HTTP/HTTPS, MySQL, PHP и встроенной веб-админкой (web-admin в разработке).
|
||||||
|
</p>
|
||||||
|
<div class="hero-buttons">
|
||||||
|
<a href="https://github.com/Falknat/vServer" class="btn btn-primary" target="_blank">
|
||||||
|
<span>Скачать vServer</span>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor">
|
||||||
|
<path d="M10 3v10m0 0l4-4m-4 4l-4-4m11 7v3a2 2 0 01-2 2H5a2 2 0 01-2-2v-3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="#docs" class="btn btn-secondary">
|
||||||
|
Документация
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="hero-stats">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value">v1.0.0</div>
|
||||||
|
<div class="stat-label">Версия</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value">Go</div>
|
||||||
|
<div class="stat-label">Язык</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value">MIT</div>
|
||||||
|
<div class="stat-label">Лицензия</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hero-visual">
|
||||||
|
<div class="code-window terminal">
|
||||||
|
<div class="code-header">
|
||||||
|
<div class="code-dots">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<div class="code-title">🚀 vServer Console</div>
|
||||||
|
</div>
|
||||||
|
<div class="code-content terminal-content">
|
||||||
|
<pre><code><span class="terminal-title">vServer 1.0.0</span>
|
||||||
|
|
||||||
|
<span class="terminal-rocket">🚀</span> <span class="terminal-text">Запуск vServer...</span>
|
||||||
|
<span class="terminal-folder">📁</span> <span class="terminal-text">Файлы сайта будут обслуживаться из папки 'www'</span>
|
||||||
|
|
||||||
|
<span class="terminal-gear">⚙️</span> <span class="terminal-text">Запуск сервисов...</span>
|
||||||
|
|
||||||
|
<span class="terminal-tag">[JSON]</span> <span class="terminal-success">config.json успешно загружен</span>
|
||||||
|
<span class="terminal-tag">[JSON]</span> <span class="terminal-success">config.json успешно прочитан</span>
|
||||||
|
|
||||||
|
<span class="terminal-tag">[HTTPS]</span> <span class="terminal-success">✅ Загрузили сертификат для: example.ru</span>
|
||||||
|
<span class="terminal-tag">[HTTPS]</span> <span class="terminal-success">✅ HTTPS сервер запущен на порту 443</span>
|
||||||
|
<span class="terminal-tag">[HTTP ]</span> <span class="terminal-success">🌐 HTTP сервер запущен на порту 80</span>
|
||||||
|
<span class="terminal-tag">[PHP ]</span> <span class="terminal-success">🌐 PHP FastCGI пул запущен (на портах 8000-8003)</span>
|
||||||
|
<span class="terminal-tag">[MySQL]</span> <span class="terminal-success">Сервер MySQL запущен на 192.168.1.9:3306</span>
|
||||||
|
|
||||||
|
<span class="terminal-success">Введите help для получения списка команд</span>
|
||||||
|
|
||||||
|
<span class="terminal-prompt">></span> <span class="terminal-cursor">_</span></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Features Section -->
|
||||||
|
<section id="features" class="features">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">Всё что нужно <span class="gradient-text">в одном месте</span></h2>
|
||||||
|
<p class="section-description">vServer объединяет мощные инструменты для веб-разработки</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="features-grid">
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">🌐</div>
|
||||||
|
<h3>HTTP/HTTPS сервер</h3>
|
||||||
|
<p>Полная поддержка SSL сертификатов с автоматической загрузкой. Wildcard сертификаты для поддоменов.</p>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>SSL/TLS шифрование</li>
|
||||||
|
<li>Wildcard сертификаты</li>
|
||||||
|
<li>Автоматический HTTPS редирект</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">🔄</div>
|
||||||
|
<h3>Proxy сервер</h3>
|
||||||
|
<p>Мощный обратный прокси для перенаправления внешних запросов на локальные сервисы.</p>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>Множественные прокси</li>
|
||||||
|
<li>HTTP/HTTPS поддержка</li>
|
||||||
|
<li>Гибкая маршрутизация</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">🐘</div>
|
||||||
|
<h3>PHP 8 сервер</h3>
|
||||||
|
<p>Встроенная поддержка PHP 8 для запуска динамических веб-приложений без дополнительной настройки.</p>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>PHP 8 из коробки</li>
|
||||||
|
<li>Готов к работе</li>
|
||||||
|
<li>Логирование ошибок</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">🗄️</div>
|
||||||
|
<h3>MySQL база данных</h3>
|
||||||
|
<p>Полноценный MySQL сервер со всеми возможностями для хранения и управления данными.</p>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>Полная поддержка MySQL</li>
|
||||||
|
<li>Простая настройка</li>
|
||||||
|
<li>Детальные логи</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="feature-card">
|
||||||
|
<div class="feature-icon">🎛️</div>
|
||||||
|
<h3>Веб-админка (разработка)</h3>
|
||||||
|
<p>Удобная веб-панель управления на порту 5555 с мониторингом всех сервисов в реальном времени.</p>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>Мониторинг в реальном времени</li>
|
||||||
|
<li>Управление сервисами</li>
|
||||||
|
<li>Удобный интерфейс</li>
|
||||||
|
</ul>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">📊</div>
|
||||||
|
<h3>Логирование</h3>
|
||||||
|
<p>Подробное логирование всех операций с разделением по типам для удобного анализа.</p>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>HTTP/HTTPS логи</li>
|
||||||
|
<li>Логи базы данных</li>
|
||||||
|
<li>Системные логи</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card highlight">
|
||||||
|
<div class="feature-icon">🔒</div>
|
||||||
|
<h3>vAccess Control</h3>
|
||||||
|
<p>Продвинутая система контроля доступа с гибкими правилами для защиты ваших веб-приложений.</p>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>IP-фильтрация</li>
|
||||||
|
<li>Контроль по типам файлов</li>
|
||||||
|
<li>Гибкие правила доступа</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- vAccess Section -->
|
||||||
|
<section id="vaccess" class="vaccess">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">
|
||||||
|
<span class="gradient-text">vAccess</span> - Система контроля доступа
|
||||||
|
</h2>
|
||||||
|
<p class="section-description">Защитите ваш веб-сервер с помощью гибкой системы правил доступа</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vaccess-content">
|
||||||
|
<div class="vaccess-features">
|
||||||
|
<div class="vaccess-feature">
|
||||||
|
<div class="vaccess-feature-icon">🛡️</div>
|
||||||
|
<h3>Многоуровневая защита</h3>
|
||||||
|
<p>Комбинируйте правила по IP-адресам, типам файлов и путям для создания сложных политик безопасности.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vaccess-feature">
|
||||||
|
<div class="vaccess-feature-icon">⚡</div>
|
||||||
|
<h3>Быстрая проверка</h3>
|
||||||
|
<p>Правила проверяются последовательно. Первое совпадение срабатывает мгновенно, обеспечивая высокую производительность.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vaccess-feature">
|
||||||
|
<div class="vaccess-feature-icon">🎯</div>
|
||||||
|
<h3>Гибкие исключения</h3>
|
||||||
|
<p>Создавайте исключения для определённых путей, чтобы точно настроить политику доступа.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vaccess-feature">
|
||||||
|
<div class="vaccess-feature-icon">📝</div>
|
||||||
|
<h3>Простая конфигурация</h3>
|
||||||
|
<p>Текстовый формат конфигурации с подробными комментариями. Легко читать и редактировать.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vaccess-examples">
|
||||||
|
<h3 class="vaccess-examples-title">Примеры использования</h3>
|
||||||
|
|
||||||
|
<div class="vaccess-examples-grid">
|
||||||
|
<div class="vaccess-example">
|
||||||
|
<div class="example-header">
|
||||||
|
<div class="example-number">01</div>
|
||||||
|
<h4>Защита от выполнения PHP в загрузках</h4>
|
||||||
|
</div>
|
||||||
|
<div class="example-description">
|
||||||
|
Запрещаем выполнение PHP файлов в папках с пользовательским контентом
|
||||||
|
</div>
|
||||||
|
<div class="code-window">
|
||||||
|
<div class="code-header">
|
||||||
|
<div class="code-dots">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<div class="code-title">vaccess.conf</div>
|
||||||
|
</div>
|
||||||
|
<div class="code-content">
|
||||||
|
<pre><code><span class="comment"># Блокируем PHP в папках uploads и templates</span>
|
||||||
|
<span class="key">type:</span> <span class="error">Disable</span>
|
||||||
|
<span class="key">type_file:</span> <span class="string">*.php, *.phtml</span>
|
||||||
|
<span class="key">path_access:</span> <span class="string">/uploads/*, /templates/*</span>
|
||||||
|
<span class="key">url_error:</span> <span class="number">404</span></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vaccess-example">
|
||||||
|
<div class="example-header">
|
||||||
|
<div class="example-number">02</div>
|
||||||
|
<h4>Доступ к админке только с разрешённых IP</h4>
|
||||||
|
</div>
|
||||||
|
<div class="example-description">
|
||||||
|
Ограничиваем доступ к административной панели списком доверенных IP
|
||||||
|
</div>
|
||||||
|
<div class="code-window">
|
||||||
|
<div class="code-header">
|
||||||
|
<div class="code-dots">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<div class="code-title">vaccess.conf</div>
|
||||||
|
</div>
|
||||||
|
<div class="code-content">
|
||||||
|
<pre><code><span class="comment"># Разрешаем админку только с офисных IP</span>
|
||||||
|
<span class="key">type:</span> <span class="success">Allow</span>
|
||||||
|
<span class="key">path_access:</span> <span class="string">/admin/*, /dashboard/*</span>
|
||||||
|
<span class="key">ip_list:</span> <span class="string">192.168.1.100, 10.0.0.5, 127.0.0.1</span>
|
||||||
|
<span class="key">url_error:</span> <span class="number">404</span></code></pre>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vaccess-example">
|
||||||
|
<div class="example-header">
|
||||||
|
<div class="example-number">03</div>
|
||||||
|
<h4>Блокировка вредоносных IP</h4>
|
||||||
|
</div>
|
||||||
|
<div class="example-description">
|
||||||
|
Запрещаем доступ для подозрительных или вредоносных IP-адресов
|
||||||
|
</div>
|
||||||
|
<div class="code-window">
|
||||||
|
<div class="code-header">
|
||||||
|
<div class="code-dots">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<div class="code-title">vaccess.conf</div>
|
||||||
|
</div>
|
||||||
|
<div class="code-content">
|
||||||
|
<pre><code><span class="comment"># Полная блокировка для списка IP</span>
|
||||||
|
<span class="key">type:</span> <span class="error">Disable</span>
|
||||||
|
<span class="key">ip_list:</span> <span class="string">192.168.1.50, 10.0.0.99, 203.0.113.0</span>
|
||||||
|
<span class="key">url_error:</span> <span class="string">https://example.com/blocked</span></code></pre>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vaccess-example">
|
||||||
|
<div class="example-header">
|
||||||
|
<div class="example-number">04</div>
|
||||||
|
<h4>Правила с исключениями</h4>
|
||||||
|
</div>
|
||||||
|
<div class="example-description">
|
||||||
|
Ограничиваем доступ по IP, но делаем исключения для публичных API и ботов
|
||||||
|
</div>
|
||||||
|
<div class="code-window">
|
||||||
|
<div class="code-header">
|
||||||
|
<div class="code-dots">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<div class="code-title">vaccess.conf</div>
|
||||||
|
</div>
|
||||||
|
<div class="code-content">
|
||||||
|
<pre><code><span class="comment"># Доступ только с локальных IP, кроме /bot/ и /api/</span>
|
||||||
|
<span class="key">type:</span> <span class="success">Allow</span>
|
||||||
|
<span class="key">ip_list:</span> <span class="string">127.0.0.1, 192.168.0.1</span>
|
||||||
|
<span class="key">exceptions_dir:</span> <span class="string">/bot/*, /api/public/*</span>
|
||||||
|
<span class="key">url_error:</span> <span class="string">https://voxsel.ru</span></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vaccess-params">
|
||||||
|
<h3 class="vaccess-params-title">Параметры правил</h3>
|
||||||
|
<div class="params-grid">
|
||||||
|
<div class="param-card">
|
||||||
|
<div class="param-name">type</div>
|
||||||
|
<div class="param-required">ОБЯЗАТЕЛЬНЫЙ</div>
|
||||||
|
<div class="param-description">
|
||||||
|
<code>Allow</code> - разрешить доступ<br>
|
||||||
|
<code>Disable</code> - запретить доступ
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-card">
|
||||||
|
<div class="param-name">type_file</div>
|
||||||
|
<div class="param-optional">Опционально</div>
|
||||||
|
<div class="param-description">
|
||||||
|
Расширения файлов через запятую<br>
|
||||||
|
Пример: <code>*.php, *.exe, *.sh</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-card">
|
||||||
|
<div class="param-name">path_access</div>
|
||||||
|
<div class="param-optional">Опционально</div>
|
||||||
|
<div class="param-description">
|
||||||
|
Пути через запятую<br>
|
||||||
|
Пример: <code>/admin/*, /api/*, /private/*</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-card">
|
||||||
|
<div class="param-name">ip_list</div>
|
||||||
|
<div class="param-optional">Опционально</div>
|
||||||
|
<div class="param-description">
|
||||||
|
IP адреса через запятую<br>
|
||||||
|
Пример: <code>192.168.1.1, 10.0.0.5</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-card">
|
||||||
|
<div class="param-name">exceptions_dir</div>
|
||||||
|
<div class="param-optional">Опционально</div>
|
||||||
|
<div class="param-description">
|
||||||
|
Пути-исключения через запятую<br>
|
||||||
|
Правило НЕ применяется к этим путям
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-card">
|
||||||
|
<div class="param-name">url_error</div>
|
||||||
|
<div class="param-optional">Опционально</div>
|
||||||
|
<div class="param-description">
|
||||||
|
Куда перенаправить при блокировке<br>
|
||||||
|
<code>404</code>, URL или путь к файлу
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vaccess-how-it-works">
|
||||||
|
<h3 class="vaccess-how-title">Как это работает?</h3>
|
||||||
|
<div class="how-it-works-steps">
|
||||||
|
<div class="work-step">
|
||||||
|
<div class="work-step-num">1</div>
|
||||||
|
<div class="work-step-content">
|
||||||
|
<h4>Запрос поступает</h4>
|
||||||
|
<p>Клиент отправляет запрос на сервер. vServer получает реальный IP и путь запроса.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="work-arrow">→</div>
|
||||||
|
<div class="work-step">
|
||||||
|
<div class="work-step-num">2</div>
|
||||||
|
<div class="work-step-content">
|
||||||
|
<h4>Проверка правил</h4>
|
||||||
|
<p>Правила проверяются сверху вниз. Первое подходящее правило срабатывает.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="work-arrow">→</div>
|
||||||
|
<div class="work-step">
|
||||||
|
<div class="work-step-num">3</div>
|
||||||
|
<div class="work-step-content">
|
||||||
|
<h4>Действие</h4>
|
||||||
|
<p>Если правило разрешает - запрос обрабатывается. Если запрещает - возвращается ошибка.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vaccess-note">
|
||||||
|
<div class="note-icon">💡</div>
|
||||||
|
<div class="note-content">
|
||||||
|
<strong>Важно:</strong> Порядок правил имеет значение! Специфичные правила размещайте выше общих.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Benefits Section -->
|
||||||
|
<!-- <section id="benefits" class="benefits">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">Почему <span class="gradient-text">vServer</span>?</h2>
|
||||||
|
<p class="section-description">Преимущества, которые делают vServer идеальным выбором</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="benefits-grid">
|
||||||
|
<div class="benefit-card">
|
||||||
|
<div class="benefit-number">01</div>
|
||||||
|
<h3>Всё в одном</h3>
|
||||||
|
<p>Не нужно устанавливать и настраивать отдельно веб-сервер, MySQL, PHP. Всё уже включено и готово к работе.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="benefit-card">
|
||||||
|
<div class="benefit-number">02</div>
|
||||||
|
<h3>Простота настройки</h3>
|
||||||
|
<p>Вся конфигурация в одном JSON файле. Изменения применяются без перезапуска через команду config_reload.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="benefit-card">
|
||||||
|
<div class="benefit-number">03</div>
|
||||||
|
<h3>Производительность Go</h3>
|
||||||
|
<p>Написан на Go для максимальной производительности и эффективного использования ресурсов системы.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="benefit-card">
|
||||||
|
<div class="benefit-number">04</div>
|
||||||
|
<h3>Open Source</h3>
|
||||||
|
<p>Полностью открытый исходный код. Вы можете изучить, модифицировать и улучшить vServer под свои нужды.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="benefit-card">
|
||||||
|
<div class="benefit-number">05</div>
|
||||||
|
<h3>Портативность</h3>
|
||||||
|
<p>Всего два файла для запуска: исполняемый файл и папка WebServer. Легко переносить между системами.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="benefit-card">
|
||||||
|
<div class="benefit-number">06</div>
|
||||||
|
<h3>Консольное управление</h3>
|
||||||
|
<p>Мощный консольный интерфейс для управления сервером и всеми его компонентами из командной строки.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section> -->
|
||||||
|
|
||||||
|
<!-- Installation Section -->
|
||||||
|
<section id="install" class="installation">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">Быстрый <span class="gradient-text">старт</span></h2>
|
||||||
|
<p class="section-description">Начните работу с vServer за несколько минут</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="install-steps">
|
||||||
|
<div class="install-step">
|
||||||
|
<div class="step-number">1</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<h3>Скачайте vServer</h3>
|
||||||
|
<p>Загрузите последний релиз vServer с готовыми компонентами</p>
|
||||||
|
<a href="https://github.com/Falknat/vServer" class="download-btn" target="_blank">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor">
|
||||||
|
<path d="M10 3v10m0 0l4-4m-4 4l-4-4m11 7v3a2 2 0 01-2 2H5a2 2 0 01-2-2v-3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
Скачать последний релиз
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="install-step">
|
||||||
|
<div class="step-number">2</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<h3>Распакуйте архив</h3>
|
||||||
|
<p>Извлеките содержимое в удобную папку на вашем компьютере</p>
|
||||||
|
<div class="step-info">
|
||||||
|
Архив содержит готовый исполняемый файл и все необходимые компоненты
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="install-step">
|
||||||
|
<div class="step-number">3</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<h3>Создайте ваш сайт</h3>
|
||||||
|
<p>Структура для размещения веб-сайта</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<code>WebServer/www/example.com/public_www/index.html</code>
|
||||||
|
</div>
|
||||||
|
<div class="step-substeps">
|
||||||
|
<div class="substep">
|
||||||
|
<span class="substep-num">→</span>
|
||||||
|
<div class="substep-content">
|
||||||
|
<strong>www/</strong> - корневая папка для сайтов
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="substep">
|
||||||
|
<span class="substep-num">→</span>
|
||||||
|
<div class="substep-content">
|
||||||
|
<strong>example.com/</strong> - папка с именем вашего домена
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="substep">
|
||||||
|
<span class="substep-num">→</span>
|
||||||
|
<div class="substep-content">
|
||||||
|
<strong>public_www/</strong> - публичная папка с файлами сайта
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="substep">
|
||||||
|
<span class="substep-num">→</span>
|
||||||
|
<div class="substep-content">
|
||||||
|
<strong>public_www/index.html</strong> - файлы вашего сайта
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="substep">
|
||||||
|
<span class="substep-num">→</span>
|
||||||
|
<div class="substep-content">
|
||||||
|
<strong>vAccess.conf</strong> - Система контроля доступа
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="install-step">
|
||||||
|
<div class="step-number">4</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<h3>Настройте конфигурацию</h3>
|
||||||
|
<p>Добавьте ваш сайт в конфигурационный файл</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<code>WebServer/config.json</code>
|
||||||
|
</div>
|
||||||
|
<div class="code-example">
|
||||||
|
<pre><code>{
|
||||||
|
<span class="key">"Site_www"</span>: [{
|
||||||
|
<span class="key">"alias"</span>: [<span class="string">"www.example.com"</span>],
|
||||||
|
<span class="key">"host"</span>: <span class="string">"example.com"</span>,
|
||||||
|
<span class="key">"name"</span>: <span class="string">"Мой сайт"</span>,
|
||||||
|
<span class="key">"root_file"</span>: <span class="string">"index.html"</span>,
|
||||||
|
<span class="key">"root_file_routing"</span>: <span class="boolean">true</span>,
|
||||||
|
<span class="key">"status"</span>: <span class="string">"active"</span>
|
||||||
|
}]
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="install-step">
|
||||||
|
<div class="step-number">5</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<h3>Запустите сервер</h3>
|
||||||
|
<p>Готово! Запустите исполняемый файл и откройте ваш сайт</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<code>vServer.exe</code>
|
||||||
|
<button class="copy-btn" onclick="copyCode(this)">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor">
|
||||||
|
<rect x="5" y="5" width="9" height="9" rx="1" stroke-width="1.5"/>
|
||||||
|
<path d="M2 11V3a1 1 0 011-1h8" stroke-width="1.5"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="install-notes">
|
||||||
|
<div class="install-note">
|
||||||
|
<div class="note-icon">💡</div>
|
||||||
|
<div class="note-content">
|
||||||
|
<strong>Совет:</strong> Для локальной разработки используйте <code>127.0.0.1</code> или <code>localhost</code> в качестве домена.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="install-note note-warning">
|
||||||
|
<div class="note-icon">⚙️</div>
|
||||||
|
<div class="note-content">
|
||||||
|
<strong>Параметры по умолчанию:</strong> MySQL пароль - <code>root</code>, веб-админка - <code>localhost:5555</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- SSL Certificates Section -->
|
||||||
|
<section id="ssl" class="ssl-section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">🔐 SSL <span class="gradient-text">Сертификаты</span></h2>
|
||||||
|
<p class="section-description">Безопасное HTTPS соединение для ваших сайтов</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ssl-content">
|
||||||
|
<div class="ssl-steps">
|
||||||
|
<h3>Установка сертификата</h3>
|
||||||
|
|
||||||
|
<div class="ssl-steps-grid">
|
||||||
|
<div class="ssl-step">
|
||||||
|
<div class="ssl-step-num">1</div>
|
||||||
|
<div class="ssl-step-content">
|
||||||
|
Откройте каталог <code>WebServer/</code> и создайте папку <code>cert/</code> (если её нет)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ssl-step">
|
||||||
|
<div class="ssl-step-num">2</div>
|
||||||
|
<div class="ssl-step-content">
|
||||||
|
Создайте папку с именем вашего домена или IP-адреса
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ssl-step">
|
||||||
|
<div class="ssl-step-num">3</div>
|
||||||
|
<div class="ssl-step-content">
|
||||||
|
Поместите в неё файлы сертификатов с <strong>точными</strong> именами:
|
||||||
|
<div class="ssl-files">
|
||||||
|
<div class="ssl-file">📄 certificate.crt</div>
|
||||||
|
<div class="ssl-file">🔑 private.key</div>
|
||||||
|
<div class="ssl-file">📦 ca_bundle.crt</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ssl-step">
|
||||||
|
<div class="ssl-step-num">4</div>
|
||||||
|
<div class="ssl-step-content">
|
||||||
|
Сертификат будет автоматически загружен при запуске сервера ✅
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ssl-structure">
|
||||||
|
<h3>📁 Структура сертификатов</h3>
|
||||||
|
<div class="code-window">
|
||||||
|
<div class="code-header">
|
||||||
|
<div class="code-dots">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<div class="code-title">Структура папок</div>
|
||||||
|
</div>
|
||||||
|
<div class="code-content">
|
||||||
|
<pre><code>WebServer/
|
||||||
|
└── cert/
|
||||||
|
├── example.com/ <span class="comment"># Основной домен</span>
|
||||||
|
│ ├── certificate.crt
|
||||||
|
│ ├── private.key
|
||||||
|
│ └── ca_bundle.crt
|
||||||
|
│
|
||||||
|
└── sub.example.com/ <span class="comment"># Поддомен (опционально)</span>
|
||||||
|
├── certificate.crt
|
||||||
|
├── private.key
|
||||||
|
└── ca_bundle.crt</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ssl-wildcard">
|
||||||
|
<h3>🎯 Работа с поддоменами</h3>
|
||||||
|
<div class="wildcard-info">
|
||||||
|
<div class="wildcard-icon">💡</div>
|
||||||
|
<div class="wildcard-text">
|
||||||
|
<strong>Важно:</strong> Если для поддомена не создана отдельная папка в <code>cert/</code>,
|
||||||
|
то автоматически будет использоваться сертификат родительского домена.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wildcard-examples">
|
||||||
|
<div class="wildcard-example">
|
||||||
|
<span class="wildcard-icon-check">✅</span>
|
||||||
|
<div>
|
||||||
|
<strong>example.com</strong>
|
||||||
|
<p>→ использует сертификат из <code>cert/example.com/</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wildcard-example">
|
||||||
|
<span class="wildcard-icon-check">✅</span>
|
||||||
|
<div>
|
||||||
|
<strong>sub.example.com</strong> (папка существует)
|
||||||
|
<p>→ использует <code>cert/sub.example.com/</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wildcard-example">
|
||||||
|
<span class="wildcard-icon-check">✅</span>
|
||||||
|
<div>
|
||||||
|
<strong>sub.example.com</strong> (папка НЕ существует)
|
||||||
|
<p>→ использует <code>cert/example.com/</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wildcard-note">
|
||||||
|
<div class="note-icon">🌟</div>
|
||||||
|
<div class="note-content">
|
||||||
|
<strong>Wildcard-сертификаты:</strong> Достаточно одного сертификата в папке основного домена для всех поддоменов!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Documentation Section -->
|
||||||
|
<section id="docs" class="documentation">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">Архитектура <span class="gradient-text">проекта</span></h2>
|
||||||
|
<p class="section-description">Простая и понятная структура vServer</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="architecture">
|
||||||
|
<div class="arch-visual">
|
||||||
|
<pre class="arch-tree">vServer/
|
||||||
|
├── 🎯 main.go <span class="comment"># Точка входа</span>
|
||||||
|
│
|
||||||
|
├── 🔧 Backend/ <span class="comment"># Основная логика</span>
|
||||||
|
│ ├── admin/ <span class="comment"># Веб-админка</span>
|
||||||
|
│ ├── config/ <span class="comment"># Конфиг Go</span>
|
||||||
|
│ ├── tools/ <span class="comment"># Утилиты</span>
|
||||||
|
│ └── WebServer/ <span class="comment"># Модули</span>
|
||||||
|
│
|
||||||
|
├── 🌐 WebServer/ <span class="comment"># Веб-контент</span>
|
||||||
|
│ ├── cert/ <span class="comment"># SSL сертификаты</span>
|
||||||
|
│ ├── soft/ <span class="comment"># MySQL и PHP</span>
|
||||||
|
│ ├── tools/ <span class="comment"># Логи</span>
|
||||||
|
│ └── www/ <span class="comment"># Сайты</span>
|
||||||
|
│
|
||||||
|
└── 📄 go.mod <span class="comment"># Go модули</span></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="arch-details">
|
||||||
|
<div class="arch-card">
|
||||||
|
<h4>🔧 Backend</h4>
|
||||||
|
<p>Основная логика сервера, написанная на Go. Включает модули для веб-сервера, прокси, админки и конфигурации.</p>
|
||||||
|
</div>
|
||||||
|
<div class="arch-card">
|
||||||
|
<h4>🌐 WebServer</h4>
|
||||||
|
<p>Содержит все ресурсы для работы: сайты, сертификаты, MySQL, PHP и логи. Единственная папка нужная для деплоя.</p>
|
||||||
|
</div>
|
||||||
|
<div class="arch-card">
|
||||||
|
<h4>📊 Логирование</h4>
|
||||||
|
<p>Все логи сохраняются в WebServer/tools/logs/ с разделением по типам: HTTP, HTTPS, MySQL, PHP, Config.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- CTA Section -->
|
||||||
|
<section class="cta">
|
||||||
|
<div class="container">
|
||||||
|
<div class="cta-content">
|
||||||
|
<h2>Готовы начать?</h2>
|
||||||
|
<p>Присоединяйтесь к open source проекту и создавайте веб-приложения быстрее</p>
|
||||||
|
<div class="cta-buttons">
|
||||||
|
<a href="https://github.com/Falknat/vServer" class="btn btn-primary" target="_blank">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
||||||
|
</svg>
|
||||||
|
Посмотреть на GitHub
|
||||||
|
</a>
|
||||||
|
<a href="https://voxsel.ru" class="btn btn-secondary" target="_blank">Сайт автора</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<div class="footer-content">
|
||||||
|
<div class="footer-about">
|
||||||
|
<div class="logo">
|
||||||
|
<span class="logo-icon">🚀</span>
|
||||||
|
<span class="logo-text">vServer</span>
|
||||||
|
</div>
|
||||||
|
<p>Функциональный веб-сервер на Go с поддержкой HTTP/HTTPS, MySQL, PHP и веб-админкой</p>
|
||||||
|
<div class="social-links">
|
||||||
|
<a href="https://github.com/Falknat/vServer" target="_blank" title="GitHub">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="https://vk.com/felias" target="_blank" title="VK">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M15.07 2H8.93C3.33 2 2 3.33 2 8.93v6.14C2 20.67 3.33 22 8.93 22h6.14c5.6 0 6.93-1.33 6.93-6.93V8.93C22 3.33 20.67 2 15.07 2zm3.65 14.5h-1.52c-.66 0-.86-.53-2.04-1.71-1.03-1.02-1.49-1.15-1.75-1.15-.36 0-.46.1-.46.58v1.56c0 .42-.13.67-1.24.67-1.85 0-3.89-1.12-5.33-3.21-2.17-3.07-2.76-5.4-2.76-5.88 0-.26.1-.5.58-.5h1.52c.43 0 .6.2.76.65.85 2.37 2.26 4.45 2.85 4.45.22 0 .32-.1.32-.65v-2.54c-.07-1.17-.69-1.27-.69-1.68 0-.21.17-.42.44-.42h2.39c.37 0 .51.2.51.63v3.44c0 .37.17.51.27.51.22 0 .4-.14.81-.55 1.24-1.4 2.13-3.56 2.13-3.56.12-.26.32-.5.75-.5h1.52c.45 0 .55.23.45.63-.18.93-2.13 3.74-2.13 3.74-.19.3-.26.44 0 .77.19.25.82.8 1.24 1.29.77.88 1.36 1.62 1.52 2.13.15.51-.08.77-.59.77z"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-links">
|
||||||
|
<div class="footer-column">
|
||||||
|
<h4>Проект</h4>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#features">Возможности</a></li>
|
||||||
|
<li><a href="#vaccess">vAccess</a></li>
|
||||||
|
<li><a href="#install">Установка</a></li>
|
||||||
|
<li><a href="#ssl">SSL_Сертификаты</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-column">
|
||||||
|
<h4>Ресурсы</h4>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://github.com/Falknat/vServer" target="_blank">Исходный код</a></li>
|
||||||
|
<li><a href="https://github.com/Falknat/vServer" target="_blank">Релизы</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-column">
|
||||||
|
<h4>Контакты</h4>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://voxsel.ru" target="_blank">voxsel.ru</a></li>
|
||||||
|
<li><a href="https://vk.com/felias" target="_blank">VK</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-bottom">
|
||||||
|
<p>© 2025 vServer. Open Source Project. Разработано с ❤️ на Go</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Smooth scroll
|
||||||
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||||
|
anchor.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const target = document.querySelector(this.getAttribute('href'));
|
||||||
|
if (target) {
|
||||||
|
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy code to clipboard
|
||||||
|
function copyCode(btn) {
|
||||||
|
const code = btn.previousElementSibling.textContent;
|
||||||
|
navigator.clipboard.writeText(code).then(() => {
|
||||||
|
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor"><path d="M13.5 3.5L6 11l-3.5-3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor"><rect x="5" y="5" width="9" height="9" rx="1" stroke-width="1.5"/><path d="M2 11V3a1 1 0 011-1h8" stroke-width="1.5"/></svg>';
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll animations
|
||||||
|
const observerOptions = {
|
||||||
|
threshold: 0.1,
|
||||||
|
rootMargin: '0px 0px -50px 0px'
|
||||||
|
};
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
entry.target.style.opacity = '1';
|
||||||
|
entry.target.style.transform = 'translateY(0)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, observerOptions);
|
||||||
|
|
||||||
|
document.querySelectorAll('.feature-card, .benefit-card, .install-step, .arch-card').forEach(el => {
|
||||||
|
el.style.opacity = '0';
|
||||||
|
el.style.transform = 'translateY(30px)';
|
||||||
|
el.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
|
||||||
|
observer.observe(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Header scroll effect
|
||||||
|
let lastScroll = 0;
|
||||||
|
const header = document.querySelector('.header');
|
||||||
|
|
||||||
|
window.addEventListener('scroll', () => {
|
||||||
|
const currentScroll = window.pageYOffset;
|
||||||
|
|
||||||
|
if (currentScroll > 100) {
|
||||||
|
header.style.background = 'rgba(10, 10, 15, 0.95)';
|
||||||
|
header.style.backdropFilter = 'blur(10px)';
|
||||||
|
header.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.3)';
|
||||||
|
} else {
|
||||||
|
header.style.background = 'transparent';
|
||||||
|
header.style.backdropFilter = 'none';
|
||||||
|
header.style.boxShadow = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
lastScroll = currentScroll;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1593
test/style.css
Normal file
1593
test/style.css
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user