|
@@ -0,0 +1,509 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="page">
|
|
|
|
|
+ <div class="page-header">
|
|
|
|
|
+ <h1>{{ $t('settings.title') }}</h1>
|
|
|
|
|
+ <p>{{ $t('settings.description') }}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="tabs">
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-for="tab in tabs"
|
|
|
|
|
+ :key="tab.id"
|
|
|
|
|
+ @click="activeTab = tab.id"
|
|
|
|
|
+ :class="['tab', { active: activeTab === tab.id }]"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ tab.label }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="tab-content">
|
|
|
|
|
+ <!-- Host Monitoring Settings -->
|
|
|
|
|
+ <div v-if="activeTab === 'host'" class="settings-section">
|
|
|
|
|
+ <h2>{{ $t('settings.hostMonitoring.title') }}</h2>
|
|
|
|
|
+ <p class="section-desc">{{ $t('settings.hostMonitoring.description') }}</p>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="loading" class="loading">{{ $t('common.loading') }}</div>
|
|
|
|
|
+
|
|
|
|
|
+ <form v-else @submit.prevent="saveHostSettings" class="settings-form">
|
|
|
|
|
+ <div class="form-row">
|
|
|
|
|
+ <label>
|
|
|
|
|
+ <input type="checkbox" v-model="hostSettings.enabled" />
|
|
|
|
|
+ {{ $t('settings.hostMonitoring.enabled') }}
|
|
|
|
|
+ </label>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <label>{{ $t('settings.hostMonitoring.cpuThreshold') }}</label>
|
|
|
|
|
+ <input type="number" v-model.number="hostSettings.cpu_threshold" min="0" max="100" />
|
|
|
|
|
+ <span class="hint">%</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <label>{{ $t('settings.hostMonitoring.memoryThreshold') }}</label>
|
|
|
|
|
+ <input type="number" v-model.number="hostSettings.memory_threshold" min="0" max="100" />
|
|
|
|
|
+ <span class="hint">%</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <label>{{ $t('settings.hostMonitoring.loadThreshold') }}</label>
|
|
|
|
|
+ <input type="number" v-model.number="hostSettings.load_threshold" min="0" step="0.1" />
|
|
|
|
|
+ <span class="hint">{{ $t('settings.hostMonitoring.loadThresholdHint') }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <label>{{ $t('settings.hostMonitoring.diskThreshold') }}</label>
|
|
|
|
|
+ <input type="number" v-model.number="hostSettings.disk_threshold" min="0" max="100" />
|
|
|
|
|
+ <span class="hint">%</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <label>{{ $t('settings.hostMonitoring.networkThreshold') }}</label>
|
|
|
|
|
+ <input type="number" v-model.number="hostSettings.network_threshold_mbps" min="0" />
|
|
|
|
|
+ <span class="hint">Mbps</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <button type="submit" class="btn btn-primary" :disabled="saving">
|
|
|
|
|
+ {{ saving ? $t('common.saving') : $t('common.save') }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Security Monitoring Settings -->
|
|
|
|
|
+ <div v-if="activeTab === 'security'" class="settings-section">
|
|
|
|
|
+ <h2>{{ $t('settings.securityMonitoring.title') }}</h2>
|
|
|
|
|
+ <p class="section-desc">{{ $t('settings.securityMonitoring.description') }}</p>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="loading" class="loading">{{ $t('common.loading') }}</div>
|
|
|
|
|
+
|
|
|
|
|
+ <form v-else @submit.prevent="saveSecuritySettings" class="settings-form">
|
|
|
|
|
+ <div class="form-row">
|
|
|
|
|
+ <label>
|
|
|
|
|
+ <input type="checkbox" v-model="securitySettings.enabled" />
|
|
|
|
|
+ {{ $t('settings.securityMonitoring.enabled') }}
|
|
|
|
|
+ </label>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <h3>{{ $t('settings.securityMonitoring.loginBruteforce') }}</h3>
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <label>{{ $t('settings.securityMonitoring.threshold') }}</label>
|
|
|
|
|
+ <input type="number" v-model.number="securitySettings.login_bruteforce_threshold" min="1" />
|
|
|
|
|
+ <span class="hint">{{ $t('settings.securityMonitoring.attempts') }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <label>{{ $t('settings.securityMonitoring.window') }}</label>
|
|
|
|
|
+ <input type="number" v-model.number="securitySettings.login_bruteforce_window_minutes" min="1" />
|
|
|
|
|
+ <span class="hint">{{ $t('settings.securityMonitoring.minutes') }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <h3>{{ $t('settings.securityMonitoring.deviceTokenBruteforce') }}</h3>
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <label>{{ $t('settings.securityMonitoring.threshold') }}</label>
|
|
|
|
|
+ <input type="number" v-model.number="securitySettings.device_token_bruteforce_threshold" min="1" />
|
|
|
|
|
+ <span class="hint">{{ $t('settings.securityMonitoring.attempts') }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <label>{{ $t('settings.securityMonitoring.window') }}</label>
|
|
|
|
|
+ <input type="number" v-model.number="securitySettings.device_token_bruteforce_window_minutes" min="1" />
|
|
|
|
|
+ <span class="hint">{{ $t('settings.securityMonitoring.minutes') }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <h3>{{ $t('settings.securityMonitoring.registrationFlood') }}</h3>
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <label>{{ $t('settings.securityMonitoring.threshold') }}</label>
|
|
|
|
|
+ <input type="number" v-model.number="securitySettings.registration_flood_threshold" min="1" />
|
|
|
|
|
+ <span class="hint">{{ $t('settings.securityMonitoring.registrations') }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <label>{{ $t('settings.securityMonitoring.window') }}</label>
|
|
|
|
|
+ <input type="number" v-model.number="securitySettings.registration_flood_window_minutes" min="1" />
|
|
|
|
|
+ <span class="hint">{{ $t('settings.securityMonitoring.minutes') }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <button type="submit" class="btn btn-primary" :disabled="saving">
|
|
|
|
|
+ {{ saving ? $t('common.saving') : $t('common.save') }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Alert Channels Settings -->
|
|
|
|
|
+ <div v-if="activeTab === 'alerts'" class="settings-section">
|
|
|
|
|
+ <h2>{{ $t('settings.alertChannels.title') }}</h2>
|
|
|
|
|
+ <p class="section-desc">{{ $t('settings.alertChannels.description') }}</p>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="loading" class="loading">{{ $t('common.loading') }}</div>
|
|
|
|
|
+
|
|
|
|
|
+ <form v-else @submit.prevent="saveAlertSettings" class="settings-form">
|
|
|
|
|
+ <!-- Telegram -->
|
|
|
|
|
+ <h3>{{ $t('settings.alertChannels.telegram') }}</h3>
|
|
|
|
|
+ <div class="form-row">
|
|
|
|
|
+ <label>
|
|
|
|
|
+ <input type="checkbox" v-model="alertSettings.telegram.enabled" />
|
|
|
|
|
+ {{ $t('settings.alertChannels.enabled') }}
|
|
|
|
|
+ </label>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <label>{{ $t('settings.alertChannels.botToken') }}</label>
|
|
|
|
|
+ <input type="text" v-model="alertSettings.telegram.bot_token" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <label>{{ $t('settings.alertChannels.chatIds') }}</label>
|
|
|
|
|
+ <textarea v-model="telegramChatIds" rows="3"></textarea>
|
|
|
|
|
+ <span class="hint">{{ $t('settings.alertChannels.chatIdsHint') }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Email -->
|
|
|
|
|
+ <h3>{{ $t('settings.alertChannels.email') }}</h3>
|
|
|
|
|
+ <div class="form-row">
|
|
|
|
|
+ <label>
|
|
|
|
|
+ <input type="checkbox" v-model="alertSettings.email.enabled" />
|
|
|
|
|
+ {{ $t('settings.alertChannels.enabled') }}
|
|
|
|
|
+ </label>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <label>{{ $t('settings.alertChannels.smtpServer') }}</label>
|
|
|
|
|
+ <input type="text" v-model="alertSettings.email.smtp_server" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <label>{{ $t('settings.alertChannels.smtpPort') }}</label>
|
|
|
|
|
+ <input type="number" v-model.number="alertSettings.email.smtp_port" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <label>{{ $t('settings.alertChannels.smtpUser') }}</label>
|
|
|
|
|
+ <input type="text" v-model="alertSettings.email.smtp_user" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <label>{{ $t('settings.alertChannels.smtpPassword') }}</label>
|
|
|
|
|
+ <input type="password" v-model="alertSettings.email.smtp_password" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <label>{{ $t('settings.alertChannels.fromAddress') }}</label>
|
|
|
|
|
+ <input type="email" v-model="alertSettings.email.from_address" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <label>{{ $t('settings.alertChannels.recipients') }}</label>
|
|
|
|
|
+ <textarea v-model="emailRecipients" rows="3"></textarea>
|
|
|
|
|
+ <span class="hint">{{ $t('settings.alertChannels.recipientsHint') }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <button type="submit" class="btn btn-primary" :disabled="saving">
|
|
|
|
|
+ {{ saving ? $t('common.saving') : $t('common.save') }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { ref, computed, onMounted } from 'vue'
|
|
|
|
|
+import { useI18n } from 'vue-i18n'
|
|
|
|
|
+import axios from '@/api/client'
|
|
|
|
|
+
|
|
|
|
|
+const { t } = useI18n()
|
|
|
|
|
+
|
|
|
|
|
+const activeTab = ref('host')
|
|
|
|
|
+const loading = ref(false)
|
|
|
|
|
+const saving = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+const tabs = computed(() => [
|
|
|
|
|
+ { id: 'host', label: t('settings.tabs.hostMonitoring') },
|
|
|
|
|
+ { id: 'security', label: t('settings.tabs.securityMonitoring') },
|
|
|
|
|
+ { id: 'alerts', label: t('settings.tabs.alertChannels') }
|
|
|
|
|
+])
|
|
|
|
|
+
|
|
|
|
|
+// Host monitoring settings
|
|
|
|
|
+const hostSettings = ref({
|
|
|
|
|
+ enabled: true,
|
|
|
|
|
+ cpu_threshold: 90,
|
|
|
|
|
+ memory_threshold: 90,
|
|
|
|
|
+ load_threshold: 2.0,
|
|
|
|
|
+ disk_threshold: 90,
|
|
|
|
|
+ network_threshold_mbps: 100
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// Security monitoring settings
|
|
|
|
|
+const securitySettings = ref({
|
|
|
|
|
+ enabled: true,
|
|
|
|
|
+ login_bruteforce_threshold: 5,
|
|
|
|
|
+ login_bruteforce_window_minutes: 5,
|
|
|
|
|
+ device_token_bruteforce_threshold: 10,
|
|
|
|
|
+ device_token_bruteforce_window_minutes: 5,
|
|
|
|
|
+ registration_flood_threshold: 10,
|
|
|
|
|
+ registration_flood_window_minutes: 10
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// Alert channels settings
|
|
|
|
|
+const alertSettings = ref({
|
|
|
|
|
+ telegram: {
|
|
|
|
|
+ enabled: false,
|
|
|
|
|
+ bot_token: '',
|
|
|
|
|
+ chat_ids: []
|
|
|
|
|
+ },
|
|
|
|
|
+ email: {
|
|
|
|
|
+ enabled: false,
|
|
|
|
|
+ smtp_server: '',
|
|
|
|
|
+ smtp_port: 587,
|
|
|
|
|
+ smtp_user: '',
|
|
|
|
|
+ smtp_password: '',
|
|
|
|
|
+ from_address: '',
|
|
|
|
|
+ recipients: []
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// Computed for text areas
|
|
|
|
|
+const telegramChatIds = computed({
|
|
|
|
|
+ get: () => alertSettings.value.telegram.chat_ids.join('\n'),
|
|
|
|
|
+ set: (val) => {
|
|
|
|
|
+ alertSettings.value.telegram.chat_ids = val.split('\n').map(id => id.trim()).filter(Boolean)
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const emailRecipients = computed({
|
|
|
|
|
+ get: () => alertSettings.value.email.recipients.join('\n'),
|
|
|
|
|
+ set: (val) => {
|
|
|
|
|
+ alertSettings.value.email.recipients = val.split('\n').map(email => email.trim()).filter(Boolean)
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+async function loadSettings() {
|
|
|
|
|
+ loading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const [hostRes, securityRes, alertsRes] = await Promise.all([
|
|
|
|
|
+ axios.get('/superadmin/settings/setting/host_monitoring'),
|
|
|
|
|
+ axios.get('/superadmin/settings/setting/security_monitoring'),
|
|
|
|
|
+ axios.get('/superadmin/settings/setting/alert_channels')
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ hostSettings.value = hostRes.data.value
|
|
|
|
|
+ securitySettings.value = securityRes.data.value
|
|
|
|
|
+ alertSettings.value = alertsRes.data.value
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Failed to load settings:', error)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function saveHostSettings() {
|
|
|
|
|
+ saving.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ await axios.put('/superadmin/settings/setting/host_monitoring', {
|
|
|
|
|
+ value: hostSettings.value
|
|
|
|
|
+ })
|
|
|
|
|
+ alert(t('settings.saved'))
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Failed to save host settings:', error)
|
|
|
|
|
+ alert(t('settings.saveFailed'))
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ saving.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function saveSecuritySettings() {
|
|
|
|
|
+ saving.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ await axios.put('/superadmin/settings/setting/security_monitoring', {
|
|
|
|
|
+ value: securitySettings.value
|
|
|
|
|
+ })
|
|
|
|
|
+ alert(t('settings.saved'))
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Failed to save security settings:', error)
|
|
|
|
|
+ alert(t('settings.saveFailed'))
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ saving.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function saveAlertSettings() {
|
|
|
|
|
+ saving.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ await axios.put('/superadmin/settings/setting/alert_channels', {
|
|
|
|
|
+ value: alertSettings.value
|
|
|
|
|
+ })
|
|
|
|
|
+ alert(t('settings.saved'))
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Failed to save alert settings:', error)
|
|
|
|
|
+ alert(t('settings.saveFailed'))
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ saving.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ loadSettings()
|
|
|
|
|
+})
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.page {
|
|
|
|
|
+ padding: 32px;
|
|
|
|
|
+ max-width: 1200px;
|
|
|
|
|
+ margin: 0 auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.page-header {
|
|
|
|
|
+ margin-bottom: 32px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.page-header h1 {
|
|
|
|
|
+ font-size: 32px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ color: #1a202c;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.page-header p {
|
|
|
|
|
+ color: #718096;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tabs {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-bottom: 24px;
|
|
|
|
|
+ border-bottom: 2px solid #e2e8f0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tab {
|
|
|
|
|
+ padding: 12px 24px;
|
|
|
|
|
+ background: none;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-bottom: 3px solid transparent;
|
|
|
|
|
+ color: #718096;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ margin-bottom: -2px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tab:hover {
|
|
|
|
|
+ color: #667eea;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tab.active {
|
|
|
|
|
+ color: #667eea;
|
|
|
|
|
+ border-bottom-color: #667eea;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tab-content {
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ padding: 32px;
|
|
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.settings-section h2 {
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ color: #1a202c;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.section-desc {
|
|
|
|
|
+ color: #718096;
|
|
|
|
|
+ margin-bottom: 24px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.settings-section h3 {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ margin-top: 24px;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+ color: #2d3748;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.settings-form {
|
|
|
|
|
+ max-width: 600px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-row {
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-row label {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-row input[type="checkbox"] {
|
|
|
|
|
+ width: 18px;
|
|
|
|
|
+ height: 18px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-group {
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-group label {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #2d3748;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-group input[type="text"],
|
|
|
|
|
+.form-group input[type="email"],
|
|
|
|
|
+.form-group input[type="password"],
|
|
|
|
|
+.form-group input[type="number"],
|
|
|
|
|
+.form-group textarea {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ padding: 10px 12px;
|
|
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ transition: border-color 0.2s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-group input:focus,
|
|
|
|
|
+.form-group textarea:focus {
|
|
|
|
|
+ outline: none;
|
|
|
|
|
+ border-color: #667eea;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-group textarea {
|
|
|
|
|
+ resize: vertical;
|
|
|
|
|
+ font-family: inherit;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.hint {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #718096;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn {
|
|
|
|
|
+ padding: 12px 24px;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn-primary {
|
|
|
|
|
+ background: #667eea;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn-primary:hover:not(:disabled) {
|
|
|
|
|
+ background: #5568d3;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn:disabled {
|
|
|
|
|
+ opacity: 0.5;
|
|
|
|
|
+ cursor: not-allowed;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.loading {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 40px;
|
|
|
|
|
+ color: #718096;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|