update resources
This commit is contained in:
parent
d649666379
commit
43d612fff3
8
.gitmodules
vendored
8
.gitmodules
vendored
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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 };
|
@ -1,6 +0,0 @@
|
||||
export * from './theme';
|
||||
|
||||
/**
|
||||
* 输入组件, 使用theme的defaultProps
|
||||
*/
|
||||
export * from './input/TextField';
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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];
|
||||
};
|
@ -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} />}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
});
|
@ -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 = () => {};
|
@ -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; /* 修复交汇时出现的白块 */
|
||||
}
|
||||
}
|
@ -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",
|
||||
]
|
||||
}
|
1
packages/kevisual-official
Submodule
1
packages/kevisual-official
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 313cba38de5c9cea0d5defd42a1a602cd2ecc0d9
|
@ -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';
|
||||
|
@ -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'}
|
||||
|
@ -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 }),
|
||||
}));
|
||||
|
113
packages/resources/src/pages/upload/DialogDirectory.tsx
Normal file
113
packages/resources/src/pages/upload/DialogDirectory.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -3,3 +3,5 @@ export * from './tools/to-file';
|
||||
export * from './utils/upload';
|
||||
|
||||
export * from './utils/upload-chunk';
|
||||
|
||||
export * from './utils/create-directory';
|
@ -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) => {
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
};
|
@ -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;
|
||||
|
@ -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
1399
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
@ -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": "创建目录成功"
|
||||
}
|
@ -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}
|
||||
|
@ -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'>
|
||||
|
@ -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
1
submodules/query-mark
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit a617a68e2f436b996b255af61cd212d191482d7e
|
1
submodules/wallnote
Submodule
1
submodules/wallnote
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 4d0e945a92eea77784dd4fdf790fc2471e20b8c7
|
@ -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';
|
||||
|
Loading…
x
Reference in New Issue
Block a user