feat: add resources
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { useResourceFileStore } from '@/pages/store/resource-file';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
type ContentShowType = {
|
||||
size: number;
|
||||
lastModified: string;
|
||||
etag: string;
|
||||
name?: string;
|
||||
};
|
||||
export const ContentForm = () => {
|
||||
const { resource } = useResourceFileStore();
|
||||
const contentShow = resource as ContentShowType;
|
||||
return (
|
||||
<Box className='p-4 border rounded-md mt-2'>
|
||||
{/* <Typography variant='h6'>{contentShow?.name || 'No Name Available'}</Typography> */}
|
||||
<Typography variant='body1'>
|
||||
<strong>Size:</strong> {contentShow?.size ? prettyBytes(contentShow?.size) : 'N/A'}
|
||||
</Typography>
|
||||
<Typography variant='body1'>
|
||||
<strong>Last Modified:</strong> {contentShow?.lastModified ? dayjs(contentShow?.lastModified).format('YYYY-MM-DD HH:mm:ss') : 'N/A'}
|
||||
</Typography>
|
||||
<Typography variant='body1'>
|
||||
<strong>ETag:</strong> {contentShow?.etag || 'N/A'}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import ReactDatePicker from 'antd/es/date-picker';
|
||||
import { useTheme } from '@mui/material';
|
||||
import 'antd/es/date-picker/style/index';
|
||||
interface DatePickerProps {
|
||||
value?: Date | null;
|
||||
onChange?: (date: Date | null) => void;
|
||||
}
|
||||
|
||||
export const DatePicker = ({ value, onChange }: DatePickerProps) => {
|
||||
const theme = useTheme();
|
||||
const primaryColor = theme.palette.primary.main;
|
||||
return (
|
||||
<div>
|
||||
<ReactDatePicker
|
||||
placement='topLeft'
|
||||
placeholder='请选择日期'
|
||||
value={value}
|
||||
showNow={false}
|
||||
// showTime={true}
|
||||
onChange={(date) => onChange?.(date)} //
|
||||
style={{
|
||||
color: primaryColor,
|
||||
}}
|
||||
popupStyle={{ zIndex: 2000 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
39
packages/resources/src/pages/file/draw/modules/DialogKey.tsx
Normal file
39
packages/resources/src/pages/file/draw/modules/DialogKey.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Button, Dialog, DialogContent, DialogTitle, FormControlLabel, TextField } from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import { useMetaStore } from './MetaForm';
|
||||
export const DialogKey = ({ onAdd }: { onAdd: (key: string) => void }) => {
|
||||
const { openPropertyModal, setOpenPropertyModal } = useMetaStore();
|
||||
const [key, setKey] = useState('');
|
||||
return (
|
||||
<Dialog open={openPropertyModal} onClose={() => setOpenPropertyModal(false)}>
|
||||
<DialogTitle>添加元数据key</DialogTitle>
|
||||
<DialogContent>
|
||||
<div className='flex flex-col items-center gap-3 px-4 pb-4'>
|
||||
<FormControlLabel
|
||||
label='key'
|
||||
labelPlacement='top'
|
||||
control={<TextField variant='outlined' size='small' name={key} value={key} onChange={(e) => setKey(e.target.value)} />}
|
||||
sx={{
|
||||
alignItems: 'flex-start',
|
||||
'& .MuiFormControlLabel-label': {
|
||||
textAlign: 'left',
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
style={{ color: 'white' }}
|
||||
onClick={() => {
|
||||
onAdd(key);
|
||||
setKey('');
|
||||
setOpenPropertyModal(false);
|
||||
}}>
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
368
packages/resources/src/pages/file/draw/modules/MetaForm.tsx
Normal file
368
packages/resources/src/pages/file/draw/modules/MetaForm.tsx
Normal file
@@ -0,0 +1,368 @@
|
||||
import { useResourceFileStore } from '@/pages/store/resource-file';
|
||||
import { FormControlLabel, Box, TextField, Button, IconButton, ButtonGroup, Tooltip, Select, MenuItem, Typography, FormGroup } from '@mui/material';
|
||||
import { Info, Plus, Save, Share, Shuffle, Trash } from 'lucide-react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { create } from 'zustand';
|
||||
import { uniq } from 'lodash-es';
|
||||
import { DatePicker } from './DatePicker';
|
||||
import { SelectPicker } from './SelectPicker';
|
||||
import dayjs from 'dayjs';
|
||||
import { DialogKey } from './DialogKey';
|
||||
|
||||
export const setShareKeysOperate = (value: 'public' | 'protected' | 'private') => {
|
||||
const keys = ['password', 'usernames', 'expiration-time'];
|
||||
const deleteKeys = keys.map((item) => {
|
||||
return {
|
||||
key: item,
|
||||
operate: 'delete',
|
||||
};
|
||||
});
|
||||
if (value === 'protected') {
|
||||
return deleteKeys.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
operate: 'add',
|
||||
};
|
||||
});
|
||||
}
|
||||
return deleteKeys;
|
||||
};
|
||||
export const keysTips = [
|
||||
{
|
||||
key: 'share',
|
||||
tips: `共享设置
|
||||
1. 设置公共可以直接访问
|
||||
2. 设置受保护需要登录后访问
|
||||
3. 设置私有只有自己可以访问。\n
|
||||
受保护可以设置密码,设置访问的用户名。切换共享状态后,需要重新设置密码和用户名。`,
|
||||
},
|
||||
{
|
||||
key: 'content-type',
|
||||
tips: `内容类型,设置文件的内容类型。默认不要修改。`,
|
||||
},
|
||||
{
|
||||
key: 'app-source',
|
||||
tips: `应用来源,上传方式。默认不要修改。`,
|
||||
},
|
||||
{
|
||||
key: 'cache-control',
|
||||
tips: `缓存控制,设置文件的缓存控制。默认不要修改。`,
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
tips: `密码,设置文件的密码。不设置默认是所有人都可以访问。`,
|
||||
},
|
||||
{
|
||||
key: 'usernames',
|
||||
tips: `用户名,设置文件的用户名。不设置默认是所有人都可以访问。`,
|
||||
parse: (value: string) => {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
return value.split(',');
|
||||
},
|
||||
stringify: (value: string[]) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
return value.join(',');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'expiration-time',
|
||||
tips: `过期时间,设置文件的过期时间。不设置默认是永久。`,
|
||||
parse: (value: Date) => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return dayjs(value);
|
||||
},
|
||||
stringify: (value?: dayjs.Dayjs) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
return value.toISOString();
|
||||
},
|
||||
},
|
||||
];
|
||||
export class KeyParse {
|
||||
static parse(metadata: Record<string, any>) {
|
||||
const keys = Object.keys(metadata);
|
||||
const newMetadata = {};
|
||||
keys.forEach((key) => {
|
||||
const tip = keysTips.find((item) => item.key === key);
|
||||
if (tip && tip.parse) {
|
||||
newMetadata[key] = tip.parse(metadata[key]);
|
||||
} else {
|
||||
newMetadata[key] = metadata[key];
|
||||
}
|
||||
});
|
||||
return newMetadata;
|
||||
}
|
||||
static stringify(metadata: Record<string, any>) {
|
||||
const keys = Object.keys(metadata);
|
||||
const newMetadata = {};
|
||||
keys.forEach((key) => {
|
||||
const tip = keysTips.find((item) => item.key === key);
|
||||
if (tip && tip.stringify) {
|
||||
newMetadata[key] = tip.stringify(metadata[key]);
|
||||
} else {
|
||||
newMetadata[key] = metadata[key];
|
||||
}
|
||||
});
|
||||
return newMetadata;
|
||||
}
|
||||
}
|
||||
export const useMetaOperate = ({
|
||||
onSave,
|
||||
metaStore,
|
||||
handleFormDataChange,
|
||||
}: {
|
||||
onSave: () => void;
|
||||
metaStore: MetaStore;
|
||||
handleFormDataChange: (key: string, value: string | Date | null | string[]) => void;
|
||||
}) => {
|
||||
const { keys, setKeys, openPropertyModal, setOpenPropertyModal } = metaStore;
|
||||
const hasShare = keys.includes('share');
|
||||
const hasPassword = keys.includes('password');
|
||||
const addMeta = (key: string) => {
|
||||
setKeys(uniq([...keys, key]));
|
||||
};
|
||||
const defaultBtnList = [
|
||||
{
|
||||
icon: <Save />,
|
||||
key: 'save',
|
||||
tooltip: '保存元数据, 修改后需要手动保存',
|
||||
onClick: () => onSave(),
|
||||
},
|
||||
{
|
||||
icon: <Plus />,
|
||||
key: 'add',
|
||||
tooltip: '添加元数据',
|
||||
onClick: () => setOpenPropertyModal(true),
|
||||
},
|
||||
];
|
||||
if (!hasShare) {
|
||||
defaultBtnList.push({
|
||||
icon: <Share />,
|
||||
key: 'share',
|
||||
tooltip: '开启共享',
|
||||
onClick: () => addMeta('share'),
|
||||
});
|
||||
}
|
||||
if (hasShare && hasPassword) {
|
||||
defaultBtnList.push({
|
||||
icon: <Shuffle />,
|
||||
key: 'password',
|
||||
tooltip: '随机生成密码',
|
||||
onClick: () => {
|
||||
const password = Math.random().toString(36).substring(2, 8);
|
||||
handleFormDataChange('password', password);
|
||||
},
|
||||
});
|
||||
}
|
||||
return defaultBtnList;
|
||||
};
|
||||
type MetaStore = {
|
||||
keys: string[];
|
||||
setKeys: (keys: string[]) => void;
|
||||
openPropertyModal: boolean;
|
||||
setOpenPropertyModal: (openPropertyModal: boolean) => void;
|
||||
};
|
||||
export const useMetaStore = create<MetaStore>((set) => ({
|
||||
keys: [],
|
||||
setKeys: (keys) => set({ keys }),
|
||||
openPropertyModal: false,
|
||||
setOpenPropertyModal: (openPropertyModal) => set({ openPropertyModal }),
|
||||
}));
|
||||
export const MetaForm = () => {
|
||||
const { resource, updateMeta } = useResourceFileStore();
|
||||
const [formData, setFormData] = useState<any>({});
|
||||
const metaStore = useMetaStore();
|
||||
const { keys, setKeys } = metaStore;
|
||||
useEffect(() => {
|
||||
// setFormData(resource?.metaData || {});
|
||||
setFormData(KeyParse.parse(resource?.metaData || {}));
|
||||
}, [resource]);
|
||||
useEffect(() => {
|
||||
setKeys(Object.keys(resource?.metaData || {}));
|
||||
}, [resource]);
|
||||
if (!keys.length) {
|
||||
return <div className='text-center text-gray-500'>没有元数据</div>;
|
||||
}
|
||||
|
||||
const handleFormDataChange = (key: string, value: string | Date | null | string[]) => {
|
||||
// setFormData({ ...formData, [key]: value });
|
||||
const _formData = { ...formData };
|
||||
if (key === 'share') {
|
||||
const shareKeysOperate = setShareKeysOperate(value as 'public' | 'protected' | 'private');
|
||||
shareKeysOperate.forEach((item) => {
|
||||
if (item.operate === 'add') {
|
||||
_formData[item.key] = '';
|
||||
} else if (item.operate === 'delete') {
|
||||
delete _formData[item.key];
|
||||
}
|
||||
});
|
||||
_formData.share = value;
|
||||
setFormData(_formData);
|
||||
const newKeys = keys
|
||||
.map((item) => {
|
||||
const operate = shareKeysOperate.find((item2) => item2.key === item);
|
||||
if (operate && operate.operate === 'delete') {
|
||||
return null;
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.filter((item) => item !== null);
|
||||
const addKeys = shareKeysOperate.filter((item) => item.operate === 'add').map((item) => item.key);
|
||||
const _newKeys = uniq([...newKeys, ...addKeys]);
|
||||
setKeys(_newKeys);
|
||||
console.log(_newKeys);
|
||||
return;
|
||||
} else {
|
||||
_formData[key] = value;
|
||||
}
|
||||
setFormData(_formData);
|
||||
};
|
||||
const deleteMeta = (key: string) => {
|
||||
setKeys(keys.filter((item) => item !== key));
|
||||
delete formData[key];
|
||||
setFormData({ ...formData });
|
||||
};
|
||||
const onSave = () => {
|
||||
const newMetadata = KeyParse.stringify(formData);
|
||||
updateMeta(newMetadata);
|
||||
};
|
||||
const addMetaKey = (key: string) => {
|
||||
if (keys.includes(key)) {
|
||||
toast.error('元数据key已存在');
|
||||
return;
|
||||
}
|
||||
formData[key] = '';
|
||||
setKeys([...keys, key]);
|
||||
setFormData({ ...formData });
|
||||
};
|
||||
const btnList = useMetaOperate({ onSave, metaStore, handleFormDataChange });
|
||||
|
||||
return (
|
||||
<div className='relative w-full h-full'>
|
||||
<Box className='sticky top-0 z-10 pointer-events-none'>
|
||||
<div className='flex justify-end mr-20'>
|
||||
<div className=' pointer-events-auto'>
|
||||
<ButtonGroup className='bg-white' variant='contained' sx={{ color: 'white' }}>
|
||||
{btnList.map((item) => {
|
||||
const icon = (
|
||||
<IconButton color='secondary' onClick={item.onClick}>
|
||||
{item.icon}
|
||||
</IconButton>
|
||||
);
|
||||
if (item.tooltip) {
|
||||
return (
|
||||
<Tooltip key={item.key} title={item.tooltip} placement='top' arrow>
|
||||
{icon}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return <>{icon}</>;
|
||||
})}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
<FormGroup>
|
||||
{keys.map((key) => {
|
||||
let control: React.ReactNode | null = null;
|
||||
if (key === 'share') {
|
||||
control = <KeyShareSelect name={key} value={formData[key] || ''} onChange={(value) => handleFormDataChange(key, value)} />;
|
||||
} 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)} />;
|
||||
} else {
|
||||
control = <KeyTextField name={key} value={formData[key] || ''} onChange={(value) => handleFormDataChange(key, value)} />;
|
||||
}
|
||||
const Label = () => {
|
||||
const tip = keysTips.find((item) => item.key === key);
|
||||
return (
|
||||
<div className='flex justify-between items-center gap-2'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Typography variant='caption' color='primary'>
|
||||
{key}
|
||||
</Typography>
|
||||
{tip && (
|
||||
<Tooltip title={tip?.tips} placement='top' arrow>
|
||||
<Info size={12} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<IconButton color='error' onClick={() => deleteMeta(key)}>
|
||||
<Trash />
|
||||
</IconButton>
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
</FormGroup>
|
||||
<DialogKey onAdd={addMetaKey} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const KeyTextField = ({ name, value, onChange }: { name: string; value: string; onChange?: (value: string) => void }) => {
|
||||
return (
|
||||
<TextField
|
||||
variant='outlined'
|
||||
size='small'
|
||||
name={name}
|
||||
defaultValue={value}
|
||||
// value={formData[key] || ''}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
sx={{
|
||||
width: '100%',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const KeyShareSelect = ({ name, value, onChange }: { name: string; value: string; onChange?: (value: string) => void }) => {
|
||||
return (
|
||||
<Select
|
||||
variant='outlined'
|
||||
size='small'
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
sx={{
|
||||
width: '100%',
|
||||
marginBottom: '16px',
|
||||
}}>
|
||||
<MenuItem value='public' title='公开'>
|
||||
公开
|
||||
</MenuItem>
|
||||
<MenuItem value='protected' title='受保护'>
|
||||
受保护
|
||||
</MenuItem>
|
||||
<MenuItem value='private' title='私有'>
|
||||
私有
|
||||
</MenuItem>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
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;
|
||||
}
|
||||
|
||||
export const SelectPickerCom = ({ value, onChange }: SelectPickerProps) => {
|
||||
return <Select style={{ width: '100%' }} showSearch={false} mode='tags' value={value} onChange={onChange} />;
|
||||
};
|
||||
|
||||
export const SelectPicker = styled(SelectPickerCom)({
|
||||
'& .ant-select-selector': {
|
||||
color: 'var(--primary-color)',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user