feat: add resources
This commit is contained in:
@@ -1,16 +1,60 @@
|
||||
import { useResourceStore } from '@/pages/store/resource';
|
||||
import { useResourceFileStore } from '@/pages/store/resource-file';
|
||||
import { Drawer } from '@mui/material';
|
||||
import { Box, Divider, Drawer, Tab, Tabs } from '@mui/material';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { QuickValues, QuickTabs } from './QuickTabs';
|
||||
|
||||
export const FileDrawer = () => {
|
||||
const { prefix } = useResourceStore();
|
||||
const { resource, openDrawer, setOpenDrawer } = useResourceFileStore();
|
||||
const [tab, setTab] = useState<string>(QuickValues[0]);
|
||||
const quickCom = useMemo(() => {
|
||||
return QuickTabs.find((item) => item.value === tab)?.component;
|
||||
}, [tab]);
|
||||
return (
|
||||
<Drawer open={openDrawer} onClose={() => setOpenDrawer(false)} anchor='right' {...(!openDrawer && { inert: true })}>
|
||||
<div className='p-4 w-[600px]'>
|
||||
<h2 className='text-2xl font-bold'>{resource?.name ? resource.name.replace(prefix, '') : resource?.prefix?.replace(prefix, '')}</h2>
|
||||
<pre className='flex flex-col gap-2'>{JSON.stringify(resource, null, 2)}</pre>
|
||||
</div>
|
||||
</Drawer>
|
||||
<>
|
||||
<Drawer
|
||||
// aria-label={'文件详情'}
|
||||
open={openDrawer}
|
||||
onClose={() => {
|
||||
// document.getElementById('focus-safe-element')?.focus();
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
if (activeElement) {
|
||||
activeElement.blur();
|
||||
}
|
||||
setOpenDrawer(false);
|
||||
}}
|
||||
ModalProps={{
|
||||
keepMounted: true,
|
||||
}}
|
||||
anchor='right'
|
||||
style={{
|
||||
zIndex: 1000,
|
||||
}}>
|
||||
<div className='p-4 w-[400px] max-w-[90%] overflow-hidden h-full sm:w-[600px]'>
|
||||
<div style={{ height: '140px' }}>
|
||||
<h2 className='text-2xl font-bold truncate py-2 pb-6 '>
|
||||
{resource?.name ? resource.name.replace(prefix, '') : resource?.prefix?.replace(prefix, '')}
|
||||
</h2>
|
||||
<Divider />
|
||||
<Box sx={{ borderBottom: 1, mt: 2, borderColor: 'divider' }}>
|
||||
<Tabs value={tab} onChange={(_, value) => setTab(value)}>
|
||||
{QuickTabs.map((item) => (
|
||||
<Tab key={item.value} label={item.label} value={item.value} icon={item.icon} iconPosition='start' />
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
</div>
|
||||
<Box className='' sx={{ p: 2, height: 'calc(100% - 140px)', overflow: 'hidden' }}>
|
||||
<div className='scrollbar' style={{ height: '100%', overflow: 'auto' }}>
|
||||
{quickCom && quickCom()}
|
||||
</div>
|
||||
</Box>
|
||||
</div>
|
||||
</Drawer>
|
||||
<button id='focus-safe-element' style={{ display: 'none' }}>
|
||||
Focus Safe Element
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
27
packages/resources/src/pages/file/draw/QuickTabs.tsx
Normal file
27
packages/resources/src/pages/file/draw/QuickTabs.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { MetaForm } from './modules/MetaForm';
|
||||
import { ContentForm } from './modules/ContextForm';
|
||||
import { Cpu, File, FileText, Rabbit } from 'lucide-react';
|
||||
import { Quick } from './quick';
|
||||
export const QuickTabs = [
|
||||
{
|
||||
label: 'Quick',
|
||||
value: 'quick',
|
||||
icon: <Rabbit />,
|
||||
index: 99,
|
||||
component: () => <Quick />,
|
||||
},
|
||||
{
|
||||
label: '元数据',
|
||||
value: 'meta',
|
||||
icon: <Cpu />,
|
||||
component: () => <MetaForm />,
|
||||
},
|
||||
{
|
||||
label: '内容',
|
||||
value: 'content',
|
||||
icon: <FileText />,
|
||||
component: () => <ContentForm />,
|
||||
},
|
||||
].sort((a, b) => (b?.index || 0) - (a?.index || 0));
|
||||
|
||||
export const QuickValues = QuickTabs.map((item) => item.value);
|
||||
@@ -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)',
|
||||
},
|
||||
});
|
||||
11
packages/resources/src/pages/file/draw/quick/QuickLink.tsx
Normal file
11
packages/resources/src/pages/file/draw/quick/QuickLink.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 链接生成,markdown超链接, 多选项。
|
||||
* 1. markdown
|
||||
* 2. HTML image
|
||||
* 3. URL https url
|
||||
* @returns
|
||||
*/
|
||||
export const QuickLink = () => {
|
||||
const url = 'https://ab.c.com/a/b/c.jpg';
|
||||
return <div>QuickLink</div>;
|
||||
};
|
||||
131
packages/resources/src/pages/file/draw/quick/QuickPreview.tsx
Normal file
131
packages/resources/src/pages/file/draw/quick/QuickPreview.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { useResourceFileStore } from '@/pages/store/resource-file';
|
||||
import { useSettingsStore } from '@/pages/store/settings';
|
||||
import { Button, Tooltip, useTheme } from '@mui/material';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import { Accordion, AccordionSummary, AccordionDetails } from '@mui/material';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { getFileType } from '../../FileIcon';
|
||||
import { toast } from 'react-toastify';
|
||||
import clsx from 'clsx';
|
||||
type AccordionItem = {
|
||||
title?: string;
|
||||
key?: string;
|
||||
url?: string;
|
||||
content?: any;
|
||||
clickCopy?: boolean;
|
||||
};
|
||||
export const QuickPreview = () => {
|
||||
const { resource } = useResourceFileStore(
|
||||
useShallow((state) => ({
|
||||
resource: state.resource,
|
||||
})),
|
||||
);
|
||||
const { settings, baseUrl } = useSettingsStore(
|
||||
useShallow((state) => ({
|
||||
settings: state.settings,
|
||||
baseUrl: state.baseUrl,
|
||||
})),
|
||||
);
|
||||
const fileType = useMemo(() => getFileType(resource?.name), [resource]);
|
||||
const accordionList = useMemo(() => {
|
||||
const username = settings?.username;
|
||||
if (!username) {
|
||||
toast.error('请先登录');
|
||||
return [];
|
||||
}
|
||||
const _url = new URL(`${baseUrl}/api/s1/share/${username}/${resource?.name}`);
|
||||
const meta = resource?.metaData ?? {};
|
||||
if (meta.password) {
|
||||
_url.searchParams.set('p', meta.password);
|
||||
}
|
||||
const url = _url.toString();
|
||||
let accordionList: AccordionItem[] = [];
|
||||
const encodeUrl = encodeURIComponent(url);
|
||||
const previewUrl = `${baseUrl}/app/preview?fileUrl=${encodeUrl}&fileType=${fileType}`;
|
||||
accordionList.push({
|
||||
title: '文件预览',
|
||||
key: 'preview-file',
|
||||
url: previewUrl,
|
||||
content: (
|
||||
<div className=''>
|
||||
<div className='text-sm break-words'>{previewUrl}</div>
|
||||
<Button variant='contained' color='primary' style={{ color: 'white' }} onClick={() => window.open(previewUrl, '_blank')}>
|
||||
点击查看
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
if (fileType === 'image') {
|
||||
accordionList.push({
|
||||
title: '预览图片',
|
||||
key: 'preview-image',
|
||||
url: url,
|
||||
content: <img className='w-full h-full border-2 border-gray-300 rounded-md' src={url} alt={resource?.name} />,
|
||||
});
|
||||
accordionList.push({
|
||||
title: 'markdown链接',
|
||||
key: 'markdown-link',
|
||||
url: url,
|
||||
clickCopy: true,
|
||||
content: ``,
|
||||
});
|
||||
accordionList.push({
|
||||
title: 'HTML图片',
|
||||
key: 'html-image',
|
||||
url: url,
|
||||
clickCopy: true,
|
||||
content: `<img style="width: 100%;height: 100%;" src="${url}" alt="${resource?.name}" />`,
|
||||
});
|
||||
accordionList.push({
|
||||
title: 'HTML超链接',
|
||||
key: 'html-link',
|
||||
url: url,
|
||||
clickCopy: true,
|
||||
content: `<a href="${url}">${resource?.name}</a>`,
|
||||
});
|
||||
}
|
||||
const downloadUrl = new URL(url);
|
||||
downloadUrl.searchParams.set('download', 'true');
|
||||
accordionList.push({
|
||||
title: '下载地址',
|
||||
key: 'download-url',
|
||||
url: downloadUrl.toString(),
|
||||
content: (
|
||||
<div className=''>
|
||||
<div className='text-sm break-words'>{downloadUrl.toString()}</div>
|
||||
<Button variant='contained' color='primary' style={{ color: 'white' }} onClick={() => window.open(downloadUrl.toString(), '_blank')}>
|
||||
点击下载
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
return accordionList;
|
||||
}, [resource, baseUrl, settings]);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<div className='p-4'>
|
||||
{accordionList.map((item) => (
|
||||
<Accordion key={item.key}>
|
||||
<AccordionSummary expandIcon={<ChevronDown color={theme.palette.text.secondary} />}>{item.title}</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<div
|
||||
className={clsx('text-sm whitespace-pre-wrap w-full overflow-ellipsis overflow-hidden', {
|
||||
'cursor-copy': item.clickCopy, // cursor-copy的有吗
|
||||
})}
|
||||
onClick={() => {
|
||||
if (item.clickCopy) {
|
||||
navigator.clipboard.writeText(item.content);
|
||||
toast.success('复制成功');
|
||||
}
|
||||
}}>
|
||||
{item.content}
|
||||
</div>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
41
packages/resources/src/pages/file/draw/quick/index.tsx
Normal file
41
packages/resources/src/pages/file/draw/quick/index.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useResourceFileStore } from '@/pages/store/resource-file';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import { QuickPreview } from './QuickPreview';
|
||||
import { useMemo } from 'react';
|
||||
import { getFileType } from '../../FileIcon';
|
||||
|
||||
const QuickModules = [
|
||||
{
|
||||
key: 'link',
|
||||
type: 'link',
|
||||
categroy: ['image', 'video', 'audio'],
|
||||
tooltips: `链接生成,markdown超链接`,
|
||||
component: () => <QuickPreview />,
|
||||
},
|
||||
{
|
||||
key: 'preview',
|
||||
type: 'all',
|
||||
tooltips: `预览页面内容`,
|
||||
component: () => <QuickPreview />,
|
||||
},
|
||||
];
|
||||
export const Quick = () => {
|
||||
const { resource } = useResourceFileStore(
|
||||
useShallow((state) => ({
|
||||
resource: state.resource,
|
||||
})),
|
||||
);
|
||||
const quickModule = useMemo(() => {
|
||||
const fileType = getFileType(resource?.name);
|
||||
const allCanUseModule = QuickModules.filter((item) => item.type === 'all');
|
||||
if (!fileType) {
|
||||
return allCanUseModule;
|
||||
}
|
||||
return QuickModules.filter((item) => item.type === fileType);
|
||||
}, [resource]);
|
||||
return (
|
||||
<>
|
||||
<QuickPreview />
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user