Настойки внешний вид
This commit is contained in:
parent
75571cc96f
commit
f8ddae0f94
11 changed files with 563 additions and 127 deletions
|
|
@ -17,10 +17,10 @@ const Button = styled.button.withConfig({
|
||||||
color: white;
|
color: white;
|
||||||
font-size: ${(props) =>
|
font-size: ${(props) =>
|
||||||
props.size === "small"
|
props.size === "small"
|
||||||
? "14px"
|
? "16px"
|
||||||
: props.size === "big"
|
: props.size === "big"
|
||||||
? "16px"
|
? "18px"
|
||||||
: "14px"}; // По умолчанию
|
: "16px"}; // По умолчанию
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {NoteDevice} from "./modalContainers/NoteDevice";
|
||||||
import { NoteWifi } from "./modalContainers/NoteWifi";
|
import { NoteWifi } from "./modalContainers/NoteWifi";
|
||||||
import {ConnectNetwork} from "./modalContainers/ConnectNetwork";
|
import {ConnectNetwork} from "./modalContainers/ConnectNetwork";
|
||||||
import {DisconnectConfirm} from "./modalContainers/disconnectConfirm";
|
import {DisconnectConfirm} from "./modalContainers/disconnectConfirm";
|
||||||
import {Settings} from "./modalContainers/Settings";
|
import {Settings} from "../settings/Settings";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
|
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
@ -21,6 +21,7 @@ import close from '@iconify/icons-ic/round-close';
|
||||||
import wifiFind from '@iconify/icons-ic/baseline-wifi-find';
|
import wifiFind from '@iconify/icons-ic/baseline-wifi-find';
|
||||||
import noteEdit from '@iconify/icons-ph/note-pencil';
|
import noteEdit from '@iconify/icons-ph/note-pencil';
|
||||||
import swapHoriz from '@iconify/icons-ic/baseline-swap-horiz';
|
import swapHoriz from '@iconify/icons-ic/baseline-swap-horiz';
|
||||||
|
import check from '@iconify/icons-ic/baseline-check';
|
||||||
|
|
||||||
export const ICONS = {
|
export const ICONS = {
|
||||||
[buttonNames[1]]: exitToApp,
|
[buttonNames[1]]: exitToApp,
|
||||||
|
|
@ -41,6 +42,7 @@ export const ICONS = {
|
||||||
[buttonNames[16]]: noteEdit,
|
[buttonNames[16]]: noteEdit,
|
||||||
[buttonNames[17]]: arrowLeft,
|
[buttonNames[17]]: arrowLeft,
|
||||||
[buttonNames[18]]: swapHoriz,
|
[buttonNames[18]]: swapHoriz,
|
||||||
|
[buttonNames[19]]: check,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type IconName = keyof typeof ICONS;
|
type IconName = keyof typeof ICONS;
|
||||||
|
|
|
||||||
|
|
@ -19,4 +19,5 @@ export const buttonNames: names ={
|
||||||
16: 'noteEdit',
|
16: 'noteEdit',
|
||||||
17: 'arrowLeft',
|
17: 'arrowLeft',
|
||||||
18: 'swapHoriz',
|
18: 'swapHoriz',
|
||||||
|
19: 'check'
|
||||||
}
|
}
|
||||||
|
|
@ -2,9 +2,11 @@ import { create } from 'zustand';
|
||||||
|
|
||||||
export type Setting = {
|
export type Setting = {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'boolean' | 'select' | 'text' | 'number';
|
type: 'boolean' | 'select' | 'text' | 'number' | 'button';
|
||||||
label: string;
|
label: string;
|
||||||
value: any;
|
value: any;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
options?: { value: any; label: string }[];
|
options?: { value: any; label: string }[];
|
||||||
sectionId: string;
|
sectionId: string;
|
||||||
};
|
};
|
||||||
|
|
@ -32,14 +34,104 @@ export const useSettingsStore = create<SettingsState>((set) => ({
|
||||||
|
|
||||||
initializeSettings: () => {
|
initializeSettings: () => {
|
||||||
const defaultSettings: Setting[] = [
|
const defaultSettings: Setting[] = [
|
||||||
|
// Узлы подсети
|
||||||
{
|
{
|
||||||
id: 'rescan-nodes',
|
id: 'rescan-nodes',
|
||||||
|
sectionId: 'Узлы подсети',
|
||||||
|
label: 'Интервальное сканирование',
|
||||||
|
type: 'boolean',
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'interval-nodes',
|
||||||
|
sectionId: 'Узлы подсети',
|
||||||
|
label: 'Интервал сканирования узлов (мин)',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
label: 'nodesNetwork',
|
value: 60,
|
||||||
value: 3000,
|
min: 1,
|
||||||
sectionId: 'appearance',
|
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 });
|
set({ settings: defaultSettings });
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue