feat: 修复Container界面和File App 和 User App

User App 添加permission
This commit is contained in:
2025-03-19 17:37:12 +08:00
parent 837457a5f7
commit 27d9bdf54e
36 changed files with 927 additions and 1349 deletions

View File

@@ -7,7 +7,18 @@ export const Button = (props: ButtonProps) => {
export const IconButton = (props: ButtonProps) => {
const { variant = 'contained', color = 'primary', sx, children, ...rest } = props;
return (
<MuiButton variant={variant} color={color} {...rest} sx={{ color: 'white', minWidth: '32px', padding: '4px', ...sx }}>
<MuiButton variant={variant} color={color} {...rest} sx={{ color: 'white', minWidth: '32px', padding: '8px', ...sx }}>
{children}
</MuiButton>
);
};
export const IconButtonItem = (props: ButtonProps) => {
const { variant = 'contained', size = 'small', color = 'primary', sx, children, ...rest } = props;
return (
<MuiButton {...props} >
{/* <MuiButton variant={'contained'} size={size} color={color} {...rest} sx={{ color: 'white', ...sx }}> */}
{children}
</MuiButton>
);

View File

@@ -0,0 +1,17 @@
import clsx from 'clsx';
import twMerge from 'tailwind-merge';
type CardBlankProps = {
number?: number;
className?: string;
};
export const CardBlank = (props: CardBlankProps) => {
const { number = 4, className } = props;
return (
<>
{new Array(number).fill(0).map((_, index) => {
return <div key={index} className={clsx('w-[300px] shark-0', className)}></div>;
})}
</>
);
};

View File

@@ -75,6 +75,12 @@ export const themeOptions: ThemeOptions = {
'&:hover': {
backgroundColor: amber[100],
},
'&.MuiButton-contained': {
color: '#ffffff',
':hover': {
color: amber[500],
},
},
},
},
},
@@ -132,6 +138,13 @@ export const themeOptions: ThemeOptions = {
},
},
},
MuiIconButton: {
styleOverrides: {
root: {
color: '#ffffff', // Set default font color to white
},
},
},
},
};
@@ -150,8 +163,8 @@ export const useTheme = () => {
/**
* 自定义主题设置。
* @param param0
* @returns
* @param param0
* @returns
*/
export const CustomThemeProvider = ({ children, themeOptions: customThemeOptions }: { children: React.ReactNode; themeOptions?: ThemeOptions }) => {
const theme = createTheme(customThemeOptions || themeOptions);

View File

@@ -1,6 +1,8 @@
@import 'tailwindcss';
@theme {
--light-color-primary: oklch(0.72 0.11 178);
--light-color-secondary: oklch(0.72 0.11 178);
}
--color-primary: #ffc107;
--color-secondary: #ffa000;
--color-success: #28a745;
--scrollbar-color: #ffc107; /* 滚动条颜色 */
}

View File

@@ -1,4 +1,5 @@
@import 'tailwindcss';
@import '@kevisual/center-components/theme/wind-theme.css';
@layer components {
.test-loading {

View File

@@ -0,0 +1,10 @@
export const iText = {
share: {
title: '共享设置',
tips: `共享设置
1. 设置公共可以直接访问
2. 设置受保护需要登录后访问
3. 设置私有只有自己可以访问。\n
受保护可以设置密码,设置访问的用户名。切换共享状态后,需要重新设置密码和用户名。`,
},
};

View File

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

View File

@@ -1,16 +1,17 @@
import ReactDatePicker from 'antd/es/date-picker';
import { useTheme } from '@mui/material';
import { styled, useTheme } from '@mui/material';
import 'antd/es/date-picker/style/index';
interface DatePickerProps {
value?: Date | null;
onChange?: (date: Date | null) => void;
className?: string;
}
export const DatePicker = ({ value, onChange }: DatePickerProps) => {
export const DatePickerCom = ({ value, onChange, className }: DatePickerProps) => {
const theme = useTheme();
const primaryColor = theme.palette.primary.main;
return (
<div>
<div className={className}>
<ReactDatePicker
placement='topLeft'
placeholder='请选择日期'
@@ -26,3 +27,12 @@ export const DatePicker = ({ value, onChange }: DatePickerProps) => {
</div>
);
};
export const DatePicker = styled(DatePickerCom)(({ theme }) => ({
'& .ant-picker-input': {
color: 'var(--color-primary)',
'& .ant-picker-suffix, .ant-picker-clear': {
color: 'var(--color-primary)',
},
},
}));

View File

@@ -9,7 +9,8 @@ import { DatePicker } from './DatePicker';
import { SelectPicker } from './SelectPicker';
import dayjs from 'dayjs';
import { DialogKey } from './DialogKey';
import { keysTips, KeyParse } from '../../modules/key-parse';
import { KeyShareSelect, KeyTextField } from '../../modules/PermissionManager';
export const setShareKeysOperate = (value: 'public' | 'protected' | 'private') => {
const keys = ['password', 'usernames', 'expiration-time'];
const deleteKeys = keys.map((item) => {
@@ -28,92 +29,7 @@ export const setShareKeysOperate = (value: 'public' | 'protected' | 'private') =
}
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,
@@ -270,7 +186,7 @@ export const MetaForm = () => {
</div>
</div>
</Box>
<FormGroup>
<form>
{keys.map((key) => {
let control: React.ReactNode | null = null;
if (key === 'share') {
@@ -320,49 +236,10 @@ export const MetaForm = () => {
</div>
);
})}
</FormGroup>
</form>
<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>
);
};

View File

@@ -5,14 +5,26 @@ import 'antd/es/select/style/index';
interface SelectPickerProps {
value: string[];
onChange: (value: string[]) => void;
className?: string;
}
export const SelectPickerCom = ({ value, onChange }: SelectPickerProps) => {
return <Select style={{ width: '100%' }} showSearch={false} mode='tags' value={value} onChange={onChange} />;
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 }) => ({
export const SelectPicker = styled(SelectPickerCom)({
'& .ant-select-selector': {
color: 'var(--primary-color)',
'& .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

@@ -8,6 +8,7 @@ import { PrefixRedirect } from './modules/PrefixRedirect';
import { UploadButton } from '../upload';
import { FileDrawer } from './draw/FileDrawer';
import { useResourceFileStore } from '../store/resource-file';
import { IconButtonItem } from '@kevisual/center-components/button/index.tsx';
export const FileApp = () => {
const { getList, prefix, setListType, listType } = useResourceStore();
const { getStatFile, prefix: statPrefix, openDrawer } = useResourceFileStore();

View File

@@ -0,0 +1,141 @@
import { useEffect, useState } from 'react';
import { KeyParse, getTips } from './key-parse';
import { FormControlLabel, TextField, Select, MenuItem, FormGroup, Tooltip } from '@mui/material';
import { DatePicker } from '../draw/modules/DatePicker';
import { SelectPicker } from '../draw/modules/SelectPicker';
import { HelpCircle } from 'lucide-react';
export 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>
);
};
export 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',
}}
/>
);
};
type PermissionManagerProps = {
value: Record<string, any>;
onChange: (value: Record<string, any>) => void;
};
export const PermissionManager = ({ value, onChange }: PermissionManagerProps) => {
const [formData, setFormData] = useState<any>(value);
const [keys, setKeys] = useState<any>([]);
useEffect(() => {
const hasShare = value?.share && value?.share === 'protected';
setFormData(KeyParse.parse(value || {}));
if (hasShare) {
setKeys(['password', 'usernames', 'expiration-time']);
} else {
setKeys([]);
}
}, [value]);
const onChangeValue = (key: string, newValue: any) => {
// setFormData({ ...formData, [key]: newValue });
let newFormData = { ...formData, [key]: newValue };
if (key === 'share') {
if (newValue === 'protected') {
newFormData = { ...newFormData, password: '', usernames: [], 'expiration-time': null };
onChange(KeyParse.stringify(newFormData));
setKeys(['password', 'usernames', 'expiration-time']);
} else {
delete newFormData.password;
delete newFormData.usernames;
delete newFormData['expiration-time'];
onChange(KeyParse.stringify(newFormData));
setKeys([]);
}
} else {
onChange(KeyParse.stringify(newFormData));
}
};
return (
<form className='w-[400px] flex flex-col gap-2'>
<FormControlLabel
labelPlacement='top'
control={<KeyShareSelect name='share' value={formData?.share} onChange={(value) => onChangeValue('share', value)} />}
label={
<div className='flex items-center gap-1'>
Share
<Tooltip title={getTips('share')}>
<HelpCircle size={16} />
</Tooltip>
</div>
}
sx={{
alignItems: 'flex-start',
'& .MuiFormControlLabel-label': {
textAlign: 'left',
width: '100%',
},
}}
/>
{keys.map((item: any) => {
let control: React.ReactNode | null = null;
if (item === 'expiration-time') {
control = <DatePicker value={formData[item] || ''} onChange={(date) => onChangeValue(item, date)} />;
} else if (item === 'usernames') {
control = <SelectPicker 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%',
},
}}
/>
);
})}
</form>
);
};

View File

@@ -0,0 +1,90 @@
import dayjs from 'dayjs';
export const getTips = (key: string) => {
return keysTips.find((item) => item.key === key)?.tips;
};
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;
}
}

View File

@@ -0,0 +1,5 @@
export * from './tools/to-file';
export * from './utils/upload';
export * from './utils/upload-chunk';

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,93 @@
const getFileExtension = (filename: string) => {
return filename.split('.').pop();
};
const getFileType = (extension: string) => {
switch (extension) {
case 'js':
return 'text/javascript';
case 'css':
return 'text/css';
case 'html':
return 'text/html';
case 'json':
return 'application/json';
case 'png':
return 'image/png';
case 'jpg':
return 'image/jpeg';
case 'jpeg':
return 'image/jpeg';
case 'gif':
return 'image/gif';
case 'svg':
return 'image/svg+xml';
case 'ico':
return 'image/x-icon';
case 'webp':
return 'image/webp';
case 'gif':
return 'image/gif';
case 'ico':
return 'image/x-icon';
default:
return 'text/plain';
}
};
const checkIsBase64 = (content: string) => {
return content.startsWith('data:');
};
export const getDirectoryAndName = (filename: string) => {
if (!filename) {
return null;
}
if (filename.startsWith('.')) {
return null;
} else {
filename = filename.replace(/^\/+/, ''); // Remove all leading slashes
}
const hasDirectory = filename.includes('/');
if (!hasDirectory) {
return { directory: '', name: filename };
}
const parts = filename.split('/');
const name = parts.pop()!; // Get the last part as the file name
const directory = parts.join('/'); // Join the remaining parts as the directory
return { directory, name };
};
/**
* 把字符串转为文件流并返回文件流根据filename的扩展名自动设置文件类型.
* 当不是文本类型自动需要把base64的字符串转为blob
* @param content 字符串
* @param filename 文件名
* @returns 文件流
*/
export const toFile = (content: string, filename: string) => {
// 如果文件名是 a/d/a.js 格式的则需要把d作为目录a.js作为文件名
const directoryAndName = getDirectoryAndName(filename);
if (!directoryAndName) {
throw new Error('Invalid filename');
}
const { name } = directoryAndName;
const extension = getFileExtension(name);
if (!extension) {
throw new Error('Invalid filename');
}
const isBase64 = checkIsBase64(content);
const type = getFileType(extension);
if (isBase64) {
// Decode base64 string
const base64Data = content.split(',')[1]; // Remove the data URL prefix
const byteCharacters = atob(base64Data);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type });
return new File([blob], filename, { type });
} else {
const blob = new Blob([content], { type });
return new File([blob], filename, { type });
}
};

View File

@@ -12,7 +12,7 @@ type ConvertOpts = {
};
export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
const { directory } = opts;
const { directory, appKey, version, username } = opts;
return new Promise(async (resolve, reject) => {
const token = localStorage.getItem('token');
if (!token) {
@@ -66,6 +66,13 @@ export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
if (directory) {
formData.append('directory', directory);
}
if (appKey && version) {
formData.append('appKey', appKey);
formData.append('version', version);
}
if (username) {
formData.append('username', username);
}
try {
const res = await fetch('/api/s1/resources/upload/chunk?taskId=' + taskId, {
method: 'POST',

View File

@@ -11,7 +11,7 @@ type ConvertOpts = {
directory?: string;
};
export const uploadFiles = async (files: File[], opts: ConvertOpts) => {
const { directory } = opts;
const { directory, appKey, version, username } = opts;
return new Promise((resolve, reject) => {
const formData = new FormData();
const webkitRelativePath = files[0]?.webkitRelativePath;
@@ -30,9 +30,13 @@ export const uploadFiles = async (files: File[], opts: ConvertOpts) => {
if (directory) {
formData.append('directory', directory);
}
console.log('formData', formData, files);
resolve(null);
return;
if (appKey && version) {
formData.append('appKey', appKey);
formData.append('version', version);
}
if (username) {
formData.append('username', username);
}
const token = localStorage.getItem('token');
if (!token) {
toastLogin();

View File

@@ -0,0 +1,19 @@
.scrollbar {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-color) #fff;
}
.scrollbar::-webkit-scrollbar {
height: 4px;
width: 4px;
}
.scrollbar::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-color);
border-radius: 10px;
}
.scrollbar::-webkit-scrollbar-track {
background: #fff;
}