дизайн меню

This commit is contained in:
2025-07-27 07:47:18 +03:00
parent bfaba2723c
commit 4752b2dedc
2 changed files with 282 additions and 223 deletions

View File

@@ -14,12 +14,14 @@ interface CategoryFieldsSelectorProps {
selectedValues: AttributeValue[]
onValuesChange: (values: AttributeValue[]) => void
disabled?: boolean
compact?: boolean // Новый проп для компактного отображения
}
export const CategoryFieldsSelector: React.FC<CategoryFieldsSelectorProps> = ({
selectedValues,
onValuesChange,
disabled = false,
compact = false, // По умолчанию обычный режим
}) => {
const {
attributes,
@@ -56,7 +58,10 @@ export const CategoryFieldsSelector: React.FC<CategoryFieldsSelectorProps> = ({
return (
<div key={attribute.id} className="space-y-2">
<Label htmlFor={attribute.id} className="text-sm font-medium">
<Label
htmlFor={attribute.id}
className={`text-sm font-medium ${compact ? 'text-xs' : ''}`}
>
{attribute.name}
{attribute.is_required && (
<span className="ml-1 text-red-500">*</span>
@@ -68,20 +73,48 @@ export const CategoryFieldsSelector: React.FC<CategoryFieldsSelectorProps> = ({
id={attribute.id}
value={value}
onChange={e => updateAttributeValue(attribute.id, e.target.value)}
placeholder={`Введите ${attribute.name.toLowerCase()}`}
placeholder={
compact ? attribute.name : `Введите ${attribute.name.toLowerCase()}`
}
className={compact ? 'h-8 text-sm' : ''}
{...fieldProps}
/>
{attribute.is_required && !value && (
<p className="flex items-center gap-1 text-xs text-red-500">
<p
className={`flex items-center gap-1 text-red-500 ${compact ? 'text-xs' : 'text-xs'}`}
>
<AlertCircle className="h-3 w-3" />
Это поле обязательно для заполнения
{compact ? 'Обязательно' : 'Это поле обязательно для заполнения'}
</p>
)}
</div>
)
}
const renderAttributeSection = (
attributesList: Attribute[],
title: string,
titleColor: string = ''
) => {
if (attributesList.length === 0) return null
return (
<div className={compact ? 'space-y-3' : 'space-y-4'}>
<div className="flex items-center gap-2">
<Label
className={`text-sm font-semibold ${titleColor} ${compact ? 'text-xs' : ''}`}
>
{title}
</Label>
</div>
<div className={compact ? 'grid grid-cols-2 gap-3' : 'space-y-4'}>
{attributesList.map(renderAttribute)}
</div>
</div>
)
}
if (isLoading) {
return (
<div className="space-y-4">
@@ -93,34 +126,16 @@ export const CategoryFieldsSelector: React.FC<CategoryFieldsSelectorProps> = ({
}
return (
<div className="space-y-6">
<div className={compact ? 'space-y-4' : 'space-y-6'}>
{/* Обязательные атрибуты */}
{requiredAttributes.length > 0 && (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Label className="text-sm font-semibold text-red-600">
Обязательные атрибуты
</Label>
</div>
<div className="space-y-4">
{requiredAttributes.map(renderAttribute)}
</div>
</div>
{renderAttributeSection(
requiredAttributes,
'Обязательные атрибуты',
'text-red-600'
)}
{/* Необязательные атрибуты */}
{optionalAttributes.length > 0 && (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Label className="text-sm font-semibold">
Дополнительные атрибуты
</Label>
</div>
<div className="space-y-4">
{optionalAttributes.map(renderAttribute)}
</div>
</div>
)}
{renderAttributeSection(optionalAttributes, 'Дополнительные атрибуты')}
{attributes.length === 0 && (
<div className="py-4 text-center">

View File

@@ -4,12 +4,12 @@ import {
TemplateFilters,
} from '@/component/TemplateManager/TemplateFilterSidebar'
import { Button } from '@/component/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card'
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
@@ -32,7 +32,18 @@ import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
import { Template, TemplateAttributeDetail } from '@/type/template'
import TemplateCard from '@/widget/template/ui/TemplateCard'
import clsx from 'clsx'
import { CheckSquare, Plus, Square, Trash2 } from 'lucide-react'
import {
CheckSquare,
Copy,
Edit3,
FileText,
Plus,
Settings2,
Square,
Tag,
Trash2,
Type,
} from 'lucide-react'
import { useCallback, useMemo, useState } from 'react'
interface AttributeValue {
@@ -65,6 +76,172 @@ const fromCategoryAttributeValues = (
}))
}
// Компактный селектор атрибутов
interface CompactCategoryFieldsSelectorProps {
selectedValues: CategoryAttributeValue[]
onValuesChange: (values: CategoryAttributeValue[]) => void
isLoading?: boolean
}
const CompactCategoryFieldsSelector: React.FC<
CompactCategoryFieldsSelectorProps
> = ({ selectedValues, onValuesChange, isLoading = false }) => {
if (isLoading) {
return (
<div className="py-4 text-center text-sm text-muted-foreground">
Загрузка атрибутов...
</div>
)
}
return (
<div className="space-y-3">
<CategoryFieldsSelector
selectedValues={selectedValues}
onValuesChange={onValuesChange}
compact={true} // Передаем флаг для компактного отображения
/>
</div>
)
}
// Общий компонент формы шаблона
interface TemplateFormProps {
title: string
description: string
icon: React.ReactNode
iconColor: string
name: string
onNameChange: (value: string) => void
templateDescription: string
onDescriptionChange: (value: string) => void
attributeValues: AttributeValue[]
onAttributeValuesChange: (values: AttributeValue[]) => void
attributesLoading: boolean
onSubmit: () => void
onCancel: () => void
submitText: string
submitIcon: React.ReactNode
isSubmitDisabled: boolean
nameLabel?: string
descriptionLabel?: string
}
const TemplateForm: React.FC<TemplateFormProps> = ({
title,
description,
icon,
iconColor,
name,
onNameChange,
templateDescription,
onDescriptionChange,
attributeValues,
onAttributeValuesChange,
attributesLoading,
onSubmit,
onCancel,
submitText,
submitIcon,
isSubmitDisabled,
nameLabel = 'Название шаблона',
descriptionLabel = 'Описание',
}) => {
return (
<>
<DialogHeader className="pb-4">
<DialogTitle className="flex items-center gap-2 text-lg">
{icon}
{title}
</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<div className="max-h-[50vh] space-y-4 overflow-y-auto pr-2">
{/* Основные настройки */}
<Card>
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<Settings2 className={`h-4 w-4 ${iconColor}`} />
<CardTitle className="text-sm">Основная информация</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label className="flex items-center gap-2 text-sm font-medium">
<Type className="h-3 w-3" />
{nameLabel}
</Label>
<Input
placeholder={`Введите ${nameLabel.toLowerCase()}`}
value={name}
onChange={e => onNameChange(e.target.value)}
className="h-9"
/>
</div>
<div className="space-y-2">
<Label className="flex items-center gap-2 text-sm font-medium">
<FileText className="h-3 w-3" />
{descriptionLabel}
</Label>
<Textarea
placeholder={`${descriptionLabel}...`}
value={templateDescription}
onChange={e => onDescriptionChange(e.target.value)}
className="min-h-[80px] resize-none"
/>
</div>
</CardContent>
</Card>
{/* Атрибуты */}
<Card>
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<Tag className="h-4 w-4 text-purple-600" />
<CardTitle className="text-sm">Атрибуты шаблона</CardTitle>
</div>
</CardHeader>
<CardContent>
<CompactCategoryFieldsSelector
selectedValues={toCategoryAttributeValues(attributeValues)}
onValuesChange={values =>
onAttributeValuesChange(fromCategoryAttributeValues(values))
}
isLoading={attributesLoading}
/>
</CardContent>
</Card>
</div>
<Separator className="my-4" />
<div className="flex items-center justify-between pt-2">
<div className="text-sm text-muted-foreground">
{!name.trim() && (
<span className="text-amber-600">
Заполните {nameLabel.toLowerCase()}
</span>
)}
</div>
<div className="flex gap-3">
<DialogClose asChild>
<Button variant="outline">Отмена</Button>
</DialogClose>
<Button
onClick={onSubmit}
disabled={isSubmitDisabled}
className="min-w-[130px]"
>
{submitIcon}
{submitText}
</Button>
</div>
</div>
</>
)
}
export const TemplatesOverviewPage = () => {
const {
templates,
@@ -360,6 +537,7 @@ export const TemplatesOverviewPage = () => {
Выбрать
</Button>
{/* Диалог создания шаблона */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button size="sm">
@@ -367,67 +545,25 @@ export const TemplatesOverviewPage = () => {
Создать
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Создать новый шаблон</DialogTitle>
<DialogDescription>
Заполните основную информацию о шаблоне
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="template-name" className="text-right">
Название
</Label>
<Input
id="template-name"
className="col-span-3"
value={newName}
onChange={e => setNewName(e.target.value)}
placeholder="Введите название шаблона"
<DialogContent className="max-h-[90vh] max-w-2xl overflow-hidden">
<TemplateForm
title="Создать новый шаблон"
description="Заполните основную информацию о шаблоне протокола"
icon={<Plus className="h-5 w-5 text-blue-600" />}
iconColor="text-blue-600"
name={newName}
onNameChange={setNewName}
templateDescription={newDescription}
onDescriptionChange={setNewDescription}
attributeValues={newAttributeValues}
onAttributeValuesChange={setNewAttributeValues}
attributesLoading={attributesLoading}
onSubmit={handleCreate}
onCancel={() => setIsDialogOpen(false)}
submitText="Создать"
submitIcon={<Plus className="mr-2 h-4 w-4" />}
isSubmitDisabled={!newName.trim()}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label
htmlFor="template-description"
className="text-right"
>
Описание
</Label>
<Textarea
id="template-description"
className="col-span-3"
value={newDescription}
onChange={e => setNewDescription(e.target.value)}
placeholder="Введите описание"
/>
</div>
<div className="grid grid-cols-4 gap-4">
<Label className="self-start pt-2 text-right">
Атрибуты
</Label>
<div className="col-span-3">
{!attributesLoading && (
<CategoryFieldsSelector
selectedValues={toCategoryAttributeValues(
newAttributeValues
)}
onValuesChange={values =>
setNewAttributeValues(
fromCategoryAttributeValues(values)
)
}
/>
)}
</div>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Отмена</Button>
</DialogClose>
<Button onClick={handleCreate}>Создать</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -436,72 +572,27 @@ export const TemplatesOverviewPage = () => {
open={isCopyDialogOpen}
onOpenChange={setIsCopyDialogOpen}
>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Копировать шаблон</DialogTitle>
<DialogDescription>
Укажите название и описание для копии шаблона
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="copy-name" className="text-right">
Название
</Label>
<Input
id="copy-name"
className="col-span-3"
value={copyName}
onChange={e => setCopyName(e.target.value)}
placeholder="Введите название"
<DialogContent className="max-h-[90vh] max-w-2xl overflow-hidden">
<TemplateForm
title="Копировать шаблон"
description="Укажите название и описание для копии шаблона"
icon={<Copy className="h-5 w-5 text-green-600" />}
iconColor="text-green-600"
name={copyName}
onNameChange={setCopyName}
templateDescription={copyDescription}
onDescriptionChange={setCopyDescription}
attributeValues={copyAttributeValues}
onAttributeValuesChange={setCopyAttributeValues}
attributesLoading={attributesLoading}
onSubmit={handleCopyConfirm}
onCancel={() => setIsCopyDialogOpen(false)}
submitText="Копировать"
submitIcon={<Copy className="mr-2 h-4 w-4" />}
isSubmitDisabled={!copyName.trim()}
nameLabel="Название копии"
descriptionLabel="Описание копии"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label
htmlFor="copy-description"
className="text-right"
>
Описание
</Label>
<Textarea
id="copy-description"
className="col-span-3"
value={copyDescription}
onChange={e => setCopyDescription(e.target.value)}
placeholder="Введите описание"
/>
</div>
<div className="grid grid-cols-4 gap-4">
<Label className="self-start pt-2 text-right">
Атрибуты
</Label>
<div className="col-span-3">
{!attributesLoading && (
<CategoryFieldsSelector
selectedValues={toCategoryAttributeValues(
copyAttributeValues
)}
onValuesChange={values =>
setCopyAttributeValues(
fromCategoryAttributeValues(values)
)
}
/>
)}
</div>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Отмена</Button>
</DialogClose>
<Button
onClick={handleCopyConfirm}
disabled={!copyName.trim()}
>
Копировать
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -510,72 +601,25 @@ export const TemplatesOverviewPage = () => {
open={isEditDialogOpen}
onOpenChange={setIsEditDialogOpen}
>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Редактировать шаблон</DialogTitle>
<DialogDescription>
Измените название и описание шаблона
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="edit-name" className="text-right">
Название
</Label>
<Input
id="edit-name"
className="col-span-3"
value={editName}
onChange={e => setEditName(e.target.value)}
placeholder="Введите название"
<DialogContent className="max-h-[90vh] max-w-2xl overflow-hidden">
<TemplateForm
title="Редактировать шаблон"
description="Измените основную информацию о шаблоне"
icon={<Edit3 className="h-5 w-5 text-orange-600" />}
iconColor="text-orange-600"
name={editName}
onNameChange={setEditName}
templateDescription={editDescription}
onDescriptionChange={setEditDescription}
attributeValues={editAttributeValues}
onAttributeValuesChange={setEditAttributeValues}
attributesLoading={attributesLoading}
onSubmit={handleEditConfirm}
onCancel={() => setIsEditDialogOpen(false)}
submitText="Сохранить"
submitIcon={<Edit3 className="mr-2 h-4 w-4" />}
isSubmitDisabled={!editName.trim()}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label
htmlFor="edit-description"
className="text-right"
>
Описание
</Label>
<Textarea
id="edit-description"
className="col-span-3"
value={editDescription}
onChange={e => setEditDescription(e.target.value)}
placeholder="Введите описание"
/>
</div>
<div className="grid grid-cols-4 gap-4">
<Label className="self-start pt-2 text-right">
Атрибуты
</Label>
<div className="col-span-3">
{!attributesLoading && (
<CategoryFieldsSelector
selectedValues={toCategoryAttributeValues(
editAttributeValues
)}
onValuesChange={values =>
setEditAttributeValues(
fromCategoryAttributeValues(values)
)
}
/>
)}
</div>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Отмена</Button>
</DialogClose>
<Button
onClick={handleEditConfirm}
disabled={!editName.trim()}
>
Сохранить
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>