VUE дизайн

This commit is contained in:
2026-02-08 05:37:13 +07:00
parent bdfa2404b5
commit caf52afcdf
73 changed files with 1148 additions and 7211 deletions

View File

@@ -9,8 +9,13 @@ const props = defineProps({
const certs = ref([])
const loading = ref(true)
const issuing = ref('')
const renewing = ref('')
const deleting = ref('')
onMounted(async () => {
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
const refreshCerts = async () => {
await certsStore.loadAll()
certs.value = certsStore.list.filter(c =>
c.dns_names?.some(d => d === props.host || d === `*.${props.host}` || props.host.match(new RegExp(d.replace('*.', '.*\\.'))))
@@ -19,25 +24,47 @@ onMounted(async () => {
const info = await certsStore.getInfo(props.host)
if (info && info.has_cert) certs.value = [info]
}
}
onMounted(async () => {
await refreshCerts()
loading.value = false
})
const issueCert = async (domain) => {
const result = await certsStore.issue(domain)
if (result === 'OK') success(t('notify.certIssued'))
else error(String(result))
issuing.value = domain
const [result] = await Promise.all([certsStore.issue(domain), sleep(1000)])
if (result && !String(result).startsWith('Error')) {
success(t('notify.certIssued'))
await refreshCerts()
} else {
error(String(result))
}
issuing.value = ''
}
const renewCert = async (domain) => {
const result = await certsStore.renew(domain)
if (result === 'OK') success(t('notify.certRenewed'))
else error(String(result))
renewing.value = domain
const [result] = await Promise.all([certsStore.renew(domain), sleep(1000)])
if (result && !String(result).startsWith('Error')) {
success(t('notify.certRenewed'))
await refreshCerts()
} else {
error(String(result))
}
renewing.value = ''
}
const deleteCert = async (domain) => {
const result = await certsStore.remove(domain)
if (result === 'OK') success(t('notify.certDeleted'))
else error(String(result))
deleting.value = domain
const [result] = await Promise.all([certsStore.remove(domain), sleep(1000)])
if (result && !String(result).startsWith('Error')) {
success(t('notify.certDeleted'))
await refreshCerts()
} else {
error(String(result))
}
deleting.value = ''
}
</script>
@@ -53,7 +80,7 @@ const deleteCert = async (domain) => {
<i class="fas fa-certificate"></i>
<h3>{{ t('certs.noCert') }}</h3>
<p>{{ t('certs.subtitle') }}</p>
<VButton icon="fas fa-plus" @click="issueCert(host)">{{ t('certs.issue') }}</VButton>
<VButton icon="fas fa-plus" :loading="issuing === host" @click="issueCert(host)">{{ t('certs.issue') }}</VButton>
</div>
<!-- Карточки сертификатов -->
@@ -64,13 +91,13 @@ const deleteCert = async (domain) => {
<h3>{{ cert.domain }}</h3>
</div>
<div class="cert-card-actions">
<VButton v-if="cert.has_cert && !cert.is_expired" variant="success" icon="fas fa-sync" @click="renewCert(cert.domain)">
<VButton v-if="cert.has_cert && !cert.is_expired" variant="success" icon="fas fa-sync" :loading="renewing === cert.domain" @click="renewCert(cert.domain)">
{{ t('certs.renew') }}
</VButton>
<VButton v-if="!cert.has_cert || cert.is_expired" icon="fas fa-plus" @click="issueCert(cert.domain)">
<VButton v-if="!cert.has_cert || cert.is_expired" icon="fas fa-plus" :loading="issuing === cert.domain" @click="issueCert(cert.domain)">
{{ t('certs.issue') }}
</VButton>
<VButton v-if="cert.has_cert" variant="danger" icon="fas fa-trash" @click="deleteCert(cert.domain)">
<VButton v-if="cert.has_cert" variant="danger" icon="fas fa-trash" :loading="deleting === cert.domain" @click="deleteCert(cert.domain)">
{{ t('certs.delete') }}
</VButton>
</div>

View File

@@ -6,11 +6,13 @@ const certsStore = useCertsStore()
onMounted(async () => {
await Promise.all([
servicesStore.load(),
sitesStore.load(),
proxiesStore.load(),
certsStore.loadAll(),
])
if (!servicesStore.loaded) {
await servicesStore.load()
}
})
</script>

View File

@@ -5,11 +5,11 @@ const proxiesStore = useProxiesStore()
const { success, error } = useNotification()
const form = reactive({
name: '',
domain: '',
localAddr: '127.0.0.1',
localPort: '',
serviceHttps: false,
autoHttps: true,
certMode: 'none',
})
@@ -18,10 +18,23 @@ const creating = ref(false)
const createProxy = async () => {
if (!form.domain || !form.localPort) return
creating.value = true
const result = await proxiesStore.load()
const result = await proxiesStore.create({
name: form.name,
domain: form.domain,
localAddr: form.localAddr,
localPort: form.localPort,
enabled: true,
serviceHttps: form.serviceHttps,
autoHttps: form.serviceHttps,
autoSSL: false,
})
creating.value = false
success(t('notify.dataSaved'))
router.push('/')
if (result && !String(result).startsWith('Error')) {
success(t('notify.proxyCreated'))
router.push('/')
} else {
error(String(result))
}
}
</script>
@@ -41,6 +54,7 @@ const createProxy = async () => {
<div class="settings-form">
<h3 class="form-subsection-title"><i class="fas fa-info-circle"></i> {{ t('common.basicInfo') }}</h3>
<VInput v-model="form.name" :label="t('sites.formName')" placeholder="My Proxy" />
<VInput v-model="form.domain" :label="t('proxies.formDomain')" :placeholder="t('proxies.formDomainPlaceholder')" :hint="t('proxies.formDomainHint')" required />
<div class="form-row">
@@ -48,21 +62,12 @@ const createProxy = async () => {
<VInput v-model="form.localPort" :label="t('proxies.formLocalPort')" placeholder="3000" required />
</div>
<div class="form-row">
<div class="form-group">
<div class="form-label-row">
<VTooltip :text="t('proxies.formServiceHttpsHint')" />
<label class="form-label">{{ t('proxies.formServiceHttps') }}</label>
</div>
<VToggle v-model="form.serviceHttps" :label="t('common.enabled')" />
</div>
<div class="form-group">
<div class="form-label-row">
<VTooltip :text="t('proxies.formAutoHttpsHint')" />
<label class="form-label">{{ t('proxies.formAutoHttps') }}</label>
</div>
<VToggle v-model="form.autoHttps" :label="t('common.enabled')" />
<div class="form-group">
<div class="form-label-row">
<VTooltip :text="t('proxies.formServiceHttpsHint')" />
<label class="form-label">HTTPS</label>
</div>
<VToggle v-model="form.serviceHttps" :label="t('common.enabled')" />
</div>
<SslUploadSection v-model="form.certMode" />

View File

@@ -10,6 +10,7 @@ const props = defineProps({
})
const form = reactive({
name: '',
domain: '',
localAddr: '127.0.0.1',
localPort: '',
@@ -19,12 +20,15 @@ const form = reactive({
autoSSL: false,
})
import { api } from '@core/api/index.js'
const saving = ref(false)
onMounted(async () => {
if (!proxiesStore.loaded) await proxiesStore.load()
const proxy = proxiesStore.list.find(p => p.ExternalDomain === props.domain)
if (proxy) {
form.name = proxy.Name || ''
form.domain = proxy.ExternalDomain
form.localAddr = proxy.LocalAddress
form.localPort = proxy.LocalPort
@@ -37,9 +41,29 @@ onMounted(async () => {
const saveProxy = async () => {
saving.value = true
success(t('notify.dataSaved'))
const config = await api.getConfig()
const idx = config.Proxy_Service.findIndex(p => p.ExternalDomain === props.domain)
if (idx >= 0) {
config.Proxy_Service[idx].Name = form.name
config.Proxy_Service[idx].ExternalDomain = form.domain
config.Proxy_Service[idx].LocalAddress = form.localAddr
config.Proxy_Service[idx].LocalPort = form.localPort
config.Proxy_Service[idx].Enable = form.enabled
config.Proxy_Service[idx].ServiceHTTPSuse = form.serviceHttps
config.Proxy_Service[idx].AutoHTTPS = form.serviceHttps
config.Proxy_Service[idx].AutoCreateSSL = form.autoSSL
const result = await api.saveConfig(JSON.stringify(config))
if (!String(result).startsWith('Error')) {
await proxiesStore.load()
success(t('notify.dataSaved'))
router.push('/')
} else {
error(result)
}
} else {
error('Proxy not found in config')
}
saving.value = false
router.push('/')
}
const confirmDelete = () => {
@@ -47,9 +71,14 @@ const confirmDelete = () => {
title: t('proxies.deleteTitle'),
message: t('proxies.deleteConfirm', { domain: form.domain }),
onConfirm: async () => {
success(t('notify.proxyDeleted'))
const result = await proxiesStore.remove(form.domain)
if (result && !String(result).startsWith('Error')) {
success(t('notify.proxyDeleted'))
router.push('/')
} else {
error(String(result))
}
modal.close()
router.push('/')
},
})
}
@@ -82,6 +111,10 @@ const confirmDelete = () => {
</div>
</div>
<!-- Имя и домен -->
<VInput v-model="form.name" :label="t('sites.formName')" />
<VInput v-model="form.domain" :label="t('proxies.externalDomain')" required />
<!-- Адрес и порт -->
<div class="form-row">
<VInput v-model="form.localAddr" :label="t('proxies.formLocalAddr')" required />
@@ -89,15 +122,11 @@ const confirmDelete = () => {
</div>
<!-- Toggles -->
<div class="form-row form-row-3">
<div class="form-row">
<div class="form-group">
<label class="form-label">{{ t('proxies.formServiceHttps') }}:</label>
<label class="form-label">HTTPS:</label>
<VToggle v-model="form.serviceHttps" :label="t('common.enabled')" />
</div>
<div class="form-group">
<label class="form-label">{{ t('proxies.formAutoHttps') }}:</label>
<VToggle v-model="form.autoHttps" :label="t('common.enabled')" />
</div>
<div class="form-group">
<label class="form-label">Auto SSL:</label>
<VToggle v-model="form.autoSSL" :label="t('common.enabled')" />

View File

@@ -40,7 +40,7 @@ const createSite = async () => {
}
const result = await sitesStore.create(siteData)
creating.value = false
if (result === 'OK') {
if (result && !String(result).startsWith('Error')) {
success(t('notify.siteCreated'))
router.push('/')
} else {

View File

@@ -19,6 +19,8 @@ const form = reactive({
autoSSL: false,
})
import { api } from '@core/api/index.js'
const saving = ref(false)
const aliasInput = ref('')
@@ -55,9 +57,28 @@ const removeAlias = (index) => {
const saveSite = async () => {
saving.value = true
success(t('notify.dataSaved'))
const config = await api.getConfig()
const idx = config.Site_www.findIndex(s => s.host === props.host)
if (idx >= 0) {
config.Site_www[idx].host = form.host
config.Site_www[idx].name = form.name
config.Site_www[idx].alias = form.alias
config.Site_www[idx].root_file = form.rootFile
config.Site_www[idx].status = form.status
config.Site_www[idx].root_file_routing = form.routing
config.Site_www[idx].AutoCreateSSL = form.autoSSL
const result = await api.saveConfig(JSON.stringify(config))
if (!String(result).startsWith('Error')) {
await sitesStore.load()
success(t('notify.dataSaved'))
router.push('/')
} else {
error(result)
}
} else {
error('Site not found in config')
}
saving.value = false
router.push('/')
}
const confirmDelete = () => {
@@ -67,7 +88,7 @@ const confirmDelete = () => {
warning: t('sites.deleteWarning'),
onConfirm: async () => {
const result = await sitesStore.remove(form.host)
if (result === 'OK') {
if (result && !String(result).startsWith('Error')) {
success(t('notify.siteDeleted'))
router.push('/')
} else {
@@ -107,6 +128,7 @@ const confirmDelete = () => {
</div>
<!-- Основная информация -->
<VInput v-model="form.host" :label="t('sites.host')" required />
<VInput v-model="form.name" :label="t('sites.formName')" required />
<!-- Alias с тегами -->

View File

@@ -1,5 +1,6 @@
<script setup>
import { api } from '@core/api/index.js'
import { useDraggable } from '@core/composables/useDraggable.js'
const { t } = useI18n()
const route = useRoute()
@@ -13,6 +14,8 @@ const activeTab = ref('rules')
const rules = ref([])
const loading = ref(true)
const { dragIndex, dragOverIndex, onDragStart, onDragOver, onDragEnter, onDragLeave, onDrop, onDragEnd } = useDraggable(rules)
onMounted(async () => {
const data = await api.getVAccessRules(props.host, false)
rules.value = data?.rules || []
@@ -91,7 +94,18 @@ const formatList = (arr) => {
</tr>
</thead>
<tbody>
<tr v-for="(rule, index) in rules" :key="index">
<tr
v-for="(rule, index) in rules"
:key="index"
draggable="true"
:class="{ 'drag-over': dragOverIndex === index, 'dragging': dragIndex === index }"
@dragstart="onDragStart(index, $event)"
@dragover="onDragOver(index, $event)"
@dragenter="onDragEnter(index, $event)"
@dragleave="onDragLeave"
@drop="onDrop(index)"
@dragend="onDragEnd($event)"
>
<td class="drag-handle"><i class="fas fa-grip-vertical"></i></td>
<td>
<VBadge :variant="rule.type === 'Allow' ? 'yes' : 'no'">{{ rule.type }}</VBadge>
@@ -310,6 +324,18 @@ const formatList = (arr) => {
color: var(--accent-purple-light);
}
.vaccess-table tbody tr.dragging {
opacity: 0.4;
}
.vaccess-table tbody tr.drag-over {
border-top: 2px solid var(--accent-purple);
}
.vaccess-table tbody tr.drag-over td {
border-top: 2px solid var(--accent-purple);
}
.mini-tags {
display: flex;
flex-wrap: wrap;