Настойки внешний вид
This commit is contained in:
parent
75571cc96f
commit
f8ddae0f94
11 changed files with 563 additions and 127 deletions
130
FRONT/DEV/src/components/settings/SettingItem.tsx
Normal file
130
FRONT/DEV/src/components/settings/SettingItem.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import {Setting, useSettingsStore} from "../../store/settingsStore";
|
||||
import {ChangeEvent} from "react";
|
||||
import styled, {useTheme} from "styled-components";
|
||||
import {IconComponent} from "../../const/buttons/icons";
|
||||
import {buttonNames} from "../../const/buttons/names";
|
||||
import SettingSelect from "./SettingSelect";
|
||||
import {MainButton} from "../UI/buttons/mainButton";
|
||||
|
||||
const SettingBlock = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
margin-bottom: 18px;
|
||||
font-size: 18px;
|
||||
width: 90%;
|
||||
input{
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
background-color: ${props => props.theme.primary};
|
||||
border: 1px solid ${props => props.theme.borderColor};
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
input[type="number"]{
|
||||
width: 80px;
|
||||
}
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
`
|
||||
const StyledCheckbox = styled.input.attrs({ type: 'checkbox' })`
|
||||
width: 22px !important;
|
||||
height: 22px !important;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: ${props => props.theme.primary};
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
&:checked::after {
|
||||
background: ${props => props.theme.accent};
|
||||
}
|
||||
|
||||
`;
|
||||
const LabelCheckbox = styled.label`
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
svg{
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
export const SettingItem = ({ setting }: { setting: Setting }) => {
|
||||
const { updateSetting } = useSettingsStore();
|
||||
const theme = useTheme();
|
||||
|
||||
const handleChange = (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||
) => {
|
||||
const value =
|
||||
setting.type === 'boolean'
|
||||
? (e.target as HTMLInputElement).checked
|
||||
: e.target.value;
|
||||
updateSetting(setting.id, value);
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingBlock>
|
||||
<label>{setting.label}</label>
|
||||
|
||||
{setting.type === 'boolean' && (
|
||||
<LabelCheckbox>
|
||||
<StyledCheckbox />
|
||||
<IconComponent name={buttonNames[19]} width="18" height="18" setColor={theme.primary} />
|
||||
</LabelCheckbox>
|
||||
)}
|
||||
|
||||
{setting.type === 'select' && setting.options && (
|
||||
<SettingSelect
|
||||
value={setting.value}
|
||||
options={setting.options}
|
||||
onChange={(value) => updateSetting(setting.id, value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{setting.type === 'text' && (
|
||||
<input
|
||||
type="text"
|
||||
value={setting.value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{setting.type === 'number' && (
|
||||
<input
|
||||
type="number"
|
||||
min={setting?.min}
|
||||
max={setting?.max}
|
||||
value={setting.value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{setting.type === 'button' && (
|
||||
<MainButton
|
||||
type="button"
|
||||
size="small"
|
||||
title={setting.value}
|
||||
/>
|
||||
)}
|
||||
</SettingBlock>
|
||||
);
|
||||
};
|
||||
118
FRONT/DEV/src/components/settings/SettingSelect.tsx
Normal file
118
FRONT/DEV/src/components/settings/SettingSelect.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { IconComponent } from '../../const/buttons/icons';
|
||||
import {buttonNames} from "../../const/buttons/names";
|
||||
|
||||
interface SettingSelectProps {
|
||||
value: any;
|
||||
options: { value: any; label: string }[];
|
||||
onChange: (value: any) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const SelectWrapper = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
`;
|
||||
|
||||
const SelectHeader = styled.div<{ $isOpen: boolean; $disabled?: boolean }>`
|
||||
padding: 6px 8px;
|
||||
border: 1px solid ${props => props.theme.borderColor};
|
||||
border-radius: 4px;
|
||||
background-color: ${props => props.theme.primary};
|
||||
cursor: ${props => props.$disabled ? 'not-allowed' : 'pointer'};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
opacity: ${props => props.$disabled ? 0.6 : 1};
|
||||
font-size: 16px;
|
||||
`;
|
||||
|
||||
const SelectOptions = styled.div<{ $isOpen: boolean }>`
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 4px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: ${props => props.theme.primary};
|
||||
border: 1px solid ${props => props.theme.borderColor || '#ccc'};
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
z-index: 10;
|
||||
display: ${props => props.$isOpen ? 'block' : 'none'};
|
||||
`;
|
||||
|
||||
const OptionItem = styled.div<{ $isSelected: boolean }>`
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
background-color: ${props => props.$isSelected ? (props.theme.accent) : 'transparent'};
|
||||
color: ${props => props.$isSelected ? (props.theme.primary) : 'inherit'};
|
||||
`;
|
||||
|
||||
const ArrowIcon = styled.span<{ $isOpen: boolean }>`
|
||||
transition: transform 0.2s;
|
||||
transform: rotate(${props => props.$isOpen ? '90deg' : '0deg'});
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const SettingSelect: React.FC<SettingSelectProps> = ({value, options, onChange, disabled = false
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const selectRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const selectedOption = options.find(opt => opt.value === value);
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (!disabled) {
|
||||
setIsOpen(!isOpen);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (optionValue: any) => {
|
||||
onChange(optionValue);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (selectRef.current && !selectRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SelectWrapper ref={selectRef}>
|
||||
<SelectHeader
|
||||
$isOpen={isOpen}
|
||||
onClick={toggleDropdown}
|
||||
$disabled={disabled}
|
||||
>
|
||||
<span>{selectedOption?.label || 'Select...'}</span>
|
||||
<ArrowIcon $isOpen={isOpen}>
|
||||
<IconComponent name={buttonNames[6]} width="16" height="16" />
|
||||
</ArrowIcon>
|
||||
</SelectHeader>
|
||||
<SelectOptions $isOpen={isOpen}>
|
||||
{options.map((option) => (
|
||||
<OptionItem
|
||||
key={option.value}
|
||||
$isSelected={value === option.value}
|
||||
onClick={() => handleSelect(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</OptionItem>
|
||||
))}
|
||||
</SelectOptions>
|
||||
</SelectWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingSelect;
|
||||
125
FRONT/DEV/src/components/settings/Settings.tsx
Normal file
125
FRONT/DEV/src/components/settings/Settings.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
// components/modalContainers/NoteDevice.tsx
|
||||
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../modalwindows/style";
|
||||
import { MainButton } from "../UI/buttons/mainButton";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import styled from "styled-components";
|
||||
import {Setting, useSettingsStore} from "../../store/settingsStore";
|
||||
import {SettingItem} from "./SettingItem";
|
||||
import {IconComponent} from "../../const/buttons/icons";
|
||||
import {buttonNames} from "../../const/buttons/names";
|
||||
import {ModalMap} from "../modalwindows/windowsType";
|
||||
import Test from "./Test";
|
||||
import {workSpaceNames} from "../../const/workspace/names";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const SectionsSidebar = styled.ul`
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
align-items: start;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
width: 20%;
|
||||
min-width: 180px;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
`
|
||||
const SectionsBTN = styled.li`
|
||||
font-size: 18px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
padding:10px 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 0 12px 12px 0;
|
||||
&.active{
|
||||
background-color: ${(props) => props.theme.accent};
|
||||
color: ${props => props.theme.primary};
|
||||
}
|
||||
`
|
||||
const SectionActive = styled.div`
|
||||
width: 80%;
|
||||
`
|
||||
const SectionLine = styled.span`
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background-color: #9D9D9D;
|
||||
`
|
||||
const CloseBtn = styled.span`
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
export const Settings = ({onClose}: Props) => {
|
||||
const {settings, activeSection, setActiveSection, initializeSettings} = useSettingsStore();
|
||||
|
||||
useEffect(() => {
|
||||
initializeSettings();
|
||||
}, [initializeSettings]);
|
||||
|
||||
const settingsBySection = useMemo(() => {
|
||||
return settings.reduce((acc, setting) => {
|
||||
if (!acc[setting.sectionId]) {
|
||||
acc[setting.sectionId] = [];
|
||||
}
|
||||
acc[setting.sectionId].push(setting);
|
||||
return acc;
|
||||
}, {} as Record<string, Setting[]>);
|
||||
}, [settings]);
|
||||
|
||||
|
||||
const sections = Object.keys(settingsBySection);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (sections.length > 0 && !activeSection) {
|
||||
setActiveSection(sections[0]);
|
||||
}
|
||||
}, [sections, activeSection, setActiveSection]);
|
||||
|
||||
return (
|
||||
<ModalContainer style={{ height: '60vh', width: '50vw'}}>
|
||||
<ModalTitle>
|
||||
<h3>Настройки</h3>
|
||||
<CloseBtn>
|
||||
<IconComponent name={buttonNames[12]} width="30" height="30" onClick={() => onClose()} />
|
||||
</CloseBtn>
|
||||
</ModalTitle>
|
||||
|
||||
<ModalContent style={{ display: 'flex', height:"100%", gap: '24px', padding:0 }}>
|
||||
<SectionsSidebar>
|
||||
{sections.map((sectionId) => (
|
||||
<SectionsBTN
|
||||
key={sectionId}
|
||||
className={activeSection === sectionId ? 'active' : ''}
|
||||
onClick={()=>setActiveSection(sectionId)}
|
||||
>
|
||||
{sectionId}
|
||||
</SectionsBTN>
|
||||
))}
|
||||
</SectionsSidebar>
|
||||
<SectionLine/>
|
||||
<SectionActive>
|
||||
{activeSection && settingsBySection[activeSection] && (
|
||||
<>
|
||||
{settingsBySection[activeSection].map((setting)=>(
|
||||
<SettingItem setting={setting} key={setting.id}/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</SectionActive>
|
||||
</ModalContent>
|
||||
|
||||
<ButtonContainer>
|
||||
<MainButton
|
||||
type="button"
|
||||
size="small"
|
||||
title='Сохранить'
|
||||
onClick={onClose}
|
||||
/>
|
||||
</ButtonContainer>
|
||||
</ModalContainer>
|
||||
);
|
||||
};
|
||||
87
FRONT/DEV/src/components/settings/Test.tsx
Normal file
87
FRONT/DEV/src/components/settings/Test.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const DraggableList = () => {
|
||||
const [items, setItems] = useState([
|
||||
'Item 1',
|
||||
'Item 2',
|
||||
'Item 3',
|
||||
'Item 4',
|
||||
'Item 5',
|
||||
]);
|
||||
const [draggingIndex, setDraggingIndex] = useState(null);
|
||||
|
||||
const handleDragStart = (index) => {
|
||||
setDraggingIndex(index);
|
||||
};
|
||||
|
||||
const handleDragOver = (index) => {
|
||||
// Останавливаем стандартное поведение браузера
|
||||
if (index !== draggingIndex) {
|
||||
setItems((prevItems) => {
|
||||
const newItems = [...prevItems];
|
||||
const [movedItem] = newItems.splice(draggingIndex, 1); // Удаляем перетаскиваемый элемент
|
||||
newItems.splice(index, 0, movedItem); // Вставляем его в новое место
|
||||
return newItems;
|
||||
});
|
||||
setDraggingIndex(index); // Обновляем индекс для перетаскивания
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = () => {
|
||||
setDraggingIndex(null); // Сбрасываем состояние перетаскивания
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledList>
|
||||
{items.map((item, index) => (
|
||||
<StyledItem
|
||||
key={index}
|
||||
className={draggingIndex === index ? 'dragging' : ''}
|
||||
draggable
|
||||
onDragStart={() => handleDragStart(index)}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault(); // Останавливаем стандартное поведение
|
||||
handleDragOver(index);
|
||||
}}
|
||||
onDragEnd={handleDrop}
|
||||
>
|
||||
{item}
|
||||
</StyledItem>
|
||||
))}
|
||||
</StyledList>
|
||||
);
|
||||
};
|
||||
|
||||
export default DraggableList;
|
||||
|
||||
// Стилизация с помощью styled-components
|
||||
|
||||
const StyledList = styled.ul`
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
const StyledItem = styled.li`
|
||||
background-color: #f4f4f9;
|
||||
margin: 8px 0;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
cursor: grab;
|
||||
transition: transform 0.2s ease, background-color 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
background-color: #d0e1f9;
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
`;
|
||||
Loading…
Add table
Add a link
Reference in a new issue