feat: add FormKit framework support and update dependencies
- Add `telecart_forms` table migration and default checkout form seeder - Implement `FormsHandler` to fetch form schemas - Update `OrderCreateService` to handle custom fields in order comments - Add `update` method to QueryBuilder and Grammar - Add `Arr::except` helper - Update composer dependencies (Carbon, Symfony, PHPUnit, etc.) - Improve `MigratorService` error handling - Add unit tests for new functionality
This commit is contained in:
124
frontend/admin/src/components/FormBuilder/IconPicker.vue
Normal file
124
frontend/admin/src/components/FormBuilder/IconPicker.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="tw:flex tw:gap-2">
|
||||
<div
|
||||
class="tw:flex-1 tw:border tw:border-gray-300 tw:rounded-md tw:p-2 tw:flex tw:items-center tw:gap-2 tw:cursor-pointer hover:tw:bg-gray-50 tw:min-h-[42px]"
|
||||
@click="visible = true"
|
||||
>
|
||||
<div v-if="modelValue" class="tw:w-5 tw:h-5 tw:text-gray-600 tw:flex tw:items-center tw:justify-center" v-html="getIconSvg(modelValue)"></div>
|
||||
<span v-if="modelValue" class="tw:text-sm tw:text-gray-700">{{ modelValue }}</span>
|
||||
<span v-else class="tw:text-sm tw:text-gray-400">Выберите иконку...</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
v-if="modelValue"
|
||||
icon="fa fa-times"
|
||||
text
|
||||
rounded
|
||||
severity="secondary"
|
||||
@click="emit('update:modelValue', null)"
|
||||
v-tooltip="'Очистить'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
v-model:visible="visible"
|
||||
modal
|
||||
header="Выберите иконку"
|
||||
:style="{ width: '50vw', maxWidth: '600px' }"
|
||||
:breakpoints="{ '960px': '75vw', '640px': '90vw' }"
|
||||
>
|
||||
<div class="tw:flex tw:flex-col tw:gap-4">
|
||||
<IconField>
|
||||
<InputIcon class="fa fa-search" />
|
||||
<InputText v-model="searchQuery" placeholder="Поиск иконки..." class="tw:w-full" />
|
||||
</IconField>
|
||||
|
||||
<div class="tw:grid tw:grid-cols-6 sm:tw:grid-cols-8 md:tw:grid-cols-12 tw:gap-1 tw:max-h-[400px] tw:overflow-y-auto tw:p-1">
|
||||
<div
|
||||
v-for="iconName in filteredIcons"
|
||||
:key="iconName"
|
||||
class="tw:flex tw:flex-col tw:items-center tw:justify-between tw:p-1 tw:border tw:rounded tw:cursor-pointer hover:tw:bg-blue-50 hover:tw:border-blue-200 tw:transition-colors tw:aspect-square"
|
||||
:class="{ 'tw:bg-blue-100 tw:border-blue-400': modelValue === iconName }"
|
||||
@click="selectIcon(iconName)"
|
||||
>
|
||||
<div class="tw:flex-1 tw:flex tw:items-center tw:justify-center tw:w-full tw:min-h-0 tw:text-gray-700 tw:[&>svg]:w-15 tw:[&>svg]:h-15" v-html="getIconSvg(iconName)"></div>
|
||||
<span class="tw:text-[9px] tw:text-gray-500 tw:truncate tw:w-full tw:text-center tw:mt-0.5 tw:flex-shrink-0" :title="iconName">{{ iconName }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredIcons.length === 0" class="tw:col-span-full tw:text-center tw:text-gray-500 tw:py-8">
|
||||
Ничего не найдено
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import * as icons from '@formkit/icons';
|
||||
import {Button, IconField, InputIcon, InputText, Dialog} from 'primevue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const visible = ref(false);
|
||||
const searchQuery = ref('');
|
||||
|
||||
// Собираем все иконки из экспорта @formkit/icons
|
||||
// genesisIcons входит сюда как подмножество, но также там есть и другие наборы (например, feather, fontawesome и т.д. если они были бы установлены,
|
||||
// но в стандартном пакете @formkit/icons есть только genesis, application, brand, currency, direction, file, input, payment, social, etc.)
|
||||
// Пройдемся по всему объекту icons и соберем все строки-SVG.
|
||||
// Но структура экспорта @formkit/icons может быть такой:
|
||||
// export { genesisIcons } ...
|
||||
// export { ... }
|
||||
// Реально пакет содержит много наборов.
|
||||
// Давайте соберем их все в один плоский список.
|
||||
|
||||
const allIconsMap = {};
|
||||
|
||||
// Функция для рекурсивного/плоского сбора иконок, если они сгруппированы
|
||||
Object.entries(icons).forEach(([key, value]) => {
|
||||
if (typeof value === 'string' && value.startsWith('<svg')) {
|
||||
// Это прямая иконка (если вдруг)
|
||||
allIconsMap[key] = value;
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
// Это группа иконок (например genesisIcons)
|
||||
Object.entries(value).forEach(([iconName, svgContent]) => {
|
||||
if (typeof svgContent === 'string' && svgContent.startsWith('<svg')) {
|
||||
// Если имя уже есть, не перезаписываем или перезаписываем - не важно, главное чтобы был доступ.
|
||||
// Лучше сохранить оригинальное имя.
|
||||
allIconsMap[iconName] = svgContent;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const allIconNames = Object.keys(allIconsMap).sort();
|
||||
|
||||
const filteredIcons = computed(() => {
|
||||
if (!searchQuery.value) return allIconNames;
|
||||
const lower = searchQuery.value.toLowerCase();
|
||||
return allIconNames.filter(name => name.toLowerCase().includes(lower));
|
||||
});
|
||||
|
||||
function getIconSvg(iconName) {
|
||||
return allIconsMap[iconName];
|
||||
}
|
||||
|
||||
function selectIcon(iconName) {
|
||||
emit('update:modelValue', iconName);
|
||||
visible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user