- 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
125 lines
5.1 KiB
Vue
125 lines
5.1 KiB
Vue
<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>
|
||
|