171 lines
6.9 KiB
Vue
171 lines
6.9 KiB
Vue
<template>
|
||
<div>
|
||
<h1 class="mb-6 text-2xl font-bold text-gray-900">Настройки</h1>
|
||
|
||
<div v-if="loading" class="text-gray-500">Загрузка...</div>
|
||
|
||
<div v-else class="space-y-6">
|
||
<!-- Settings groups -->
|
||
<div v-for="group in settingsGroups" :key="group.title" class="rounded-xl border border-gray-200 bg-white">
|
||
<div class="border-b border-gray-200 px-5 py-3">
|
||
<h2 class="text-sm font-bold uppercase text-gray-500">{{ group.title }}</h2>
|
||
</div>
|
||
<div class="divide-y divide-gray-100">
|
||
<div v-for="item in group.items" :key="item.key" class="flex items-center justify-between px-5 py-4">
|
||
<div>
|
||
<p class="text-sm font-medium text-gray-900">{{ item.label }}</p>
|
||
<p class="text-xs text-gray-400">{{ item.key }}</p>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<input
|
||
v-model="settingsValues[item.key]"
|
||
class="input-field !w-64 !py-1.5 text-sm text-right"
|
||
:placeholder="item.placeholder || ''"
|
||
@keydown.enter="saveKey(item.key)"
|
||
/>
|
||
<button
|
||
@click="saveKey(item.key)"
|
||
:disabled="savingKey === item.key"
|
||
class="rounded-lg bg-primary-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-primary-700 disabled:opacity-50"
|
||
>
|
||
{{ savingKey === item.key ? '...' : 'Сохранить' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Custom settings -->
|
||
<div class="rounded-xl border border-gray-200 bg-white">
|
||
<div class="border-b border-gray-200 px-5 py-3 flex items-center justify-between">
|
||
<h2 class="text-sm font-bold uppercase text-gray-500">Все настройки</h2>
|
||
<button @click="showAddModal = true" class="text-xs font-medium text-primary-600 hover:text-primary-700">
|
||
+ Добавить
|
||
</button>
|
||
</div>
|
||
<div v-if="allSettings.length === 0" class="px-5 py-8 text-center text-sm text-gray-400">
|
||
Настроек пока нет
|
||
</div>
|
||
<div v-else class="divide-y divide-gray-100">
|
||
<div v-for="s in allSettings" :key="s.key" class="flex items-center justify-between px-5 py-3">
|
||
<div class="flex-1 min-w-0 mr-4">
|
||
<p class="text-sm font-mono text-gray-700 truncate">{{ s.key }}</p>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-sm text-gray-900 max-w-xs truncate">{{ s.value }}</span>
|
||
<span class="text-xs text-gray-400">{{ formatDate(s.updated_at) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Add setting modal -->
|
||
<Teleport to="body">
|
||
<div v-if="showAddModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/30" @click.self="showAddModal = false">
|
||
<div class="w-full max-w-sm rounded-xl bg-white p-6 shadow-2xl">
|
||
<h3 class="text-lg font-bold text-gray-900 mb-4">Новая настройка</h3>
|
||
<form @submit.prevent="addSetting" class="space-y-3">
|
||
<div>
|
||
<label class="mb-1 block text-xs font-semibold uppercase text-gray-500">Ключ</label>
|
||
<input v-model="newKey" required class="input-field" placeholder="time_rate_per_hour" />
|
||
</div>
|
||
<div>
|
||
<label class="mb-1 block text-xs font-semibold uppercase text-gray-500">Значение</label>
|
||
<input v-model="newValue" required class="input-field" placeholder="200" />
|
||
</div>
|
||
<div class="flex justify-end gap-2 pt-2">
|
||
<button type="button" @click="showAddModal = false" class="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50">
|
||
Отмена
|
||
</button>
|
||
<button type="submit" class="btn-primary text-sm">Создать</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</Teleport>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted } from 'vue'
|
||
import api from '../../api/client'
|
||
|
||
const loading = ref(true)
|
||
const allSettings = ref([])
|
||
const settingsValues = ref({})
|
||
const savingKey = ref(null)
|
||
const showAddModal = ref(false)
|
||
const newKey = ref('')
|
||
const newValue = ref('')
|
||
|
||
const settingsGroups = [
|
||
{
|
||
title: 'Расчёт стоимости',
|
||
items: [
|
||
{ key: 'time_rate_per_hour', label: 'Ставка за час печати (руб)', placeholder: '200' },
|
||
{ key: 'sanding_cost', label: 'Стоимость шлифовки (руб/шт)', placeholder: '300' },
|
||
{ key: 'painting_cost', label: 'Стоимость покраски (руб/шт)', placeholder: '500' },
|
||
{ key: 'threading_cost', label: 'Стоимость резьбы (руб/шт)', placeholder: '200' },
|
||
{ key: 'acetone_smoothing_cost', label: 'Ацетоновая обработка (руб/шт)', placeholder: '400' },
|
||
],
|
||
},
|
||
{
|
||
title: 'AI-ассистент',
|
||
items: [
|
||
{ key: 'ai_use_proxy', label: 'Использовать AI-прокси (true/false)', placeholder: 'true' },
|
||
{ key: 'ai_proxy_url', label: 'URL прокси', placeholder: 'http://82.22.174.14:8001' },
|
||
{ key: 'ai_proxy_salt', label: 'Секретная соль прокси', placeholder: '' },
|
||
{ key: 'ai_direct_api_key', label: 'Qwen API Key (DashScope)', placeholder: '' },
|
||
],
|
||
},
|
||
{
|
||
title: 'Уведомления',
|
||
items: [
|
||
{ key: 'telegram_enabled', label: 'Telegram уведомления (true/false)', placeholder: 'true' },
|
||
{ key: 'company_name', label: 'Название компании', placeholder: 'Bambu Russia' },
|
||
{ key: 'company_phone', label: 'Телефон', placeholder: '+7 (999) 123-45-67' },
|
||
{ key: 'company_email', label: 'Email', placeholder: 'info@bamburussia.ru' },
|
||
],
|
||
},
|
||
]
|
||
|
||
onMounted(() => loadSettings())
|
||
|
||
async function loadSettings() {
|
||
loading.value = true
|
||
try {
|
||
const { data } = await api.get('/admin/settings')
|
||
allSettings.value = data
|
||
const map = {}
|
||
data.forEach((s) => { map[s.key] = s.value })
|
||
settingsValues.value = map
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function saveKey(key) {
|
||
savingKey.value = key
|
||
try {
|
||
await api.put(`/admin/settings/${key}`, { value: settingsValues.value[key] || '' })
|
||
await loadSettings()
|
||
} finally {
|
||
savingKey.value = null
|
||
}
|
||
}
|
||
|
||
async function addSetting() {
|
||
await api.put(`/admin/settings/${newKey.value}`, { value: newValue.value })
|
||
showAddModal.value = false
|
||
newKey.value = ''
|
||
newValue.value = ''
|
||
await loadSettings()
|
||
}
|
||
|
||
function formatDate(iso) {
|
||
if (!iso) return ''
|
||
return new Date(iso).toLocaleString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })
|
||
}
|
||
</script>
|