feat: responsive settings ui

This commit is contained in:
鲁树人 2023-06-10 00:45:41 +01:00
parent 1f87a655ac
commit 725f130e42
6 changed files with 294 additions and 171 deletions

View File

@ -1,20 +1,95 @@
import { Tab, TabList, TabPanel, TabPanels, Tabs, Text } from '@chakra-ui/react'; import {
import { PanelQMC } from './panels/PanelQMC'; Button,
Flex,
Menu,
MenuButton,
MenuItem,
MenuList,
Portal,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
Text,
useBreakpointValue,
} from '@chakra-ui/react';
import { PanelQMCv2Key } from './panels/PanelQMCv2Key';
import { useState } from 'react';
import { MdExpandMore, MdMenu } from 'react-icons/md';
const TABS: { name: string; Tab: () => JSX.Element }[] = [
{ name: 'QMCv2 密钥', Tab: PanelQMCv2Key },
{
name: '其它/待定',
Tab: () => (
<TabPanel>
<Text></Text>
</TabPanel>
),
},
];
export function Settings() { export function Settings() {
const isLargeWidthDevice =
useBreakpointValue({
base: false,
lg: true,
}) ?? false;
const [tabIndex, setTabIndex] = useState(0);
const handleTabChange = (idx: number) => {
setTabIndex(idx);
};
return ( return (
<Tabs orientation="vertical" align="start" variant="line-i" flex={1}> <Flex flexDir="column" flex={1}>
<TabList minW={0} width="8em" textAlign="right" justifyContent="center"> <Menu>
<Tab>QQ </Tab> <MenuButton
<Tab></Tab> as={Button}
leftIcon={<MdMenu />}
rightIcon={<MdExpandMore />}
colorScheme="gray"
variant="outline"
w="full"
flexShrink={0}
hidden={isLargeWidthDevice}
mb="4"
>
{TABS[tabIndex].name}
</MenuButton>
<Portal>
<MenuList w="100px">
{TABS.map(({ name }, i) => (
<MenuItem key={name} onClick={() => setTabIndex(i)}>
{name}
</MenuItem>
))}
</MenuList>
</Portal>
</Menu>
<Tabs
orientation={isLargeWidthDevice ? 'vertical' : 'horizontal'}
align="start"
variant="line-i"
display="flex"
flex={1}
index={tabIndex}
onChange={handleTabChange}
>
<TabList hidden={!isLargeWidthDevice} minW="8em" width="8em" textAlign="right" justifyContent="center">
{TABS.map(({ name }) => (
<Tab key={name}>{name}</Tab>
))}
</TabList> </TabList>
<TabPanels> <TabPanels>
<PanelQMC /> {TABS.map(({ name, Tab }) => (
<TabPanel> <Tab key={name} />
<Text></Text> ))}
</TabPanel>
</TabPanels> </TabPanels>
</Tabs> </Tabs>
</Flex>
); );
} }

View File

@ -1,153 +0,0 @@
import {
Box,
Button,
ButtonGroup,
Flex,
HStack,
Heading,
Icon,
IconButton,
Input,
InputGroup,
InputLeftElement,
InputRightElement,
List,
ListItem,
Menu,
MenuButton,
MenuDivider,
MenuItem,
MenuList,
Spacer,
TabPanel,
Text,
VStack,
} from '@chakra-ui/react';
import { useSelector } from 'react-redux';
import { selectQM2CSettings } from '../settingsSlice';
import React, { useEffect, useState } from 'react';
import { nanoid } from 'nanoid';
import { produce } from 'immer';
import { MdAdd, MdDeleteForever, MdExpandMore, MdFileUpload, MdVpnKey } from 'react-icons/md';
interface InternalQMCKeys {
id: string;
name: string;
key: string;
}
export function PanelQMC() {
const qmcSettings = useSelector(selectQM2CSettings);
const [qmcKeys, setQMCKeys] = useState<InternalQMCKeys[]>([]);
const resetQmcKeys = () => {
const result: InternalQMCKeys[] = [];
for (const [name, key] of Object.entries(qmcSettings.keys)) {
result.push({ id: name, name, key });
}
setQMCKeys(result);
};
const addRow = () => {
setQMCKeys((prev) => [...prev, { id: nanoid(), key: '', name: '' }]);
};
const updateKey = (prop: 'name' | 'key', id: string, e: React.ChangeEvent<HTMLInputElement>) => {
setQMCKeys((prev) =>
produce(prev, (draft) => {
const item = draft.find((item) => item.id === id);
if (item) {
item[prop] = e.target.value;
}
})
);
};
const applyChanges = () => {
//
};
const clearAll = () => setQMCKeys([]);
useEffect(resetQmcKeys, [qmcSettings.keys]);
return (
<Flex as={TabPanel} flexDir="column" h="100%">
<Box flex={1} minH={0} overflow="auto">
<Heading as="h2" size="lg">
</Heading>
<Box p="4" pr="0" borderStart="2px solid" borderColor="gray.200">
<List spacing={3}>
{qmcKeys.map(({ id, key, name }, i) => (
<ListItem key={id}>
<HStack>
<Text w="2em" textAlign="center">
{i + 1}
</Text>
<VStack flex={1}>
<Input
variant="flushed"
placeholder="文件名"
value={name}
onChange={(e) => updateKey('name', id, e)}
/>
<InputGroup size="xs">
<InputLeftElement pr="2">
<Icon as={MdVpnKey} />
</InputLeftElement>
<Input
variant="flushed"
placeholder="密钥"
value={key}
onChange={(e) => updateKey('key', id, e)}
/>
<InputRightElement>
<Text pl="2" color={key.length ? 'green.500' : 'red.500'}>
<code>{key.length || '?'}</code>
</Text>
</InputRightElement>
</InputGroup>
</VStack>
</HStack>
</ListItem>
))}
</List>
{qmcKeys.length === 0 && <Text></Text>}
</Box>
</Box>
<VStack mt="4" alignItems="flex-start">
<Text>使</Text>
<Flex flexDir="row" gap="2" w="full">
<Box>
<ButtonGroup isAttached variant="outline">
<Button onClick={addRow}>
<Icon as={MdAdd} />
</Button>
<Menu>
<MenuButton as={IconButton} icon={<MdExpandMore />}>
<Icon as={MdExpandMore} />1
</MenuButton>
<MenuList>
<MenuItem onClick={() => alert('TODO!')} icon={<Icon as={MdFileUpload} h={18} w={18} />}>
(JSON)
</MenuItem>
<MenuDivider />
<MenuItem color="red" onClick={clearAll} icon={<Icon as={MdDeleteForever} h={18} w={18} />}>
</MenuItem>
</MenuList>
</Menu>
</ButtonGroup>
</Box>
<Spacer />
<Box>
<Button onClick={resetQmcKeys} colorScheme="red" variant="ghost">
</Button>
<Button onClick={applyChanges}></Button>
</Box>
</Flex>
</VStack>
</Flex>
);
}

View File

@ -0,0 +1,183 @@
import {
Box,
Button,
ButtonGroup,
Center,
Flex,
HStack,
Heading,
Icon,
IconButton,
Input,
InputGroup,
InputLeftElement,
InputRightElement,
List,
ListItem,
Menu,
MenuButton,
MenuDivider,
MenuItem,
MenuList,
Spacer,
TabPanel,
Text,
VStack,
useToast,
} from '@chakra-ui/react';
import { useDispatch, useSelector } from 'react-redux';
import { selectQM2CSettings, updateQMC2Keys } from '../settingsSlice';
import React, { useEffect, useState } from 'react';
import { nanoid } from 'nanoid';
import { produce } from 'immer';
import { MdAdd, MdAndroid, MdDeleteForever, MdExpandMore, MdFileUpload, MdVpnKey } from 'react-icons/md';
import { objectify } from 'radash';
interface InternalQMCKeys {
id: string;
name: string;
key: string;
}
export function PanelQMCv2Key() {
const toast = useToast();
const dispatch = useDispatch();
const qmcSettings = useSelector(selectQM2CSettings);
const [isModified, setIsModified] = useState(false);
const [qmcKeys, setQMCKeys] = useState<InternalQMCKeys[]>([]);
const resetQmcKeys = () => {
const result: InternalQMCKeys[] = [];
for (const [name, key] of Object.entries(qmcSettings.keys)) {
result.push({ id: name, name, key });
}
setQMCKeys(result);
};
const addRow = () => {
setIsModified(true);
setQMCKeys((prev) => [...prev, { id: nanoid(), key: '', name: '' }]);
};
const updateKey = (prop: 'name' | 'key', id: string, e: React.ChangeEvent<HTMLInputElement>) => {
setIsModified(true);
setQMCKeys((prev) =>
produce(prev, (draft) => {
const item = draft.find((item) => item.id === id);
if (item) {
item[prop] = e.target.value;
}
})
);
};
const applyChanges = () => {
dispatch(
updateQMC2Keys(
objectify(
qmcKeys,
(item) => item.name,
(item) => item.key
)
)
);
toast({
title: 'QMCv2 密钥的更改已保存。',
status: 'success',
isClosable: true,
duration: 2500,
});
};
const clearAll = () => setQMCKeys([]);
useEffect(resetQmcKeys, [qmcSettings.keys]);
return (
<Flex as={TabPanel} flexDir="column" h="100%">
<Heading as="h2" size="lg">
</Heading>
<Box pb="2">
<ButtonGroup isAttached variant="outline">
<Button onClick={addRow} leftIcon={<Icon as={MdAdd} />}>
</Button>
<Menu>
<MenuButton as={IconButton} icon={<MdExpandMore />}></MenuButton>
<MenuList>
<MenuItem onClick={() => alert('TODO!')} icon={<Icon as={MdFileUpload} boxSize={5} />}>
JSON
</MenuItem>
{/* 需要加入 SQL.js 再处理 */}
<MenuItem hidden onClick={() => alert('TODO!')} icon={<Icon as={MdAndroid} boxSize={5} />}>
</MenuItem>
<MenuDivider />
<MenuItem color="red" onClick={clearAll} icon={<Icon as={MdDeleteForever} boxSize={5} />}>
</MenuItem>
</MenuList>
</Menu>
</ButtonGroup>
</Box>
<Box flex={1} minH={0} overflow="auto" pr="4">
<List spacing={3}>
{qmcKeys.map(({ id, key, name }, i) => (
<ListItem key={id}>
<HStack>
<Text w="2em" textAlign="center">
{i + 1}
</Text>
<VStack flex={1}>
<Input
variant="flushed"
placeholder="文件名"
value={name}
onChange={(e) => updateKey('name', id, e)}
/>
<InputGroup size="xs">
<InputLeftElement pr="2">
<Icon as={MdVpnKey} />
</InputLeftElement>
<Input variant="flushed" placeholder="密钥" value={key} onChange={(e) => updateKey('key', id, e)} />
<InputRightElement>
<Text pl="2" color={key.length ? 'green.500' : 'red.500'}>
<code>{key.length || '?'}</code>
</Text>
</InputRightElement>
</InputGroup>
</VStack>
</HStack>
</ListItem>
))}
</List>
{qmcKeys.length === 0 && <Text></Text>}
</Box>
<VStack mt="4" alignItems="flex-start" w="full">
<Flex flexDir="row" gap="2" w="full">
<Center>
<Text as={Box} color="gray">
</Text>
</Center>
<Spacer />
<HStack gap="2" justifyContent="flex-end">
<Button
disabled={!isModified}
onClick={resetQmcKeys}
colorScheme="red"
variant="ghost"
title="重置为更改前的状态"
>
</Button>
<Button disabled={!isModified} onClick={applyChanges}>
</Button>
</HStack>
</Flex>
</VStack>
</Flex>
);
}

View File

@ -21,13 +21,16 @@ export const settingsSlice = createSlice({
updateSettings: (_state, { payload }: PayloadAction<SettingsState>) => { updateSettings: (_state, { payload }: PayloadAction<SettingsState>) => {
return payload; return payload;
}, },
updateQMC2Keys: (state, { payload }: PayloadAction<QMCSettings['keys']>) => {
state.qmc2.keys = payload;
},
resetConfig: () => { resetConfig: () => {
return initialState; return initialState;
}, },
}, },
}); });
export const { updateSettings, resetConfig } = settingsSlice.actions; export const { updateSettings, resetConfig, updateQMC2Keys } = settingsSlice.actions;
export const selectQM2CSettings = (state: RootState) => state.settings.qmc2; export const selectQM2CSettings = (state: RootState) => state.settings.qmc2;

View File

@ -1,9 +1,14 @@
import { Container, Flex } from '@chakra-ui/react'; import { Container, Flex, useBreakpointValue } from '@chakra-ui/react';
import { Settings } from '~/features/settings/Settings'; import { Settings } from '~/features/settings/Settings';
export function SettingsTab() { export function SettingsTab() {
const containerProps = useBreakpointValue({
base: { p: '0' },
lg: { p: undefined },
});
return ( return (
<Container as={Flex} maxW="container.lg"> <Container as={Flex} maxW="container.lg" {...containerProps}>
<Settings /> <Settings />
</Container> </Container>
); );

View File

@ -17,6 +17,13 @@ const variantLineInvert = definePartsStyle((props) => {
[borderProp]: '2px solid', [borderProp]: '2px solid',
borderColor: 'inherit', borderColor: 'inherit',
}, },
tabpanels: {
flex: 1,
minH: 0,
},
tabpanel: {
padding: 0,
},
tab: { tab: {
[borderProp]: '2px solid', [borderProp]: '2px solid',
borderColor: 'transparent', borderColor: 'transparent',
@ -42,7 +49,10 @@ const variantLineInvert = definePartsStyle((props) => {
bg: $bg.reference, bg: $bg.reference,
}, },
root: { root: {
display: 'flex',
flexDir: isVertical ? 'row' : 'column',
gap: 8, gap: 8,
minH: 0,
}, },
}; };
}); });