feat: 修复Container界面和File App 和 User App
User App 添加permission
This commit is contained in:
parent
837457a5f7
commit
27d9bdf54e
7
.cursorignore
Normal file
7
.cursorignore
Normal file
@ -0,0 +1,7 @@
|
||||
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
@ -21,6 +21,7 @@
|
||||
"@kevisual/codemirror": "^0.0.2",
|
||||
"@kevisual/container": "1.0.0",
|
||||
"@kevisual/query": "^0.0.8",
|
||||
"@kevisual/resources": "workspace:*",
|
||||
"@kevisual/system-ui": "^0.0.3",
|
||||
"@kevisual/ui": "^0.0.2",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
@ -45,7 +46,6 @@
|
||||
"react-router": "^7.3.0",
|
||||
"react-router-dom": "^7.3.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"@kevisual/resources": "workspace:*",
|
||||
"vite-plugin-tsconfig-paths": "^1.4.1",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
@ -67,6 +67,7 @@
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"lucide-react": "^0.482.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"postcss-import": "^16.1.0",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
|
@ -7,7 +7,18 @@ export const Button = (props: ButtonProps) => {
|
||||
export const IconButton = (props: ButtonProps) => {
|
||||
const { variant = 'contained', color = 'primary', sx, children, ...rest } = props;
|
||||
return (
|
||||
<MuiButton variant={variant} color={color} {...rest} sx={{ color: 'white', minWidth: '32px', padding: '4px', ...sx }}>
|
||||
<MuiButton variant={variant} color={color} {...rest} sx={{ color: 'white', minWidth: '32px', padding: '8px', ...sx }}>
|
||||
{children}
|
||||
</MuiButton>
|
||||
);
|
||||
};
|
||||
|
||||
export const IconButtonItem = (props: ButtonProps) => {
|
||||
const { variant = 'contained', size = 'small', color = 'primary', sx, children, ...rest } = props;
|
||||
return (
|
||||
<MuiButton {...props} >
|
||||
{/* <MuiButton variant={'contained'} size={size} color={color} {...rest} sx={{ color: 'white', ...sx }}> */}
|
||||
|
||||
{children}
|
||||
</MuiButton>
|
||||
);
|
||||
|
17
packages/components/src/card/CardBlank.tsx
Normal file
17
packages/components/src/card/CardBlank.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import clsx from 'clsx';
|
||||
import twMerge from 'tailwind-merge';
|
||||
|
||||
type CardBlankProps = {
|
||||
number?: number;
|
||||
className?: string;
|
||||
};
|
||||
export const CardBlank = (props: CardBlankProps) => {
|
||||
const { number = 4, className } = props;
|
||||
return (
|
||||
<>
|
||||
{new Array(number).fill(0).map((_, index) => {
|
||||
return <div key={index} className={clsx('w-[300px] shark-0', className)}></div>;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
@ -75,6 +75,12 @@ export const themeOptions: ThemeOptions = {
|
||||
'&:hover': {
|
||||
backgroundColor: amber[100],
|
||||
},
|
||||
'&.MuiButton-contained': {
|
||||
color: '#ffffff',
|
||||
':hover': {
|
||||
color: amber[500],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -132,6 +138,13 @@ export const themeOptions: ThemeOptions = {
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiIconButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: '#ffffff', // Set default font color to white
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@theme {
|
||||
--light-color-primary: oklch(0.72 0.11 178);
|
||||
--light-color-secondary: oklch(0.72 0.11 178);
|
||||
--color-primary: #ffc107;
|
||||
--color-secondary: #ffa000;
|
||||
--color-success: #28a745;
|
||||
--scrollbar-color: #ffc107; /* 滚动条颜色 */
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
@import 'tailwindcss';
|
||||
@import '@kevisual/center-components/theme/wind-theme.css';
|
||||
|
||||
@layer components {
|
||||
.test-loading {
|
||||
|
10
packages/resources/src/i-text/index.ts
Normal file
10
packages/resources/src/i-text/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export const iText = {
|
||||
share: {
|
||||
title: '共享设置',
|
||||
tips: `共享设置
|
||||
1. 设置公共可以直接访问
|
||||
2. 设置受保护需要登录后访问
|
||||
3. 设置私有只有自己可以访问。\n
|
||||
受保护可以设置密码,设置访问的用户名。切换共享状态后,需要重新设置密码和用户名。`,
|
||||
},
|
||||
};
|
3
packages/resources/src/index.ts
Normal file
3
packages/resources/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { KeyParse, keysTips } from './pages/file/modules/key-parse';
|
||||
export { iText } from './i-text/index.ts';
|
||||
export * from './pages/upload/app';
|
@ -1,16 +1,17 @@
|
||||
import ReactDatePicker from 'antd/es/date-picker';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { styled, useTheme } from '@mui/material';
|
||||
import 'antd/es/date-picker/style/index';
|
||||
interface DatePickerProps {
|
||||
value?: Date | null;
|
||||
onChange?: (date: Date | null) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const DatePicker = ({ value, onChange }: DatePickerProps) => {
|
||||
export const DatePickerCom = ({ value, onChange, className }: DatePickerProps) => {
|
||||
const theme = useTheme();
|
||||
const primaryColor = theme.palette.primary.main;
|
||||
return (
|
||||
<div>
|
||||
<div className={className}>
|
||||
<ReactDatePicker
|
||||
placement='topLeft'
|
||||
placeholder='请选择日期'
|
||||
@ -26,3 +27,12 @@ export const DatePicker = ({ value, onChange }: DatePickerProps) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const DatePicker = styled(DatePickerCom)(({ theme }) => ({
|
||||
'& .ant-picker-input': {
|
||||
color: 'var(--color-primary)',
|
||||
'& .ant-picker-suffix, .ant-picker-clear': {
|
||||
color: 'var(--color-primary)',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
@ -9,7 +9,8 @@ import { DatePicker } from './DatePicker';
|
||||
import { SelectPicker } from './SelectPicker';
|
||||
import dayjs from 'dayjs';
|
||||
import { DialogKey } from './DialogKey';
|
||||
|
||||
import { keysTips, KeyParse } from '../../modules/key-parse';
|
||||
import { KeyShareSelect, KeyTextField } from '../../modules/PermissionManager';
|
||||
export const setShareKeysOperate = (value: 'public' | 'protected' | 'private') => {
|
||||
const keys = ['password', 'usernames', 'expiration-time'];
|
||||
const deleteKeys = keys.map((item) => {
|
||||
@ -28,92 +29,7 @@ export const setShareKeysOperate = (value: 'public' | 'protected' | 'private') =
|
||||
}
|
||||
return deleteKeys;
|
||||
};
|
||||
export const keysTips = [
|
||||
{
|
||||
key: 'share',
|
||||
tips: `共享设置
|
||||
1. 设置公共可以直接访问
|
||||
2. 设置受保护需要登录后访问
|
||||
3. 设置私有只有自己可以访问。\n
|
||||
受保护可以设置密码,设置访问的用户名。切换共享状态后,需要重新设置密码和用户名。`,
|
||||
},
|
||||
{
|
||||
key: 'content-type',
|
||||
tips: `内容类型,设置文件的内容类型。默认不要修改。`,
|
||||
},
|
||||
{
|
||||
key: 'app-source',
|
||||
tips: `应用来源,上传方式。默认不要修改。`,
|
||||
},
|
||||
{
|
||||
key: 'cache-control',
|
||||
tips: `缓存控制,设置文件的缓存控制。默认不要修改。`,
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
tips: `密码,设置文件的密码。不设置默认是所有人都可以访问。`,
|
||||
},
|
||||
{
|
||||
key: 'usernames',
|
||||
tips: `用户名,设置文件的用户名。不设置默认是所有人都可以访问。`,
|
||||
parse: (value: string) => {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
return value.split(',');
|
||||
},
|
||||
stringify: (value: string[]) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
return value.join(',');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'expiration-time',
|
||||
tips: `过期时间,设置文件的过期时间。不设置默认是永久。`,
|
||||
parse: (value: Date) => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return dayjs(value);
|
||||
},
|
||||
stringify: (value?: dayjs.Dayjs) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
return value.toISOString();
|
||||
},
|
||||
},
|
||||
];
|
||||
export class KeyParse {
|
||||
static parse(metadata: Record<string, any>) {
|
||||
const keys = Object.keys(metadata);
|
||||
const newMetadata = {};
|
||||
keys.forEach((key) => {
|
||||
const tip = keysTips.find((item) => item.key === key);
|
||||
if (tip && tip.parse) {
|
||||
newMetadata[key] = tip.parse(metadata[key]);
|
||||
} else {
|
||||
newMetadata[key] = metadata[key];
|
||||
}
|
||||
});
|
||||
return newMetadata;
|
||||
}
|
||||
static stringify(metadata: Record<string, any>) {
|
||||
const keys = Object.keys(metadata);
|
||||
const newMetadata = {};
|
||||
keys.forEach((key) => {
|
||||
const tip = keysTips.find((item) => item.key === key);
|
||||
if (tip && tip.stringify) {
|
||||
newMetadata[key] = tip.stringify(metadata[key]);
|
||||
} else {
|
||||
newMetadata[key] = metadata[key];
|
||||
}
|
||||
});
|
||||
return newMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
export const useMetaOperate = ({
|
||||
onSave,
|
||||
metaStore,
|
||||
@ -270,7 +186,7 @@ export const MetaForm = () => {
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
<FormGroup>
|
||||
<form>
|
||||
{keys.map((key) => {
|
||||
let control: React.ReactNode | null = null;
|
||||
if (key === 'share') {
|
||||
@ -320,49 +236,10 @@ export const MetaForm = () => {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</FormGroup>
|
||||
</form>
|
||||
<DialogKey onAdd={addMetaKey} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const KeyTextField = ({ name, value, onChange }: { name: string; value: string; onChange?: (value: string) => void }) => {
|
||||
return (
|
||||
<TextField
|
||||
variant='outlined'
|
||||
size='small'
|
||||
name={name}
|
||||
defaultValue={value}
|
||||
// value={formData[key] || ''}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
sx={{
|
||||
width: '100%',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const KeyShareSelect = ({ name, value, onChange }: { name: string; value: string; onChange?: (value: string) => void }) => {
|
||||
return (
|
||||
<Select
|
||||
variant='outlined'
|
||||
size='small'
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
sx={{
|
||||
width: '100%',
|
||||
marginBottom: '16px',
|
||||
}}>
|
||||
<MenuItem value='public' title='公开'>
|
||||
公开
|
||||
</MenuItem>
|
||||
<MenuItem value='protected' title='受保护'>
|
||||
受保护
|
||||
</MenuItem>
|
||||
<MenuItem value='private' title='私有'>
|
||||
私有
|
||||
</MenuItem>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -5,14 +5,26 @@ import 'antd/es/select/style/index';
|
||||
interface SelectPickerProps {
|
||||
value: string[];
|
||||
onChange: (value: string[]) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SelectPickerCom = ({ value, onChange }: SelectPickerProps) => {
|
||||
return <Select style={{ width: '100%' }} showSearch={false} mode='tags' value={value} onChange={onChange} />;
|
||||
export const SelectPickerCom = ({ value, onChange, className }: SelectPickerProps) => {
|
||||
return <Select className={className} style={{ width: '100%' }} popupClassName='hidden' showSearch={false} mode='tags' value={value} onChange={onChange} />;
|
||||
};
|
||||
export const SelectPicker = styled(SelectPickerCom)(({ theme }) => ({
|
||||
|
||||
export const SelectPicker = styled(SelectPickerCom)({
|
||||
'& .ant-select-selector': {
|
||||
color: 'var(--primary-color)',
|
||||
'& .ant-select-selection-item': {
|
||||
backgroundColor: 'var(--color-primary) !important',
|
||||
color: 'white !important',
|
||||
},
|
||||
});
|
||||
'& svg': {
|
||||
color: 'white !important',
|
||||
},
|
||||
'& svg:hover': {
|
||||
color: '#ccc !important',
|
||||
},
|
||||
'& .ant-select-arrow': {
|
||||
// color: 'var(--color-primary) !important',
|
||||
display: 'none !important',
|
||||
},
|
||||
}));
|
||||
|
@ -8,6 +8,7 @@ import { PrefixRedirect } from './modules/PrefixRedirect';
|
||||
import { UploadButton } from '../upload';
|
||||
import { FileDrawer } from './draw/FileDrawer';
|
||||
import { useResourceFileStore } from '../store/resource-file';
|
||||
import { IconButtonItem } from '@kevisual/center-components/button/index.tsx';
|
||||
export const FileApp = () => {
|
||||
const { getList, prefix, setListType, listType } = useResourceStore();
|
||||
const { getStatFile, prefix: statPrefix, openDrawer } = useResourceFileStore();
|
||||
|
141
packages/resources/src/pages/file/modules/PermissionManager.tsx
Normal file
141
packages/resources/src/pages/file/modules/PermissionManager.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { KeyParse, getTips } from './key-parse';
|
||||
import { FormControlLabel, TextField, Select, MenuItem, FormGroup, Tooltip } from '@mui/material';
|
||||
import { DatePicker } from '../draw/modules/DatePicker';
|
||||
import { SelectPicker } from '../draw/modules/SelectPicker';
|
||||
import { HelpCircle } from 'lucide-react';
|
||||
export const KeyShareSelect = ({ name, value, onChange }: { name: string; value: string; onChange?: (value: string) => void }) => {
|
||||
return (
|
||||
<Select
|
||||
variant='outlined'
|
||||
size='small'
|
||||
name={name}
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
sx={{
|
||||
width: '100%',
|
||||
marginBottom: '16px',
|
||||
}}>
|
||||
<MenuItem value='public' title='公开'>
|
||||
公开
|
||||
</MenuItem>
|
||||
<MenuItem value='protected' title='受保护'>
|
||||
受保护
|
||||
</MenuItem>
|
||||
<MenuItem value='private' title='私有'>
|
||||
私有
|
||||
</MenuItem>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
export const KeyTextField = ({ name, value, onChange }: { name: string; value: string; onChange?: (value: string) => void }) => {
|
||||
return (
|
||||
<TextField
|
||||
variant='outlined'
|
||||
size='small'
|
||||
name={name}
|
||||
defaultValue={value}
|
||||
// value={formData[key] || ''}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
sx={{
|
||||
width: '100%',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type PermissionManagerProps = {
|
||||
value: Record<string, any>;
|
||||
onChange: (value: Record<string, any>) => void;
|
||||
};
|
||||
export const PermissionManager = ({ value, onChange }: PermissionManagerProps) => {
|
||||
const [formData, setFormData] = useState<any>(value);
|
||||
const [keys, setKeys] = useState<any>([]);
|
||||
useEffect(() => {
|
||||
const hasShare = value?.share && value?.share === 'protected';
|
||||
setFormData(KeyParse.parse(value || {}));
|
||||
if (hasShare) {
|
||||
setKeys(['password', 'usernames', 'expiration-time']);
|
||||
} else {
|
||||
setKeys([]);
|
||||
}
|
||||
}, [value]);
|
||||
const onChangeValue = (key: string, newValue: any) => {
|
||||
// setFormData({ ...formData, [key]: newValue });
|
||||
let newFormData = { ...formData, [key]: newValue };
|
||||
if (key === 'share') {
|
||||
if (newValue === 'protected') {
|
||||
newFormData = { ...newFormData, password: '', usernames: [], 'expiration-time': null };
|
||||
onChange(KeyParse.stringify(newFormData));
|
||||
setKeys(['password', 'usernames', 'expiration-time']);
|
||||
} else {
|
||||
delete newFormData.password;
|
||||
delete newFormData.usernames;
|
||||
delete newFormData['expiration-time'];
|
||||
onChange(KeyParse.stringify(newFormData));
|
||||
setKeys([]);
|
||||
}
|
||||
} else {
|
||||
onChange(KeyParse.stringify(newFormData));
|
||||
}
|
||||
};
|
||||
return (
|
||||
<form className='w-[400px] flex flex-col gap-2'>
|
||||
<FormControlLabel
|
||||
labelPlacement='top'
|
||||
control={<KeyShareSelect name='share' value={formData?.share} onChange={(value) => onChangeValue('share', value)} />}
|
||||
label={
|
||||
<div className='flex items-center gap-1'>
|
||||
Share
|
||||
<Tooltip title={getTips('share')}>
|
||||
<HelpCircle size={16} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
sx={{
|
||||
alignItems: 'flex-start',
|
||||
'& .MuiFormControlLabel-label': {
|
||||
textAlign: 'left',
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{keys.map((item: any) => {
|
||||
let control: React.ReactNode | null = null;
|
||||
if (item === 'expiration-time') {
|
||||
control = <DatePicker value={formData[item] || ''} onChange={(date) => onChangeValue(item, date)} />;
|
||||
} else if (item === 'usernames') {
|
||||
control = <SelectPicker value={formData[item] || []} onChange={(value) => onChangeValue(item, value)} />;
|
||||
} else {
|
||||
control = <KeyTextField name={item} value={formData[item] || ''} onChange={(value) => onChangeValue(item, value)} />;
|
||||
}
|
||||
const tips = getTips(item);
|
||||
return (
|
||||
<FormControlLabel
|
||||
labelPlacement='top'
|
||||
key={item}
|
||||
control={control}
|
||||
label={
|
||||
<div className='flex items-center gap-1'>
|
||||
{item}
|
||||
{tips && (
|
||||
<Tooltip title={tips}>
|
||||
<HelpCircle size={16} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
sx={{
|
||||
alignItems: 'flex-start',
|
||||
'& .MuiFormControlLabel-label': {
|
||||
textAlign: 'left',
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</form>
|
||||
);
|
||||
};
|
90
packages/resources/src/pages/file/modules/key-parse.ts
Normal file
90
packages/resources/src/pages/file/modules/key-parse.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import dayjs from 'dayjs';
|
||||
export const getTips = (key: string) => {
|
||||
return keysTips.find((item) => item.key === key)?.tips;
|
||||
};
|
||||
export const keysTips = [
|
||||
{
|
||||
key: 'share',
|
||||
tips: `共享设置
|
||||
1. 设置公共可以直接访问
|
||||
2. 设置受保护需要登录后访问
|
||||
3. 设置私有只有自己可以访问。\n
|
||||
受保护可以设置密码,设置访问的用户名。切换共享状态后,需要重新设置密码和用户名。 不设置,默认是只能自己访问。`,
|
||||
},
|
||||
{
|
||||
key: 'content-type',
|
||||
tips: `内容类型,设置文件的内容类型。默认不要修改。`,
|
||||
},
|
||||
{
|
||||
key: 'app-source',
|
||||
tips: `应用来源,上传方式。默认不要修改。`,
|
||||
},
|
||||
{
|
||||
key: 'cache-control',
|
||||
tips: `缓存控制,设置文件的缓存控制。默认不要修改。`,
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
tips: `密码,设置文件的密码。不设置默认是所有人都可以访问。`,
|
||||
},
|
||||
{
|
||||
key: 'usernames',
|
||||
tips: `用户名,设置文件的用户名。不设置默认是所有人都可以访问。`,
|
||||
parse: (value: string) => {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
return value.split(',');
|
||||
},
|
||||
stringify: (value: string[]) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
return value.join(',');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'expiration-time',
|
||||
tips: `过期时间,设置文件的过期时间。不设置默认是永久。`,
|
||||
parse: (value: Date) => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return dayjs(value);
|
||||
},
|
||||
stringify: (value?: dayjs.Dayjs) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
return value.toISOString();
|
||||
},
|
||||
},
|
||||
];
|
||||
export class KeyParse {
|
||||
static parse(metadata: Record<string, any>) {
|
||||
const keys = Object.keys(metadata);
|
||||
const newMetadata = {};
|
||||
keys.forEach((key) => {
|
||||
const tip = keysTips.find((item) => item.key === key);
|
||||
if (tip && tip.parse) {
|
||||
newMetadata[key] = tip.parse(metadata[key]);
|
||||
} else {
|
||||
newMetadata[key] = metadata[key];
|
||||
}
|
||||
});
|
||||
return newMetadata;
|
||||
}
|
||||
static stringify(metadata: Record<string, any>) {
|
||||
const keys = Object.keys(metadata);
|
||||
const newMetadata = {};
|
||||
keys.forEach((key) => {
|
||||
const tip = keysTips.find((item) => item.key === key);
|
||||
if (tip && tip.stringify) {
|
||||
newMetadata[key] = tip.stringify(metadata[key]);
|
||||
} else {
|
||||
newMetadata[key] = metadata[key];
|
||||
}
|
||||
});
|
||||
return newMetadata;
|
||||
}
|
||||
}
|
5
packages/resources/src/pages/upload/app.ts
Normal file
5
packages/resources/src/pages/upload/app.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './tools/to-file';
|
||||
|
||||
export * from './utils/upload';
|
||||
|
||||
export * from './utils/upload-chunk';
|
1
packages/resources/src/pages/upload/tools/test-pnt.ts
Normal file
1
packages/resources/src/pages/upload/tools/test-pnt.ts
Normal file
File diff suppressed because one or more lines are too long
93
packages/resources/src/pages/upload/tools/to-file.ts
Normal file
93
packages/resources/src/pages/upload/tools/to-file.ts
Normal file
@ -0,0 +1,93 @@
|
||||
const getFileExtension = (filename: string) => {
|
||||
return filename.split('.').pop();
|
||||
};
|
||||
const getFileType = (extension: string) => {
|
||||
switch (extension) {
|
||||
case 'js':
|
||||
return 'text/javascript';
|
||||
case 'css':
|
||||
return 'text/css';
|
||||
case 'html':
|
||||
return 'text/html';
|
||||
case 'json':
|
||||
return 'application/json';
|
||||
case 'png':
|
||||
return 'image/png';
|
||||
case 'jpg':
|
||||
return 'image/jpeg';
|
||||
case 'jpeg':
|
||||
return 'image/jpeg';
|
||||
case 'gif':
|
||||
return 'image/gif';
|
||||
case 'svg':
|
||||
return 'image/svg+xml';
|
||||
case 'ico':
|
||||
return 'image/x-icon';
|
||||
case 'webp':
|
||||
return 'image/webp';
|
||||
case 'gif':
|
||||
return 'image/gif';
|
||||
case 'ico':
|
||||
return 'image/x-icon';
|
||||
default:
|
||||
return 'text/plain';
|
||||
}
|
||||
};
|
||||
const checkIsBase64 = (content: string) => {
|
||||
return content.startsWith('data:');
|
||||
};
|
||||
export const getDirectoryAndName = (filename: string) => {
|
||||
if (!filename) {
|
||||
return null;
|
||||
}
|
||||
if (filename.startsWith('.')) {
|
||||
return null;
|
||||
} else {
|
||||
filename = filename.replace(/^\/+/, ''); // Remove all leading slashes
|
||||
}
|
||||
const hasDirectory = filename.includes('/');
|
||||
if (!hasDirectory) {
|
||||
return { directory: '', name: filename };
|
||||
}
|
||||
const parts = filename.split('/');
|
||||
const name = parts.pop()!; // Get the last part as the file name
|
||||
const directory = parts.join('/'); // Join the remaining parts as the directory
|
||||
return { directory, name };
|
||||
};
|
||||
/**
|
||||
* 把字符串转为文件流,并返回文件流,根据filename的扩展名,自动设置文件类型.
|
||||
* 当不是文本类型,自动需要把base64的字符串转为blob
|
||||
* @param content 字符串
|
||||
* @param filename 文件名
|
||||
* @returns 文件流
|
||||
*/
|
||||
export const toFile = (content: string, filename: string) => {
|
||||
// 如果文件名是 a/d/a.js 格式的,则需要把d作为目录,a.js作为文件名
|
||||
const directoryAndName = getDirectoryAndName(filename);
|
||||
if (!directoryAndName) {
|
||||
throw new Error('Invalid filename');
|
||||
}
|
||||
const { name } = directoryAndName;
|
||||
const extension = getFileExtension(name);
|
||||
if (!extension) {
|
||||
throw new Error('Invalid filename');
|
||||
}
|
||||
const isBase64 = checkIsBase64(content);
|
||||
const type = getFileType(extension);
|
||||
|
||||
if (isBase64) {
|
||||
// Decode base64 string
|
||||
const base64Data = content.split(',')[1]; // Remove the data URL prefix
|
||||
const byteCharacters = atob(base64Data);
|
||||
const byteNumbers = new Array(byteCharacters.length);
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||
}
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
const blob = new Blob([byteArray], { type });
|
||||
return new File([blob], filename, { type });
|
||||
} else {
|
||||
const blob = new Blob([content], { type });
|
||||
return new File([blob], filename, { type });
|
||||
}
|
||||
};
|
@ -12,7 +12,7 @@ type ConvertOpts = {
|
||||
};
|
||||
|
||||
export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
|
||||
const { directory } = opts;
|
||||
const { directory, appKey, version, username } = opts;
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
@ -66,6 +66,13 @@ export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
|
||||
if (directory) {
|
||||
formData.append('directory', directory);
|
||||
}
|
||||
if (appKey && version) {
|
||||
formData.append('appKey', appKey);
|
||||
formData.append('version', version);
|
||||
}
|
||||
if (username) {
|
||||
formData.append('username', username);
|
||||
}
|
||||
try {
|
||||
const res = await fetch('/api/s1/resources/upload/chunk?taskId=' + taskId, {
|
||||
method: 'POST',
|
||||
|
@ -11,7 +11,7 @@ type ConvertOpts = {
|
||||
directory?: string;
|
||||
};
|
||||
export const uploadFiles = async (files: File[], opts: ConvertOpts) => {
|
||||
const { directory } = opts;
|
||||
const { directory, appKey, version, username } = opts;
|
||||
return new Promise((resolve, reject) => {
|
||||
const formData = new FormData();
|
||||
const webkitRelativePath = files[0]?.webkitRelativePath;
|
||||
@ -30,9 +30,13 @@ export const uploadFiles = async (files: File[], opts: ConvertOpts) => {
|
||||
if (directory) {
|
||||
formData.append('directory', directory);
|
||||
}
|
||||
console.log('formData', formData, files);
|
||||
resolve(null);
|
||||
return;
|
||||
if (appKey && version) {
|
||||
formData.append('appKey', appKey);
|
||||
formData.append('version', version);
|
||||
}
|
||||
if (username) {
|
||||
formData.append('username', username);
|
||||
}
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
toastLogin();
|
||||
|
19
packages/resources/src/style.css
Normal file
19
packages/resources/src/style.css
Normal file
@ -0,0 +1,19 @@
|
||||
.scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--scrollbar-color) #fff;
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: var(--scrollbar-color);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-scrollbar-track {
|
||||
background: #fff;
|
||||
}
|
||||
|
925
pnpm-lock.yaml
generated
925
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -31,10 +31,10 @@ const AntProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
colorPrimaryActive: primaryColor,
|
||||
borderRadius: 4,
|
||||
colorBorder: primaryColor,
|
||||
// colorText: primaryColor,
|
||||
colorIcon: primaryColor,
|
||||
colorIconHover: secondaryColor,
|
||||
colorInfoHover: secondaryColor,
|
||||
zIndexPopupBase: 2000,
|
||||
},
|
||||
components: {
|
||||
DatePicker: {
|
||||
@ -42,6 +42,9 @@ const AntProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
colorPrimaryHover: secondaryColor,
|
||||
colorPrimaryActive: primaryColor,
|
||||
},
|
||||
Tooltip: {
|
||||
zIndexPopupBase: 2000,
|
||||
}
|
||||
},
|
||||
}}>
|
||||
{children}
|
||||
@ -50,7 +53,6 @@ const AntProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
};
|
||||
export const App = () => {
|
||||
return (
|
||||
<div>
|
||||
<CustomThemeProvider>
|
||||
<AntProvider>
|
||||
<div className='w-full h-full'>
|
||||
@ -73,6 +75,5 @@ export const App = () => {
|
||||
<div id='for-modal'></div>
|
||||
<ToastContainer />
|
||||
</CustomThemeProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
1
src/components/card/index.tsx
Normal file
1
src/components/card/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export * from './CardBlank';
|
@ -1,11 +1,8 @@
|
||||
@import 'tailwindcss';
|
||||
@import './assets/styles.css';
|
||||
@import './index.css';
|
||||
@import '@kevisual/center-components/theme/wind-theme.css';
|
||||
|
||||
@theme {
|
||||
--color-primary: white;
|
||||
--color-secondary: #14171a;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
@ -51,7 +48,7 @@ h3 {
|
||||
}
|
||||
|
||||
@utility layout-menu {
|
||||
@apply bg-gray-900 p-2 text-white flex justify-between h-12;
|
||||
@apply bg-secondary p-2 text-white flex justify-between h-12 ;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
@utility no-drag {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useLayoutStore } from './store';
|
||||
import clsx from 'clsx';
|
||||
import { Button } from 'antd';
|
||||
import { Button } from '@mui/material';
|
||||
import { message } from '@/modules/message';
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
@ -16,6 +16,7 @@ import {
|
||||
SmileOutlined,
|
||||
SwitcherOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { X } from 'lucide-react';
|
||||
import { useNewNavigate } from '../navicate';
|
||||
const meun = [
|
||||
{
|
||||
@ -58,11 +59,13 @@ export const LayoutMenu = () => {
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}></div>
|
||||
<div className='w-[300px] h-full absolute top-0 left-0 bg-white'>
|
||||
<div className='w-[300px] h-full absolute top-0 left-0 bg-amber-900 text-primary'>
|
||||
<div className='flex justify-between p-6 mt-4 font-bold items-center'>
|
||||
Envision Center
|
||||
<div>
|
||||
<Button icon={<CloseOutlined />} onClick={() => setOpen(false)}></Button>
|
||||
<Button onClick={() => setOpen(false)}>
|
||||
<X />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-3 font-medium'>
|
||||
@ -70,7 +73,7 @@ export const LayoutMenu = () => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className='flex items-center p-4 gap-3 cursor-pointer hover:bg-slate-200 rounded-md'
|
||||
className='flex items-center p-4 gap-3 cursor-pointer hover:bg-secondary hover:text-white rounded-md'
|
||||
onClick={() => {
|
||||
if (item.link) navigate(`${item.link}`);
|
||||
else {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { MenuOutlined, SwapOutlined } from '@ant-design/icons';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { LayoutMenu } from './Menu';
|
||||
import { useLayoutStore, usePlatformStore } from './store';
|
||||
@ -9,7 +9,7 @@ import { LayoutUser } from './LayoutUser';
|
||||
import PandaPNG from '@/assets/panda.png';
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { IconButton as Button } from '@mui/material';
|
||||
type LayoutMainProps = {
|
||||
title?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
@ -58,19 +58,21 @@ export const LayoutMain = (props: LayoutMainProps) => {
|
||||
className={clsx('mr-4 cursor-pointer no-drag', isMac && 'ml-16')}
|
||||
onClick={() => {
|
||||
menuStore.setOpen(true);
|
||||
}}
|
||||
icon={<MenuOutlined />}></Button>
|
||||
<div className='flex grow justify-between'>
|
||||
}}>
|
||||
<MenuOutlined />
|
||||
</Button>
|
||||
<div className='flex grow justify-between pl-4 items-center'>
|
||||
{props.title}
|
||||
<div className='mr-4 flex gap-4 items-center no-drag'>
|
||||
{menuStore.me?.type === 'org' && (
|
||||
<div>
|
||||
<Tooltip title='Switch To User'>
|
||||
<Button
|
||||
icon={<SwapOutlined />}
|
||||
onClick={() => {
|
||||
menuStore.switchOrg('', 'user');
|
||||
}}></Button>
|
||||
}}>
|
||||
<SwapOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
@ -189,11 +189,7 @@ export const AppVersionList = () => {
|
||||
icon={<LinkOutlined />}
|
||||
onClick={() => {
|
||||
if (isRunning) {
|
||||
let baseUri = 'https://kevisual.xiongxiao.me';
|
||||
// if (DEV_SERVER) {
|
||||
// baseUri = 'http://localhost:3005';
|
||||
// }
|
||||
const link = new URL(`/test/${item.id}`, baseUri);
|
||||
const link = new URL(`/test/${item.id}`, location.origin);
|
||||
window.open(link.toString(), '_blank');
|
||||
} else {
|
||||
message.error('The app is not running');
|
||||
|
@ -1,13 +1,26 @@
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useUserAppStore } from '../store';
|
||||
import { useEffect } from 'react';
|
||||
import { Button, Form, Input, Space, Modal, Select, Tooltip, Switch } from 'antd';
|
||||
import { CodeOutlined, DeleteOutlined, EditOutlined, LinkOutlined, PlusOutlined, UnorderedListOutlined } from '@ant-design/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Form, Input, Modal, Select, Switch } from 'antd';
|
||||
import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
|
||||
import EditOutlined from '@ant-design/icons/EditOutlined';
|
||||
import LinkOutlined from '@ant-design/icons/LinkOutlined';
|
||||
import PlusOutlined from '@ant-design/icons/PlusOutlined';
|
||||
import UnorderedListOutlined from '@ant-design/icons/UnorderedListOutlined';
|
||||
import CodeOutlined from '@ant-design/icons/CodeOutlined';
|
||||
import ShareAltOutlined from '@ant-design/icons/ShareAltOutlined';
|
||||
|
||||
import { isObjectNull } from '@/utils/is-null';
|
||||
import { useNewNavigate } from '@/modules';
|
||||
import { DialogActions, Tooltip } from '@mui/material';
|
||||
import { marked } from 'marked';
|
||||
import clsx from 'clsx';
|
||||
import { IconButton } from '@kevisual/center-components/button/index.tsx';
|
||||
import { iText } from '@kevisual/resources/index.ts';
|
||||
import { PermissionManager } from '@kevisual/resources/pages/file/modules/PermissionManager.tsx';
|
||||
import { Button } from '@mui/material';
|
||||
import { message } from '@/modules/message';
|
||||
import { Dialog, DialogContent, DialogTitle, ButtonGroup } from '@mui/material';
|
||||
const FormModal = () => {
|
||||
const [form] = Form.useForm();
|
||||
const containerStore = useUserAppStore(
|
||||
@ -40,17 +53,23 @@ const FormModal = () => {
|
||||
};
|
||||
const isEdit = containerStore.formData.id;
|
||||
return (
|
||||
<Modal
|
||||
<Dialog
|
||||
title={isEdit ? 'Edit' : 'Add'}
|
||||
open={containerStore.showEdit}
|
||||
onClose={() => containerStore.setShowEdit(false)}
|
||||
destroyOnClose
|
||||
footer={false}
|
||||
width={800}
|
||||
onCancel={onClose}>
|
||||
sx={{
|
||||
'& .MuiDialog-paper': {
|
||||
width: '800px',
|
||||
},
|
||||
}}>
|
||||
<DialogTitle>{isEdit ? 'Edit' : 'Add'}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
initialValues={{
|
||||
proxy: true,
|
||||
}}
|
||||
labelCol={{
|
||||
span: 4,
|
||||
}}
|
||||
@ -63,7 +82,7 @@ const FormModal = () => {
|
||||
<Form.Item name='title' label='title'>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name='domain' label='domain'>
|
||||
<Form.Item name='domain' label='domain' tooltip='域名自定义绑定'>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name='key' label='key'>
|
||||
@ -72,7 +91,7 @@ const FormModal = () => {
|
||||
<Form.Item name='description' label='description'>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item name='proxy' label='proxy' tooltip='静态网站设置,如果是静态网站,不需要重定向到index.html的页面。'>
|
||||
<Form.Item name='proxy' label='proxy' tooltip='设置为true,则后端直接代理请求minio服务进行转发,不会缓存下载到服务器。'>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name='status' label='status'>
|
||||
@ -84,18 +103,80 @@ const FormModal = () => {
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label=' ' colon={false}>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
Submit
|
||||
</Button>
|
||||
<Button className='ml-2' htmlType='reset' onClick={onClose}>
|
||||
Cancel
|
||||
<Button type='submit'>提交</Button>
|
||||
<Button className='ml-2' type='reset' onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
const ShareModal = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [permission, setPermission] = useState<any>(null);
|
||||
const containerStore = useUserAppStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
showEdit: state.showShareEdit,
|
||||
setShowEdit: state.setShowShareEdit,
|
||||
formData: state.formData,
|
||||
updateData: state.updateData,
|
||||
};
|
||||
}),
|
||||
);
|
||||
useEffect(() => {
|
||||
const open = containerStore.showEdit;
|
||||
if (open) {
|
||||
// form.setFieldsValue(containerStore.formData);
|
||||
const permission = containerStore.formData?.data?.permission || {};
|
||||
if (isObjectNull(permission)) {
|
||||
setPermission(null);
|
||||
} else {
|
||||
setPermission(permission);
|
||||
}
|
||||
}
|
||||
}, [containerStore.showEdit]);
|
||||
const onFinish = async () => {
|
||||
const values = {
|
||||
...containerStore.formData,
|
||||
data: {
|
||||
permission,
|
||||
},
|
||||
};
|
||||
containerStore.updateData(values);
|
||||
};
|
||||
const onClose = () => {
|
||||
containerStore.setShowEdit(false);
|
||||
form.resetFields();
|
||||
};
|
||||
return (
|
||||
<Dialog
|
||||
open={containerStore.showEdit}
|
||||
onClose={() => {
|
||||
containerStore.setShowEdit(false);
|
||||
}}>
|
||||
<DialogTitle>{iText.share.title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<PermissionManager
|
||||
value={permission}
|
||||
onChange={(value) => {
|
||||
setPermission(value);
|
||||
}}
|
||||
/>
|
||||
<DialogActions>
|
||||
<Button type='submit' variant='contained' onClick={onFinish}>
|
||||
提交
|
||||
</Button>
|
||||
<Button className='ml-2' type='reset' onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const List = () => {
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
|
||||
@ -108,6 +189,7 @@ export const List = () => {
|
||||
formData: state.formData,
|
||||
setFormData: state.setFormData,
|
||||
deleteData: state.deleteData,
|
||||
setShowShareEdit: state.setShowShareEdit,
|
||||
};
|
||||
}),
|
||||
);
|
||||
@ -116,19 +198,29 @@ export const List = () => {
|
||||
userAppStore.getList();
|
||||
}, []);
|
||||
return (
|
||||
<div className='w-full h-full flex bg-slate-100'>
|
||||
<div className='w-full h-full flex bg-slate-100 text-primary'>
|
||||
<div className='p-2 h-full bg-white flex flex-col gap-2'>
|
||||
<Button
|
||||
<Tooltip title='添加一个应用'>
|
||||
<IconButton
|
||||
sx={{
|
||||
padding: '8px',
|
||||
}}
|
||||
onClick={() => {
|
||||
userAppStore.setShowEdit(true);
|
||||
}}>
|
||||
<PlusOutlined />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title='转到Container管理界面'>
|
||||
<IconButton
|
||||
sx={{
|
||||
padding: '8px',
|
||||
}}
|
||||
icon={<PlusOutlined />}></Button>
|
||||
<Tooltip title='To Container'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navicate('/container');
|
||||
}}
|
||||
icon={<CodeOutlined />}></Button>
|
||||
}}>
|
||||
<CodeOutlined />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='grow'>
|
||||
@ -140,40 +232,55 @@ export const List = () => {
|
||||
const hasDescription = !!item.description;
|
||||
const content = marked.parse(item.description);
|
||||
return (
|
||||
<div className='card border-t border-gray-200 w-[300px] ' key={item.id}>
|
||||
<div className='card-title flex justify-between' onClick={() => {}}>
|
||||
<div className='card w-[300px] ' key={item.id}>
|
||||
<div className='card-title flex font-bold justify-between' onClick={() => {}}>
|
||||
{item.title}
|
||||
<div>
|
||||
<Tooltip title={isRunning ? '网页可正常访问' : '网页被关闭'}>
|
||||
<div className={`${isRunning ? 'bg-green-500' : 'bg-red-500'} w-4 h-4 rounded-full`}></div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className='text-xs'>domain: {item.domain}</div>
|
||||
{item.domain && <div className='text-xs'>访问域名: {item.domain}</div>}
|
||||
<div className='text-xs'>version: {item.version}</div>
|
||||
<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>
|
||||
</div>
|
||||
<div className='mt-2'>
|
||||
<Space.Compact>
|
||||
<ButtonGroup
|
||||
variant='contained'
|
||||
color='primary'
|
||||
sx={{ color: 'white', '& .MuiButton-root': { color: 'white', minWidth: '32px', width: '32px', height: '32px', padding: '6px' } }}>
|
||||
<Tooltip title={'Edit'}>
|
||||
<Button
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => {
|
||||
userAppStore.setFormData(item);
|
||||
userAppStore.setShowEdit(true);
|
||||
}}></Button>
|
||||
}}>
|
||||
<EditOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title={'App Version List'}>
|
||||
<Button
|
||||
icon={<UnorderedListOutlined />}
|
||||
onClick={() => {
|
||||
navicate(`/app/${item.key}/version/list`);
|
||||
}}></Button>
|
||||
}}>
|
||||
<UnorderedListOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title={iText.share.tips}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
userAppStore.setFormData(item);
|
||||
userAppStore.setShowShareEdit(true);
|
||||
}}>
|
||||
<ShareAltOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title={'To App'}>
|
||||
<Button
|
||||
icon={<LinkOutlined />}
|
||||
onClick={() => {
|
||||
if (isRunning) {
|
||||
let baseUri = location.origin;
|
||||
@ -192,11 +299,12 @@ export const List = () => {
|
||||
} else {
|
||||
message.error('The app is not running');
|
||||
}
|
||||
}}></Button>
|
||||
}}>
|
||||
<LinkOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title={'Delete'}>
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={(e) => {
|
||||
console.log('delete', item);
|
||||
modal.confirm({
|
||||
@ -207,9 +315,11 @@ export const List = () => {
|
||||
},
|
||||
});
|
||||
e.stopPropagation();
|
||||
}}></Button>
|
||||
}}>
|
||||
<DeleteOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Space.Compact>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -220,6 +330,7 @@ export const List = () => {
|
||||
</div>
|
||||
{contextHolder}
|
||||
<FormModal />
|
||||
<ShareModal />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -12,6 +12,8 @@ type UserAppStore = {
|
||||
getList: () => Promise<void>;
|
||||
updateData: (data: any) => Promise<void>;
|
||||
deleteData: (id: string) => Promise<void>;
|
||||
showShareEdit: boolean;
|
||||
setShowShareEdit: (showShareEdit: boolean) => void;
|
||||
};
|
||||
export const useUserAppStore = create<UserAppStore>((set, get) => {
|
||||
return {
|
||||
@ -45,7 +47,7 @@ export const useUserAppStore = create<UserAppStore>((set, get) => {
|
||||
});
|
||||
if (res.code === 200) {
|
||||
message.success('Success');
|
||||
set({ showEdit: false, formData: res.data });
|
||||
set({ showEdit: false, showShareEdit: false, formData: res.data });
|
||||
getList();
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
@ -65,5 +67,7 @@ export const useUserAppStore = create<UserAppStore>((set, get) => {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
showShareEdit: false,
|
||||
setShowShareEdit: (showShareEdit) => set({ showShareEdit }),
|
||||
};
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Button, Input, Modal, Select, Space, Switch, Table, Tooltip } from 'antd';
|
||||
import { Input, Modal, Select, Space, Switch } from 'antd';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { TextArea } from '../components/TextArea';
|
||||
import { useContainerStore } from '../store';
|
||||
@ -7,6 +7,9 @@ import { Form } from 'antd';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { useNewNavigate } from '@/modules';
|
||||
import { message } from '@/modules/message';
|
||||
import { Dialog, DialogTitle, DialogContent, Tooltip, Button, ButtonGroup } from '@mui/material';
|
||||
import { IconButton } from '@kevisual/center-components/button/index.tsx';
|
||||
import { getDirectoryAndName, toFile, uploadFileChunked } from '@kevisual/resources/index.ts';
|
||||
import {
|
||||
EditOutlined,
|
||||
SettingOutlined,
|
||||
@ -54,14 +57,9 @@ const FormModal = () => {
|
||||
};
|
||||
const isEdit = containerStore.formData.id;
|
||||
return (
|
||||
<Modal
|
||||
title={isEdit ? 'Edit' : 'Add'}
|
||||
open={containerStore.showEdit}
|
||||
onClose={() => containerStore.setShowEdit(false)}
|
||||
destroyOnClose
|
||||
footer={false}
|
||||
width={800}
|
||||
onCancel={onClose}>
|
||||
<Dialog open={containerStore.showEdit} onClose={() => containerStore.setShowEdit(false)}>
|
||||
<DialogTitle>{isEdit ? 'Edit' : 'Add'}</DialogTitle>
|
||||
<DialogContent sx={{ padding: '20px', minWidth: '600px' }}>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
@ -87,15 +85,16 @@ const FormModal = () => {
|
||||
<TextArea />
|
||||
</Form.Item>
|
||||
<Form.Item label=' ' colon={false}>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
Submit
|
||||
<Button variant='contained' type='submit'>
|
||||
提交
|
||||
</Button>
|
||||
<Button className='ml-2' htmlType='reset' onClick={onClose}>
|
||||
Cancel
|
||||
<Button className='ml-2' onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
const PublishFormModal = () => {
|
||||
@ -106,7 +105,6 @@ const PublishFormModal = () => {
|
||||
showEdit: state.showPublish,
|
||||
setShowEdit: state.setShowPublish,
|
||||
formData: state.formData,
|
||||
publishData: state.publishData,
|
||||
updateData: state.updateData,
|
||||
};
|
||||
}),
|
||||
@ -122,8 +120,41 @@ const PublishFormModal = () => {
|
||||
}
|
||||
}
|
||||
}, [containerStore.showEdit]);
|
||||
const onFinish = async (values: any) => {
|
||||
containerStore.publishData(values);
|
||||
const onFinish = async () => {
|
||||
const values = form.getFieldsValue();
|
||||
const success = await containerStore.updateData(values, { closePublish: false });
|
||||
if (success) {
|
||||
const formData = containerStore.formData;
|
||||
const code = formData.code;
|
||||
const fileName = values['publish']?.['fileName'];
|
||||
let directoryAndName: ReturnType<typeof getDirectoryAndName> | null = null;
|
||||
try {
|
||||
directoryAndName = getDirectoryAndName(fileName);
|
||||
if (!directoryAndName) {
|
||||
message.error('Invalid filename');
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('Invalid filename');
|
||||
return;
|
||||
}
|
||||
|
||||
const key = values['publish']['key'];
|
||||
const version = values['publish']['version'];
|
||||
const file = toFile(code, directoryAndName.name);
|
||||
console.log('key', key, version, directoryAndName.directory, directoryAndName.name);
|
||||
const res = await uploadFileChunked(file, {
|
||||
appKey: key,
|
||||
version,
|
||||
directory: directoryAndName.directory,
|
||||
});
|
||||
// @ts-ignore
|
||||
if (res.code === 200) {
|
||||
message.success('upload success');
|
||||
} else {
|
||||
message.error('upload failed');
|
||||
}
|
||||
}
|
||||
};
|
||||
const onUpdate = async () => {
|
||||
const values = form.getFieldsValue();
|
||||
@ -133,16 +164,10 @@ const PublishFormModal = () => {
|
||||
containerStore.setShowEdit(false);
|
||||
form.resetFields();
|
||||
};
|
||||
const isEdit = containerStore.formData.id;
|
||||
return (
|
||||
<Modal
|
||||
title={'Publish'}
|
||||
open={containerStore.showEdit}
|
||||
onClose={() => containerStore.setShowEdit(false)}
|
||||
destroyOnClose
|
||||
footer={false}
|
||||
width={800}
|
||||
onCancel={onClose}>
|
||||
<Dialog open={containerStore.showEdit} onClose={() => containerStore.setShowEdit(false)}>
|
||||
<DialogTitle>Publish</DialogTitle>
|
||||
<DialogContent sx={{ padding: '20px', minWidth: '600px' }}>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
@ -155,35 +180,37 @@ const PublishFormModal = () => {
|
||||
<Form.Item name='id' hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name={['publish', 'title']} label='title'>
|
||||
<Form.Item name={['publish', 'key']} label='App key' required>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name={['publish', 'description']} label='description'>
|
||||
<Form.Item name={['publish', 'version']} label='App Version' required>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name={['publish', 'fileName']} label='Filename' tooltip='可以是文件夹格式,比如(directory/a.name)' required>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name={['publish', 'description']} label='Description'>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item name={['publish', 'version']} label='version' required>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name={['publish', 'key']} label='key' required>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name={['publish', 'fileName']} label='file name' required>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name={['publish', 'saveHTML']} label='save html'>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label=' ' colon={false}>
|
||||
<div className='flex gap-3'>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
Save And Publish
|
||||
<Tooltip
|
||||
placement='top'
|
||||
title='根据文件名和code的字符串的内容,自动生成文件。并保存。如果是其他文件类型,转成base64上传。比如图片以类似data:image/jpeg;开头'>
|
||||
<Button variant='contained' color='primary' type='submit'>
|
||||
上传
|
||||
</Button>
|
||||
<Button onClick={onUpdate}> Save</Button>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Button variant='contained' onClick={onUpdate}>
|
||||
保存
|
||||
</Button>
|
||||
<Button onClick={onClose}>取消</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
export const ContainerList = () => {
|
||||
@ -216,11 +243,14 @@ export const ContainerList = () => {
|
||||
containerStore.setFormData({});
|
||||
containerStore.setShowEdit(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='w-full h-full flex '>
|
||||
<div className='p-2 flex flex-col gap-2'>
|
||||
<Tooltip title='add'>
|
||||
<Button onClick={onAdd} icon={<PlusOutlined />}></Button>
|
||||
<Tooltip title='添加'>
|
||||
<IconButton variant='contained' onClick={onAdd} sx={{ padding: '8px' }}>
|
||||
<PlusOutlined />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='flex grow overflow-hidden h-full'>
|
||||
@ -255,41 +285,48 @@ export const ContainerList = () => {
|
||||
<TextArea className='max-h-[240px] scrollbar' value={item.code} readonly />
|
||||
</div>
|
||||
<div className='flex mt-2 '>
|
||||
<Space.Compact>
|
||||
<ButtonGroup variant='contained' color='primary'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
// containerStore.publishData(item);
|
||||
}}
|
||||
icon={<SettingOutlined />}></Button>
|
||||
}}>
|
||||
<SettingOutlined />
|
||||
</Button>
|
||||
<Tooltip title='编辑'>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
containerStore.setFormData(item);
|
||||
containerStore.setShowEdit(true);
|
||||
setCodeEdit(false);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
icon={<EditOutlined />}></Button>
|
||||
<Tooltip title='预览'>
|
||||
}}>
|
||||
<EditOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{/* <Tooltip title='预览'>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
// navicate('/container/preview/' + item.id);
|
||||
window.open('/container/preview/' + item.id);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
icon={<LinkOutlined />}></Button>
|
||||
</Tooltip>
|
||||
<Tooltip title='publish'>
|
||||
}}>
|
||||
<LinkOutlined />
|
||||
</Button>
|
||||
</Tooltip> */}
|
||||
<Tooltip title='发布到 user app当中'>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
// containerStore.publishData(item);
|
||||
containerStore.setFormData(item);
|
||||
containerStore.setShowPublish(true);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
icon={<CloudUploadOutlined />}></Button>
|
||||
}}>
|
||||
<CloudUploadOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title='删除'>
|
||||
<Button
|
||||
// color='error'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
modal.confirm({
|
||||
@ -300,10 +337,11 @@ export const ContainerList = () => {
|
||||
},
|
||||
});
|
||||
e.stopPropagation();
|
||||
}}
|
||||
icon={<DeleteOutlined />}></Button>
|
||||
}}>
|
||||
<DeleteOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Space.Compact>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
@ -325,24 +363,17 @@ export const ContainerList = () => {
|
||||
onClick={() => {
|
||||
setCodeEdit(false);
|
||||
containerStore.setFormData({});
|
||||
}}
|
||||
icon={<LeftOutlined />}></Button>
|
||||
}}>
|
||||
<LeftOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title='保存'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
console.log('save', containerStore.formData);
|
||||
containerStore.updateData({ ...containerStore.formData, code });
|
||||
}}
|
||||
icon={<SaveOutlined />}></Button>
|
||||
</Tooltip>
|
||||
<Tooltip title='预览'>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
// navicate('/container/preview/' + item.id);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
icon={<LinkOutlined />}></Button>
|
||||
}}>
|
||||
<SaveOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -12,9 +12,8 @@ type ContainerStore = {
|
||||
setLoading: (loading: boolean) => void;
|
||||
list: any[];
|
||||
getList: () => Promise<void>;
|
||||
updateData: (data: any) => Promise<void>;
|
||||
updateData: (data: any, opts?: { closePublish?: boolean; closeEdit?: boolean }) => Promise<Boolean>;
|
||||
deleteData: (id: string) => Promise<void>;
|
||||
publishData: (data: any) => Promise<void>;
|
||||
};
|
||||
export const useContainerStore = create<ContainerStore>((set, get) => {
|
||||
return {
|
||||
@ -40,8 +39,10 @@ export const useContainerStore = create<ContainerStore>((set, get) => {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
updateData: async (data) => {
|
||||
updateData: async (data, opts) => {
|
||||
const { getList } = get();
|
||||
const closePublish = opts?.closePublish ?? true;
|
||||
const closeEdit = opts?.closeEdit ?? true;
|
||||
const res = await query.post({
|
||||
path: 'container',
|
||||
key: 'update',
|
||||
@ -49,11 +50,18 @@ export const useContainerStore = create<ContainerStore>((set, get) => {
|
||||
});
|
||||
if (res.code === 200) {
|
||||
message.success('Success');
|
||||
set({ showEdit: false, showPublish: false, formData: res.data });
|
||||
set({ formData: res.data });
|
||||
getList();
|
||||
if (closePublish) {
|
||||
set({ showPublish: false });
|
||||
}
|
||||
if (closeEdit) {
|
||||
set({ showEdit: false });
|
||||
}
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
return res.code === 200;
|
||||
},
|
||||
deleteData: async (id) => {
|
||||
const { getList } = get();
|
||||
@ -69,17 +77,5 @@ export const useContainerStore = create<ContainerStore>((set, get) => {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
publishData: async (data) => {
|
||||
const res = await query.post({
|
||||
path: 'container',
|
||||
key: 'publish',
|
||||
data,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
message.success('Success');
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -146,12 +146,12 @@ export const List = () => {
|
||||
<div className='flex gap-2 border border-gray-200 shadow-md p-2'>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<Tooltip title='所有的资源文件' placement='right'>
|
||||
<IconButton sx={{ py: 1 }} color={tab !== 'folder' ? 'primary' : 'secondary'} onClick={() => setTab('folder')}>
|
||||
<IconButton color={tab !== 'folder' ? 'primary' : 'secondary'} onClick={() => setTab('folder')}>
|
||||
<FolderOutlined />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title='上传资源文件管理' placement='right'>
|
||||
<IconButton sx={{ py: 1 }} color={tab !== 'upload' ? 'primary' : 'secondary'} onClick={() => setTab('upload')}>
|
||||
<IconButton color={tab !== 'upload' ? 'primary' : 'secondary'} onClick={() => setTab('upload')}>
|
||||
<UploadOutlined />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
@ -1,14 +1,21 @@
|
||||
import { Button, Input, Modal, Space, Table } from 'antd';
|
||||
import { Button, Input, Modal, Space } from 'antd';
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { useUserStore } from '../store';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { Form } from 'antd';
|
||||
import { useNewNavigate } from '@/modules';
|
||||
import { EditOutlined, SettingOutlined, LinkOutlined, SaveOutlined, DeleteOutlined, LeftOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import EditOutlined from '@ant-design/icons/EditOutlined';
|
||||
import SettingOutlined from '@ant-design/icons/SettingOutlined';
|
||||
import LinkOutlined from '@ant-design/icons/LinkOutlined';
|
||||
import SaveOutlined from '@ant-design/icons/SaveOutlined';
|
||||
import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
|
||||
import LeftOutlined from '@ant-design/icons/LeftOutlined';
|
||||
import PlusOutlined from '@ant-design/icons/PlusOutlined';
|
||||
import clsx from 'clsx';
|
||||
import { isObjectNull } from '@/utils/is-null';
|
||||
import { CardBlank } from '@/components/card/CardBlank';
|
||||
import { CardBlank } from '@kevisual/center-components/card/CardBlank.tsx';
|
||||
import { message } from '@/modules/message';
|
||||
import { Dialog } from '@mui/material';
|
||||
const FormModal = () => {
|
||||
const [form] = Form.useForm();
|
||||
const userStore = useUserStore(
|
||||
@ -41,14 +48,15 @@ const FormModal = () => {
|
||||
};
|
||||
const isEdit = userStore.formData.id;
|
||||
return (
|
||||
<Modal
|
||||
<Dialog
|
||||
title={isEdit ? 'Edit' : 'Add'}
|
||||
open={userStore.showEdit}
|
||||
onClose={() => userStore.setShowEdit(false)}
|
||||
destroyOnClose
|
||||
footer={false}
|
||||
width={800}
|
||||
onCancel={onClose}>
|
||||
sx={{
|
||||
'& .MuiDialog-paper': {
|
||||
width: '800px',
|
||||
},
|
||||
}}>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
@ -76,11 +84,10 @@ const FormModal = () => {
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
export const List = () => {
|
||||
const navicate = useNewNavigate();
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const userStore = useUserStore(
|
||||
useShallow((state) => {
|
||||
|
@ -5,7 +5,7 @@ import { useShallow } from 'zustand/react/shallow';
|
||||
import { isObjectNull } from '@/utils/is-null';
|
||||
import { useLayoutStore } from '@/modules/layout/store';
|
||||
import { AvatarUpload } from '../module/AvatarUpload';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import UploadOutlined from '@ant-design/icons/UploadOutlined';
|
||||
import PandaPNG from '@/assets/panda.png';
|
||||
import { FileUpload } from '../module/FileUpload';
|
||||
export const Profile = () => {
|
||||
|
@ -47,6 +47,9 @@ if (true) {
|
||||
cookieDomainRewrite: 'localhost',
|
||||
rewrite: (path: any) => path.replace(/^\/user/, '/user'),
|
||||
},
|
||||
'/test': {
|
||||
target: backend,
|
||||
},
|
||||
};
|
||||
}
|
||||
// https://vitejs.dev/config/
|
||||
|
Loading…
x
Reference in New Issue
Block a user