feat: add app and config

This commit is contained in:
2025-03-23 02:59:41 +08:00
parent 063837350b
commit 1d97ff88b9
24 changed files with 351 additions and 176 deletions

View File

@@ -1 +1,6 @@
export * from './theme';
export * from './theme';
/**
* 输入组件, 使用theme的defaultProps
*/
export * from './input/TextField';

View File

@@ -0,0 +1,23 @@
import { TextField as MuiTextField, TextFieldProps, Tooltip } from '@mui/material';
import { useTheme } from '../theme';
import { HelpCircle } from 'lucide-react';
export const TextField = (props: TextFieldProps) => {
const theme = useTheme();
const defaultProps = theme.components?.MuiTextField?.defaultProps;
return <MuiTextField {...defaultProps} {...props} />;
};
export const TextFieldLabel = ({ children, tips, label }: { children?: React.ReactNode; tips?: string; label?: any }) => {
return (
<div className='flex items-center gap-1'>
{label}
{children}
{tips && (
<Tooltip title={tips}>
<HelpCircle size={16} />
</Tooltip>
)}
</div>
);
};

View File

@@ -6,9 +6,10 @@ type TagsInputProps = {
value: string[];
onChange: (value: string[]) => void;
placeholder?: string;
label?: string;
label?: any;
showLabel?: boolean;
};
export const TagsInput = ({ value, onChange, placeholder = 'Add a tag', label = 'Tags' }: TagsInputProps) => {
export const TagsInput = ({ value, onChange, placeholder = '', label = '', showLabel = false }: TagsInputProps) => {
const [tags, setTags] = useState<string[]>(value);
useEffect(() => {
setTags(value);
@@ -26,6 +27,9 @@ export const TagsInput = ({ value, onChange, placeholder = 'Add a tag', label =
// setTags(newValue as string[]);
onChange(newValue as string[]);
}}
sx={{
width: '100%',
}}
renderTags={(value: string[], getTagProps) => {
const id = randomid();
const com = value.map((option: string, index: number) => (
@@ -33,6 +37,13 @@ export const TagsInput = ({ value, onChange, placeholder = 'Add a tag', label =
variant='outlined'
sx={{
borderColor: 'primary.main',
borderRadius: '4px',
'&:hover': {
borderColor: 'primary.main',
},
'& .MuiChip-deleteIcon': {
color: 'secondary.main',
},
}}
label={option}
{...getTagProps({ index })}
@@ -41,7 +52,7 @@ export const TagsInput = ({ value, onChange, placeholder = 'Add a tag', label =
));
return <Fragment key={id}>{com}</Fragment>;
}}
renderInput={(params) => <TextField {...params} variant='outlined' label={label} placeholder={placeholder} />}
renderInput={(params) => <TextField {...params} label={showLabel ? label : ''} placeholder={placeholder} />}
/>
);
};

View File

@@ -35,19 +35,21 @@ const generateShadows = (color: string): Shadows => {
`0px 13px 17px -8px ${color}`,
];
};
const primaryMain = amber[300]; // #ffc107
const secondaryMain = amber[500]; // #ffa000
export const themeOptions: ThemeOptions = {
// @ts-ignore
// cssVariables: true,
palette: {
primary: {
main: '#ffc107', // amber[300]
main: primaryMain, // amber[300]
},
secondary: {
main: '#ffa000', // amber[500]
main: secondaryMain, // amber[500]
},
divider: amber[200],
common: {
white: '#ffa000',
white: secondaryMain,
},
text: {
primary: amber[600],
@@ -78,7 +80,7 @@ export const themeOptions: ThemeOptions = {
'&.MuiButton-contained': {
color: '#ffffff',
':hover': {
color: amber[500],
color: secondaryMain,
},
},
},
@@ -96,6 +98,7 @@ export const themeOptions: ThemeOptions = {
MuiTextField: {
defaultProps: {
fullWidth: true,
size: 'small',
slotProps: {
inputLabel: {
shrink: true,
@@ -121,6 +124,11 @@ export const themeOptions: ThemeOptions = {
},
},
},
MuiAutocomplete: {
defaultProps: {
size: 'small',
},
},
MuiSelect: {
styleOverrides: {
root: {
@@ -156,7 +164,10 @@ export const themeOptions: ThemeOptions = {
MuiFormControlLabel: {
defaultProps: {
labelPlacement: 'top',
sx: {
},
styleOverrides: {
root: {
color: amber[600],
alignItems: 'flex-start',
'& .MuiFormControlLabel-label': {
textAlign: 'left',
@@ -170,9 +181,18 @@ export const themeOptions: ThemeOptions = {
},
},
},
},
MuiMenuItem: {
styleOverrides: {
root: {
color: amber[600],
'&.Mui-selected': {
backgroundColor: amber[500],
color: '#ffffff',
'&:hover': {
backgroundColor: amber[600],
color: '#ffffff',
},
},
},
},
},
@@ -201,3 +221,6 @@ export const CustomThemeProvider = ({ children, themeOptions: customThemeOptions
const theme = createTheme(customThemeOptions || themeOptions);
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
};
// TODO: think
export const getComponentProps = () => {};

View File

@@ -1,3 +1,5 @@
export { KeyParse, keysTips } from './pages/file/modules/key-parse';
export { PermissionManager } from './pages/file/modules/PermissionManager.tsx';
export { PermissionModal, usePermissionModal } from './pages/file/modules/PermissionModal.tsx';
export { iText } from './i-text/index.ts';
export * from './pages/upload/app';
export * from './pages/upload/app';

View File

@@ -1,6 +1,7 @@
import ReactDatePicker from 'antd/es/date-picker';
import { styled, useTheme } from '@mui/material';
import 'antd/es/date-picker/style/index';
import { useTranslation } from 'react-i18next';
interface DatePickerProps {
value?: Date | null;
onChange?: (date: Date | null) => void;
@@ -8,13 +9,14 @@ interface DatePickerProps {
}
export const DatePickerCom = ({ value, onChange, className }: DatePickerProps) => {
const { t } = useTranslation();
const theme = useTheme();
const primaryColor = theme.palette.primary.main;
return (
<div className={className}>
<ReactDatePicker
placement='topLeft'
placeholder='请选择日期'
placeholder={t('Select Date')}
value={value}
showNow={false}
// showTime={true}

View File

@@ -7,10 +7,10 @@ import { toast } from 'react-toastify';
import { create } from 'zustand';
import { uniq } from 'lodash-es';
import { DatePicker } from './DatePicker';
import { SelectPicker } from './SelectPicker';
import { DialogKey } from './DialogKey';
import { keysTips, KeyParse } from '../../modules/key-parse';
import { KeyShareSelect, KeyTextField } from '../../modules/PermissionManager';
import { TagsInput } from '@kevisual/center-components/select/TagsInput.tsx';
export const setShareKeysOperate = (value: 'public' | 'protected' | 'private') => {
const keys = ['password', 'usernames', 'expiration-time'];
const deleteKeys = keys.map((item) => {
@@ -187,7 +187,7 @@ export const MetaForm = () => {
</div>
</div>
</Box>
<form>
<form className='flex flex-col gap-1 pb-4'>
{keys.map((key) => {
let control: React.ReactNode | null = null;
if (key === 'share') {
@@ -195,7 +195,7 @@ export const MetaForm = () => {
} else if (key === 'expiration-time') {
control = <DatePicker value={formData[key]} onChange={(date) => handleFormDataChange(key, date)} />;
} else if (key === 'usernames') {
control = <SelectPicker value={formData[key] || []} onChange={(value) => handleFormDataChange(key, value)} />;
control = <TagsInput value={formData[key] || []} showLabel={false} onChange={(value) => handleFormDataChange(key, value)} />;
} else {
control = <KeyTextField name={key} value={formData[key] || ''} onChange={(value) => handleFormDataChange(key, value)} />;
}
@@ -226,23 +226,7 @@ export const MetaForm = () => {
</div>
);
};
return (
<div key={key} className='flex flex-col gap-2'>
<FormControlLabel
key={key}
label={<Label />}
labelPlacement='top'
control={control}
sx={{
alignItems: 'flex-start',
'& .MuiFormControlLabel-label': {
textAlign: 'left',
width: '100%',
},
}}
/>
</div>
);
return <FormControlLabel key={key} labelPlacement={'top'} label={<Label />} control={control} />;
})}
</form>
<DialogKey onAdd={addMetaKey} />

View File

@@ -1,30 +0,0 @@
import { styled } from '@mui/material';
import Select from 'antd/es/select';
import 'antd/es/select/style/index';
interface SelectPickerProps {
value: string[];
onChange: (value: string[]) => void;
className?: string;
}
export const SelectPickerCom = ({ value, onChange, className }: SelectPickerProps) => {
return <Select className={className} style={{ width: '100%' }} popupClassName='hidden' showSearch={false} mode='tags' value={value} onChange={onChange} />;
};
export const SelectPicker = styled(SelectPickerCom)(({ theme }) => ({
'& .ant-select-selection-item': {
backgroundColor: 'var(--color-primary) !important',
color: 'white !important',
},
'& svg': {
color: 'white !important',
},
'& svg:hover': {
color: '#ccc !important',
},
'& .ant-select-arrow': {
// color: 'var(--color-primary) !important',
display: 'none !important',
},
}));

View File

@@ -1,12 +1,13 @@
import { useEffect, useState } from 'react';
import { KeyParse, getTips } from './key-parse';
import { FormControlLabel, TextField, Select, MenuItem, FormGroup, Tooltip } from '@mui/material';
import { FormControlLabel, TextField, Select, MenuItem, Tooltip } from '@mui/material';
import { DatePicker } from '../draw/modules/DatePicker';
import { SelectPicker } from '../draw/modules/SelectPicker';
import { TagsInput } from '@kevisual/center-components/select/TagsInput.tsx';
import { HelpCircle } from 'lucide-react';
import clsx from 'clsx';
import { useTranslation } from 'react-i18next';
export const KeyShareSelect = ({ name, value, onChange }: { name: string; value: string; onChange?: (value: string) => void }) => {
const { t } = useTranslation();
return (
<Select
variant='outlined'
@@ -18,14 +19,14 @@ export const KeyShareSelect = ({ name, value, onChange }: { name: string; value:
width: '100%',
marginBottom: '16px',
}}>
<MenuItem value='public' title='公开'>
<MenuItem value='public' title={t('Public')}>
{t('Public')}
</MenuItem>
<MenuItem value='protected' title='受保护'>
<MenuItem value='protected' title={t('Protected')}>
{t('Protected')}
</MenuItem>
<MenuItem value='private' title='私有'>
<MenuItem value='private' title={t('Private')}>
{t('Private')}
</MenuItem>
</Select>
);
@@ -85,8 +86,9 @@ export const PermissionManager = ({ value, onChange, className }: PermissionMana
};
const tips = getTips('share', i18n.language);
return (
<form className={clsx('flex flex-col gap-2', className)}>
<form className={clsx('flex flex-col w-full gap-2', className)}>
<FormControlLabel
labelPlacement='top'
control={<KeyShareSelect name='share' value={formData?.share} onChange={(value) => onChangeValue('share', value)} />}
label={
<div className='flex items-center gap-1'>
@@ -99,38 +101,26 @@ export const PermissionManager = ({ value, onChange, className }: PermissionMana
/>
{keys.map((item: any) => {
let control: React.ReactNode | null = null;
const tips = getTips(item);
const label = (
<div className='flex items-center gap-1'>
{t(item)}
{tips && (
<Tooltip title={tips}>
<HelpCircle size={16} />
</Tooltip>
)}
</div>
);
if (item === 'expiration-time') {
control = <DatePicker value={formData[item] || ''} onChange={(date) => onChangeValue(item, date)} />;
control = <DatePicker className='mt-1' value={formData[item] || ''} onChange={(date) => onChangeValue(item, date)} />;
} else if (item === 'usernames') {
control = <SelectPicker value={formData[item] || []} onChange={(value) => onChangeValue(item, value)} />;
control = <TagsInput value={formData[item] || []} onChange={(value) => onChangeValue(item, value)} />;
} else {
control = <KeyTextField name={item} value={formData[item] || ''} onChange={(value) => onChangeValue(item, value)} />;
}
const tips = getTips(item);
return (
<FormControlLabel
labelPlacement='top'
key={item}
control={control}
label={
<div className='flex items-center gap-1'>
{item}
{tips && (
<Tooltip title={tips}>
<HelpCircle size={16} />
</Tooltip>
)}
</div>
}
sx={{
alignItems: 'flex-start',
'& .MuiFormControlLabel-label': {
textAlign: 'left',
width: '100%',
},
}}
/>
);
return <FormControlLabel key={item} labelPlacement='top' control={control} label={label} />;
})}
</form>
);

View File

@@ -0,0 +1,63 @@
import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { PermissionManager } from './PermissionManager';
import { useEffect, useState } from 'react';
type PermissionModalProps = {
open: boolean;
onClose: () => void;
value: Record<string, any>;
onChange: (value: Record<string, any>) => void;
onSave: () => void;
};
export const PermissionModal = ({ open, onClose, value, onChange, onSave }: PermissionModalProps) => {
const { t } = useTranslation();
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>{t('app.share')}</DialogTitle>
<DialogContent sx={{ width: '400px' }}>
<PermissionManager value={value} onChange={onChange} />
<DialogActions>
<Button onClick={onClose}>{t('Cancel')}</Button>
<Button color='primary' onClick={onSave}>
{t('Submit')}
</Button>
</DialogActions>
</DialogContent>
</Dialog>
);
};
type UsePermissionModalProps = {
/**
* 保存回调
*/
onSave: (values: Record<string, any>) => Promise<boolean>;
};
export const usePermissionModal = ({ onSave }: UsePermissionModalProps) => {
const [open, setOpen] = useState(false);
const [formData, setFormData] = useState<Record<string, any>>({});
const onChange = (value: Record<string, any>) => {
setFormData(value);
};
const _onSave = async () => {
const res = await onSave(formData);
if (res) {
setOpen(false);
}
};
const contextHolder = <PermissionModal open={open} onClose={() => setOpen(false)} value={formData} onChange={onChange} onSave={_onSave} />;
return {
open,
setOpen: (open: boolean, fromData?: any) => {
setOpen(open);
if (open) {
setFormData(fromData ?? {});
}
},
setFormData: (values: any) => {
setFormData({ ...formData, ...values });
},
contextHolder,
};
};