update resources

This commit is contained in:
xion 2025-03-27 19:41:28 +08:00
parent d649666379
commit 43d612fff3
35 changed files with 1862 additions and 764 deletions

8
.gitmodules vendored
View File

@ -3,4 +3,10 @@
url = git@git.xiongxiao.me:kevisual/kevsiual-query-login.git
[submodule "submodules/query-config"]
path = submodules/query-config
url = git@git.xiongxiao.me:kevisual/kevsiual-query-config.git
url = git@git.xiongxiao.me:kevisual/kevsiual-query-config.git
[submodule "submodules/wallnote"]
path = submodules/wallnote
url = git@git.xiongxiao.me:tailored/wallnote.git
[submodule "packages/kevisual-official"]
path = packages/kevisual-official
url = git@git.xiongxiao.me:kevisual/official-website.git

View File

@ -1,31 +0,0 @@
{
"name": "@kevisual/components",
"version": "0.0.1",
"description": "center components",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"files": [
"src"
],
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me>",
"license": "MIT",
"type": "module",
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/material": "^6.4.7",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-hook-form": "^7.54.2"
},
"exports": {
".": "./src/index.tsx",
"./*": "./src/*"
},
"devDependencies": {
"clsx": "^2.1.1",
"tailwind-merge": "^3.0.2"
}
}

View File

@ -1,25 +0,0 @@
import MuiButton, { ButtonProps } from '@mui/material/Button';
export const Button = (props: ButtonProps) => {
return <MuiButton {...props} />;
};
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: '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

@ -1,17 +0,0 @@
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

@ -1,7 +0,0 @@
import clsx, { ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export const clsxMerge = (...args: ClassValue[]) => {
return twMerge(clsx(...args));
};
export { clsx };

View File

@ -1,6 +0,0 @@
export * from './theme';
/**
* , 使theme的defaultProps
*/
export * from './input/TextField';

View File

@ -1,23 +0,0 @@
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

@ -1,47 +0,0 @@
import { FormControlLabel, TextField } from '@mui/material';
import { useForm, Controller } from 'react-hook-form';
export const InputControl = ({ name, value, onChange }: { name: string; value: string; onChange?: (value: string) => void }) => {
return (
<TextField
variant='outlined'
size='small'
name={name}
value={value || ''}
onChange={(e) => onChange?.(e.target.value)}
sx={{
width: '100%',
marginBottom: '16px',
}}
/>
);
};
type FormProps = {
onSubmit?: (data: any) => void;
children?: React.ReactNode;
};
export const FormDemo = (props: FormProps) => {
const { control, handleSubmit } = useForm();
const { onSubmit = () => {}, children } = props;
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name='name'
control={control}
defaultValue=''
rules={{ required: 'Name is required' }}
render={({ field, fieldState: { error } }) => (
<TextField
{...field}
label='Name'
variant='outlined'
margin='normal'
fullWidth //
error={!!error}
helperText={<>{error?.message}</>}
/>
)}
/>
</form>
);
};

View File

@ -1,99 +0,0 @@
import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Button } from '@mui/material';
import { useRef, useState } from 'react';
import type { ModalFuncProps } from 'antd';
export const Confirm = ({
open,
onClose,
title,
content,
onConfirm,
okText = '确认',
cancelText = '取消',
}: {
open: boolean;
onClose: () => void;
title: string;
content: string;
onConfirm?: () => void;
okText?: string;
cancelText?: string;
}) => {
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'>
<DialogTitle id='alert-dialog-title' className='text-secondary min-w-[300px]'>
{title}
</DialogTitle>
<DialogContent>
<DialogContentText id='alert-dialog-description'>{content}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color='primary'>
{cancelText || '取消'}
</Button>
<Button onClick={onConfirm} variant='contained' color='primary' autoFocus>
{okText || '确认'}
</Button>
</DialogActions>
</Dialog>
);
};
type Fn = () => void;
export const useModal = () => {
const [open, setOpen] = useState(false);
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const fns = useRef<{
onConfirm: Fn;
onCancel: Fn;
okText: string;
cancelText: string;
}>({
onConfirm: () => {},
onCancel: () => {},
okText: '确认',
cancelText: '取消',
});
const modal = {
confirm: (props: ModalFuncProps) => {
setOpen(true);
setTitle(props.title as string);
setContent(props.content as string);
fns.current.onConfirm = async () => {
const isClose = await props.onOk?.();
if (!isClose) {
setOpen(false);
}
};
fns.current.onCancel = async () => {
await props.onCancel?.();
setOpen(false);
};
fns.current.okText = props.okText as string;
fns.current.cancelText = props.cancelText as string;
},
cancel: () => {
setOpen(false);
fns.current.onCancel();
},
};
const contextHolder = (
<Confirm
open={open}
okText={fns.current.okText}
cancelText={fns.current.cancelText}
onClose={() => {
setOpen(false);
fns.current.onCancel();
}}
title={title}
content={content}
onConfirm={fns.current.onConfirm}
/>
);
return [modal, contextHolder] as [typeof modal, React.ReactNode];
};

View File

@ -1,58 +0,0 @@
import { Fragment, useEffect, useState } from 'react';
import Autocomplete from '@mui/material/Autocomplete';
import { TextField, Chip } from '@mui/material';
type TagsInputProps = {
value: string[];
onChange: (value: string[]) => void;
placeholder?: string;
label?: any;
showLabel?: boolean;
};
export const TagsInput = ({ value, onChange, placeholder = '', label = '', showLabel = false }: TagsInputProps) => {
const [tags, setTags] = useState<string[]>(value);
useEffect(() => {
setTags(value);
}, [value]);
const randomid = () => {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
};
return (
<Autocomplete
multiple
freeSolo
options={[]}
value={tags}
onChange={(event, newValue) => {
// 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) => (
<Chip
variant='outlined'
sx={{
borderColor: 'primary.main',
borderRadius: '4px',
'&:hover': {
borderColor: 'primary.main',
},
'& .MuiChip-deleteIcon': {
color: 'secondary.main',
},
}}
label={option}
{...getTagProps({ index })}
key={`${id}-${index}`}
/>
));
return <Fragment key={id}>{com}</Fragment>;
}}
renderInput={(params) => <TextField {...params} label={showLabel ? label : ''} placeholder={placeholder} />}
/>
);
};

View File

@ -1,20 +0,0 @@
import { MenuItem, Select as MuiSelect, SelectProps as MuiSelectProps } from '@mui/material';
import React from 'react';
type SelectProps = {
options?: { label: string; value: string }[];
} & MuiSelectProps;
export const Select = React.forwardRef((props: SelectProps, ref) => {
const { options, ...rest } = props;
console.log(props, 'props');
return (
<MuiSelect {...rest} ref={ref}>
{options?.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</MuiSelect>
);
});

View File

@ -1,226 +0,0 @@
import { createTheme, Shadows, ThemeOptions } from '@mui/material/styles';
import { useTheme as useMuiTheme, Theme } from '@mui/material/styles';
import { amber, red } from '@mui/material/colors';
import { ThemeProvider } from '@mui/material/styles';
const generateShadows = (color: string): Shadows => {
return [
'none',
`0px 2px 1px -1px ${color}`,
`0px 1px 1px 0px ${color}`,
`0px 1px 3px 0px ${color}`,
`0px 2px 4px -1px ${color}`,
`0px 3px 5px -1px ${color}`,
`0px 3px 5px -1px ${color}`,
`0px 4px 5px -2px ${color}`,
`0px 5px 5px -3px ${color}`,
`0px 5px 6px -3px ${color}`,
`0px 6px 6px -3px ${color}`,
`0px 6px 7px -4px ${color}`,
`0px 7px 8px -4px ${color}`,
`0px 7px 8px -4px ${color}`,
`0px 8px 9px -5px ${color}`,
`0px 8px 9px -5px ${color}`,
`0px 9px 10px -5px ${color}`,
`0px 9px 11px -6px ${color}`,
`0px 10px 12px -6px ${color}`,
`0px 10px 13px -6px ${color}`,
`0px 11px 13px -7px ${color}`,
`0px 11px 14px -7px ${color}`,
`0px 12px 15px -7px ${color}`,
`0px 12px 16px -8px ${color}`,
`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: primaryMain, // amber[300]
},
secondary: {
main: secondaryMain, // amber[500]
},
divider: amber[200],
common: {
white: secondaryMain,
},
text: {
primary: amber[600],
secondary: amber[600],
},
background: {
default: '#ffffff', // 设置默认背景颜色
// paper: '#f5f5f5', // 设置纸张背景颜色
},
error: {
main: red[500], // 设置错误颜色 "#f44336"
},
},
shadows: generateShadows('rgba(255, 193, 7, 0.2)'),
typography: {
// fontFamily: 'Roboto, sans-serif',
},
components: {
MuiButtonBase: {
defaultProps: {
disableRipple: true,
},
styleOverrides: {
root: {
'&:hover': {
backgroundColor: amber[100],
},
'&.MuiButton-contained': {
color: '#ffffff',
':hover': {
color: secondaryMain,
},
},
},
},
},
MuiButtonGroup: {
styleOverrides: {
root: {
'& .MuiButton-root': {
borderColor: amber[600],
},
},
},
},
MuiTextField: {
defaultProps: {
fullWidth: true,
size: 'small',
slotProps: {
inputLabel: {
shrink: true,
},
},
},
styleOverrides: {
root: {
'& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: amber[300],
},
'&:hover fieldset': {
borderColor: amber[500],
},
'& .MuiInputBase-input': {
color: amber[600],
},
},
'& .MuiInputLabel-root': {
color: amber[600],
},
},
},
},
MuiAutocomplete: {
defaultProps: {
size: 'small',
},
},
MuiSelect: {
styleOverrides: {
root: {
'& .MuiOutlinedInput-notchedOutline': {
borderColor: amber[300],
},
'&:hover .MuiOutlinedInput-notchedOutline': {
borderColor: amber[500],
},
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderColor: amber[500],
},
'& .MuiSelect-icon': {
color: amber[500], // Set arrow icon color to primary
},
},
},
},
MuiCard: {
styleOverrides: {
root: {
// border: `1px solid ${amber[300]}`,
},
},
},
MuiIconButton: {
styleOverrides: {
root: {
color: '#ffffff', // Set default font color to white
},
},
},
MuiFormControlLabel: {
defaultProps: {
labelPlacement: 'top',
},
styleOverrides: {
root: {
color: amber[600],
alignItems: 'flex-start',
'& .MuiFormControlLabel-label': {
textAlign: 'left',
width: '100%',
},
'& .MuiFormControlLabel-root': {
width: '100%',
},
'& .MuiInputBase-root': {
width: '100%',
},
},
},
},
MuiMenuItem: {
styleOverrides: {
root: {
'&.Mui-selected': {
backgroundColor: amber[500],
color: '#ffffff',
'&:hover': {
backgroundColor: amber[600],
color: '#ffffff',
},
},
},
},
},
},
};
/**
* https://bareynol.github.io/mui-theme-creator/
*/
export const theme = createTheme(themeOptions);
/**
*
* @returns
*/
export const useTheme = () => {
return useMuiTheme<Theme>();
};
/**
*
* @param param0
* @returns
*/
export const CustomThemeProvider = ({ children, themeOptions: customThemeOptions }: { children: React.ReactNode; themeOptions?: ThemeOptions }) => {
const theme = createTheme(customThemeOptions || themeOptions);
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
};
// TODO: think
export const getComponentProps = () => {};

View File

@ -1,76 +0,0 @@
@import 'tailwindcss';
@theme {
--color-primary: #ffc107;
--color-secondary: #ffa000;
--color-success: #28a745;
--color-scrollbar-thumb: #999999;
--color-scrollbar-track: rgba(0, 0, 0, 0.1);
--color-scrollbar-thumb-hover: #666666;
--scrollbar-color: #ffc107; /* 滚动条颜色 */
}
html,
body {
width: 100%;
height: 100%;
font-size: 16px;
font-family: 'Montserrat', sans-serif;
}
/* font-family */
@utility font-family-mon {
font-family: 'Montserrat', sans-serif;
}
@utility font-family-rob {
font-family: 'Roboto', sans-serif;
}
@utility font-family-int {
font-family: 'Inter', sans-serif;
}
@utility font-family-orb {
font-family: 'Orbitron', sans-serif;
}
@utility font-family-din {
font-family: 'DIN', sans-serif;
}
@utility flex-row-center {
@apply flex flex-row items-center justify-center;
}
@utility flex-col-center {
@apply flex flex-col items-center justify-center;
}
@utility scrollbar {
overflow: auto;
/* 整个滚动条 */
&::-webkit-scrollbar {
width: 3px;
height: 3px;
}
&::-webkit-scrollbar-track {
background-color: var(--color-scrollbar-track);
}
/* 滚动条有滑块的轨道部分 */
&::-webkit-scrollbar-track-piece {
background-color: transparent;
border-radius: 1px;
}
/* 滚动条滑块(竖向:vertical 横向:horizontal) */
&::-webkit-scrollbar-thumb {
cursor: pointer;
background-color: var(--color-scrollbar-thumb);
border-radius: 5px;
}
/* 滚动条滑块hover */
&::-webkit-scrollbar-thumb:hover {
background-color: var(--color-scrollbar-thumb-hover);
}
/* 同时有垂直和水平滚动条时交汇的部分 */
&::-webkit-scrollbar-corner {
display: block; /* 修复交汇时出现的白块 */
}
}

View File

@ -1,40 +0,0 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"baseUrl": "./",
"typeRoots": [
"node_modules/@types",
"node_modules/@kevisual/types",
],
"paths": {
"@kevisual/components/*": [
"src/*"
]
},
/* Linting */
"strict": true,
"noImplicitAny": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
"include": [
"src",
"typings.d.ts",
]
}

@ -0,0 +1 @@
Subproject commit 313cba38de5c9cea0d5defd42a1a602cd2ecc0d9

View File

@ -2,4 +2,6 @@ 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 { uploadFiles, uploadFileChunked, getDirectoryAndName, toFile, createDirectory } from './pages/upload/app';
export { DialogDirectory, DialogDeleteDirectory } from './pages/upload/DialogDirectory';

View File

@ -1,34 +1,39 @@
import { useEffect, useMemo } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useResourceStore } from '../store/resource';
import { Box, Button, Typography, ButtonGroup } from '@mui/material';
import { FileText, Table, Grid } from 'lucide-react';
import { Box, Button, Typography, ButtonGroup, Tooltip } from '@mui/material';
import { FileText, Table, Grid, Trash, Upload, FolderPlus, RefreshCw } from 'lucide-react';
import { FileTable } from './list/FileTable';
import { FileCard } from './list/FileCard';
import { PrefixRedirect } from './modules/PrefixRedirect';
import { UploadButton } from '../upload';
import { FileDrawer } from './draw/FileDrawer';
import { useResourceFileStore } from '../store/resource-file';
import { IconButtonItem } from '@kevisual/components/button/index.tsx';
import { IconButtonItem, IconButton } from '@kevisual/components/button/index.tsx';
import { useTranslation } from 'react-i18next';
import { DialogDeleteDirectory, DialogDirectory } from '../upload/DialogDirectory';
export const FileApp = () => {
const { getList, prefix, setListType, listType } = useResourceStore();
const { getList, prefix, setListType, listType, onOpenPrefix } = useResourceStore();
const { getStatFile, prefix: statPrefix, openDrawer } = useResourceFileStore();
useEffect(() => {
getList();
}, []);
const directory = useMemo(() => {
const _prefix = prefix.split('/');
let dir = _prefix.slice(2).join('/');
if (dir.endsWith('/')) {
dir = dir.slice(0, -1);
}
return dir;
}, [prefix]);
useEffect(() => {
if (statPrefix && openDrawer) {
getStatFile();
}
}, [statPrefix, openDrawer]);
const handleUpload = (res: any) => {
const paths = ['root', ...prefix.split('/').filter((item) => item)];
const [_mockUsername, appKey, version, ...directory] = paths;
const directoryPath = directory.join('/');
const [dialogDeleteDirectory, setDialogDeleteDirectory] = useState(false);
const [dialogDirectory, setDialogDirectory] = useState(false);
const { t } = useTranslation();
const onDirectoryClick = (prefix: string) => {
onOpenPrefix(prefix);
};
const onUloadFinish = (res: any) => {
getList();
};
return (
@ -55,7 +60,75 @@ export const FileApp = () => {
</div>
<Box className='flex items-center gap-2 mb-4'>
<PrefixRedirect />
<UploadButton prefix={directory} onUpload={handleUpload} />
<Tooltip title={t('refresh')} placement='bottom'>
<IconButton
color='primary'
onClick={() => {
onDirectoryClick(prefix);
}}>
<RefreshCw />
</IconButton>
</Tooltip>
{true && (
<>
<Tooltip title={t('create_directory')} placement='bottom'>
<IconButton color='primary' onClick={() => setDialogDirectory(true)}>
<FolderPlus />
</IconButton>
</Tooltip>
<DialogDirectory
open={dialogDirectory}
onClose={() => setDialogDirectory(false)}
onSuccess={(newPrefix) => {
const currentPath = [appKey, version, newPrefix].filter(Boolean).join('/');
onDirectoryClick(currentPath + '/');
setDialogDirectory(false);
}}
prefix={directoryPath}
opts={{
appKey,
version,
}}
/>
</>
)}
{true && (
<>
<Tooltip title={t('uploadDirectory')} placement='bottom'>
<IconButton color='primary'>
<UploadButton onlyIcon uploadDirectory icon={<Upload />} directory={directoryPath} onUpload={onUloadFinish} />
</IconButton>
</Tooltip>
<Tooltip title={t('upload')} placement='bottom'>
<IconButton color='primary'>
<UploadButton onlyIcon directory={directoryPath} onUpload={onUloadFinish} />
</IconButton>
</Tooltip>
</>
)}
{directoryPath !== '' && (
<>
<Tooltip title={t('deleteDirectory')} placement='bottom'>
<IconButton
color='primary'
onClick={() => {
setDialogDeleteDirectory(true);
}}>
<Trash />
</IconButton>
</Tooltip>
<DialogDeleteDirectory
open={dialogDeleteDirectory}
onClose={() => setDialogDeleteDirectory(false)}
onSuccess={(prefix) => {
const newPrefix = prefix.split('/').slice(0, -1).join('/');
setDialogDeleteDirectory(false);
onDirectoryClick(newPrefix + '/');
}}
prefix={appKey + '/' + version + '/' + directoryPath}
/>
</>
)}
<ButtonGroup className='ml-auto' variant='contained' color='primary' sx={{ color: 'white' }}>
<Button
variant={listType === 'table' ? 'contained' : 'outlined'}

View File

@ -16,7 +16,20 @@ interface ResourceFileStore {
*/
getStatFile: () => Promise<any>;
updateMeta: (metadata: any) => Promise<any>;
/**
*
* @param resource
* @param opts
* @returns
*/
deleteFile: (resource: Resource, opts?: { onSuccess?: (res: any) => void }) => Promise<void>;
/**
*
* @param prefix
* @param opts
* @returns
*/
deleteDirectory: (prefix: string, opts?: { onSuccess?: (res: any) => void }) => Promise<void>;
once: ((data: any) => any) | null;
setOnce: (data: any) => void;
}
@ -81,6 +94,30 @@ export const useResourceFileStore = create<ResourceFileStore>((set, get) => ({
toast.error(res.message || 'Request failed');
}
},
deleteDirectory: async (prefix: string, opts?: { onSuccess?: (res: any) => void }) => {
if (!prefix) {
toast.error('Directory name is required');
return;
}
if (prefix.endsWith('/')) {
toast.error('Directory name cannot end with a slash');
return;
}
const res = await query.post({
path: 'file',
key: 'delete-all',
data: {
directory: prefix,
},
});
if (res.code === 200) {
toast.success('Delete directory success');
opts?.onSuccess?.(res);
} else {
toast.error(res.message || 'Request failed');
}
},
once: null,
setOnce: (data: any) => set({ once: data }),
}));

View File

@ -0,0 +1,113 @@
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, TextField } from '@mui/material';
import { useTheme } from '@kevisual/components/theme/index.js';
import { useTranslation } from 'react-i18next';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { createDirectory } from './utils/create-directory';
import { ConvertOpts } from './utils/upload-chunk';
import { useResourceFileStore } from '../store/resource-file';
type DialogDirectoryProps = {
open: boolean;
onClose: () => void;
onSuccess: (directory: string) => void;
prefix?: string;
opts?: ConvertOpts;
};
/**
*
* @param props
* @returns
*/
export const DialogDirectory = (props: DialogDirectoryProps) => {
const { open, onClose, onSuccess, prefix, opts } = props;
const theme = useTheme();
const [directory, setDirectory] = useState('');
const defaultProps = theme.components?.MuiTextField?.defaultProps as any;
const { t } = useTranslation();
useEffect(() => {
setDirectory('');
}, [open]);
const onClick = async () => {
if (!directory) {
toast.error(t('directory_name_required'));
return;
}
if (directory.startsWith('.') || directory.endsWith('.')) {
toast.error(t('directory_name_invalid'));
return;
}
if (directory.startsWith('/') || directory.endsWith('/')) {
toast.error(t('directory_name_invalid'));
return;
}
if (directory.includes('//')) {
toast.error(t('directory_name_invalid'));
return;
}
const res = await createDirectory(prefix ? `${prefix}/${directory}` : directory, opts);
if (res?.code === 200) {
if (onSuccess) {
onSuccess(prefix ? `${prefix}/${directory}` : directory);
} else {
toast.success(t('create_directory_success'));
onClose();
}
} else {
toast.error(res.message);
}
};
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>{t('create_directory')}</DialogTitle>
<DialogContent>
<div className='min-w-[400px] py-4'>
<TextField {...defaultProps} value={directory} onChange={(e) => setDirectory(e.target.value)} label='目录名' />
</div>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>{t('Cancel')}</Button>
<Button onClick={onClick}>{t('Submit')}</Button>
</DialogActions>
</Dialog>
);
};
type DialogDeleteDirectoryProps = {
open: boolean;
onClose: () => void;
onSuccess: (directory: string) => void;
prefix?: string;
};
export const DialogDeleteDirectory = (props: DialogDeleteDirectoryProps) => {
const { open, onClose, onSuccess, prefix } = props;
const { t } = useTranslation();
const { deleteDirectory } = useResourceFileStore();
const onClick = async () => {
if (!prefix) {
toast.error(t('directory_name_required'));
return;
}
await deleteDirectory(prefix!, {
onSuccess: () => {
if (onSuccess) {
onSuccess(prefix!);
} else {
onClose();
}
},
});
};
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>{t('delete_directory')}</DialogTitle>
<DialogContent>
<div className='min-w-[400px]'>: {`${prefix}/`}</div>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>{t('Cancel')}</Button>
<Button onClick={onClick}>{t('Submit')}</Button>
</DialogActions>
</Dialog>
);
};

View File

@ -3,3 +3,5 @@ export * from './tools/to-file';
export * from './utils/upload';
export * from './utils/upload-chunk';
export * from './utils/create-directory';

View File

@ -4,7 +4,7 @@ import { uploadFiles } from './utils/upload';
import { FileText, CloudUpload as UploadIcon } from 'lucide-react';
import { uploadFileChunked } from './utils/upload-chunk';
import { filterFiles } from './utils/filter-files';
import { IconButton } from '@kevisual/components/button/index.tsx';
type UploadButtonProps = {
/**
*
@ -77,18 +77,7 @@ export const UploadButton = (props: UploadButtonProps) => {
if (onlyIcon) {
return uploadCom;
}
return (
<Box>
<Button
color='primary'
sx={{
minWidth: 'unset',
padding: '2px',
}}>
{uploadCom}
</Button>
</Box>
);
return <IconButton color='primary'>{uploadCom}</IconButton>;
};
export const Upload = ({ uploadDirectory = false }: { uploadDirectory?: boolean }) => {
const onDrop = async (acceptedFiles) => {

View File

@ -32,6 +32,11 @@ const getFileType = (extension: string) => {
const checkIsBase64 = (content: string) => {
return content.startsWith('data:');
};
/**
*
* @param filename
* @returns
*/
export const getDirectoryAndName = (filename: string) => {
if (!filename) {
return null;
@ -87,3 +92,14 @@ export const toFile = (content: string, filename: string) => {
return new File([blob], filename, { type });
}
};
/**
*
* @param content
* @param filename
* @returns
*/
export const toTextFile = (content: string = 'keep directory exist', filename: string = 'keep.txt') => {
const file = toFile(content, filename);
return file;
};

View File

@ -0,0 +1,50 @@
import { toTextFile } from '../app';
import { ConvertOpts, uploadFileChunked } from './upload-chunk';
/**
* directory的路径进行解析
* nameA/nameB/nameC nameA/nameB/nameC
*
* ..//
* @param directory
* @param opts
* @returns
*/
export const createDirectory = async (directory: string = '', opts?: ConvertOpts) => {
const directoryPath = directory || opts?.directory;
const error = '目录名不能以.开头,不能以.结尾,不能以/开头,不能以/结尾,不能包含//';
const errorMsg = () => {
return {
code: 400,
message: error,
success: false,
};
};
if (directoryPath) {
if (directoryPath.startsWith('.')) {
return errorMsg();
}
if (directoryPath.endsWith('.')) {
return errorMsg();
}
if (directoryPath.startsWith('/')) {
return errorMsg();
}
if (directoryPath.endsWith('/')) {
return errorMsg();
}
if (directoryPath.includes('//')) {
return errorMsg();
}
}
const res = await uploadFileChunked(toTextFile('keep directory exist', 'keep.txt'), {
directory,
...opts,
});
return res as {
code: number;
message?: string;
success?: boolean;
data?: any;
[key: string]: any;
};
};

View File

@ -4,7 +4,7 @@ import { toast } from 'react-toastify';
import { nanoid } from 'nanoid';
import { toastLogin } from '@kevisual/resources/pages/message/ToastLogin';
type ConvertOpts = {
export type ConvertOpts = {
appKey?: string;
version?: string;
username?: string;

View File

@ -11,6 +11,19 @@ type ConvertOpts = {
};
export const uploadFiles = async (files: File[], opts: ConvertOpts) => {
const { directory, appKey, version, username } = opts;
const length = files.length;
const maxSize = 10 * 1024 * 1024; // 10MB
const totalSize = files.reduce((acc, file) => acc + file.size, 0);
if (totalSize > maxSize) {
toast.error('有文件大小不能超过10MB');
return;
}
const maxCount = 10;
if (length > maxCount) {
toast.error(`最多只能上传${maxCount}个文件`);
return;
}
toast.info(`上传中,共${length}个文件`);
return new Promise((resolve, reject) => {
const formData = new FormData();
const webkitRelativePath = files[0]?.webkitRelativePath;
@ -43,10 +56,7 @@ export const uploadFiles = async (files: File[], opts: ConvertOpts) => {
return;
}
const taskId = nanoid();
// 49.232.155.236:11015
// const eventSource = new EventSource('https://kevisual.silkyai.cn/api/s1/events?taskId=' + taskId);
const eventSource = new EventSource('/api/s1/events?taskId=' + taskId);
// const eventSource = new EventSource('http://49.232.155.236:11015/api/s1/events?taskId=' + taskId);
const load = toast.loading('上传中...');
NProgress.start();
eventSource.onopen = async function (event) {

1399
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -79,5 +79,10 @@
"Workspace Config Tips": "Set the default workspace, when there is no specific space to open, the default workspace will be opened. The collection information module, the position of the added resource, will be placed here by default. For example, the WeChat public account's essay.",
"Add Config Tips": "Add configuration, when the key exists, the data update of the key will be queried first, if the key does not exist, the new configuration will be added.",
"VIP Config": "VIP Config",
"VIP Config Tips": "VIP Config, only the admin can configure."
"VIP Config Tips": "VIP Config, only the admin can configure.",
"directory_name_required": "Directory name is required",
"delete_directory": "Delete Directory",
"delete_directory_success": "Delete directory success",
"create_directory": "Create Directory",
"create_directory_success": "Create directory success"
}

View File

@ -79,5 +79,10 @@
"Workspace Config Tips": "设置默认的工作空间,当没有打开具体的空间的时候,默认打开的工作空间。收集信息的模块,添加的资源的位置,默认放到这里。比如,微信公众号的随笔。",
"Add Config Tips": "添加配置当key存在的时候会优先查询key数据的更新如果key不存在则新增配置。",
"VIP Config": "VIP配置",
"VIP Config Tips": "VIP配置只有管理员才能配置。"
"VIP Config Tips": "VIP配置只有管理员才能配置。",
"directory_name_required": "目录名称是必须的",
"delete_directory": "删除目录",
"delete_directory_success": "删除目录成功",
"create_directory": "创建目录",
"create_directory_success": "创建目录成功"
}

View File

@ -31,28 +31,6 @@ export const LayoutUser = () => {
}, []);
const navigate = useNewNavigate();
const { t } = useTranslation();
const meun = [
{
title: t('Your profile'),
icon: <SquareUser size={16} />,
link: '/user/profile',
},
{
title: t('Your orgs'),
icon: <SwitcherOutlined />,
link: '/org/edit/list',
},
{
title: t('Site Map'),
icon: <Map size={16} />,
link: '/map',
},
{
title: t('Domain'),
icon: <ArrowDownLeftFromSquareIcon size={16} />,
link: '/domain/edit/list',
},
];
const items = useMemo(() => {
const orgs = store.me?.orgs || [];
return orgs.map((item) => {
@ -63,7 +41,43 @@ export const LayoutUser = () => {
};
});
}, [store.me]);
const menu = useMemo(() => {
const orgs = store.me?.orgs || [];
const hasOrg = orgs.length > 0;
const items = [
{
title: t('Your profile'),
icon: <SquareUser size={16} />,
link: '/user/profile',
},
{
title: t('Your orgs'),
icon: <SwitcherOutlined />,
link: '/org/edit/list',
isOrg: true,
},
{
title: t('Site Map'),
icon: <Map size={16} />,
link: '/map',
},
{
title: t('Domain'),
icon: <ArrowDownLeftFromSquareIcon size={16} />,
link: '/domain/edit/list',
isAdmin: true,
},
];
return items.filter((item) => {
if (item.isOrg) {
return hasOrg;
}
if (item.isAdmin) {
return isAdmin;
}
return true;
});
}, [store.me]);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
@ -101,7 +115,7 @@ export const LayoutUser = () => {
</div>
</div>
<div className='mt-3 font-medium'>
{meun.map((item, index) => {
{menu.map((item, index) => {
return (
<div
key={index}

View File

@ -88,11 +88,11 @@ const FormModal = () => {
<DialogContent>
<form className='flex flex-col gap-4 pt-2' onSubmit={handleSubmit(onFinish)}>
<Controller name='title' control={control} render={({ field }) => <TextField {...defaultProps} {...field} label='title' />} />
<Controller
{/* <Controller
name='domain'
control={control}
render={({ field }) => <TextField {...defaultProps} {...field} label='domain' variant='outlined' helperText='域名自定义绑定' />}
/>
/> */}
<Controller name='key' control={control} render={({ field }) => <TextField {...defaultProps} {...field} label='key' fullWidth />} />
<Controller
name='description'
@ -277,6 +277,17 @@ export const List = () => {
<CodeOutlined />
</IconButton>
</Tooltip>
<Tooltip title='域名自定义绑定'>
<IconButton
sx={{
padding: '8px',
}}
onClick={() => {
message.info('联系管理员');
}}>
<LinkOutlined />
</IconButton>
</Tooltip>
</div>
<div className='grow'>
<div className='w-full h-full p-4'>
@ -307,9 +318,9 @@ export const List = () => {
{item.id}
</div>
</Tooltip>
{item.domain && (
{item?.data?.domain && (
<div className='text-xs'>
{t('app.domain')}: {item.domain}
{t('app.domain')}: {item?.data?.domain}
</div>
)}
<div className='text-xs'>

View File

@ -12,9 +12,10 @@ import UploadOutlined from '@ant-design/icons/lib/icons/UploadOutlined';
import { Tooltip } from '@mui/material';
import { useResourceFileStore } from '@kevisual/resources/pages/store/resource-file.ts';
import { FileDrawerApp } from '@kevisual/resources/pages/file/draw/FileDrawer.tsx';
import { RefreshCw, Upload } from 'lucide-react';
import { Delete, FolderPlus, RefreshCw, Trash, Upload } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { UploadButton } from '@kevisual/resources/pages/upload/index.tsx';
import { DialogDirectory, DialogDeleteDirectory } from '@kevisual/resources/pages/upload/DialogDirectory.tsx';
export const CardPath = ({ children }: any) => {
const userAppStore = useFileStore(
useShallow((state) => {
@ -26,6 +27,8 @@ export const CardPath = ({ children }: any) => {
};
}),
);
const [dialogDirectory, setDialogDirectory] = useState(false);
const [dialogDeleteDirectory, setDialogDeleteDirectory] = useState(false);
const paths = ['root', ...userAppStore.path.split('/').filter((item) => item)];
const onDirectoryClick = (prefix: string) => {
if (prefix === 'root') {
@ -38,13 +41,14 @@ export const CardPath = ({ children }: any) => {
};
const { t } = useTranslation();
const [usrname, appKey, version] = paths;
const [_username, appKey, version, ...directory] = paths;
const onUloadFinish = (res: any) => {
console.log(res);
userAppStore.getList();
};
const directoryPath = directory.join('/');
return (
<div className='border border-gray-200 rounded'>
<div className='border border-gray-200 rounded h-full overflow-hidden'>
<div className='p-2'>
<div className='flex flex-col'>
<div className='flex justify-between'>
@ -78,25 +82,81 @@ export const CardPath = ({ children }: any) => {
<RefreshCw />
</IconButton>
</Tooltip>
{version && (
<>
<Tooltip title={t('create_directory')} placement='bottom'>
<IconButton color='primary' onClick={() => setDialogDirectory(true)}>
<FolderPlus />
</IconButton>
</Tooltip>
<DialogDirectory
open={dialogDirectory}
onClose={() => setDialogDirectory(false)}
onSuccess={(newPrefix) => {
const currentPath = appKey + '/' + version + '/' + newPrefix;
onDirectoryClick(currentPath);
setDialogDirectory(false);
}}
prefix={directoryPath}
opts={{
appKey,
version,
}}
/>
</>
)}
{version && (
<>
<Tooltip title={t('uploadDirectory')} placement='bottom'>
<IconButton color='primary'>
<UploadButton onlyIcon uploadDirectory icon={<Upload />} appKey={appKey} version={version} username={usrname} onUpload={onUloadFinish} />
<UploadButton
onlyIcon
uploadDirectory
icon={<Upload />}
directory={directoryPath}
appKey={appKey}
version={version}
onUpload={onUloadFinish}
/>
</IconButton>
</Tooltip>
<Tooltip title={t('upload')} placement='bottom'>
<IconButton color='primary'>
<UploadButton onlyIcon appKey={appKey} version={version} username={usrname} onUpload={onUloadFinish} />
<UploadButton onlyIcon appKey={appKey} version={version} directory={directoryPath} onUpload={onUloadFinish} />
</IconButton>
</Tooltip>
</>
)}
{version && directoryPath && (
<>
<Tooltip title={t('deleteDirectory')} placement='bottom'>
<IconButton
color='primary'
onClick={() => {
setDialogDeleteDirectory(true);
}}>
<Trash />
</IconButton>
</Tooltip>
<DialogDeleteDirectory
open={dialogDeleteDirectory}
onClose={() => setDialogDeleteDirectory(false)}
onSuccess={(prefix) => {
const newPrefix = prefix.split('/').slice(0, -1).join('/');
setDialogDeleteDirectory(false);
onDirectoryClick(newPrefix);
}}
prefix={appKey + '/' + version + '/' + directoryPath}
/>
</>
)}
</div>
</div>
</div>
</div>
<div className=''>{children}</div>
<div className='scrollbar' style={{ height: 'calc(100% - 20px)' }}>
{children}
</div>
</div>
);
};

@ -1 +1 @@
Subproject commit e0bf83f06293c3bea51f62558ffdf1eb07192249
Subproject commit 05f037383436cd0e30a6a12e7a4e08bf5b884ea8

1
submodules/query-mark Submodule

@ -0,0 +1 @@
Subproject commit a617a68e2f436b996b255af61cd212d191482d7e

1
submodules/wallnote Submodule

@ -0,0 +1 @@
Subproject commit 4d0e945a92eea77784dd4fdf790fc2471e20b8c7

View File

@ -3,22 +3,8 @@ import react from '@vitejs/plugin-react';
import path from 'path';
import tailwindcss from '@tailwindcss/vite';
import basicSsl from '@vitejs/plugin-basic-ssl';
const isDev = process.env.NODE_ENV === 'development';
const unamiPlugin = {
name: 'html-transform',
transformIndexHtml(html: string) {
return html.replace(
'</head>',
`<script defer src="https://umami.xiongxiao.me/script.js" data-website-id="79e7aa98-9e6e-4eef-bc8b-9cbd0ecb11c3"></script></head>`,
);
},
};
const plugins: any[] = [basicSsl()];
if (!isDev) {
plugins.push(unamiPlugin);
}
plugins.push(tailwindcss());
const devBackend = 'https://kevisual.silkyai.cn';
// const meBackend = 'https://kevisual.xiongxiao.me';