up
This commit is contained in:
@@ -2,36 +2,135 @@
|
|||||||
<AppLayout>
|
<AppLayout>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h1 class="text-2xl font-bold mb-6">Application Settings</h1>
|
<h1 class="text-2xl font-bold mb-6">Application Settings</h1>
|
||||||
<form @submit.prevent="saveSettings" class="space-y-4 max-w-lg">
|
<form @submit.prevent="saveSettings" class="space-y-6 max-w-2xl">
|
||||||
<div v-for="(value, key) in settings" :key="key">
|
<div v-for="field in meta" :key="field.key" class="border-b border-gray-200 pb-4">
|
||||||
<label class="block text-sm font-medium text-gray-700">{{ key }}</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
<input v-model="settings[key]" type="text" class="input-field mt-1" />
|
{{ field.label }}
|
||||||
|
<span v-if="field.required" class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- Текстовое поле -->
|
||||||
|
<input
|
||||||
|
v-if="field.type === 'text' || field.type === 'number'"
|
||||||
|
v-model="values[field.key]"
|
||||||
|
:type="field.type"
|
||||||
|
:required="field.required"
|
||||||
|
class="input-field mt-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Текстовая область -->
|
||||||
|
<textarea
|
||||||
|
v-else-if="field.type === 'textarea'"
|
||||||
|
v-model="values[field.key]"
|
||||||
|
:rows="field.rows || 3"
|
||||||
|
class="input-field mt-1"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<!-- Чекбокс для булевых значений -->
|
||||||
|
<div v-else-if="field.type === 'boolean'" class="flex items-center mt-1">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="values[field.key] === 'true'"
|
||||||
|
@change="values[field.key] = $event.target.checked ? 'true' : 'false'"
|
||||||
|
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
<span class="ml-2 text-sm text-gray-600">Enabled</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Выпадающий список -->
|
||||||
|
<select
|
||||||
|
v-else-if="field.type === 'select'"
|
||||||
|
v-model="values[field.key]"
|
||||||
|
class="input-field mt-1"
|
||||||
|
>
|
||||||
|
<option v-for="opt in field.options" :key="opt.value" :value="opt.value">
|
||||||
|
{{ opt.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<p v-if="field.description" class="mt-1 text-xs text-gray-500">
|
||||||
|
{{ field.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end space-x-3 pt-4">
|
||||||
|
<button type="button" @click="loadData" class="btn-secondary">Reset</button>
|
||||||
<button type="submit" class="btn-primary">Save Changes</button>
|
<button type="submit" class="btn-primary">Save Changes</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div v-if="message" class="mt-4 p-3 rounded-lg" :class="messageClass">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue';
|
||||||
import AppLayout from '../components/Layout/AppLayout.vue'
|
import AppLayout from '../components/Layout/AppLayout.vue';
|
||||||
|
|
||||||
const settings = ref<Record<string, string>>({})
|
interface FieldMeta {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
type: string;
|
||||||
|
required?: boolean;
|
||||||
|
rows?: number;
|
||||||
|
options?: Array<{ value: string; label: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadSettings() {
|
const meta = ref<FieldMeta[]>([]);
|
||||||
const res = await fetch('/api/settings')
|
const values = ref<Record<string, string>>({});
|
||||||
settings.value = await res.json()
|
const message = ref('');
|
||||||
|
const messageClass = ref('');
|
||||||
|
|
||||||
|
async function loadMeta() {
|
||||||
|
const res = await fetch('/api/settings/meta');
|
||||||
|
if (res.ok) {
|
||||||
|
meta.value = await res.json();
|
||||||
|
} else {
|
||||||
|
showMessage('Failed to load settings metadata', 'bg-red-50 text-red-800');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadValues() {
|
||||||
|
const res = await fetch('/api/settings/all');
|
||||||
|
if (res.ok) {
|
||||||
|
values.value = await res.json();
|
||||||
|
} else {
|
||||||
|
showMessage('Failed to load settings values', 'bg-red-50 text-red-800');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
await Promise.all([loadMeta(), loadValues()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSettings() {
|
async function saveSettings() {
|
||||||
await fetch('/api/admin/settings', {
|
try {
|
||||||
|
const res = await fetch('/api/admin/settings', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(settings.value)
|
body: JSON.stringify(values.value),
|
||||||
})
|
});
|
||||||
alert('Settings saved')
|
if (res.ok) {
|
||||||
|
showMessage('Settings saved successfully', 'bg-green-50 text-green-800');
|
||||||
|
} else {
|
||||||
|
showMessage('Failed to save settings', 'bg-red-50 text-red-800');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showMessage('Network error', 'bg-red-50 text-red-800');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadSettings)
|
function showMessage(text: string, cssClass: string) {
|
||||||
|
message.value = text;
|
||||||
|
messageClass.value = cssClass;
|
||||||
|
setTimeout(() => {
|
||||||
|
message.value = '';
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadData);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -304,6 +304,20 @@ public class MainVerticle extends AbstractVerticle {
|
|||||||
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
|
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Получить метаданные всех настроек (для построения формы)
|
||||||
|
router.get("/api/settings/meta").handler(rc -> {
|
||||||
|
settingsService.getMetadata()
|
||||||
|
.onSuccess(meta -> rc.response().putHeader("Content-Type", "application/json").end(meta.encode()))
|
||||||
|
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получить все настройки со значениями по умолчанию
|
||||||
|
router.get("/api/settings/all").handler(rc -> {
|
||||||
|
settingsService.getAllWithDefaults()
|
||||||
|
.onSuccess(settings -> rc.response().putHeader("Content-Type", "application/json").end(settings.encode()))
|
||||||
|
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
|
||||||
|
});
|
||||||
|
|
||||||
// Обновление настроек (админ)
|
// Обновление настроек (админ)
|
||||||
router.put("/api/admin/settings").handler(rc -> {
|
router.put("/api/admin/settings").handler(rc -> {
|
||||||
JsonObject body = rc.body().asJsonObject();
|
JsonObject body = rc.body().asJsonObject();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package su.xserver.iikocon;
|
package su.xserver.iikocon;
|
||||||
|
|
||||||
import io.vertx.core.Future;
|
import io.vertx.core.Future;
|
||||||
|
import io.vertx.core.json.JsonArray;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
import io.vertx.sqlclient.Pool;
|
import io.vertx.sqlclient.Pool;
|
||||||
import io.vertx.sqlclient.templates.SqlTemplate;
|
import io.vertx.sqlclient.templates.SqlTemplate;
|
||||||
@@ -12,6 +13,111 @@ public class SettingsService {
|
|||||||
|
|
||||||
public SettingsService(Pool pool) { this.pool = pool; }
|
public SettingsService(Pool pool) { this.pool = pool; }
|
||||||
|
|
||||||
|
public Future<JsonArray> getMetadata() {
|
||||||
|
JsonArray meta = new JsonArray();
|
||||||
|
meta.add(new JsonObject()
|
||||||
|
.put("key", "site_name")
|
||||||
|
.put("label", "Site Name")
|
||||||
|
.put("description", "The name of the application shown in the header and title")
|
||||||
|
.put("type", "text")
|
||||||
|
.put("required", true)
|
||||||
|
);
|
||||||
|
meta.add(new JsonObject()
|
||||||
|
.put("key", "site_description")
|
||||||
|
.put("label", "Site Description")
|
||||||
|
.put("description", "Brief description for SEO and meta tags")
|
||||||
|
.put("type", "textarea")
|
||||||
|
.put("rows", 2)
|
||||||
|
);
|
||||||
|
meta.add(new JsonObject()
|
||||||
|
.put("key", "theme")
|
||||||
|
.put("label", "Theme")
|
||||||
|
.put("description", "Default color scheme")
|
||||||
|
.put("type", "select")
|
||||||
|
.put("options", new JsonArray()
|
||||||
|
.add(new JsonObject().put("value", "light").put("label", "Light"))
|
||||||
|
.add(new JsonObject().put("value", "dark").put("label", "Dark"))
|
||||||
|
.add(new JsonObject().put("value", "auto").put("label", "Auto (system preference)"))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
meta.add(new JsonObject()
|
||||||
|
.put("key", "items_per_page")
|
||||||
|
.put("label", "Items Per Page")
|
||||||
|
.put("description", "Number of items shown in tables")
|
||||||
|
.put("type", "number")
|
||||||
|
.put("required", true)
|
||||||
|
);
|
||||||
|
meta.add(new JsonObject()
|
||||||
|
.put("key", "enable_registration")
|
||||||
|
.put("label", "Allow Public Registration")
|
||||||
|
.put("description", "If disabled, only admin can create users")
|
||||||
|
.put("type", "boolean")
|
||||||
|
);
|
||||||
|
meta.add(new JsonObject()
|
||||||
|
.put("key", "maintenance_mode")
|
||||||
|
.put("label", "Maintenance Mode")
|
||||||
|
.put("description", "When enabled, only admins can access the site")
|
||||||
|
.put("type", "boolean")
|
||||||
|
);
|
||||||
|
meta.add(new JsonObject()
|
||||||
|
.put("key", "default_language")
|
||||||
|
.put("label", "Default Language")
|
||||||
|
.put("description", "Interface language")
|
||||||
|
.put("type", "select")
|
||||||
|
.put("options", new JsonArray()
|
||||||
|
.add(new JsonObject().put("value", "en").put("label", "English"))
|
||||||
|
.add(new JsonObject().put("value", "ru").put("label", "Русский"))
|
||||||
|
.add(new JsonObject().put("value", "es").put("label", "Español"))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
meta.add(new JsonObject()
|
||||||
|
.put("key", "session_timeout_minutes")
|
||||||
|
.put("label", "Session Timeout (minutes)")
|
||||||
|
.put("description", "How long until inactive users are logged out")
|
||||||
|
.put("type", "number")
|
||||||
|
.put("required", true)
|
||||||
|
);
|
||||||
|
meta.add(new JsonObject()
|
||||||
|
.put("key", "logo_url")
|
||||||
|
.put("label", "Logo URL")
|
||||||
|
.put("description", "Path or URL to custom logo image")
|
||||||
|
.put("type", "text")
|
||||||
|
);
|
||||||
|
return Future.succeededFuture(meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Future<JsonObject> getAllWithDefaults() {
|
||||||
|
return getAll().compose(values -> {
|
||||||
|
JsonObject result = new JsonObject();
|
||||||
|
// Получаем метаданные, чтобы знать ключи
|
||||||
|
return getMetadata().map(meta -> {
|
||||||
|
for (Object item : meta) {
|
||||||
|
JsonObject m = (JsonObject) item;
|
||||||
|
String key = m.getString("key");
|
||||||
|
String defaultValue = getDefaultValueByKey(key);
|
||||||
|
String current = values.getString(key);
|
||||||
|
result.put(key, current != null ? current : defaultValue);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getDefaultValueByKey(String key) {
|
||||||
|
return switch (key) {
|
||||||
|
case "site_name" -> "Admin Panel";
|
||||||
|
case "site_description" -> "";
|
||||||
|
case "theme" -> "light";
|
||||||
|
case "items_per_page" -> "20";
|
||||||
|
case "enable_registration" -> "true";
|
||||||
|
case "maintenance_mode" -> "false";
|
||||||
|
case "default_language" -> "en";
|
||||||
|
case "session_timeout_minutes" -> "60";
|
||||||
|
case "logo_url" -> "";
|
||||||
|
default -> "";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public Future<Void> initDatabase() {
|
public Future<Void> initDatabase() {
|
||||||
String createTable = """
|
String createTable = """
|
||||||
CREATE TABLE IF NOT EXISTS app_settings (
|
CREATE TABLE IF NOT EXISTS app_settings (
|
||||||
|
|||||||
Reference in New Issue
Block a user