дизайн меню

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

View File

@@ -4,12 +4,12 @@ import {
TemplateFilters, TemplateFilters,
} from '@/component/TemplateManager/TemplateFilterSidebar' } from '@/component/TemplateManager/TemplateFilterSidebar'
import { Button } from '@/component/ui/button' import { Button } from '@/component/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card'
import { import {
Dialog, Dialog,
DialogClose, DialogClose,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
@@ -32,7 +32,18 @@ import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
import { Template, TemplateAttributeDetail } from '@/type/template' import { Template, TemplateAttributeDetail } from '@/type/template'
import TemplateCard from '@/widget/template/ui/TemplateCard' import TemplateCard from '@/widget/template/ui/TemplateCard'
import clsx from 'clsx' 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' import { useCallback, useMemo, useState } from 'react'
interface AttributeValue { 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 = () => { export const TemplatesOverviewPage = () => {
const { const {
templates, templates,
@@ -360,6 +537,7 @@ export const TemplatesOverviewPage = () => {
Выбрать Выбрать
</Button> </Button>
{/* Диалог создания шаблона */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button size="sm"> <Button size="sm">
@@ -367,67 +545,25 @@ export const TemplatesOverviewPage = () => {
Создать Создать
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-2xl"> <DialogContent className="max-h-[90vh] max-w-2xl overflow-hidden">
<DialogHeader> <TemplateForm
<DialogTitle>Создать новый шаблон</DialogTitle> title="Создать новый шаблон"
<DialogDescription> description="Заполните основную информацию о шаблоне протокола"
Заполните основную информацию о шаблоне icon={<Plus className="h-5 w-5 text-blue-600" />}
</DialogDescription> iconColor="text-blue-600"
</DialogHeader> name={newName}
<div className="grid gap-4 py-4"> onNameChange={setNewName}
<div className="grid grid-cols-4 items-center gap-4"> templateDescription={newDescription}
<Label htmlFor="template-name" className="text-right"> onDescriptionChange={setNewDescription}
Название attributeValues={newAttributeValues}
</Label> onAttributeValuesChange={setNewAttributeValues}
<Input attributesLoading={attributesLoading}
id="template-name" onSubmit={handleCreate}
className="col-span-3" onCancel={() => setIsDialogOpen(false)}
value={newName} submitText="Создать"
onChange={e => setNewName(e.target.value)} submitIcon={<Plus className="mr-2 h-4 w-4" />}
placeholder="Введите название шаблона" 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> </DialogContent>
</Dialog> </Dialog>
@@ -436,72 +572,27 @@ export const TemplatesOverviewPage = () => {
open={isCopyDialogOpen} open={isCopyDialogOpen}
onOpenChange={setIsCopyDialogOpen} onOpenChange={setIsCopyDialogOpen}
> >
<DialogContent className="max-w-md"> <DialogContent className="max-h-[90vh] max-w-2xl overflow-hidden">
<DialogHeader> <TemplateForm
<DialogTitle>Копировать шаблон</DialogTitle> title="Копировать шаблон"
<DialogDescription> description="Укажите название и описание для копии шаблона"
Укажите название и описание для копии шаблона icon={<Copy className="h-5 w-5 text-green-600" />}
</DialogDescription> iconColor="text-green-600"
</DialogHeader> name={copyName}
<div className="grid gap-4 py-4"> onNameChange={setCopyName}
<div className="grid grid-cols-4 items-center gap-4"> templateDescription={copyDescription}
<Label htmlFor="copy-name" className="text-right"> onDescriptionChange={setCopyDescription}
Название attributeValues={copyAttributeValues}
</Label> onAttributeValuesChange={setCopyAttributeValues}
<Input attributesLoading={attributesLoading}
id="copy-name" onSubmit={handleCopyConfirm}
className="col-span-3" onCancel={() => setIsCopyDialogOpen(false)}
value={copyName} submitText="Копировать"
onChange={e => setCopyName(e.target.value)} submitIcon={<Copy className="mr-2 h-4 w-4" />}
placeholder="Введите название" isSubmitDisabled={!copyName.trim()}
/> nameLabel="Название копии"
</div> descriptionLabel="Описание копии"
<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> </DialogContent>
</Dialog> </Dialog>
@@ -510,72 +601,25 @@ export const TemplatesOverviewPage = () => {
open={isEditDialogOpen} open={isEditDialogOpen}
onOpenChange={setIsEditDialogOpen} onOpenChange={setIsEditDialogOpen}
> >
<DialogContent className="max-w-md"> <DialogContent className="max-h-[90vh] max-w-2xl overflow-hidden">
<DialogHeader> <TemplateForm
<DialogTitle>Редактировать шаблон</DialogTitle> title="Редактировать шаблон"
<DialogDescription> description="Измените основную информацию о шаблоне"
Измените название и описание шаблона icon={<Edit3 className="h-5 w-5 text-orange-600" />}
</DialogDescription> iconColor="text-orange-600"
</DialogHeader> name={editName}
<div className="grid gap-4 py-4"> onNameChange={setEditName}
<div className="grid grid-cols-4 items-center gap-4"> templateDescription={editDescription}
<Label htmlFor="edit-name" className="text-right"> onDescriptionChange={setEditDescription}
Название attributeValues={editAttributeValues}
</Label> onAttributeValuesChange={setEditAttributeValues}
<Input attributesLoading={attributesLoading}
id="edit-name" onSubmit={handleEditConfirm}
className="col-span-3" onCancel={() => setIsEditDialogOpen(false)}
value={editName} submitText="Сохранить"
onChange={e => setEditName(e.target.value)} submitIcon={<Edit3 className="mr-2 h-4 w-4" />}
placeholder="Введите название" 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> </DialogContent>
</Dialog> </Dialog>
</> </>