новый дизайн

This commit is contained in:
2026-07-25 18:36:45 +04:00
parent df7f98fe8b
commit c43a73e148
54 changed files with 4234 additions and 1110 deletions
@@ -0,0 +1,69 @@
<script setup>
import { useForm } from '@inertiajs/vue3';
const props = defineProps({
category: { type: Object, default: null },
action: { type: String, required: true },
submitLabel: { type: String, required: true },
});
const form = useForm({
name: props.category?.name || '',
icon: props.category?.icon || '',
requiredRank: props.category?.required_rank ?? 0,
flags: props.category?.flags ?? 0,
enabled: props.category?.enabled ?? true,
});
function submit() {
const options = {
preserveScroll: true,
onSuccess: () => {
if (!props.category) form.reset();
},
};
if (props.category) {
form.patch(props.action, options);
} else {
form.post(props.action, options);
}
}
</script>
<template>
<form class="admin-form" @submit.prevent="submit">
<div class="admin-form-grid">
<label class="admin-field admin-field--wide">
<span>Название</span>
<input v-model="form.name" maxlength="120" required />
<small v-if="form.errors.name">{{ form.errors.name }}</small>
</label>
<label class="admin-field admin-field--wide">
<span>Иконка клиента</span>
<input v-model="form.icon" maxlength="255" placeholder="inv_helmet_96" />
<small v-if="form.errors.icon">{{ form.errors.icon }}</small>
</label>
<label class="admin-field">
<span>Требуемый ранг</span>
<input v-model="form.requiredRank" type="number" min="0" max="255" required />
<small v-if="form.errors.requiredRank">{{ form.errors.requiredRank }}</small>
</label>
<label class="admin-field">
<span>Флаги</span>
<input v-model="form.flags" type="number" min="0" required />
<small v-if="form.errors.flags">{{ form.errors.flags }}</small>
</label>
</div>
<label class="admin-check">
<input v-model="form.enabled" type="checkbox" />
<span class="admin-check__box"></span>
<span>Категория включена</span>
</label>
<button class="btn btn-primary admin-submit" type="submit" :disabled="form.processing">
{{ form.processing ? 'Сохраняем…' : submitLabel }}
</button>
</form>
</template>
@@ -0,0 +1,90 @@
<script setup>
import { router, useForm } from '@inertiajs/vue3';
const props = defineProps({
item: { type: Object, default: null },
action: { type: String, required: true },
destroyUrl: { type: String, default: null },
allowImage: { type: Boolean, default: false },
labels: { type: Object, required: true },
initial: { type: Object, default: () => ({}) },
});
const form = useForm({
title: props.item?.title ?? props.initial.title ?? '',
sort_order: props.item?.sort_order ?? props.initial.sort_order ?? 0,
is_published: props.item?.is_published ?? props.initial.is_published ?? true,
body: props.item?.body ?? props.initial.body ?? '',
...(props.allowImage ? { image: null } : {}),
});
function submit() {
const options = {
preserveScroll: true,
forceFormData: props.allowImage,
onSuccess: () => {
if (!props.item) form.reset();
},
};
if (props.item && props.allowImage) {
form.transform((data) => ({ ...data, _method: 'patch' })).post(props.action, options);
} else if (props.item) {
form.patch(props.action, options);
} else {
form.post(props.action, options);
}
}
function destroyItem() {
if (props.destroyUrl && window.confirm(props.labels.deleteConfirm)) {
router.delete(props.destroyUrl, { preserveScroll: true });
}
}
</script>
<template>
<form class="admin-form" @submit.prevent="submit">
<div class="admin-form-grid">
<label class="admin-field admin-field--wide">
<span>{{ labels.title }}</span>
<input v-model="form.title" maxlength="120" required />
<small v-if="form.errors.title">{{ form.errors.title }}</small>
</label>
<label class="admin-field">
<span>Порядок вывода</span>
<input v-model="form.sort_order" type="number" min="0" max="9999" required />
<small v-if="form.errors.sort_order">{{ form.errors.sort_order }}</small>
</label>
</div>
<label class="admin-check">
<input v-model="form.is_published" type="checkbox" />
<span class="admin-check__box"></span>
<span>{{ labels.published }}</span>
</label>
<label class="admin-field">
<span>{{ labels.body }}</span>
<textarea v-model="form.body" rows="10" required></textarea>
<em v-if="labels.bodyHint">{{ labels.bodyHint }}</em>
<small v-if="form.errors.body">{{ form.errors.body }}</small>
</label>
<label v-if="allowImage" class="admin-field admin-upload">
<span>{{ item ? 'Заменить картинку' : 'Картинка' }}</span>
<input type="file" accept="image/*" @input="form.image = $event.target.files[0]" />
<em>{{ item ? 'Оставь пустым, чтобы сохранить текущую.' : 'Максимум 2 МБ. Необязательно.' }}</em>
<small v-if="form.errors.image">{{ form.errors.image }}</small>
</label>
<div class="admin-form-actions">
<button class="btn btn-primary admin-submit" type="submit" :disabled="form.processing">
{{ form.processing ? 'Сохраняем…' : labels.submit }}
</button>
<button v-if="destroyUrl" class="btn btn-ghost admin-danger-button" type="button" @click="destroyItem">
{{ labels.delete }}
</button>
</div>
</form>
</template>
@@ -0,0 +1,149 @@
<script setup>
import { useForm } from '@inertiajs/vue3';
const props = defineProps({
product: { type: Object, default: null },
categories: { type: Array, required: true },
productTypes: { type: Array, required: true },
action: { type: String, required: true },
submitLabel: { type: String, required: true },
});
const initialRewards = Object.fromEntries(
Array.from({ length: 8 }, (_, index) => {
const slot = index + 1;
const reward = props.product?.rewards?.find((item) => item.slot === slot);
return [
[`reward_${slot}`, reward?.id ?? 0],
[`rewardcount_${slot}`, reward?.count ?? 0],
];
}).flat(),
);
const form = useForm({
category_ids: props.product?.category_ids || [],
type: props.product?.type ?? 1,
name: props.product?.name || '',
tooltipName: props.product?.tooltip_name || '',
tooltipType: props.product?.tooltip_type || '',
tooltipText: props.product?.tooltip_text || '',
icon: props.product?.icon || '',
price: props.product?.price ?? 1,
hyperlinkId: props.product?.hyperlink_id ?? 0,
creatureEntry: props.product?.creature_entry ?? 0,
discountAmount: props.product?.discount_amount ?? 0,
flags: props.product?.flags ?? 0,
...initialRewards,
new: props.product?.is_new ?? false,
enabled: props.product?.enabled ?? true,
});
function submit() {
const options = { preserveScroll: true };
if (props.product) {
form.patch(props.action, options);
} else {
form.post(props.action, {
...options,
onSuccess: () => form.reset(),
});
}
}
</script>
<template>
<form class="admin-form" @submit.prevent="submit">
<div class="admin-form-grid">
<label class="admin-field admin-field--wide">
<span>Категории</span>
<select v-model="form.category_ids" multiple :size="Math.min(Math.max(categories.length, 2), 8)" required>
<option v-for="category in categories" :key="category.id" :value="category.id">{{ category.name }}</option>
</select>
<em>Можно выбрать несколько с Ctrl/Cmd.</em>
<small v-if="form.errors.category_ids">{{ form.errors.category_ids }}</small>
</label>
<label class="admin-field">
<span>Тип</span>
<select v-model="form.type" required>
<option v-for="type in productTypes" :key="type.value" :value="type.value">{{ type.label }} ({{ type.value }})</option>
</select>
<small v-if="form.errors.type">{{ form.errors.type }}</small>
</label>
<label class="admin-field admin-field--wide">
<span>Название</span>
<textarea v-model="form.name" rows="3" required></textarea>
<small v-if="form.errors.name">{{ form.errors.name }}</small>
</label>
<label class="admin-field">
<span>Название подсказки</span>
<input v-model="form.tooltipName" />
</label>
<label class="admin-field">
<span>Тип подсказки</span>
<input v-model="form.tooltipType" placeholder="item или spell" />
</label>
<label class="admin-field admin-field--wide">
<span>Текст подсказки</span>
<textarea v-model="form.tooltipText" rows="5"></textarea>
</label>
<label class="admin-field admin-field--wide">
<span>Иконка клиента</span>
<input v-model="form.icon" placeholder="inv_sword_22" />
</label>
<label class="admin-field">
<span>Цена, Слёзы Элуны</span>
<input v-model="form.price" type="number" min="1" required />
<small v-if="form.errors.price">{{ form.errors.price }}</small>
</label>
<label class="admin-field">
<span>Скидка, %</span>
<input v-model="form.discountAmount" type="number" min="0" max="100" />
</label>
<label class="admin-field">
<span>Hyperlink ID</span>
<input v-model="form.hyperlinkId" type="number" min="0" />
</label>
<label class="admin-field">
<span>Creature Entry</span>
<input v-model="form.creatureEntry" type="number" min="0" />
</label>
<label class="admin-field">
<span>Флаги</span>
<input v-model="form.flags" type="number" min="0" />
</label>
</div>
<details class="admin-subform">
<summary>Награды · 8 слотов</summary>
<div class="admin-rewards">
<div v-for="slot in 8" :key="slot">
<label class="admin-field">
<span>Награда {{ slot }}</span>
<input v-model="form[`reward_${slot}`]" type="number" min="0" />
</label>
<label class="admin-field">
<span>Количество {{ slot }}</span>
<input v-model="form[`rewardcount_${slot}`]" type="number" min="0" />
</label>
</div>
</div>
</details>
<div class="admin-check-row">
<label class="admin-check">
<input v-model="form.new" type="checkbox" />
<span class="admin-check__box"></span>
<span>Пометка «Новинка»</span>
</label>
<label class="admin-check">
<input v-model="form.enabled" type="checkbox" />
<span class="admin-check__box"></span>
<span>Позиция включена</span>
</label>
</div>
<button class="btn btn-primary admin-submit" type="submit" :disabled="form.processing">
{{ form.processing ? 'Сохраняем…' : submitLabel }}
</button>
</form>
</template>
@@ -0,0 +1,15 @@
<script setup>
defineProps({
items: { type: Array, required: true },
});
</script>
<template>
<div class="admin-stat-grid">
<article v-for="item in items" :key="item.label" class="portal-card admin-stat">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
<small v-if="item.note">{{ item.note }}</small>
</article>
</div>
</template>