feat: 去掉antd

This commit is contained in:
熊潇 2025-03-20 21:47:50 +08:00
parent c206add7eb
commit cfd263a1e7
36 changed files with 1369 additions and 769 deletions

View File

@ -20,7 +20,7 @@
"@kevisual/center-components": "workspace:*", "@kevisual/center-components": "workspace:*",
"@kevisual/codemirror": "workspace:*", "@kevisual/codemirror": "workspace:*",
"@kevisual/container": "1.0.0", "@kevisual/container": "1.0.0",
"@kevisual/query": "^0.0.8", "@kevisual/query": "^0.0.9",
"@kevisual/resources": "workspace:*", "@kevisual/resources": "workspace:*",
"@kevisual/system-ui": "^0.0.3", "@kevisual/system-ui": "^0.0.3",
"@kevisual/ui": "^0.0.2", "@kevisual/ui": "^0.0.2",
@ -41,14 +41,14 @@
"immer": "^10.1.1", "immer": "^10.1.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"marked": "^15.0.7", "marked": "^15.0.7",
"nanoid": "^5.1.4", "nanoid": "^5.1.5",
"react": "19.0.0", "react": "19.0.0",
"react-dom": "19.0.0", "react-dom": "19.0.0",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.54.2",
"react-i18next": "^15.4.1", "react-i18next": "^15.4.1",
"react-resizable-panels": "^2.1.7", "react-resizable-panels": "^2.1.7",
"react-router": "^7.3.0", "react-router": "^7.4.0",
"react-router-dom": "^7.3.0", "react-router-dom": "^7.4.0",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"vite-plugin-tsconfig-paths": "^1.4.1", "vite-plugin-tsconfig-paths": "^1.4.1",
"zustand": "^5.0.3" "zustand": "^5.0.3"
@ -61,7 +61,7 @@
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^22.13.10", "@types/node": "^22.13.10",
"@types/path-browserify": "^1.0.3", "@types/path-browserify": "^1.0.3",
"@types/react": "^19.0.11", "@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"@vitejs/plugin-basic-ssl": "^2.0.0", "@vitejs/plugin-basic-ssl": "^2.0.0",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
@ -71,7 +71,7 @@
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0", "globals": "^16.0.0",
"lucide-react": "^0.482.0", "lucide-react": "^0.483.0",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
@ -80,7 +80,7 @@
"tailwindcss": "^4.0.14", "tailwindcss": "^4.0.14",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"typescript-eslint": "^8.26.1", "typescript-eslint": "^8.27.0",
"vite": "^6.2.2" "vite": "^6.2.2"
} }
} }

View File

@ -20,7 +20,6 @@
"@codemirror/autocomplete": "^6.18.6", "@codemirror/autocomplete": "^6.18.6",
"@codemirror/basic-setup": "^0.20.0", "@codemirror/basic-setup": "^0.20.0",
"@codemirror/commands": "^6.8.0", "@codemirror/commands": "^6.8.0",
"@codemirror/history": "^0.19.2",
"@codemirror/lang-css": "^6.3.1", "@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9", "@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.3", "@codemirror/lang-javascript": "^6.2.3",

View File

@ -5,10 +5,10 @@ import { html } from '@codemirror/lang-html';
import { css } from '@codemirror/lang-css'; import { css } from '@codemirror/lang-css';
import { json } from '@codemirror/lang-json'; import { json } from '@codemirror/lang-json';
import { yaml } from '@codemirror/lang-yaml'; import { yaml } from '@codemirror/lang-yaml';
import { history } from '@codemirror/history'; import { history } from '@codemirror/commands';
import { vscodeLight } from '@uiw/codemirror-theme-vscode'; import { vscodeLight } from '@uiw/codemirror-theme-vscode';
import { formatKeymap } from './modules/keymap'; import { formatKeymap } from './modules/keymap';
import { Compartment, EditorState, Extension } from '@codemirror/state'; import { Compartment, Extension } from '@codemirror/state';
import { defaultKeymap } from '@codemirror/commands'; import { defaultKeymap } from '@codemirror/commands';
import { autocompletion, Completion } from '@codemirror/autocomplete'; import { autocompletion, Completion } from '@codemirror/autocomplete';
import { getFileType } from './utils/get-file-type'; import { getFileType } from './utils/get-file-type';
@ -58,7 +58,7 @@ export class BaseEditor {
vscodeLight, vscodeLight,
formatKeymap, formatKeymap,
keymap.of(defaultKeymap), // keymap.of(defaultKeymap), //
// history(), history(),
]; ];
if (this.autoComplete?.open) { if (this.autoComplete?.open) {
extensions.push( extensions.push(

View File

@ -1,13 +1,13 @@
import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Button } from '@mui/material'; import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Button } from '@mui/material';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import type { ModalFuncProps } from 'antd';
export const Confirm = ({ export const Confirm = ({
open, open,
onClose, onClose,
title, title,
content, content,
onConfirm, onConfirm,
confirmText = '确认', okText = '确认',
cancelText = '取消', cancelText = '取消',
}: { }: {
open: boolean; open: boolean;
@ -15,11 +15,15 @@ export const Confirm = ({
title: string; title: string;
content: string; content: string;
onConfirm?: () => void; onConfirm?: () => void;
confirmText?: string; okText?: string;
cancelText?: string; cancelText?: string;
}) => { }) => {
return ( return (
<Dialog open={open} onClose={onClose} aria-labelledby='alert-dialog-title' aria-describedby='alert-dialog-description'> <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]'> <DialogTitle id='alert-dialog-title' className='text-secondary min-w-[300px]'>
{title} {title}
</DialogTitle> </DialogTitle>
@ -31,7 +35,7 @@ export const Confirm = ({
{cancelText || '取消'} {cancelText || '取消'}
</Button> </Button>
<Button onClick={onConfirm} variant='contained' color='primary' autoFocus> <Button onClick={onConfirm} variant='contained' color='primary' autoFocus>
{confirmText || '确认'} {okText || '确认'}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
@ -39,26 +43,48 @@ export const Confirm = ({
}; };
type Fn = () => void; type Fn = () => void;
export const useConfirm = () => { export const useModal = () => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [content, setContent] = useState(''); const [content, setContent] = useState('');
const fns = useRef<{ const fns = useRef<{
onConfirm: Fn; onConfirm: Fn;
onCancel: Fn; onCancel: Fn;
confirmText: string; okText: string;
cancelText: string; cancelText: string;
}>({ }>({
onConfirm: () => {}, onConfirm: () => {},
onCancel: () => {}, onCancel: () => {},
confirmText: '确认', okText: '确认',
cancelText: '取消', cancelText: '取消',
}); });
return { const modal = {
contextHolder: ( 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 <Confirm
open={open} open={open}
confirmText={fns.current.confirmText} okText={fns.current.okText}
cancelText={fns.current.cancelText} cancelText={fns.current.cancelText}
onClose={() => { onClose={() => {
setOpen(false); setOpen(false);
@ -68,24 +94,6 @@ export const useConfirm = () => {
content={content} content={content}
onConfirm={fns.current.onConfirm} onConfirm={fns.current.onConfirm}
/> />
), );
confirm: ( return [modal, contextHolder] as [typeof modal, React.ReactNode];
title: string,
content: string,
opts?: {
onConfirm: () => void;
confirmText?: string;
cancelText?: string;
onCancel?: () => void;
},
) => {
setOpen(true);
setTitle(title);
setContent(content);
fns.current.onConfirm = opts?.onConfirm || (() => {});
fns.current.onCancel = opts?.onCancel || (() => {});
fns.current.confirmText = opts?.confirmText || '确认';
fns.current.cancelText = opts?.cancelText || '取消';
},
};
}; };

View File

@ -0,0 +1,47 @@
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?: string;
};
export const TagsInput = ({ value, onChange, placeholder = 'Add a tag', label = 'Tags' }: 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[]);
}}
renderTags={(value: string[], getTagProps) => {
const id = randomid();
const com = value.map((option: string, index: number) => (
<Chip
variant='outlined'
sx={{
borderColor: 'primary.main',
}}
label={option}
{...getTagProps({ index })}
key={`${id}-${index}`}
/>
));
return <Fragment key={id}>{com}</Fragment>;
}}
renderInput={(params) => <TextField {...params} variant='outlined' label={label} placeholder={placeholder} />}
/>
);
};

View File

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

View File

@ -58,7 +58,7 @@ export const themeOptions: ThemeOptions = {
// paper: '#f5f5f5', // 设置纸张背景颜色 // paper: '#f5f5f5', // 设置纸张背景颜色
}, },
error: { error: {
main: red[500], main: red[500], // 设置错误颜色 "#f44336"
}, },
}, },
shadows: generateShadows('rgba(255, 193, 7, 0.2)'), shadows: generateShadows('rgba(255, 193, 7, 0.2)'),
@ -94,6 +94,14 @@ export const themeOptions: ThemeOptions = {
}, },
}, },
MuiTextField: { MuiTextField: {
defaultProps: {
fullWidth: true,
slotProps: {
inputLabel: {
shrink: true,
},
},
},
styleOverrides: { styleOverrides: {
root: { root: {
'& .MuiOutlinedInput-root': { '& .MuiOutlinedInput-root': {
@ -145,6 +153,29 @@ export const themeOptions: ThemeOptions = {
}, },
}, },
}, },
MuiFormControlLabel: {
defaultProps: {
labelPlacement: 'top',
sx: {
alignItems: 'flex-start',
'& .MuiFormControlLabel-label': {
textAlign: 'left',
width: '100%',
},
'& .MuiFormControlLabel-root': {
width: '100%',
},
'& .MuiInputBase-root': {
width: '100%',
},
},
},
styleOverrides: {
root: {
color: amber[600],
},
},
},
}, },
}; };

View File

@ -1,12 +1,14 @@
import { useResourceStore } from '@kevisual/resources/pages/store/resource'; import { useResourceStore } from '@kevisual/resources/pages/store/resource';
import { useResourceFileStore } from '@kevisual/resources/pages/store/resource-file'; import { useResourceFileStore } from '@kevisual/resources/pages/store/resource-file';
import { Box, Divider, Drawer, Tab, Tabs } from '@mui/material'; import { Box, Button, Divider, Drawer, Tab, Tabs } from '@mui/material';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { QuickValues, QuickTabs } from './QuickTabs'; import { QuickValues, QuickTabs } from './QuickTabs';
import { Delete, Trash } from 'lucide-react';
import { InitProvider } from '../../App';
export const FileDrawer = () => { export const FileDrawer = () => {
const { prefix } = useResourceStore(); const { prefix, getList } = useResourceStore();
const { resource, openDrawer, setOpenDrawer } = useResourceFileStore(); const { resource, openDrawer, setOpenDrawer, deleteFile } = useResourceFileStore();
const [tab, setTab] = useState<string>(QuickValues[0]); const [tab, setTab] = useState<string>(QuickValues[0]);
const quickCom = useMemo(() => { const quickCom = useMemo(() => {
return QuickTabs.find((item) => item.value === tab)?.component; return QuickTabs.find((item) => item.value === tab)?.component;
@ -31,10 +33,26 @@ export const FileDrawer = () => {
style={{ style={{
zIndex: 1000, zIndex: 1000,
}}> }}>
<div className='p-4 w-[400px] max-w-[90%] overflow-hidden h-full sm:w-[600px]'> <div className='p-4 overflow-hidden h-full sm:w-[600px]'>
<div style={{ height: '140px' }}> <div style={{ height: '140px' }}>
<h2 className='text-2xl font-bold truncate py-2 pb-6 '> <h2 className='text-2xl font-bold py-2 pb-6 flex '>
{resource?.name ? resource.name.replace(prefix, '') : resource?.prefix?.replace(prefix, '')} <div className='grow truncate'>{resource?.name ? resource.name.replace(prefix, '') : resource?.prefix?.replace(prefix, '')}</div>
<Button
sx={{ ml: 2 }}
color='primary'
size='small'
onClick={() => {
if (resource) {
deleteFile(resource, {
onSuccess: () => {
getList();
setOpenDrawer(false);
},
});
}
}}>
<Trash />
</Button>
</h2> </h2>
<Divider /> <Divider />
<Box sx={{ borderBottom: 1, mt: 2, borderColor: 'divider' }}> <Box sx={{ borderBottom: 1, mt: 2, borderColor: 'divider' }}>
@ -58,3 +76,11 @@ export const FileDrawer = () => {
</> </>
); );
}; };
export const FileDrawerApp = () => {
return (
<InitProvider>
<FileDrawer />
</InitProvider>
);
};

View File

@ -1,13 +1,13 @@
import { useResourceFileStore } from '@kevisual/resources/pages/store/resource-file'; import { useResourceFileStore } from '@kevisual/resources/pages/store/resource-file';
import { FormControlLabel, Box, TextField, Button, IconButton, ButtonGroup, Tooltip, Select, MenuItem, Typography, FormGroup } from '@mui/material'; import { FormControlLabel, Box, ButtonGroup, Tooltip, Typography } from '@mui/material';
import { IconButton } from '@kevisual/center-components/button/index.tsx';
import { Info, Plus, Save, Share, Shuffle, Trash } from 'lucide-react'; import { Info, Plus, Save, Share, Shuffle, Trash } from 'lucide-react';
import { useState, useEffect, useMemo } from 'react'; import { useState, useEffect } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { create } from 'zustand'; import { create } from 'zustand';
import { uniq } from 'lodash-es'; import { uniq } from 'lodash-es';
import { DatePicker } from './DatePicker'; import { DatePicker } from './DatePicker';
import { SelectPicker } from './SelectPicker'; import { SelectPicker } from './SelectPicker';
import dayjs from 'dayjs';
import { DialogKey } from './DialogKey'; import { DialogKey } from './DialogKey';
import { keysTips, KeyParse } from '../../modules/key-parse'; import { keysTips, KeyParse } from '../../modules/key-parse';
import { KeyShareSelect, KeyTextField } from '../../modules/PermissionManager'; import { KeyShareSelect, KeyTextField } from '../../modules/PermissionManager';
@ -139,7 +139,7 @@ export const MetaForm = () => {
} else { } else {
_formData[key] = value; _formData[key] = value;
} }
setFormData(_formData); setFormData({ ..._formData });
}; };
const deleteMeta = (key: string) => { const deleteMeta = (key: string) => {
setKeys(keys.filter((item) => item !== key)); setKeys(keys.filter((item) => item !== key));
@ -166,13 +166,14 @@ export const MetaForm = () => {
<Box className='sticky top-0 z-10 pointer-events-none'> <Box className='sticky top-0 z-10 pointer-events-none'>
<div className='flex justify-end mr-20'> <div className='flex justify-end mr-20'>
<div className=' pointer-events-auto '> <div className=' pointer-events-auto '>
<ButtonGroup className='bg-white' variant='contained' sx={{ color: 'white' }}> <ButtonGroup
variant='contained'
sx={{
mt: 1,
backgroundColor: 'primary.main',
}}>
{btnList.map((item) => { {btnList.map((item) => {
const icon = ( const icon = <IconButton onClick={item.onClick}>{item.icon}</IconButton>;
<IconButton color='secondary' onClick={item.onClick}>
{item.icon}
</IconButton>
);
if (item.tooltip) { if (item.tooltip) {
return ( return (
<Tooltip key={item.key} title={item.tooltip} placement='top' arrow> <Tooltip key={item.key} title={item.tooltip} placement='top' arrow>
@ -212,7 +213,14 @@ export const MetaForm = () => {
</Tooltip> </Tooltip>
)} )}
</div> </div>
<IconButton color='error' onClick={() => deleteMeta(key)}> <IconButton
variant='text'
sx={{
color: 'primay.main',
}}
color='error'
size='small'
onClick={() => deleteMeta(key)}>
<Trash /> <Trash />
</IconButton> </IconButton>
</div> </div>
@ -241,5 +249,3 @@ export const MetaForm = () => {
</div> </div>
); );
}; };

View File

@ -6,12 +6,12 @@ import { getIcon } from '../FileIcon';
import { Download, Trash } from 'lucide-react'; import { Download, Trash } from 'lucide-react';
import clsx from 'clsx'; import clsx from 'clsx';
import { useResourceFileStore } from '@kevisual/resources/pages/store/resource-file'; import { useResourceFileStore } from '@kevisual/resources/pages/store/resource-file';
import { useConfirm } from '@kevisual/center-components/modal/Confirm.tsx'; import { useModal } from '@kevisual/center-components/modal/Confirm.tsx';
export const FileTable = () => { export const FileTable = () => {
const { list, prefix, download, onOpenPrefix, deleteFile } = useResourceStore(); const { list, prefix, download, onOpenPrefix, getList } = useResourceStore();
const { setOpenDrawer, setPrefix } = useResourceFileStore(); const { setOpenDrawer, setPrefix, deleteFile } = useResourceFileStore();
const { confirm, contextHolder } = useConfirm(); const [modal, contextHolder] = useModal();
return ( return (
<> <>
{contextHolder} {contextHolder}
@ -95,9 +95,15 @@ export const FileTable = () => {
className='ml-2!' className='ml-2!'
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
confirm('删除文件', '确定删除该文件吗?', { modal.confirm({
onConfirm: () => { title: '删除文件',
deleteFile(row); content: '确定删除该文件吗?',
onOk: () => {
deleteFile(row, {
onSuccess: () => {
getList();
},
});
}, },
}); });
}} }}

View File

@ -4,6 +4,8 @@ import { FormControlLabel, TextField, Select, MenuItem, FormGroup, Tooltip } fro
import { DatePicker } from '../draw/modules/DatePicker'; import { DatePicker } from '../draw/modules/DatePicker';
import { SelectPicker } from '../draw/modules/SelectPicker'; import { SelectPicker } from '../draw/modules/SelectPicker';
import { HelpCircle } from 'lucide-react'; import { HelpCircle } from 'lucide-react';
import clsx from 'clsx';
import { useTranslation } from 'react-i18next';
export const KeyShareSelect = ({ name, value, onChange }: { name: string; value: string; onChange?: (value: string) => void }) => { export const KeyShareSelect = ({ name, value, onChange }: { name: string; value: string; onChange?: (value: string) => void }) => {
return ( return (
<Select <Select
@ -34,8 +36,7 @@ export const KeyTextField = ({ name, value, onChange }: { name: string; value: s
variant='outlined' variant='outlined'
size='small' size='small'
name={name} name={name}
defaultValue={value} value={value}
// value={formData[key] || ''}
onChange={(e) => onChange?.(e.target.value)} onChange={(e) => onChange?.(e.target.value)}
sx={{ sx={{
width: '100%', width: '100%',
@ -48,8 +49,10 @@ export const KeyTextField = ({ name, value, onChange }: { name: string; value: s
type PermissionManagerProps = { type PermissionManagerProps = {
value: Record<string, any>; value: Record<string, any>;
onChange: (value: Record<string, any>) => void; onChange: (value: Record<string, any>) => void;
className?: string;
}; };
export const PermissionManager = ({ value, onChange }: PermissionManagerProps) => { export const PermissionManager = ({ value, onChange, className }: PermissionManagerProps) => {
const { t, i18n } = useTranslation();
const [formData, setFormData] = useState<any>(value); const [formData, setFormData] = useState<any>(value);
const [keys, setKeys] = useState<any>([]); const [keys, setKeys] = useState<any>([]);
useEffect(() => { useEffect(() => {
@ -80,26 +83,19 @@ export const PermissionManager = ({ value, onChange }: PermissionManagerProps) =
onChange(KeyParse.stringify(newFormData)); onChange(KeyParse.stringify(newFormData));
} }
}; };
const tips = getTips('share', i18n.language);
return ( return (
<form className='w-[400px] flex flex-col gap-2'> <form className={clsx('flex flex-col gap-2', className)}>
<FormControlLabel <FormControlLabel
labelPlacement='top'
control={<KeyShareSelect name='share' value={formData?.share} onChange={(value) => onChangeValue('share', value)} />} control={<KeyShareSelect name='share' value={formData?.share} onChange={(value) => onChangeValue('share', value)} />}
label={ label={
<div className='flex items-center gap-1'> <div className='flex items-center gap-1'>
Share {t('Share')}
<Tooltip title={getTips('share')}> <Tooltip title={tips}>
<HelpCircle size={16} /> <HelpCircle size={16} />
</Tooltip> </Tooltip>
</div> </div>
} }
sx={{
alignItems: 'flex-start',
'& .MuiFormControlLabel-label': {
textAlign: 'left',
width: '100%',
},
}}
/> />
{keys.map((item: any) => { {keys.map((item: any) => {
let control: React.ReactNode | null = null; let control: React.ReactNode | null = null;

View File

@ -1,6 +1,13 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
export const getTips = (key: string) => { export const getTips = (key: string, lang?: string) => {
return keysTips.find((item) => item.key === key)?.tips; const tip = keysTips.find((item) => item.key === key);
if (tip) {
if (lang === 'en') {
return tip.enTips;
}
return tip.tips;
}
return '';
}; };
export const keysTips = [ export const keysTips = [
{ {
@ -10,26 +17,35 @@ export const keysTips = [
2. 访 2. 访
3. 访\n 3. 访\n
访 访`, 访 访`,
enTips: `1. Set public to directly access
2. Set protected to access after login
3. Set private to access only yourself.
Protected can set a password and set the username for access. After switching the shared state, you need to reset the password and username. If not set, it defaults to only being accessible to yourself.`,
}, },
{ {
key: 'content-type', key: 'content-type',
tips: `内容类型,设置文件的内容类型。默认不要修改。`, tips: `内容类型,设置文件的内容类型。默认不要修改。`,
enTips: `Content type, set the content type of the file. Default do not modify.`,
}, },
{ {
key: 'app-source', key: 'app-source',
tips: `应用来源,上传方式。默认不要修改。`, tips: `应用来源,上传方式。默认不要修改。`,
enTips: `App source, upload method. Default do not modify.`,
}, },
{ {
key: 'cache-control', key: 'cache-control',
tips: `缓存控制,设置文件的缓存控制。默认不要修改。`, tips: `缓存控制,设置文件的缓存控制。默认不要修改。`,
enTips: `Cache control, set the cache control of the file. Default do not modify.`,
}, },
{ {
key: 'password', key: 'password',
tips: `密码,设置文件的密码。不设置默认是所有人都可以访问。`, tips: `密码,设置文件的密码。不设置默认是所有人都可以访问。`,
enTips: `Password, set the password of the file. If not set, it defaults to everyone can access.`,
}, },
{ {
key: 'usernames', key: 'usernames',
tips: `用户名,设置文件的用户名。不设置默认是所有人都可以访问。`, tips: `用户名,设置文件的用户名。不设置默认是所有人都可以访问。`,
enTips: `Username, set the username of the file. If not set, it defaults to everyone can access.`,
parse: (value: string) => { parse: (value: string) => {
if (!value) { if (!value) {
return []; return [];
@ -46,6 +62,7 @@ export const keysTips = [
{ {
key: 'expiration-time', key: 'expiration-time',
tips: `过期时间,设置文件的过期时间。不设置默认是永久。`, tips: `过期时间,设置文件的过期时间。不设置默认是永久。`,
enTips: `Expiration time, set the expiration time of the file. If not set, it defaults to permanent.`,
parse: (value: Date) => { parse: (value: Date) => {
if (!value) { if (!value) {
return null; return null;

View File

@ -10,8 +10,15 @@ interface ResourceFileStore {
setOpenDrawer: (openDrawer: boolean) => void; setOpenDrawer: (openDrawer: boolean) => void;
prefix: string; prefix: string;
setPrefix: (prefix: string, replace?: string) => void; setPrefix: (prefix: string, replace?: string) => void;
/**
* prefix
* @returns
*/
getStatFile: () => Promise<any>; getStatFile: () => Promise<any>;
updateMeta: (metadata: any) => Promise<any>; updateMeta: (metadata: any) => Promise<any>;
deleteFile: (resource: Resource, opts?: { onSuccess?: (res: any) => void }) => Promise<void>;
once: ((data: any) => any) | null;
setOnce: (data: any) => void;
} }
export const useResourceFileStore = create<ResourceFileStore>((set, get) => ({ export const useResourceFileStore = create<ResourceFileStore>((set, get) => ({
@ -45,10 +52,35 @@ export const useResourceFileStore = create<ResourceFileStore>((set, get) => ({
}, },
}); });
if (res.code === 200) { if (res.code === 200) {
// set({ resource: { ...res.data, name: resource?.name } }); toast.success('Update metadata success');
getStatFile(); getStatFile();
} else { } else {
toast.error(res.message || '更新元数据失败'); toast.error(res.message || '更新元数据失败');
} }
}, },
deleteFile: async (resource: Resource, opts?: { onSuccess?: (res: any) => void }) => {
const { once, setOnce } = get();
const name = resource.name;
if (!name) {
toast.error('Resource is not a file');
return;
}
const res = await query.post({
path: 'file',
key: 'delete',
data: {
prefix: name,
},
});
if (res.code === 200) {
toast.success('Delete file success');
opts?.onSuccess?.(res);
once?.(res);
setOnce(null);
} else {
toast.error(res.message || 'Request failed');
}
},
once: null,
setOnce: (data: any) => set({ once: data }),
})); }));

View File

@ -3,30 +3,67 @@ import { useDropzone } from 'react-dropzone';
import { uploadFiles } from './utils/upload'; import { uploadFiles } from './utils/upload';
import { FileText, CloudUpload as UploadIcon } from 'lucide-react'; import { FileText, CloudUpload as UploadIcon } from 'lucide-react';
import { uploadFileChunked } from './utils/upload-chunk'; import { uploadFileChunked } from './utils/upload-chunk';
export const UploadButton = (props: { prefix?: string; onUpload?: (res: any) => void; hasDirectory?: boolean; uploadDirectory?: boolean }) => { import { filterFiles } from './utils/filter-files';
type UploadButtonProps = {
/**
*
*/
directory?: string;
/**
* key
*/
appKey?: string;
/**
*
*/
version?: string;
/**
*
*/
username?: string;
/**
*
*/
onUpload?: (res: any) => void;
/**
*
*/
uploadDirectory?: boolean;
/**
*
*/
onlyIcon?: boolean;
/**
*
*/
icon?: React.ReactNode;
};
/**
*
* @param props
* @returns
*/
export const UploadButton = (props: UploadButtonProps) => {
const { onlyIcon = false, icon } = props;
const { appKey, version, username, directory } = props;
const onDrop = async (acceptedFiles) => { const onDrop = async (acceptedFiles) => {
console.log(acceptedFiles); console.log(acceptedFiles);
acceptedFiles = filterFiles(acceptedFiles);
if (acceptedFiles.length > 1) { if (acceptedFiles.length > 1) {
const res = await uploadFiles(acceptedFiles, { directory: props.prefix }); const res = await uploadFiles(acceptedFiles, { directory, appKey, version, username });
console.log('uploadFiles res', res); console.log('uploadFiles res', res);
props.onUpload?.(res); props.onUpload?.(res);
} else if (acceptedFiles.length === 1) { } else if (acceptedFiles.length === 1) {
const res = await uploadFileChunked(acceptedFiles[0], { directory: props.prefix }); const res = await uploadFileChunked(acceptedFiles[0], { directory, appKey, version, username });
console.log('uploadFiles res', res); console.log('uploadFiles res', res);
props.onUpload?.(res); props.onUpload?.(res);
} }
}; };
const { getRootProps, getInputProps } = useDropzone({ onDrop }); const { getRootProps, getInputProps } = useDropzone({ onDrop });
return ( const uploadCom = (
<Box {...getRootProps()}> <div {...getRootProps()}>
<Button {icon || <UploadIcon />}
color='primary'
sx={{
minWidth: 'unset',
padding: '2px',
}}>
<UploadIcon />
</Button>
<input <input
type='file' type='file'
style={{ display: 'none' }} style={{ display: 'none' }}
@ -35,12 +72,27 @@ export const UploadButton = (props: { prefix?: string; onUpload?: (res: any) =>
webkitdirectory={props.uploadDirectory ? 'true' : undefined} webkitdirectory={props.uploadDirectory ? 'true' : undefined}
mozdirectory={props.uploadDirectory ? 'true' : undefined} mozdirectory={props.uploadDirectory ? 'true' : undefined}
/> />
</div>
);
if (onlyIcon) {
return uploadCom;
}
return (
<Box>
<Button
color='primary'
sx={{
minWidth: 'unset',
padding: '2px',
}}>
{uploadCom}
</Button>
</Box> </Box>
); );
}; };
export const Upload = ({ uploadDirectory = false }: { uploadDirectory?: boolean }) => { export const Upload = ({ uploadDirectory = false }: { uploadDirectory?: boolean }) => {
const onDrop = async (acceptedFiles) => { const onDrop = async (acceptedFiles) => {
console.log(acceptedFiles); acceptedFiles = filterFiles(acceptedFiles);
if (acceptedFiles.length > 1) { if (acceptedFiles.length > 1) {
const res = await uploadFiles(acceptedFiles, {}); const res = await uploadFiles(acceptedFiles, {});
console.log('uploadFiles res', res); console.log('uploadFiles res', res);

View File

@ -21,12 +21,8 @@ const getFileType = (extension: string) => {
return 'image/gif'; return 'image/gif';
case 'svg': case 'svg':
return 'image/svg+xml'; return 'image/svg+xml';
case 'ico':
return 'image/x-icon';
case 'webp': case 'webp':
return 'image/webp'; return 'image/webp';
case 'gif':
return 'image/gif';
case 'ico': case 'ico':
return 'image/x-icon'; return 'image/x-icon';
default: default:

View File

@ -0,0 +1,23 @@
/**
* , .DS_Store, node_modules, . __开头的文件
* @param files
* @returns
*/
export const filterFiles = (files: File[]) => {
files = files.filter((file) => {
if (file.webkitRelativePath.startsWith('__MACOSX')) {
return false;
}
// 过滤node_modules
if (file.webkitRelativePath.includes('node_modules')) {
return false;
}
// 过滤文件 .DS_Store
if (file.name === '.DS_Store') {
return false;
}
// 过滤以.开头的文件
return !file.name.startsWith('.');
});
return files;
};

View File

@ -18,6 +18,7 @@ export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
if (!token) { if (!token) {
console.log('uploadFileChunked token', token);
toastLogin(); toastLogin();
return; return;
} }
@ -33,6 +34,7 @@ export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
searchParams.set('public', 'true'); searchParams.set('public', 'true');
} }
const eventSource = new EventSource('/api/s1/events?' + searchParams.toString()); const eventSource = new EventSource('/api/s1/events?' + searchParams.toString());
let isError = false;
// 监听服务器推送的进度更新 // 监听服务器推送的进度更新
eventSource.onmessage = function (event) { eventSource.onmessage = function (event) {
console.log('Progress update:', event.data); console.log('Progress update:', event.data);
@ -55,6 +57,7 @@ export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
}; };
eventSource.onerror = function (event) { eventSource.onerror = function (event) {
console.log('eventSource.onerror', event); console.log('eventSource.onerror', event);
isError = true;
reject(event); reject(event);
}; };
@ -91,6 +94,15 @@ export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
}, },
}).then((response) => response.json()); }).then((response) => response.json());
fetch('/api/s1/events/close?taskId=' + taskId); fetch('/api/s1/events/close?taskId=' + taskId);
if (res?.code !== 200) {
toast.error('上传失败');
isError = true;
NProgress.done();
eventSource.close();
toast.dismiss(load);
reject(new Error(res?.message || '上传失败'));
return;
}
if (isLast) { if (isLast) {
NProgress.done(); NProgress.done();
eventSource.close(); eventSource.close();

View File

@ -3,7 +3,6 @@ import 'nprogress/nprogress.css';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { toastLogin } from '@kevisual/resources/pages/message/ToastLogin'; import { toastLogin } from '@kevisual/resources/pages/message/ToastLogin';
type ConvertOpts = { type ConvertOpts = {
appKey?: string; appKey?: string;
version?: string; version?: string;
@ -39,6 +38,7 @@ export const uploadFiles = async (files: File[], opts: ConvertOpts) => {
} }
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
if (!token) { if (!token) {
console.log('uploadFiles token', token);
toastLogin(); toastLogin();
return; return;
} }

583
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -41,5 +41,13 @@
"User List": "User List", "User List": "User List",
"Switch to Org": "Switch to Org", "Switch to Org": "Switch to Org",
"Login": "Login", "Login": "Login",
"uploadDirectory": "Upload Directory" "uploadDirectory": "Upload Directory",
"refresh": "Refresh",
"upload": "Upload",
"app": {
"domain": "Domain",
"version": "Version",
"runtime": "Can run environment"
},
"Share": "Share"
} }

View File

@ -41,5 +41,13 @@
"User List": "用户列表", "User List": "用户列表",
"Switch to Org": "切换组织", "Switch to Org": "切换组织",
"Login": "登录", "Login": "登录",
"uploadDirectory": "上传文件夹" "uploadDirectory": "上传文件夹",
"refresh": "刷新",
"upload": "上传",
"app": {
"domain": "访问域名",
"version": "版本",
"runtime": "可以运行的环境"
},
"Share": "分享"
} }

View File

@ -10,19 +10,30 @@ import { Redirect } from './modules/Redirect';
import { CustomThemeProvider } from '@kevisual/center-components/theme/index.tsx'; import { CustomThemeProvider } from '@kevisual/center-components/theme/index.tsx';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import { ToastContainer } from 'react-toastify'; import { ToastContainer } from 'react-toastify';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn'; import 'dayjs/locale/zh-cn';
import 'dayjs/locale/en';
import zhCN from 'antd/locale/zh_CN'; import zhCN from 'antd/locale/zh_CN';
import enUS from 'antd/locale/en_US';
import ConfigProvider from 'antd/es/config-provider'; import ConfigProvider from 'antd/es/config-provider';
import { useTranslation } from 'react-i18next';
import { useEffect, useState } from 'react';
const AntProvider = ({ children }: { children: React.ReactNode }) => { const AntProvider = ({ children }: { children: React.ReactNode }) => {
const theme = useTheme(); const theme = useTheme();
const primaryColor = theme.palette.primary.main; const primaryColor = theme.palette.primary.main;
const secondaryColor = theme.palette.secondary.main; const secondaryColor = theme.palette.secondary.main;
const { i18n } = useTranslation();
const [locale, setLocale] = useState(zhCN);
useEffect(() => {
if (i18n.language === 'en') {
setLocale(enUS);
} else {
setLocale(zhCN);
}
}, [i18n.language]);
return ( return (
<ConfigProvider <ConfigProvider
locale={zhCN} locale={locale}
theme={{ theme={{
token: { token: {
colorPrimary: primaryColor, colorPrimary: primaryColor,
@ -43,7 +54,7 @@ const AntProvider = ({ children }: { children: React.ReactNode }) => {
}, },
Tooltip: { Tooltip: {
zIndexPopupBase: 2000, zIndexPopupBase: 2000,
} },
}, },
}}> }}>
{children} {children}

View File

@ -57,8 +57,9 @@ h3 {
} }
} }
.cm-editor { .cm-editor {
@apply h-full; @apply h-full;
} }
#for-message {
z-index: 9999 !important;
}

View File

@ -1,3 +1,2 @@
import { message } from '@kevisual/system-ui/dist/message'; import { message } from '@kevisual/system-ui/dist/message';
export { message }; export { message };

View File

@ -2,8 +2,14 @@ import { useNavigation, useParams } from 'react-router';
import { useAppVersionStore } from '../store'; import { useAppVersionStore } from '../store';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { Form, Input, Modal, Tooltip } from 'antd'; import CloudUploadOutlined from '@ant-design/icons/CloudUploadOutlined';
import { CloudUploadOutlined, DeleteOutlined, EditOutlined, FileOutlined, LeftOutlined, LinkOutlined, PlusOutlined } from '@ant-design/icons'; import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
import FileOutlined from '@ant-design/icons/FileOutlined';
import LeftOutlined from '@ant-design/icons/LeftOutlined';
import LinkOutlined from '@ant-design/icons/LinkOutlined';
import PlusOutlined from '@ant-design/icons/PlusOutlined';
import { useModal } from '@kevisual/center-components/modal/Confirm.tsx';
import { Tooltip } from '@mui/material';
import { isObjectNull } from '@/utils/is-null'; import { isObjectNull } from '@/utils/is-null';
import { FileUpload } from '../modules/FileUpload'; import { FileUpload } from '../modules/FileUpload';
import clsx from 'clsx'; import clsx from 'clsx';
@ -13,9 +19,13 @@ import { Button } from '@mui/material';
import { Dialog, DialogContent, DialogTitle, ButtonGroup } from '@mui/material'; import { Dialog, DialogContent, DialogTitle, ButtonGroup } from '@mui/material';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { IconButton } from '@kevisual/center-components/button/index.tsx'; import { IconButton } from '@kevisual/center-components/button/index.tsx';
import { useForm, Controller } from 'react-hook-form';
import { TextField } from '@mui/material';
import { pick } from 'lodash-es';
const FormModal = () => { const FormModal = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); const { control, handleSubmit, reset } = useForm();
const containerStore = useAppVersionStore( const containerStore = useAppVersionStore(
useShallow((state) => { useShallow((state) => {
return { return {
@ -26,62 +36,56 @@ const FormModal = () => {
}; };
}), }),
); );
useEffect(() => { useEffect(() => {
const open = containerStore.showEdit; const open = containerStore.showEdit;
if (open) {
if (open) { if (open) {
const isNull = isObjectNull(containerStore.formData); const isNull = isObjectNull(containerStore.formData);
if (isNull) { if (isNull) {
form.setFieldsValue({}); reset({});
} else form.setFieldsValue(containerStore.formData); } else {
reset(containerStore.formData);
} }
} }
}, [containerStore.showEdit]); }, [containerStore.showEdit]);
const onFinish = async (values: any) => { const onFinish = async (values: any) => {
containerStore.updateData(values); const pickValues = pick(values, ['id', 'key', 'version']);
containerStore.updateData(pickValues);
}; };
const onClose = () => { const onClose = () => {
containerStore.setShowEdit(false); containerStore.setShowEdit(false);
form.resetFields(); reset();
}; };
const isEdit = containerStore.formData.id; const isEdit = containerStore.formData.id;
return ( return (
<Modal <Dialog
title={isEdit ? 'Edit' : 'Add'}
open={containerStore.showEdit} open={containerStore.showEdit}
onClose={() => containerStore.setShowEdit(false)} onClose={() => containerStore.setShowEdit(false)}
destroyOnClose sx={{
footer={false} '& .MuiDialog-paper': {
width={800} width: '800px',
onCancel={onClose}> },
<Form
form={form}
onFinish={onFinish}
labelCol={{
span: 4,
}}
wrapperCol={{
span: 20,
}}> }}>
<Form.Item name='id' hidden> <DialogTitle>{isEdit ? 'Edit' : 'Add'}</DialogTitle>
<Input /> <DialogContent>
</Form.Item> <form className='flex flex-col gap-6' onSubmit={handleSubmit(onFinish)}>
<Form.Item name='key' label='key'> <Controller name='key' control={control} defaultValue='' render={({ field }) => <TextField label='key' {...field} disabled />} />
<Input disabled /> <Controller name='version' control={control} defaultValue='' render={({ field }) => <TextField label='version' {...field} />} />
</Form.Item> <div>
<Form.Item name='version' label='version'>
<Input />
</Form.Item>
<Form.Item label=' ' colon={false}>
<Button type='submit' variant='contained' color='primary'> <Button type='submit' variant='contained' color='primary'>
{t('submit')} {t('submit')}
</Button> </Button>
<Button className='ml-2' onClick={onClose}> <Button className='ml-2' onClick={onClose}>
{t('cancel')} {t('cancel')}
</Button> </Button>
</Form.Item> </div>
</Form> </form>
</Modal> </DialogContent>
</Dialog>
); );
}; };
@ -105,7 +109,7 @@ export const AppVersionList = () => {
}), }),
); );
const navigate = useNewNavigate(); const navigate = useNewNavigate();
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = useModal();
const [isUpload, setIsUpload] = useState(false); const [isUpload, setIsUpload] = useState(false);
useEffect(() => { useEffect(() => {
// fetch app version list // fetch app version list

View File

@ -1,7 +1,8 @@
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { useUserAppStore } from '../store'; import { useUserAppStore } from '../store';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Form, Input, Modal, Select, Switch } from 'antd'; import { useModal } from '@kevisual/center-components/modal/Confirm.tsx';
import DeleteOutlined from '@ant-design/icons/DeleteOutlined'; import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
import EditOutlined from '@ant-design/icons/EditOutlined'; import EditOutlined from '@ant-design/icons/EditOutlined';
import LinkOutlined from '@ant-design/icons/LinkOutlined'; import LinkOutlined from '@ant-design/icons/LinkOutlined';
@ -9,26 +10,43 @@ import PlusOutlined from '@ant-design/icons/PlusOutlined';
import UnorderedListOutlined from '@ant-design/icons/UnorderedListOutlined'; import UnorderedListOutlined from '@ant-design/icons/UnorderedListOutlined';
import CodeOutlined from '@ant-design/icons/CodeOutlined'; import CodeOutlined from '@ant-design/icons/CodeOutlined';
import ShareAltOutlined from '@ant-design/icons/ShareAltOutlined'; import ShareAltOutlined from '@ant-design/icons/ShareAltOutlined';
import { FormControlLabel, Switch } from '@mui/material';
import { isObjectNull } from '@/utils/is-null'; import { isObjectNull } from '@/utils/is-null';
import { useNewNavigate } from '@/modules'; import { useNewNavigate } from '@/modules';
import { DialogActions, Tooltip } from '@mui/material'; import { DialogActions, Tooltip } from '@mui/material';
import { marked } from 'marked'; import { marked } from 'marked';
import clsx from 'clsx'; import clsx from 'clsx';
import { IconButton } from '@kevisual/center-components/button/index.tsx'; import { IconButton } from '@kevisual/center-components/button/index.tsx';
import { Select } from '@kevisual/center-components/select/index.tsx';
import { iText } from '@kevisual/resources/index.ts'; import { iText } from '@kevisual/resources/index.ts';
import { PermissionManager } from '@kevisual/resources/pages/file/modules/PermissionManager.tsx'; import { PermissionManager } from '@kevisual/resources/pages/file/modules/PermissionManager.tsx';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import { message } from '@/modules/message'; import { message } from '@/modules/message';
import { Dialog, DialogContent, DialogTitle, ButtonGroup } from '@mui/material'; import { Dialog, DialogContent, DialogTitle, ButtonGroup } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { TextField, InputAdornment } from '@mui/material';
import { useForm, Controller } from 'react-hook-form';
import { pick } from 'lodash-es';
const FormModal = () => { const FormModal = () => {
const [form] = Form.useForm(); const defaultValues = {
id: '',
title: '',
domain: '',
key: '',
description: '',
proxy: true,
status: 'running',
};
const { control, handleSubmit, reset } = useForm({
defaultValues,
});
const containerStore = useUserAppStore( const containerStore = useUserAppStore(
useShallow((state) => { useShallow((state) => {
return { return {
showEdit: state.showEdit, showEdit: state.showEdit,
setShowEdit: state.setShowEdit, setShowEdit: state.setShowEdit,
formData: state.formData, userApp: state.userApp,
updateData: state.updateData, updateData: state.updateData,
}; };
}), }),
@ -36,121 +54,113 @@ const FormModal = () => {
useEffect(() => { useEffect(() => {
const open = containerStore.showEdit; const open = containerStore.showEdit;
if (open) { if (open) {
if (open) { const isNull = isObjectNull(containerStore.userApp);
const isNull = isObjectNull(containerStore.formData); console.log('isNull', containerStore.userApp);
if (isNull) { if (isNull) {
form.setFieldsValue({}); reset(defaultValues);
} else form.setFieldsValue(containerStore.formData); } else {
reset(containerStore.userApp);
} }
} }
}, [containerStore.showEdit]); }, [containerStore.showEdit, containerStore.userApp]);
const onFinish = async (values: any) => { const onFinish = async (values: any) => {
containerStore.updateData(values); const pickValues = pick(values, ['id', 'title', 'domain', 'key', 'description', 'proxy', 'status']);
containerStore.updateData(pickValues);
}; };
const onClose = () => { const onClose = () => {
containerStore.setShowEdit(false); containerStore.setShowEdit(false);
form.resetFields(); reset();
}; };
const isEdit = containerStore.formData.id; const isEdit = containerStore?.userApp?.id;
return ( return (
<Dialog <Dialog
title={isEdit ? 'Edit' : 'Add'}
open={containerStore.showEdit} open={containerStore.showEdit}
onClose={() => containerStore.setShowEdit(false)} onClose={() => containerStore.setShowEdit(false)}
sx={{ sx={{
'& .MuiDialog-paper': { '& .MuiDialog-paper': {
width: '800px', width: '1000px',
}, },
}}> }}>
<DialogTitle>{isEdit ? 'Edit' : 'Add'}</DialogTitle> <DialogTitle>{isEdit ? 'Edit' : 'Add'}</DialogTitle>
<DialogContent> <DialogContent>
<Form <form className='flex flex-col gap-4 pt-2' onSubmit={handleSubmit(onFinish)}>
form={form} <Controller name='title' control={control} render={({ field }) => <TextField {...field} label='title' fullWidth />} />
onFinish={onFinish} <Controller
initialValues={{ name='domain'
proxy: true, control={control}
}} render={({ field }) => <TextField {...field} label='domain' variant='outlined' helperText='域名自定义绑定' />}
labelCol={{ />
span: 4, <Controller name='key' control={control} render={({ field }) => <TextField {...field} label='key' fullWidth />} />
}} <Controller name='description' control={control} render={({ field }) => <TextField {...field} label='description' multiline rows={4} fullWidth />} />
wrapperCol={{ <Controller name='proxy' control={control} render={({ field }) => <Switch {...field} checked={field.value} />} />
span: 20, <Controller
}}> name='status'
<Form.Item name='id' hidden> control={control}
<Input /> render={({ field }) => (
</Form.Item>
<Form.Item name='title' label='title'>
<Input />
</Form.Item>
<Form.Item name='domain' label='domain' tooltip='域名自定义绑定'>
<Input />
</Form.Item>
<Form.Item name='key' label='key'>
<Input />
</Form.Item>
<Form.Item name='description' label='description'>
<Input.TextArea rows={4} />
</Form.Item>
<Form.Item name='proxy' label='proxy' tooltip='设置为true则后端直接代理请求minio服务进行转发不会缓存下载到服务器。'>
<Switch />
</Form.Item>
<Form.Item name='status' label='status'>
<Select <Select
{...field}
sx={{
width: '100%',
}}
options={[ options={[
{ label: 'Running', value: 'running' }, { label: 'Running', value: 'running' },
{ label: 'Stop', value: 'stop' }, { label: 'Stop', value: 'stop' },
]} ]}
/> />
</Form.Item> )}
<Form.Item label=' ' colon={false}> />
<div>
<Button type='submit'></Button> <Button type='submit'></Button>
<Button className='ml-2' type='reset' onClick={onClose}> <Button className='ml-2' type='reset' onClick={onClose}>
</Button> </Button>
</Form.Item> </div>
</Form> </form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
}; };
const ShareModal = () => { const ShareModal = () => {
const [form] = Form.useForm();
const [permission, setPermission] = useState<any>(null); const [permission, setPermission] = useState<any>(null);
const [runtime, setRuntime] = useState<string[]>([]);
const containerStore = useUserAppStore( const containerStore = useUserAppStore(
useShallow((state) => { useShallow((state) => {
return { return {
showEdit: state.showShareEdit, showEdit: state.showShareEdit,
setShowEdit: state.setShowShareEdit, setShowEdit: state.setShowShareEdit,
formData: state.formData,
updateData: state.updateData, updateData: state.updateData,
userApp: state.userApp,
}; };
}), }),
); );
useEffect(() => { useEffect(() => {
const open = containerStore.showEdit; const open = containerStore.showEdit;
if (open) { if (open) {
// form.setFieldsValue(containerStore.formData); const permission = containerStore.userApp?.data?.permission || {};
const permission = containerStore.formData?.data?.permission || {}; const runtime = containerStore.userApp?.data?.runtime || [];
if (isObjectNull(permission)) { if (isObjectNull(permission)) {
setPermission(null); setPermission(null);
} else { } else {
setPermission(permission); setPermission(permission);
} }
setRuntime(runtime);
} }
}, [containerStore.showEdit]); }, [containerStore.showEdit, containerStore.userApp]);
const onFinish = async () => { const onFinish = async () => {
const values = { const values = {
...containerStore.formData, id: containerStore.userApp.id,
data: { data: {
permission, permission,
runtime,
}, },
}; };
containerStore.updateData(values); containerStore.updateData(values);
}; };
const onClose = () => { const onClose = () => {
containerStore.setShowEdit(false); containerStore.setShowEdit(false);
form.resetFields();
}; };
const { t } = useTranslation();
console.log('runtime', runtime);
return ( return (
<Dialog <Dialog
open={containerStore.showEdit} open={containerStore.showEdit}
@ -159,27 +169,52 @@ const ShareModal = () => {
}}> }}>
<DialogTitle>{iText.share.title}</DialogTitle> <DialogTitle>{iText.share.title}</DialogTitle>
<DialogContent> <DialogContent>
<div className='flex flex-col gap-2 w-[400px] '>
<PermissionManager <PermissionManager
value={permission} value={permission}
onChange={(value) => { onChange={(value) => {
setPermission(value); setPermission(value);
}} }}
/> />
<FormControlLabel
label={t('app.runtime')}
control={
<Select
multiple
size='small'
value={runtime}
onChange={(e) => {
setRuntime(e.target.value as string[]);
}}
options={[
{
label: 'Node',
value: 'node',
},
{
label: 'Browser',
value: 'browser',
},
]}
/>
}
/>
</div>
</DialogContent>
<DialogActions> <DialogActions>
<Button type='submit' variant='contained' onClick={onFinish}> <Button type='submit' variant='contained' onClick={onFinish}>
{t('Submit')}
</Button> </Button>
<Button className='ml-2' type='reset' onClick={onClose}> <Button className='ml-2' type='reset' onClick={onClose}>
{t('Cancel')}
</Button> </Button>
</DialogActions> </DialogActions>
</DialogContent>
</Dialog> </Dialog>
); );
}; };
export const List = () => { export const List = () => {
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = useModal();
const { t } = useTranslation();
const userAppStore = useUserAppStore( const userAppStore = useUserAppStore(
useShallow((state) => { useShallow((state) => {
return { return {
@ -190,6 +225,7 @@ export const List = () => {
setFormData: state.setFormData, setFormData: state.setFormData,
deleteData: state.deleteData, deleteData: state.deleteData,
setShowShareEdit: state.setShowShareEdit, setShowShareEdit: state.setShowShareEdit,
getUserApp: state.getUserApp,
}; };
}), }),
); );
@ -206,6 +242,7 @@ export const List = () => {
padding: '8px', padding: '8px',
}} }}
onClick={() => { onClick={() => {
userAppStore.setFormData({});
userAppStore.setShowEdit(true); userAppStore.setShowEdit(true);
}}> }}>
<PlusOutlined /> <PlusOutlined />
@ -242,8 +279,14 @@ export const List = () => {
</div> </div>
</div> </div>
<div> <div>
{item.domain && <div className='text-xs'>访: {item.domain}</div>} {item.domain && (
<div className='text-xs'>version: {item.version}</div> <div className='text-xs'>
{t('app.domain')}: {item.domain}
</div>
)}
<div className='text-xs'>
{t('app.version')}: {item.version}
</div>
<div className={clsx('text-sm border border-gray-200 p-2 max-h-[140px] scrollbar my-1', !hasDescription && 'hidden')}> <div className={clsx('text-sm border border-gray-200 p-2 max-h-[140px] scrollbar my-1', !hasDescription && 'hidden')}>
<div dangerouslySetInnerHTML={{ __html: content }}></div> <div dangerouslySetInnerHTML={{ __html: content }}></div>
</div> </div>
@ -273,6 +316,7 @@ export const List = () => {
<Tooltip title={iText.share.tips}> <Tooltip title={iText.share.tips}>
<Button <Button
onClick={() => { onClick={() => {
userAppStore.getUserApp(item.id);
userAppStore.setFormData(item); userAppStore.setFormData(item);
userAppStore.setShowShareEdit(true); userAppStore.setShowShareEdit(true);
}}> }}>
@ -294,7 +338,7 @@ export const List = () => {
if (DEV_SERVER) { if (DEV_SERVER) {
baseUri = 'http://localhost:3005'; baseUri = 'http://localhost:3005';
} }
const link = new URL(`/${item.user}/${item.key}`, baseUri); const link = new URL(`/${item.user}/${item.key}/`, baseUri);
window.open(link.toString(), '_blank'); window.open(link.toString(), '_blank');
} else { } else {
message.error('The app is not running'); message.error('The app is not running');

View File

@ -14,6 +14,9 @@ type UserAppStore = {
deleteData: (id: string) => Promise<void>; deleteData: (id: string) => Promise<void>;
showShareEdit: boolean; showShareEdit: boolean;
setShowShareEdit: (showShareEdit: boolean) => void; setShowShareEdit: (showShareEdit: boolean) => void;
userApp: any;
setUserApp: (userApp: any) => void;
getUserApp: (id: string) => Promise<void>;
}; };
export const useUserAppStore = create<UserAppStore>((set, get) => { export const useUserAppStore = create<UserAppStore>((set, get) => {
return { return {
@ -69,5 +72,20 @@ export const useUserAppStore = create<UserAppStore>((set, get) => {
}, },
showShareEdit: false, showShareEdit: false,
setShowShareEdit: (showShareEdit) => set({ showShareEdit }), setShowShareEdit: (showShareEdit) => set({ showShareEdit }),
userApp: {},
setUserApp: (userApp) => set({ userApp }),
getUserApp: async (id) => {
set({ userApp: null });
const res = await query.post({
path: 'user-app',
key: 'get',
id,
});
if (res.code === 200) {
set({ userApp: res.data });
} else {
message.error(res.message || 'Request failed');
}
},
}; };
}); });

View File

@ -1,9 +1,6 @@
import { Input, Modal, Select } from 'antd';
import { Fragment, Suspense, useEffect, useState } from 'react'; import { Fragment, Suspense, useEffect, useState } from 'react';
import { TextArea } from '../components/TextArea';
import { useContainerStore } from '../store'; import { useContainerStore } from '../store';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { Form } from 'antd';
// import copy from 'copy-to-clipboard'; // import copy from 'copy-to-clipboard';
import { useNewNavigate } from '@/modules'; import { useNewNavigate } from '@/modules';
import { message } from '@/modules/message'; import { message } from '@/modules/message';
@ -19,10 +16,16 @@ import { isObjectNull } from '@/utils/is-null';
import { CardBlank } from '@/components/card/CardBlank'; import { CardBlank } from '@/components/card/CardBlank';
import { Settings } from 'lucide-react'; import { Settings } from 'lucide-react';
import React from 'react'; import React from 'react';
import { useModal } from '@kevisual/center-components/modal/Confirm.tsx';
import { useForm, Controller } from 'react-hook-form';
import { TextField } from '@mui/material';
import { pick } from 'lodash-es';
import { useTranslation } from 'react-i18next';
import { TagsInput } from '@kevisual/center-components/select/TagsInput.tsx';
const DrawEdit = React.lazy(() => import('../module/DrawEdit')); const DrawEdit = React.lazy(() => import('../module/DrawEdit'));
const FormModal = () => { const FormModal = () => {
const [form] = Form.useForm(); const { control, handleSubmit, reset, setValue } = useForm();
const containerStore = useContainerStore( const containerStore = useContainerStore(
useShallow((state) => { useShallow((state) => {
return { return {
@ -33,70 +36,75 @@ const FormModal = () => {
}; };
}), }),
); );
useEffect(() => { useEffect(() => {
const open = containerStore.showEdit; const open = containerStore.showEdit;
if (open) {
if (open) { if (open) {
const isNull = isObjectNull(containerStore.formData); const isNull = isObjectNull(containerStore.formData);
if (isNull) { if (isNull) {
form.setFieldsValue({}); reset({});
} else form.setFieldsValue(containerStore.formData); } else {
Object.keys(containerStore.formData).forEach((key) => {
setValue(key, containerStore.formData[key]);
});
} }
} }
}, [containerStore.showEdit]); return () => {
const onFinish = async (values: any) => { reset({});
containerStore.updateData(values);
}; };
}, [containerStore.showEdit]);
const onFinish = async (values: any) => {
const pickValues = pick(values, ['id', 'title', 'description', 'tags', 'code']);
containerStore.updateData(pickValues);
};
const onClose = () => { const onClose = () => {
containerStore.setShowEdit(false); containerStore.setShowEdit(false);
form.resetFields(); reset();
}; };
const isEdit = containerStore.formData.id; const isEdit = containerStore.formData.id;
const { t } = useTranslation();
return ( return (
<Dialog open={containerStore.showEdit} onClose={() => containerStore.setShowEdit(false)}> <Dialog open={containerStore.showEdit} onClose={onClose}>
<DialogTitle>{isEdit ? 'Edit' : 'Add'}</DialogTitle> <DialogTitle>{isEdit ? 'Edit' : 'Add'}</DialogTitle>
<DialogContent sx={{ padding: '20px', minWidth: '600px' }}> <DialogContent sx={{ padding: '20px', minWidth: '600px' }}>
<Form <form className='flex flex-col gap-6 pt-2' onSubmit={handleSubmit(onFinish)}>
form={form} <Controller name='title' control={control} defaultValue='' render={({ field }) => <TextField {...field} label='Title' fullWidth />} />
onFinish={onFinish} <Controller
labelCol={{ name='description'
span: 4, control={control}
defaultValue=''
render={({ field }) => <TextField {...field} label='Description' multiline rows={4} fullWidth />}
/>
<Controller
name='tags'
control={control}
defaultValue={[]}
render={({ field }) => {
return <TagsInput key={'tags'} label='Tags' placeholder='添加标签' value={field.value} onChange={(value) => field.onChange(value)} />;
}} }}
wrapperCol={{ />
span: 20,
}}>
<Form.Item name='id' hidden>
<Input />
</Form.Item>
<Form.Item name='title' label='title'>
<Input />
</Form.Item>
<Form.Item name='description' label='description'>
<Input.TextArea rows={4} />
</Form.Item>
<Form.Item name='tags' label='tags'>
<Select mode='tags' />
</Form.Item>
{!isEdit && ( {!isEdit && (
<Form.Item name='code' label='code'> <Controller name='code' control={control} defaultValue='' render={({ field }) => <TextField {...field} label='Code' multiline fullWidth />} />
<TextArea />
</Form.Item>
)} )}
<Form.Item label=' ' colon={false}> <div>
<Button variant='contained' type='submit'> <Button variant='contained' type='submit'>
{t('Submit')}
</Button> </Button>
<Button className='ml-2' onClick={onClose}> <Button className='ml-2' onClick={onClose}>
{t('Cancel')}
</Button> </Button>
</Form.Item> </div>
</Form> </form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
}; };
const PublishFormModal = () => { const PublishFormModal = () => {
const [form] = Form.useForm(); const { control, handleSubmit, reset, setValue, getValues } = useForm();
const { t } = useTranslation();
const containerStore = useContainerStore( const containerStore = useContainerStore(
useShallow((state) => { useShallow((state) => {
return { return {
@ -107,20 +115,25 @@ const PublishFormModal = () => {
}; };
}), }),
); );
useEffect(() => { useEffect(() => {
const open = containerStore.showEdit; const open = containerStore.showEdit;
if (open) {
if (open) { if (open) {
const isNull = isObjectNull(containerStore.formData); const isNull = isObjectNull(containerStore.formData);
if (isNull) { if (isNull) {
form.setFieldsValue({}); reset({});
} else form.setFieldsValue(containerStore.formData); } else {
Object.keys(containerStore.formData).forEach((key) => {
setValue(key, containerStore.formData[key]);
});
} }
} }
}, [containerStore.showEdit]); }, [containerStore.showEdit]);
const onFinish = async () => { const onFinish = async () => {
const values = form.getFieldsValue(); const values = getValues();
const containerRes = await containerStore.updateData(values, { closePublish: false }); const pickValues = pick(values, ['id', 'publish.key', 'publish.version', 'publish.fileName', 'publish.description']);
const containerRes = await containerStore.updateData(pickValues, { closePublish: false });
if (containerRes.code === 200) { if (containerRes.code === 200) {
const code = containerRes.data?.code || '-'; const code = containerRes.data?.code || '-';
const fileName = values['publish']?.['fileName']; const fileName = values['publish']?.['fileName'];
@ -139,12 +152,11 @@ const PublishFormModal = () => {
const key = values['publish']['key']; const key = values['publish']['key'];
const version = values['publish']['version']; const version = values['publish']['version'];
const file = toFile(code, directoryAndName.name); const file = toFile(code, directoryAndName.name);
const res = await uploadFileChunked(file, { const res = (await uploadFileChunked(file, {
appKey: key, appKey: key,
version, version,
directory: directoryAndName.directory, directory: directoryAndName.directory,
}); })) as any;
// @ts-ignore
if (res.code === 200) { if (res.code === 200) {
message.success('upload success'); message.success('upload success');
} else { } else {
@ -152,66 +164,67 @@ const PublishFormModal = () => {
} }
} }
}; };
const onUpdate = async () => { const onUpdate = async () => {
const values = form.getFieldsValue(); const values = getValues();
containerStore.updateData(values); containerStore.updateData(values);
}; };
const onClose = () => { const onClose = () => {
containerStore.setShowEdit(false); containerStore.setShowEdit(false);
form.resetFields(); reset();
}; };
return ( return (
<Dialog open={containerStore.showEdit} onClose={() => containerStore.setShowEdit(false)}> <Dialog open={containerStore.showEdit} onClose={onClose}>
<DialogTitle>Publish</DialogTitle> <DialogTitle>Publish</DialogTitle>
<DialogContent sx={{ padding: '10px', minWidth: '600px' }}> <DialogContent sx={{ padding: '10px', minWidth: '600px' }}>
<Form <form className='flex flex-col gap-6 pt-2' onSubmit={handleSubmit(onFinish)}>
form={form} <Controller
onFinish={onFinish} name='publish.key'
labelCol={{ control={control}
span: 6, defaultValue=''
}} render={({ field }) => <TextField {...field} label='App key' required fullWidth />}
wrapperCol={{ />
span: 18, <Controller
}}> name='publish.version'
<Form.Item name='id' hidden> control={control}
<Input /> defaultValue=''
</Form.Item> render={({ field }) => <TextField {...field} label='App Version' required fullWidth />}
<Form.Item name={['publish', 'key']} label='App key' required> />
<Input /> <Controller
</Form.Item> name='publish.fileName'
<Form.Item name={['publish', 'version']} label='App Version' required> control={control}
<Input /> defaultValue=''
</Form.Item> render={({ field }) => <TextField {...field} label='Filename' required fullWidth />}
<Form.Item name={['publish', 'fileName']} label='Filename' tooltip='可以是文件夹格式,比如(directory/a.name)' required> />
<Input /> <Controller
</Form.Item> name='publish.description'
<Form.Item name={['publish', 'description']} label='Description'> control={control}
<Input.TextArea rows={4} /> defaultValue=''
</Form.Item> render={({ field }) => <TextField {...field} label='Description' multiline rows={4} fullWidth />}
<Form.Item label=' ' colon={false}> />
<div className='flex gap-3'> <div className='flex gap-3'>
<Tooltip <Tooltip
placement='top' placement='top'
title='根据文件名和code的字符串的内容自动生成文件。并保存。如果是其他文件类型转成base64上传。比如图片以类似data:image/jpeg;开头'> title='根据文件名和code的字符串的内容自动生成文件。并保存。如果是其他文件类型转成base64上传。比如图片以类似data:image/jpeg;开头'>
<Button variant='contained' color='primary' type='submit'> <Button variant='contained' color='primary' type='submit'>
{t('Upload')}
</Button> </Button>
</Tooltip> </Tooltip>
<Button variant='contained' onClick={onUpdate}> <Button variant='contained' onClick={onUpdate}>
{t('Submit')}
</Button> </Button>
<Button onClick={onClose}></Button> <Button onClick={onClose}>{t('Cancel')}</Button>
</div> </div>
</Form.Item> </form>
</Form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
}; };
export const ContainerList = () => { export const ContainerList = () => {
const navicate = useNewNavigate(); const navicate = useNewNavigate();
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = useModal();
const containerStore = useContainerStore( const containerStore = useContainerStore(
useShallow((state) => { useShallow((state) => {
return { return {
@ -271,7 +284,7 @@ export const ContainerList = () => {
e.stopPropagation(); e.stopPropagation();
}}> }}>
{item.title || '-'} {item.title || '-'}
<div className='font-thin card-key ml-3'>{item.tags ? item.tags.join(', ') : ''}</div> <div className='font-thin card-key ml-3'>{item.tags ? item.tags?.join?.(', ') : ''}</div>
</div> </div>
<div className='font-light text-xs mt-2'>{item.description ? item.description : '-'}</div> <div className='font-light text-xs mt-2'>{item.description ? item.description : '-'}</div>
</div> </div>

View File

@ -16,4 +16,3 @@ export const App = () => {
); );
}; };
export * from './module/Select';

View File

@ -1,39 +0,0 @@
import { query } from '@/modules';
import { Select as AntSelect, SelectProps } from 'antd';
import { useEffect, useState } from 'react';
import { message } from '@/modules/message';
export const Select = (props: SelectProps) => {
const [options, setOptions] = useState<{ value: string; id: string }[]>([]);
useEffect(() => {
fetch();
}, []);
const fetch = async () => {
const res = await query.post({
path: 'container',
key: 'list',
});
if (res.code !== 200) {
message.error(res.message || '获取容器列表失败');
return;
}
const data = res.data || [];
setOptions(
data.map((item: any) => {
return {
label: item.title,
value: item.id,
};
}),
);
};
return (
<AntSelect
{...props}
options={options}
// onChange={(e) => {
// const labelValue = options.find((item) => item.value === e);
// props.onChange?.(e, options);
// }}
/>
);
};

View File

@ -4,13 +4,17 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import path from 'path-browserify'; import path from 'path-browserify';
import prettyBytes from 'pretty-bytes'; import prettyBytes from 'pretty-bytes';
import clsx from 'clsx'; import clsx from 'clsx';
import { isObjectNull } from '@/utils/is-null';
import FileOutlined from '@ant-design/icons/FileOutlined'; import FileOutlined from '@ant-design/icons/FileOutlined';
import FolderOutlined from '@ant-design/icons/FolderOutlined'; import FolderOutlined from '@ant-design/icons/FolderOutlined';
import { IconButton } from '@kevisual/center-components/button/index.tsx'; import { IconButton } from '@kevisual/center-components/button/index.tsx';
import { render, unmount } from '@kevisual/resources/pages/Bootstrap.tsx'; import { render, unmount } from '@kevisual/resources/pages/Bootstrap.tsx';
import UploadOutlined from '@ant-design/icons/lib/icons/UploadOutlined'; import UploadOutlined from '@ant-design/icons/lib/icons/UploadOutlined';
import { Tooltip } from '@mui/material'; 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 { useTranslation } from 'react-i18next';
import { UploadButton } from '@kevisual/resources/pages/upload/index.tsx';
export const CardPath = ({ children }: any) => { export const CardPath = ({ children }: any) => {
const userAppStore = useFileStore( const userAppStore = useFileStore(
useShallow((state) => { useShallow((state) => {
@ -32,10 +36,18 @@ export const CardPath = ({ children }: any) => {
userAppStore.setPath(prefix.replace('root/', '') + '/'); userAppStore.setPath(prefix.replace('root/', '') + '/');
userAppStore.getList(); userAppStore.getList();
}; };
const { t } = useTranslation();
const [usrname, appKey, version] = paths;
const onUloadFinish = (res: any) => {
console.log(res);
userAppStore.getList();
};
return ( return (
<div className='border border-gray-200 rounded'> <div className='border border-gray-200 rounded'>
<div className='p-2'> <div className='p-2'>
<div className='flex flex-col'> <div className='flex flex-col'>
<div className='flex justify-between'>
<div className='flex gap-2 py-2'> <div className='flex gap-2 py-2'>
<div>Path: </div> <div>Path: </div>
<div className='flex'> <div className='flex'>
@ -56,6 +68,32 @@ export const CardPath = ({ children }: any) => {
})} })}
</div> </div>
</div> </div>
<div className='flex gap-2'>
<Tooltip title={t('refresh')} placement='bottom'>
<IconButton
color='primary'
onClick={() => {
userAppStore.getList();
}}>
<RefreshCw />
</IconButton>
</Tooltip>
{version && (
<>
<Tooltip title={t('uploadDirectory')} placement='bottom'>
<IconButton color='primary'>
<UploadButton onlyIcon uploadDirectory icon={<Upload />} appKey={appKey} version={version} username={usrname} onUpload={onUloadFinish} />
</IconButton>
</Tooltip>
<Tooltip title={t('upload')} placement='bottom'>
<IconButton color='primary'>
<UploadButton onlyIcon appKey={appKey} version={version} username={usrname} onUpload={onUloadFinish} />
</IconButton>
</Tooltip>
</>
)}
</div>
</div>
</div> </div>
</div> </div>
<div className=''>{children}</div> <div className=''>{children}</div>
@ -65,6 +103,17 @@ export const CardPath = ({ children }: any) => {
export const List = () => { export const List = () => {
const [tab, setTab] = useState<'folder' | 'upload'>('folder'); const [tab, setTab] = useState<'folder' | 'upload'>('folder');
const uploadRef = useRef<HTMLDivElement>(null); const uploadRef = useRef<HTMLDivElement>(null);
const { setOpenDrawer, setPrefix, setResource, getStatFile, setOnce } = useResourceFileStore(
useShallow((state) => {
return {
setOpenDrawer: state.setOpenDrawer,
setPrefix: state.setPrefix,
setResource: state.setResource,
getStatFile: state.getStatFile,
setOnce: state.setOnce,
};
}),
);
const userAppStore = useFileStore( const userAppStore = useFileStore(
useShallow((state) => { useShallow((state) => {
return { return {
@ -79,6 +128,9 @@ export const List = () => {
); );
useEffect(() => { useEffect(() => {
userAppStore.getList(); userAppStore.getList();
return () => {
setOnce(null);
};
}, []); }, []);
const onDirectoryClick = (prefix: string) => { const onDirectoryClick = (prefix: string) => {
userAppStore.setPath(prefix); userAppStore.setPath(prefix);
@ -114,7 +166,15 @@ export const List = () => {
className=' border-t border-gray-200 flex gap-2 p-2' className=' border-t border-gray-200 flex gap-2 p-2'
key={index} key={index}
onClick={() => { onClick={() => {
userAppStore.getFile(item.name); setPrefix(item.name);
setResource(item);
getStatFile();
setOpenDrawer(true);
setOnce((data: any) => {
// console.log(data);
userAppStore.getList();
});
console.log(item);
}}> }}>
<div> <div>
<FileOutlined /> <FileOutlined />
@ -126,9 +186,7 @@ export const List = () => {
})} })}
</div> </div>
</CardPath> </CardPath>
<div> <FileDrawerApp />
<pre>{!isObjectNull(userAppStore.file) && JSON.stringify(userAppStore.file, null, 2)}</pre>
</div>
</> </>
); );
}, [userAppStore.list, userAppStore.path]); }, [userAppStore.list, userAppStore.path]);

View File

@ -1,8 +1,6 @@
import { Input, Modal } from 'antd';
import { Fragment, useEffect, useState } from 'react'; import { Fragment, useEffect, useState } from 'react';
import { useOrgStore } from '../store'; import { useOrgStore } from '../store';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { Form } from 'antd';
import { useNewNavigate } from '@/modules'; import { useNewNavigate } from '@/modules';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Tooltip, Button, ButtonGroup, Dialog, DialogTitle, DialogContent } from '@mui/material'; import { Tooltip, Button, ButtonGroup, Dialog, DialogTitle, DialogContent } from '@mui/material';
@ -18,10 +16,14 @@ import clsx from 'clsx';
import { isObjectNull } from '@/utils/is-null'; import { isObjectNull } from '@/utils/is-null';
import { CardBlank } from '@/components/card/CardBlank'; import { CardBlank } from '@/components/card/CardBlank';
import { useLayoutStore } from '@/modules/layout/store'; import { useLayoutStore } from '@/modules/layout/store';
import { useModal } from '@kevisual/center-components/modal/Confirm.tsx';
import { useForm, Controller } from 'react-hook-form';
import { TextField } from '@mui/material';
import { pick } from 'lodash-es';
const FormModal = () => { const FormModal = () => {
const [form] = Form.useForm();
const { t } = useTranslation(); const { t } = useTranslation();
const { control, handleSubmit, reset } = useForm();
const userStore = useOrgStore( const userStore = useOrgStore(
useShallow((state) => { useShallow((state) => {
return { return {
@ -33,47 +35,50 @@ const FormModal = () => {
}; };
}), }),
); );
useEffect(() => { useEffect(() => {
const open = userStore.showEdit; const open = userStore.showEdit;
if (open) { if (open) {
const isNull = isObjectNull(userStore.formData); const isNull = isObjectNull(userStore.formData);
if (isNull) { if (isNull) {
form.setFieldsValue({}); reset({});
} else form.setFieldsValue(userStore.formData); } else reset(userStore.formData);
} }
}, [userStore.showEdit, userStore.formData]); return () => {
const onFinish = async (values: any) => { reset({});
userStore.updateData(values);
}; };
}, [userStore.showEdit, userStore.formData, reset]);
const onFinish = async (values: any) => {
const pickValues = pick(values, ['id', 'username', 'description']);
userStore.updateData(pickValues);
};
const onClose = () => { const onClose = () => {
userStore.setShowEdit(false); userStore.setShowEdit(false);
form.setFieldsValue({}); reset({});
userStore.setFormData({}); userStore.setFormData({});
}; };
const isEdit = userStore.formData.id; const isEdit = userStore.formData.id;
return ( return (
<Dialog open={userStore.showEdit} onClose={() => userStore.setShowEdit(false)}> <Dialog open={userStore.showEdit} onClose={() => userStore.setShowEdit(false)}>
<DialogTitle>{isEdit ? 'Edit' : 'Add'}</DialogTitle> <DialogTitle>{isEdit ? 'Edit' : 'Add'}</DialogTitle>
<DialogContent sx={{ padding: '20px', minWidth: '600px' }}> <DialogContent sx={{ padding: '20px', minWidth: '600px' }}>
<Form <form onSubmit={handleSubmit(onFinish)}>
form={form} <Controller
onFinish={onFinish} name='username'
labelCol={{ control={control}
span: 4, defaultValue=''
}} render={({ field }) => <TextField {...field} label='username' disabled={isEdit} fullWidth margin='normal' />}
wrapperCol={{ />
span: 20, <Controller
}}> name='description'
<Form.Item name='id' hidden> control={control}
<Input /> defaultValue=''
</Form.Item> render={({ field }) => <TextField {...field} label='description' multiline rows={4} fullWidth margin='normal' />}
<Form.Item name='username' label='username'> />
<Input disabled={isEdit} />
</Form.Item>
<Form.Item name='description' label='description'>
<Input.TextArea rows={4} />
</Form.Item>
<Form.Item label=' ' colon={false}>
<div className='flex gap-2'> <div className='flex gap-2'>
<Button variant='contained' type='submit'> <Button variant='contained' type='submit'>
{t('Submit')} {t('Submit')}
@ -82,8 +87,7 @@ const FormModal = () => {
{t('Cancel')} {t('Cancel')}
</Button> </Button>
</div> </div>
</Form.Item> </form>
</Form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
@ -111,9 +115,7 @@ export const List = () => {
}; };
}), }),
); );
const [codeEdit, setCodeEdit] = useState(false); const [modal, contextHolder] = useModal();
const [modal, contextHolder] = Modal.useModal();
const [code, setCode] = useState('');
useEffect(() => { useEffect(() => {
userStore.getList(); userStore.getList();
}, []); }, []);
@ -143,9 +145,7 @@ export const List = () => {
className='flex text-sm gap flex-col w-[400px] max-h-[400px] bg-white p-4 rounded-lg' className='flex text-sm gap flex-col w-[400px] max-h-[400px] bg-white p-4 rounded-lg'
key={item.id} key={item.id}
onClick={() => { onClick={() => {
setCode(item.code); // userStore.setFormData(item);
userStore.setFormData(item);
setCodeEdit(true);
}}> }}>
<div className='px-4 cursor-pointer'> <div className='px-4 cursor-pointer'>
<div <div
@ -168,7 +168,6 @@ export const List = () => {
onClick={(e) => { onClick={(e) => {
userStore.setFormData(item); userStore.setFormData(item);
userStore.setShowEdit(true); userStore.setShowEdit(true);
setCodeEdit(false);
e.stopPropagation(); e.stopPropagation();
}}> }}>
<EditOutlined /> <EditOutlined />
@ -214,35 +213,6 @@ export const List = () => {
<CardBlank className='w-[400px]' /> <CardBlank className='w-[400px]' />
</div> </div>
</div> </div>
<div className={clsx('bg-gray-100 border-gray-200 border-bg-slate-300 w-[600px] shark-0', !codeEdit && 'hidden', 'hidden')}>
<div className='bg-white p-2'>
<div className='mt-2 ml-2 flex gap-2'>
<Button
onClick={() => {
setCodeEdit(false);
userStore.setFormData({});
}}>
<LeftOutlined />
</Button>
<Button
onClick={() => {
userStore.updateData({ ...userStore.formData, code });
}}>
<SaveOutlined />
</Button>
</div>
</div>
<div className='h-[94%] p-2 rounded-2 shadow-xs'>
<Input.TextArea
value={code}
onChange={(value) => {
// setCode(value);
}}
className='h-full max-h-full scrollbar'
style={{ overflow: 'auto' }}
/>
</div>
</div>
</div> </div>
<FormModal /> <FormModal />
{contextHolder} {contextHolder}

View File

@ -2,22 +2,24 @@ import { useShallow } from 'zustand/react/shallow';
import { useOrgStore } from '../store'; import { useOrgStore } from '../store';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Input, Modal, Select } from 'antd';
import { message } from '@/modules/message'; import { message } from '@/modules/message';
import DeleteOutlined from '@ant-design/icons/DeleteOutlined'; import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
import LeftOutlined from '@ant-design/icons/LeftOutlined'; import LeftOutlined from '@ant-design/icons/LeftOutlined';
import PlusOutlined from '@ant-design/icons/PlusOutlined'; import PlusOutlined from '@ant-design/icons/PlusOutlined';
import { Tooltip, Button, ButtonGroup, Dialog, DialogTitle, DialogContent } from '@mui/material'; import { Tooltip, Button, ButtonGroup, Dialog, DialogTitle, DialogContent } from '@mui/material';
import { IconButton } from '@kevisual/center-components/button/index.tsx'; import { IconButton } from '@kevisual/center-components/button/index.tsx';
import { Form } from 'antd';
import { useNewNavigate } from '@/modules'; import { useNewNavigate } from '@/modules';
import { isObjectNull } from '@/utils/is-null'; import { isObjectNull } from '@/utils/is-null';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import clsx from 'clsx'; import { useModal } from '@kevisual/center-components/modal/Confirm.tsx';
import { TextField } from '@mui/material';
import { Select } from '@kevisual/center-components/select/index.tsx';
import { useForm, Controller } from 'react-hook-form';
import EditOutlined from '@ant-design/icons/EditOutlined';
const FormModal = () => { const FormModal = () => {
const [form] = Form.useForm(); const { control, handleSubmit, reset } = useForm();
const userStore = useOrgStore( const userStore = useOrgStore(
useShallow((state) => { useShallow((state) => {
return { return {
@ -29,17 +31,21 @@ const FormModal = () => {
}; };
}), }),
); );
useEffect(() => { useEffect(() => {
const open = userStore.showEdit; const open = userStore.showEdit;
if (open) { if (open) {
const isNull = isObjectNull(userStore.formData); const isNull = isObjectNull(userStore.formData);
if (isNull) { if (isNull) {
form.setFieldsValue({}); reset({});
} else form.setFieldsValue(userStore.formData); } else reset(userStore.formData);
} }
}, [userStore.showEdit, userStore.formData]); return () => {
reset({});
};
}, [userStore.showEdit, userStore.formData, reset]);
const onFinish = async (values: any) => { const onFinish = async (values: any) => {
//
console.log(values); console.log(values);
const username = values.username; const username = values.username;
const role = values.role; const role = values.role;
@ -47,47 +53,46 @@ const FormModal = () => {
message.error('username is required'); message.error('username is required');
return; return;
} }
userStore.addUser({ username: username, role }); const res = await userStore.addUser({ username: username, role });
if (res?.code === 200) {
userStore.setShowEdit(false);
}
}; };
const onClose = () => { const onClose = () => {
userStore.setShowEdit(false); userStore.setShowEdit(false);
userStore.setFormData({}); userStore.setFormData({});
}; };
const isEdit = userStore.formData.id; const isEdit = userStore.formData.id;
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Dialog open={userStore.showEdit} onClose={() => userStore.setShowEdit(false)}> <Dialog open={userStore.showEdit} onClose={() => userStore.setShowEdit(false)}>
<DialogTitle>{isEdit ? 'Edit' : 'Add'}</DialogTitle> <DialogTitle>{isEdit ? 'Edit' : 'Add'}</DialogTitle>
<DialogContent sx={{ padding: '20px', minWidth: '600px' }}> <DialogContent sx={{ padding: '20px', minWidth: '600px' }}>
<Form <form className='flex flex-col gap-6' onSubmit={handleSubmit(onFinish)}>
form={form} <Controller
onFinish={onFinish} name='username'
labelCol={{ control={control}
span: 4, defaultValue=''
}} render={({ field }) => <TextField {...field} label='username' disabled={isEdit} fullWidth margin='normal' />}
wrapperCol={{ />
span: 20, <Controller
}}> name='role'
<Form.Item name='id' hidden> control={control}
<Input /> defaultValue=''
</Form.Item> render={({ field }) => (
<Form.Item name='username' label='username'>
<Input disabled={isEdit} />
</Form.Item>
<Form.Item name='role' label='role'>
<Select <Select
{...field}
options={[ options={[
{ { label: 'admin', value: 'admin' },
label: 'admin', { label: 'user', value: 'user' },
value: 'admin', ]}
}, fullWidth
{ />
label: 'member', )}
value: 'member', />
},
]}></Select>
</Form.Item>
<Form.Item label=' ' colon={false}>
<div className='flex gap-2'> <div className='flex gap-2'>
<Button variant='contained' type='submit'> <Button variant='contained' type='submit'>
{t('Submit')} {t('Submit')}
@ -96,8 +101,7 @@ const FormModal = () => {
{t('Cancel')} {t('Cancel')}
</Button> </Button>
</div> </div>
</Form.Item> </form>
</Form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
@ -106,7 +110,7 @@ const FormModal = () => {
export const UserList = () => { export const UserList = () => {
const param = useParams(); const param = useParams();
const navicate = useNewNavigate(); const navicate = useNewNavigate();
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = useModal();
const { t } = useTranslation(); const { t } = useTranslation();
const orgStore = useOrgStore( const orgStore = useOrgStore(
useShallow((state) => { useShallow((state) => {
@ -173,8 +177,18 @@ export const UserList = () => {
variant='contained' variant='contained'
color='primary' color='primary'
sx={{ color: 'white', '& .MuiButton-root': { color: 'white', minWidth: '32px', width: '32px', height: '32px', padding: '6px' } }}> sx={{ color: 'white', '& .MuiButton-root': { color: 'white', minWidth: '32px', width: '32px', height: '32px', padding: '6px' } }}>
{/* <Tooltip title='edit'>
<Button
disabled={isOwner}
onClick={() => {
orgStore.setUserFormData(item);
orgStore.setShowUserEdit(true);
}}>
<EditOutlined />
</Button>
</Tooltip> */}
<Tooltip title='delete'> <Tooltip title='delete'>
<IconButton <Button
disabled={isOwner} disabled={isOwner}
onClick={() => { onClick={() => {
modal.confirm({ modal.confirm({
@ -186,7 +200,7 @@ export const UserList = () => {
}); });
}}> }}>
<DeleteOutlined /> <DeleteOutlined />
</IconButton> </Button>
</Tooltip> </Tooltip>
</ButtonGroup> </ButtonGroup>
</div> </div>

View File

@ -22,7 +22,7 @@ type OrgStore = {
orgId: string; orgId: string;
setOrgId: (orgId: string) => void; setOrgId: (orgId: string) => void;
getOrg: () => Promise<any>; getOrg: () => Promise<any>;
addUser: (data: { userId?: string; username?: string; role?: string }) => Promise<void>; addUser: (data: { userId?: string; username?: string; role?: string }) => Promise<any>;
removeUser: (userId: string) => Promise<void>; removeUser: (userId: string) => Promise<void>;
}; };
export const useOrgStore = create<OrgStore>((set, get) => { export const useOrgStore = create<OrgStore>((set, get) => {
@ -113,6 +113,7 @@ export const useOrgStore = create<OrgStore>((set, get) => {
} else { } else {
message.error(res.message || 'Request failed'); message.error(res.message || 'Request failed');
} }
return res
}, },
removeUser: async (userId: string) => { removeUser: async (userId: string) => {
const { orgId } = get(); const { orgId } = get();

View File

@ -1,8 +1,6 @@
import { Input, Modal } from 'antd';
import { Fragment, useEffect, useState } from 'react'; import { Fragment, useEffect, useState } from 'react';
import { useUserStore } from '../store'; import { useUserStore } from '../store';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { Form } from 'antd';
import EditOutlined from '@ant-design/icons/EditOutlined'; import EditOutlined from '@ant-design/icons/EditOutlined';
import SaveOutlined from '@ant-design/icons/SaveOutlined'; import SaveOutlined from '@ant-design/icons/SaveOutlined';
import DeleteOutlined from '@ant-design/icons/DeleteOutlined'; import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
@ -14,8 +12,13 @@ import { CardBlank } from '@kevisual/center-components/card/CardBlank.tsx';
import { Dialog, ButtonGroup, Button, DialogContent, DialogTitle } from '@mui/material'; import { Dialog, ButtonGroup, Button, DialogContent, DialogTitle } from '@mui/material';
import { IconButton } from '@kevisual/center-components/button/index.tsx'; import { IconButton } from '@kevisual/center-components/button/index.tsx';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useModal } from '@kevisual/center-components/modal/Confirm.tsx';
import { TextField } from '@mui/material';
import { useForm, Controller } from 'react-hook-form';
import { pick } from 'lodash-es';
const FormModal = () => { const FormModal = () => {
const [form] = Form.useForm(); const { control, handleSubmit, reset } = useForm();
const userStore = useUserStore( const userStore = useUserStore(
useShallow((state) => { useShallow((state) => {
return { return {
@ -27,25 +30,34 @@ const FormModal = () => {
}; };
}), }),
); );
useEffect(() => { useEffect(() => {
const open = userStore.showEdit; const open = userStore.showEdit;
if (open) { if (open) {
const isNull = isObjectNull(userStore.formData); const isNull = isObjectNull(userStore.formData);
if (isNull) { if (isNull) {
form.setFieldsValue({}); reset({});
} else form.setFieldsValue(userStore.formData); } else reset(userStore.formData);
} }
}, [userStore.showEdit]); return () => {
const onFinish = async (values: any) => { reset({});
userStore.updateData(values);
}; };
}, [userStore.showEdit]);
const onFinish = (values: any) => {
const pickValues = pick(values, ['id', 'username', 'description']);
userStore.updateData(pickValues);
};
const onClose = () => { const onClose = () => {
userStore.setShowEdit(false); userStore.setShowEdit(false);
form.setFieldsValue({}); reset({});
userStore.setFormData({}); userStore.setFormData({});
}; };
const isEdit = userStore.formData.id; const isEdit = userStore.formData.id;
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Dialog <Dialog
open={userStore.showEdit} open={userStore.showEdit}
@ -57,39 +69,30 @@ const FormModal = () => {
}}> }}>
<DialogTitle>{isEdit ? t('Edit') : t('Add')}</DialogTitle> <DialogTitle>{isEdit ? t('Edit') : t('Add')}</DialogTitle>
<DialogContent> <DialogContent>
<Form <form onSubmit={handleSubmit(onFinish)}>
form={form} <Controller name='username' control={control} defaultValue='' render={({ field }) => <TextField {...field} label='username' fullWidth />} />
onFinish={onFinish} <Controller
labelCol={{ name='description'
span: 4, control={control}
}} defaultValue=''
wrapperCol={{ render={({ field }) => <TextField {...field} label='description' multiline rows={4} fullWidth />}
span: 20, />
}}> <div>
<Form.Item name='id' hidden>
<Input />
</Form.Item>
<Form.Item name='username' label='username'>
<Input />
</Form.Item>
<Form.Item name='description' label='description'>
<Input.TextArea rows={4} />
</Form.Item>
<Form.Item label=' ' colon={false}>
<Button type='submit' variant='contained'> <Button type='submit' variant='contained'>
{t('Submit')} {t('Submit')}
</Button> </Button>
<Button className='ml-2' type='reset' onClick={onClose}> <Button className='ml-2' type='button' onClick={onClose}>
{t('Cancel')} {t('Cancel')}
</Button> </Button>
</Form.Item> </div>
</Form> </form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
}; };
export const List = () => { export const List = () => {
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = useModal();
const userStore = useUserStore( const userStore = useUserStore(
useShallow((state) => { useShallow((state) => {
return { return {
@ -104,8 +107,6 @@ export const List = () => {
}; };
}), }),
); );
const [codeEdit, setCodeEdit] = useState(false);
const [code, setCode] = useState('');
useEffect(() => { useEffect(() => {
userStore.getList(); userStore.getList();
}, []); }, []);
@ -132,9 +133,7 @@ export const List = () => {
className='flex text-sm gap flex-col w-[400px] max-h-[400px] bg-white p-4 rounded-lg' className='flex text-sm gap flex-col w-[400px] max-h-[400px] bg-white p-4 rounded-lg'
key={item.id} key={item.id}
onClick={() => { onClick={() => {
setCode(item.code); // userStore.setFormData(item);
userStore.setFormData(item);
setCodeEdit(true);
}}> }}>
<div className='px-4 cursor-pointer'> <div className='px-4 cursor-pointer'>
<div <div
@ -156,7 +155,6 @@ export const List = () => {
onClick={(e) => { onClick={(e) => {
userStore.setFormData(item); userStore.setFormData(item);
userStore.setShowEdit(true); userStore.setShowEdit(true);
setCodeEdit(false);
e.stopPropagation(); e.stopPropagation();
}}> }}>
<EditOutlined /> <EditOutlined />
@ -188,36 +186,6 @@ export const List = () => {
)} )}
</div> </div>
</div> </div>
<div className={clsx('bg-gray-100 border-l-gray-200 border-bg-slate-300 w-[600px] shark-0', !codeEdit && 'hidden', 'hidden')}>
<div className='bg-white p-2'>
<div className='mt-2 ml-2 flex gap-2'>
<Button
onClick={() => {
setCodeEdit(false);
userStore.setFormData({});
}}>
<LeftOutlined />
</Button>
<Button
onClick={() => {
console.log('save', userStore.formData);
userStore.updateData({ ...userStore.formData, code });
}}>
<SaveOutlined />
</Button>
</div>
</div>
<div className='h-[94%] p-2 rounded-2 shadow-xs'>
<Input.TextArea
value={code}
onChange={(value) => {
// setCode(value);
}}
className='h-full max-h-full scrollbar'
style={{ overflow: 'auto' }}
/>
</div>
</div>
</div> </div>
<FormModal /> <FormModal />
{contextHolder} {contextHolder}

View File

@ -32,5 +32,6 @@
}, },
"include": [ "include": [
"src", "src",
"packages/**/*"
] ]
} }