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:
2025-11-15 01:23:17 +03:00
committed by Nikita Kiselev
parent ae9771dec4
commit 6a59dcc0c9
69 changed files with 12529 additions and 416 deletions

View 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>