add domain manager

This commit is contained in:
xion 2025-03-25 19:19:09 +08:00
parent 45443709af
commit d649666379
9 changed files with 319 additions and 16 deletions

View File

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

View File

@ -7,6 +7,7 @@ import { App as FileApp } from './pages/file';
import { App as OrgApp } from './pages/org';
import { App as ConfigApp } from './pages/config';
import { App as PayApp } from './pages/pay';
import { App as DomainApp } from './pages/domain';
import { basename } from './modules/basename';
import { Redirect } from './modules/Redirect';
import { CustomThemeProvider } from '@kevisual/components/theme/index.tsx';
@ -80,6 +81,7 @@ export const App = () => {
<Route path='/app/*' element={<UserAppApp />} />
<Route path='/file/*' element={<FileApp />} />
<Route path='/pay/*' element={<PayApp />} />
<Route path='/domain/*' element={<DomainApp />} />
<Route path='/404' element={<div>404</div>} />
<Route path='*' element={<div>404</div>} />
</Routes>

View File

@ -6,10 +6,10 @@ import { Button } from '@mui/material';
import { message } from '@/modules/message';
import SmileOutlined from '@ant-design/icons/SmileOutlined';
import SwitcherOutlined from '@ant-design/icons/SwitcherOutlined';
import { useMemo } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { query, queryLogin } from '../query';
import { useNewNavigate } from '../navicate';
import { LogOut, Map, SquareUser, Users, X } from 'lucide-react';
import { LogOut, Map, SquareUser, Users, X, ArrowDownLeftFromSquareIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import React from 'react';
@ -22,6 +22,13 @@ export const LayoutUser = () => {
switchOrg: state.switchOrg,
})),
);
const [isAdmin, setIsAdmin] = useState<boolean>(false);
useEffect(() => {
queryLogin.cacheStore.getCurrentUser().then((res) => {
const org = res?.orgs || [];
setIsAdmin(org.includes('admin'));
});
}, []);
const navigate = useNewNavigate();
const { t } = useTranslation();
const meun = [
@ -40,6 +47,11 @@ export const LayoutUser = () => {
icon: <Map size={16} />,
link: '/map',
},
{
title: t('Domain'),
icon: <ArrowDownLeftFromSquareIcon size={16} />,
link: '/domain/edit/list',
},
];
const items = useMemo(() => {
const orgs = store.me?.orgs || [];

View File

@ -27,6 +27,7 @@ import { useTranslation } from 'react-i18next';
import { TextField, InputAdornment } from '@mui/material';
import { useForm, Controller } from 'react-hook-form';
import { pick } from 'lodash-es';
import copy from 'copy-to-clipboard';
const FormModal = () => {
const defaultValues = {
@ -295,7 +296,17 @@ export const List = () => {
</Tooltip>
</div>
</div>
<div>
<div className='flex flex-col gap-2'>
<Tooltip title={'复制App ID到剪贴板'}>
<div
className='text-xs cursor-copy'
onClick={() => {
copy(item.id);
message.success('复制成功');
}}>
{item.id}
</div>
</Tooltip>
{item.domain && (
<div className='text-xs'>
{t('app.domain')}: {item.domain}

View File

@ -0,0 +1,174 @@
import { useEffect } from 'react';
import { appDomainStatus, useDomainStore } from '../store';
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Button,
Modal,
TextField,
Dialog,
DialogContent,
DialogTitle,
useTheme,
} from '@mui/material';
import { useForm, Controller } from 'react-hook-form';
import { pick } from 'lodash-es';
import { Select } from '@kevisual/components/select/index.tsx';
const TableList = () => {
const { list, setList, getDomainList, updateDomain, setShowEditModal, setFormData, deleteDomain } = useDomainStore();
useEffect(() => {
getDomainList();
}, []);
return (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>ID</TableCell>
<TableCell>Domain</TableCell>
<TableCell>App ID</TableCell>
<TableCell>UID</TableCell>
<TableCell>Status</TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{list.map((domain) => (
<TableRow key={domain.id}>
<TableCell>{domain.id}</TableCell>
<TableCell>{domain.domain}</TableCell>
<TableCell>{domain.appId}</TableCell>
<TableCell>{domain.uid}</TableCell>
<TableCell>{domain.status}</TableCell>
<TableCell>
<Button
variant='contained'
color='primary'
onClick={() => {
setShowEditModal(true);
setFormData(domain);
}}>
</Button>
<Button variant='contained' color='error' onClick={() => deleteDomain(domain)}>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};
const FomeModal = () => {
const { showEditModal, setShowEditModal, formData, updateDomain } = useDomainStore();
const {
register,
handleSubmit,
formState: { errors },
reset,
getValues,
setValue,
control,
} = useForm();
useEffect(() => {
if (!showEditModal) return;
if (formData?.id) {
reset(formData);
} else {
reset({
status: 'running',
});
}
}, [formData]);
const onSubmit = async (data: any) => {
// 处理表单提交逻辑
const _formData = pick(data, ['domain', 'appId', 'status', 'id']);
if (formData.id) {
_formData.id = formData.id;
}
const res = await updateDomain(_formData);
if (res.code === 200) {
setShowEditModal(false);
}
};
const theme = useTheme();
const defultProps = theme.components?.MuiTextField?.defaultProps;
return (
<Dialog open={showEditModal} onClose={() => setShowEditModal(false)}>
<DialogTitle></DialogTitle>
<DialogContent>
<div className='p-4 w-[500px]'>
<form className='w-full h-full flex flex-col gap-4' onSubmit={handleSubmit(onSubmit)}>
<Controller
name='domain'
control={control}
defaultValue=''
rules={{ required: 'Domain is required' }}
render={({ field }) => <TextField {...defultProps} label='Domain' {...field} error={!!errors.domain} />}
/>
<Controller
name='appId'
control={control}
defaultValue=''
rules={{ required: 'App ID is required' }}
render={({ field }) => <TextField {...defultProps} label='App ID' {...field} error={!!errors.appId} />}
/>
<Controller
name='status'
control={control}
defaultValue=''
render={({ field }) => (
<Select
{...field}
options={[
...appDomainStatus.map((item) => ({
label: item,
value: item,
})),
]}
error={!!errors.status}
/>
)}
/>
<Button type='submit' variant='contained' color='primary'>
</Button>
</form>
</div>
</DialogContent>
</Dialog>
);
};
export const List = () => {
const { setShowEditModal, setFormData } = useDomainStore();
return (
<div className='p-4 w-full h-full bg-amber-900'>
<div className='flex'>
<Button
variant='contained'
color='primary'
onClick={() => {
setShowEditModal(true);
setFormData({});
}}>
</Button>
</div>
<div>
<TableList />
</div>
<FomeModal />
</div>
);
};

View File

@ -0,0 +1,14 @@
import { Route, Routes } from 'react-router-dom';
import { Main } from './layouts';
import { List } from './edit/List';
import { Redirect } from '@/modules/Redirect';
export const App = () => {
return (
<Routes>
<Route element={<Main />}>
<Route path='/' element={<Redirect to='/domain/edit/list' />}></Route>
<Route path='edit/list' element={<List />} />
</Route>
</Routes>
);
};

View File

@ -0,0 +1,5 @@
import { LayoutMain } from '@/modules/layout';
export const Main = () => {
return <LayoutMain title='Domain' />;
};

View File

@ -0,0 +1,91 @@
import { create } from 'zustand';
import { query } from '@/modules/query';
import { toast } from 'react-toastify';
// 审核,通过,驳回
export const appDomainStatus = ['audit', 'auditReject', 'auditPending', 'running', 'stop'] as const;
type AppDomainStatus = (typeof appDomainStatus)[number];
type Domain = {
id: string;
domain: string;
appId?: string;
status: AppDomainStatus;
data?: any;
uid?: string;
createdAt: string;
updatedAt: string;
};
interface Store {
getDomainList: () => Promise<any>;
updateDomain: (data: { domain: string; id: string; [key: string]: any }, opts?: { refresh?: boolean }) => Promise<any>;
deleteDomain: (data: { id: string }) => Promise<any>;
getDomainDetail: (data: { domain?: string; id?: string }) => Promise<any>;
list: Domain[];
setList: (list: Domain[]) => void;
formData: any;
setFormData: (formData: any) => void;
showEditModal: boolean;
setShowEditModal: (showEditModal: boolean) => void;
}
export const useDomainStore = create<Store>((set, get) => ({
getDomainList: async () => {
const res = await query.get({
path: 'app.domain.manager',
key: 'list',
});
if (res.code === 200) {
set({ list: res.data?.list || [] });
}
return res;
},
updateDomain: async (data: any, opts?: { refresh?: boolean }) => {
const res = await query.post({
path: 'app.domain.manager',
key: 'update',
data,
});
if (res.code === 200) {
const list = get().list;
set({ list: list.map((item) => (item.id === data.id ? res.data : item)) });
toast.success('更新成功');
if (opts?.refresh ?? true) {
get().getDomainList();
}
} else {
toast.error(res.message || '更新失败');
}
return res;
},
deleteDomain: async (data: any) => {
const res = await query.post({
path: 'app.domain.manager',
key: 'delete',
data,
});
if (res.code === 200) {
const list = get().list;
set({ list: list.filter((item) => item.id !== data.id) });
toast.success('删除成功');
}
return res;
},
getDomainDetail: async (data: any) => {
const res = await query.post({
path: 'app.domain.manager',
key: 'get',
data,
});
if (res.code === 200) {
set({ formData: res.data });
}
return res;
},
list: [],
setList: (list: any[]) => set({ list }),
formData: {},
setFormData: (formData: any) => set({ formData }),
showEditModal: false,
setShowEditModal: (showEditModal: boolean) => set({ showEditModal }),
}));

View File

@ -27,7 +27,7 @@ const meBackend = 'https://kevisual.cn';
const backendWss = devBackend.replace(/^https:/, 'wss:');
const backend = meBackend;
let proxy = {};
if (true) {
if (false) {
proxy = {
'/api': {
target: backend,
@ -48,14 +48,6 @@ if (true) {
};
}
function processImageName(fileName: string): string {
if (fileName.includes('panda')) {
return fileName; // 保留原名
}
// 其他图片文件名处理逻辑
return `${fileName}.jpg`; // 示例:添加后缀
}
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), ...plugins],
@ -96,7 +88,7 @@ export default defineConfig({
changeOrigin: true,
},
'/api': {
target: 'https://localhost:4005',
target: 'http://localhost:4005',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api'),
},