Настойки внешний вид

This commit is contained in:
test 2025-06-27 17:50:08 +05:00
parent 75571cc96f
commit f8ddae0f94
11 changed files with 563 additions and 127 deletions

View file

@ -17,10 +17,10 @@ const Button = styled.button.withConfig({
color: white;
font-size: ${(props) =>
props.size === "small"
? "14px"
: props.size === "big"
? "16px"
: "14px"}; // По умолчанию
: props.size === "big"
? "18px"
: "16px"}; // По умолчанию
cursor: pointer;
display: flex;
justify-content: center;

View file

@ -1,47 +0,0 @@
import { useSettingsStore } from '../stores/settingsStore';
export const SettingItem = ({ setting }: { setting: Setting }) => {
const { updateSetting } = useSettingsStore();
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const value =
setting.type === 'boolean'
? (e.target as HTMLInputElement).checked
: e.target.value;
updateSetting(setting.id, value);
};
return (
<div>
<label>{setting.label}</label>
{setting.type === 'boolean' && (
<input
type="checkbox"
checked={setting.value}
onChange={handleChange}
/>
)}
{setting.type === 'select' && setting.options && (
<select value={setting.value} onChange={handleChange}>
{setting.options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
)}
{setting.type === 'text' && (
<input
type="text"
value={setting.value}
onChange={handleChange}
/>
)}
</div>
);
};

View file

@ -1,72 +0,0 @@
// components/modalContainers/NoteDevice.tsx
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
import { MainButton } from "../../UI/buttons/mainButton";
import {useEffect, useState} from "react";
import styled from "styled-components";
import { useSettingsStore } from "../../../store/settingsStore";
interface Props {
onClose: () => void;
}
const SectionsSidebar = styled.div`
display: flex;
justify-content: start;
align-items: start;
flex-direction: column;
gap: 12px;
width: 20%;
`
const SectionsBTN = styled.button`
font-size: 18px;
border: none;
cursor: pointer;
`
const SectionActive = styled.div`
width: 80%;
background-color: red;
`
const sections = [
{id: 'nodesNetwork', label: 'Узлы подсети'},
{id: 'wifiNetwork', label: 'Точки доступа'},
{id: 'appearance', label: 'Внешний вид'},
]
export const Settings = ({onClose}: Props) => {
const {activeSection, setActiveSection, initializeSettings} = useSettingsStore();
useEffect(() => {
initializeSettings();
}, [initializeSettings]);
return (
<ModalContainer style={{ height: '60vh', width: '50vw'}}>
<ModalTitle>
<h3>Настройки</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', height:"100%", gap: '8px' }}>
<SectionsSidebar>
{sections.map((section) => (
<SectionsBTN>{section.label}</SectionsBTN>
))}
</SectionsSidebar>
<SectionActive>
{activeSection && <SettingItem setting={} />}
</SectionActive>
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title='Сохранить'
onClick={onClose}
/>
</ButtonContainer>
</ModalContainer>
);
};

View file

@ -7,7 +7,7 @@ import {NoteDevice} from "./modalContainers/NoteDevice";
import { NoteWifi } from "./modalContainers/NoteWifi";
import {ConnectNetwork} from "./modalContainers/ConnectNetwork";
import {DisconnectConfirm} from "./modalContainers/disconnectConfirm";
import {Settings} from "./modalContainers/Settings";
import {Settings} from "../settings/Settings";
type Props = {
isOpen: boolean,

View 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>
);
};

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

View 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>
);
};

View 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;
}
`;

View file

@ -21,6 +21,7 @@ import close from '@iconify/icons-ic/round-close';
import wifiFind from '@iconify/icons-ic/baseline-wifi-find';
import noteEdit from '@iconify/icons-ph/note-pencil';
import swapHoriz from '@iconify/icons-ic/baseline-swap-horiz';
import check from '@iconify/icons-ic/baseline-check';
export const ICONS = {
[buttonNames[1]]: exitToApp,
@ -41,6 +42,7 @@ export const ICONS = {
[buttonNames[16]]: noteEdit,
[buttonNames[17]]: arrowLeft,
[buttonNames[18]]: swapHoriz,
[buttonNames[19]]: check,
} as const;
type IconName = keyof typeof ICONS;

View file

@ -19,4 +19,5 @@ export const buttonNames: names ={
16: 'noteEdit',
17: 'arrowLeft',
18: 'swapHoriz',
19: 'check'
}

View file

@ -2,9 +2,11 @@ import { create } from 'zustand';
export type Setting = {
id: string;
type: 'boolean' | 'select' | 'text' | 'number';
type: 'boolean' | 'select' | 'text' | 'number' | 'button';
label: string;
value: any;
min?: number;
max?: number;
options?: { value: any; label: string }[];
sectionId: string;
};
@ -32,14 +34,104 @@ export const useSettingsStore = create<SettingsState>((set) => ({
initializeSettings: () => {
const defaultSettings: Setting[] = [
// Узлы подсети
{
id: 'rescan-nodes',
sectionId: 'Узлы подсети',
label: 'Интервальное сканирование',
type: 'boolean',
value: false,
},
{
id: 'interval-nodes',
sectionId: 'Узлы подсети',
label: 'Интервал сканирования узлов (мин)',
type: 'number',
label: 'nodesNetwork',
value: 3000,
sectionId: 'appearance',
value: 60,
min: 1,
max: 2880
},
{
id: 'netnames-nodes',
sectionId: 'Узлы подсети',
label: 'Отображаемые имена при отсутствии',
type: 'select',
value: 'ip',
options: [
{ value: 'ip', label: 'IP адрес' },
{ value: 'unknown', label: 'Неизвестно' },
],
},
{
id: 'paginate-nodes',
type: 'number',
label: 'Количество строк на таблице',
sectionId: 'Узлы подсети',
value: 20,
min: 1,
},
{
id: 'customize-nodes',
type: 'button',
label: 'Отображение узлов',
sectionId: 'Узлы подсети',
value: 'Настроить',
},
{
id: 'clean-nodes',
type: 'button',
label: 'Очистить данные сессии',
sectionId: 'Узлы подсети',
value: 'Очистить',
},
{
id: 'export-nodes',
type: 'button',
label: 'Экспорт данных узлов',
sectionId: 'Узлы подсети',
value: 'Экспортировать',
},
// Точки доступа
{
id: 'clean-network',
type: 'button',
label: 'Очистить данные сессии',
sectionId: 'Точки доступа',
value: 'Очистить',
},
{
id: 'export-network',
type: 'button',
label: 'Экспорт данных точек доступа',
sectionId: 'Точки доступа',
value: 'Экспортировать',
},
// Внешний вид
{
id: 'theme',
sectionId: 'Внешний вид',
label: 'Цветовая тема',
type: 'select',
value: 'light',
options: [
{ value: 'light', label: 'Светлая' },
{ value: 'dark', label: 'Тёмная' },
{ value: 'system', label: 'Как в системе' }
],
},
{
id: 'language',
sectionId: 'Внешний вид',
label: 'Язык интерфейса',
type: 'select',
value: 'ru',
options: [
{ value: 'ru', label: 'Русский' },
{ value: 'en', label: 'Английский' },
],
},
];
set({ settings: defaultSettings });
},