mirror of
https://github.com/chaitin/MonkeyCode.git
synced 2026-02-02 14:53:55 +08:00
@@ -1,6 +1,5 @@
|
||||
import Logo from '@/assets/images/logo.png';
|
||||
import { Avatar as MuiAvatar, type SxProps } from '@mui/material';
|
||||
import { Icon } from '@c-x/ui';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
interface AvatarProps {
|
||||
@@ -41,21 +40,15 @@ function stringAvatar(name: string) {
|
||||
|
||||
const Avatar = (props: AvatarProps) => {
|
||||
const src = props.src;
|
||||
const avatarObj = props.name ? stringAvatar(props.name) : undefined;
|
||||
return (
|
||||
<MuiAvatar
|
||||
// sx={{
|
||||
// img: { objectFit: 'contain' },
|
||||
// bgcolor: 'transparent',
|
||||
// ...props.sx,
|
||||
// }}
|
||||
|
||||
sx={props.name ? { ...avatarObj!.sx, ...props.sx } : props.sx}
|
||||
{...(props.name
|
||||
? stringAvatar(props.name)
|
||||
: {
|
||||
children: <img src={Logo} alt='logo' />,
|
||||
})}
|
||||
? { children: avatarObj!.children }
|
||||
: { children: <img src={Logo} alt='logo' /> })}
|
||||
src={src}
|
||||
></MuiAvatar>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
60
ui/src/components/markDown/code.tsx
Normal file
60
ui/src/components/markDown/code.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
|
||||
const Code = () => {
|
||||
return (
|
||||
<div style={{ height: 420 }}>
|
||||
<MonacoEditor
|
||||
height='100%'
|
||||
language={getBaseLanguageId(data?.program_language || 'plaintext')}
|
||||
value={editorValue}
|
||||
theme='vs-dark'
|
||||
options={{
|
||||
readOnly: true,
|
||||
minimap: { enabled: false },
|
||||
fontSize: 14,
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
lineNumbers: 'on',
|
||||
glyphMargin: false,
|
||||
folding: false,
|
||||
overviewRulerLanes: 0,
|
||||
guides: {
|
||||
indentation: true,
|
||||
highlightActiveIndentation: true,
|
||||
highlightActiveBracketPair: false,
|
||||
},
|
||||
renderLineHighlight: 'none',
|
||||
cursorStyle: 'line',
|
||||
cursorBlinking: 'solid',
|
||||
cursorWidth: 0,
|
||||
contextmenu: false,
|
||||
selectionHighlight: false,
|
||||
selectOnLineNumbers: false,
|
||||
occurrencesHighlight: 'off',
|
||||
links: false,
|
||||
hover: { enabled: false },
|
||||
codeLens: false,
|
||||
dragAndDrop: false,
|
||||
mouseWheelZoom: false,
|
||||
accessibilitySupport: 'off',
|
||||
bracketPairColorization: { enabled: false },
|
||||
matchBrackets: 'never',
|
||||
}}
|
||||
onMount={(editor) => {
|
||||
editorRef.current = editor;
|
||||
setEditorReady(true);
|
||||
// 隐藏光标
|
||||
const editorDom = editor.getDomNode();
|
||||
if (editorDom) {
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = `.monaco-editor .cursor { display: none !important; }`;
|
||||
editorDom.appendChild(style);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Code;
|
||||
43
ui/src/components/user/index.tsx
Normal file
43
ui/src/components/user/index.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Stack, Link, Typography } from '@mui/material';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import Avatar from '../avatar';
|
||||
|
||||
const User = ({
|
||||
id,
|
||||
username = '',
|
||||
email = '',
|
||||
}: {
|
||||
id?: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
}) => {
|
||||
return (
|
||||
<Stack>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={id ? `/dashboard/member/${id}` : ''}
|
||||
sx={{
|
||||
'&:hover': {
|
||||
color: id ? 'info.main' : 'text.primary',
|
||||
},
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Stack direction={'row'} alignItems={'center'} gap={1}>
|
||||
<Avatar
|
||||
name={username}
|
||||
sx={{ width: 22, height: 22, fontSize: 14 }}
|
||||
/>
|
||||
<Typography>{username}</Typography>
|
||||
</Stack>
|
||||
</Link>
|
||||
{email && (
|
||||
<Typography color='text.auxiliary' sx={{ fontSize: 14 }}>
|
||||
{email}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default User;
|
||||
@@ -17,7 +17,7 @@ import dayjs from 'dayjs';
|
||||
import { DomainAdminUser } from '@/api/types';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import User from '@/components/user';
|
||||
|
||||
const AddAdminModal = ({
|
||||
open,
|
||||
@@ -182,6 +182,9 @@ const AdminTable = () => {
|
||||
{
|
||||
title: '账号',
|
||||
dataIndex: 'username',
|
||||
render: (text) => {
|
||||
return <User username={text} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '最近活跃时间',
|
||||
|
||||
@@ -5,7 +5,8 @@ import dayjs from 'dayjs';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { getAdminLoginHistory } from '@/api/User';
|
||||
import { ColumnsType } from '@c-x/ui/dist/Table';
|
||||
import { DomainListAdminLoginHistoryResp, DomainAdminUser } from '@/api/types';
|
||||
import { DomainListAdminLoginHistoryResp } from '@/api/types';
|
||||
import User from '@/components/user';
|
||||
|
||||
type LoginHistory = NonNullable<
|
||||
DomainListAdminLoginHistoryResp['login_histories']
|
||||
@@ -18,7 +19,7 @@ const LoginHistory = () => {
|
||||
title: '账号',
|
||||
dataIndex: 'user',
|
||||
render: (user, record) => {
|
||||
return record?.user?.username;
|
||||
return <User username={record!.user!.username!} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Table, Ellipsis } from '@c-x/ui';
|
||||
import { Table } from '@c-x/ui';
|
||||
import { getListChatRecord } from '@/api/Billing';
|
||||
import { aggregatedTime } from '@/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import { convertTokensToRMB } from '@/utils';
|
||||
|
||||
import Card from '@/components/card';
|
||||
import { Box, Stack, styled, Chip } from '@mui/material';
|
||||
import { Box } from '@mui/material';
|
||||
import StyledLabel from '@/components/label';
|
||||
|
||||
import ChatDetailModal from './chatDetailModal';
|
||||
import { ColumnsType } from '@c-x/ui/dist/Table';
|
||||
import { DomainChatRecord } from '@/api/types';
|
||||
import { DomainChatRecord, DomainUser } from '@/api/types';
|
||||
import { addCommasToNumber } from '@/utils';
|
||||
|
||||
const StyledHighlightText = styled('span')(({ theme }) => ({
|
||||
color: theme.vars.palette.text.primary,
|
||||
fontWeight: 700,
|
||||
}));
|
||||
import User from '@/components/user';
|
||||
|
||||
const Chat = () => {
|
||||
const [page, setPage] = useState(1);
|
||||
@@ -47,9 +41,15 @@ const Chat = () => {
|
||||
{
|
||||
dataIndex: 'user',
|
||||
title: '成员',
|
||||
width: 160,
|
||||
render(value: DomainChatRecord['user']) {
|
||||
return value?.username;
|
||||
width: 260,
|
||||
render(value: DomainUser) {
|
||||
return (
|
||||
<User
|
||||
id={value.id!}
|
||||
username={value.username!}
|
||||
email={value.email!}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { DomainCompletionRecord } from '@/api/types';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { DomainCompletionRecord, DomainUser } from '@/api/types';
|
||||
import { getListCompletionRecord } from '@/api/Billing';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { Table } from '@c-x/ui';
|
||||
import Card from '@/components/card';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonBase,
|
||||
Chip,
|
||||
Stack,
|
||||
alpha,
|
||||
MenuItem,
|
||||
Select,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Autocomplete,
|
||||
TextField,
|
||||
InputAdornment,
|
||||
} from '@mui/material';
|
||||
import { getListUser } from '@/api/User';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -33,6 +22,7 @@ import CompletionDetailModal from './completionDetailModal';
|
||||
import StyledLabel from '@/components/label';
|
||||
import { LANG_OPTIONS } from './constant';
|
||||
import { ArrowDropDown as ArrowDropDownIcon } from '@mui/icons-material';
|
||||
import User from '@/components/user';
|
||||
|
||||
// 防抖 hook
|
||||
function useDebounce(fn: (...args: any[]) => void, delay: number) {
|
||||
@@ -113,8 +103,14 @@ const Completion = () => {
|
||||
{
|
||||
dataIndex: 'user',
|
||||
title: '成员',
|
||||
render(value: DomainCompletionRecord['user']) {
|
||||
return value?.username;
|
||||
render(value: DomainUser) {
|
||||
return (
|
||||
<User
|
||||
id={value.id!}
|
||||
username={value.username!}
|
||||
email={value.email!}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
64
ui/src/pages/dashboard/components/contributionModal.tsx
Normal file
64
ui/src/pages/dashboard/components/contributionModal.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Box, Stack } from '@mui/material';
|
||||
import { DomainUserCodeRank } from '@/api/types';
|
||||
import { Modal } from '@c-x/ui';
|
||||
import { StyledItem, StyledSerialNumber, StyledText } from './statisticCard';
|
||||
|
||||
const ContributionModal = ({
|
||||
open,
|
||||
onCancel,
|
||||
data,
|
||||
}: {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
data: DomainUserCodeRank[];
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
title='用户贡献榜'
|
||||
showCancel={false}
|
||||
okText='关闭'
|
||||
>
|
||||
<Box
|
||||
gap={0.5}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{data.map((item, index) => (
|
||||
<Stack
|
||||
direction='row'
|
||||
alignItems='center'
|
||||
justifyContent='space-between'
|
||||
key={item.username}
|
||||
>
|
||||
<StyledItem>
|
||||
<StyledSerialNumber num={index + 1}>
|
||||
{index + 1}
|
||||
</StyledSerialNumber>
|
||||
<Stack
|
||||
direction='row'
|
||||
alignItems='center'
|
||||
gap={1.5}
|
||||
sx={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
{/* <Avatar size={24} src={item.avatar} /> */}
|
||||
<StyledText className='active-user-name'>
|
||||
{item.username}
|
||||
</StyledText>
|
||||
</Stack>
|
||||
</StyledItem>
|
||||
<Box sx={{ fontSize: 14 }}>{item.lines}</Box>
|
||||
</Stack>
|
||||
))}
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContributionModal;
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import { styled, Stack, Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import { styled, Stack, Box, Button } from '@mui/material';
|
||||
import { Empty } from '@c-x/ui';
|
||||
import dayjs from 'dayjs';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { TimeRange } from '../index';
|
||||
import ContributionModal from './contributionModal';
|
||||
|
||||
import Card from '@/components/card';
|
||||
import {
|
||||
@@ -23,13 +24,13 @@ const StyledCardValue = styled('div')(({ theme }) => ({
|
||||
fontWeight: 700,
|
||||
}));
|
||||
|
||||
const StyledItem = styled('div')(({ theme }) => ({
|
||||
export const StyledItem = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledText = styled('a')(({ theme }) => ({
|
||||
export const StyledText = styled('a')(({ theme }) => ({
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.primary,
|
||||
overflow: 'hidden',
|
||||
@@ -37,20 +38,22 @@ const StyledText = styled('a')(({ theme }) => ({
|
||||
whiteSpace: 'nowrap',
|
||||
}));
|
||||
|
||||
const StyledSerialNumber = styled('span')<{ num: number }>(({ theme, num }) => {
|
||||
const numToColor = {
|
||||
1: '#FE4545',
|
||||
2: '#FF6600',
|
||||
3: '#FFC600',
|
||||
};
|
||||
const color = numToColor[num as 1] || '#BCBCBC';
|
||||
return {
|
||||
color: color,
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
width: 8,
|
||||
};
|
||||
});
|
||||
export const StyledSerialNumber = styled('span')<{ num: number }>(
|
||||
({ theme, num }) => {
|
||||
const numToColor = {
|
||||
1: '#FE4545',
|
||||
2: '#FF6600',
|
||||
3: '#FFC600',
|
||||
};
|
||||
const color = numToColor[num as 1] || '#BCBCBC';
|
||||
return {
|
||||
color: color,
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
width: 8,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const ContributionCard = ({
|
||||
data = [],
|
||||
@@ -60,11 +63,34 @@ export const ContributionCard = ({
|
||||
timeRange: TimeRange;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [contributionModalOpen, setContributionModalOpen] = useState(false);
|
||||
return (
|
||||
<Card sx={{ height: '100%' }}>
|
||||
<ContributionModal
|
||||
open={contributionModalOpen}
|
||||
onCancel={() => setContributionModalOpen(false)}
|
||||
data={data}
|
||||
/>
|
||||
<Stack direction='row' justifyContent='space-between' alignItems='center'>
|
||||
<Box sx={{ fontWeight: 700 }}>用户贡献榜</Box>
|
||||
<Stack
|
||||
direction='row'
|
||||
alignItems='center'
|
||||
gap={1}
|
||||
sx={{ fontWeight: 700 }}
|
||||
>
|
||||
用户贡献榜
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: 12,
|
||||
color: 'info.main',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 400,
|
||||
}}
|
||||
onClick={() => setContributionModalOpen(true)}
|
||||
>
|
||||
查看更多
|
||||
</Box>
|
||||
</Stack>
|
||||
<Box sx={{ fontSize: 12, color: 'text.tertiary' }}>
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}
|
||||
</Box>
|
||||
|
||||
@@ -52,13 +52,13 @@ const User = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack gap={2}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={6}>
|
||||
<Stack gap={2} sx={{ height: '100%' }}>
|
||||
<Grid container spacing={2} sx={{ height: '100%' }}>
|
||||
<Grid size={6} sx={{ height: '100%' }}>
|
||||
<MemberManage />
|
||||
</Grid>
|
||||
<Grid size={6} container spacing={2}>
|
||||
<Grid size={12}>
|
||||
<Grid size={6} container sx={{ height: '100%' }}>
|
||||
<Stack gap={2} sx={{ height: '100%' }}>
|
||||
<StyledCard>
|
||||
<StyledLabel>强制成员启用两步认证</StyledLabel>
|
||||
<Switch
|
||||
@@ -68,8 +68,6 @@ const User = () => {
|
||||
}}
|
||||
/>
|
||||
</StyledCard>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<StyledCard>
|
||||
<StyledLabel>禁止成员使用密码登录</StyledLabel>
|
||||
<Switch
|
||||
@@ -79,10 +77,7 @@ const User = () => {
|
||||
}
|
||||
/>
|
||||
</StyledCard>
|
||||
</Grid>
|
||||
|
||||
<Grid size={12}>
|
||||
<StyledCard sx={{ height: '100%' }}>
|
||||
<StyledCard>
|
||||
<StyledLabel>
|
||||
第三方登录
|
||||
<Box
|
||||
@@ -105,11 +100,8 @@ const User = () => {
|
||||
配置
|
||||
</Button>
|
||||
</StyledCard>
|
||||
</Grid>
|
||||
|
||||
<Grid size={12}>
|
||||
<LoginHistory />
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Table } from '@c-x/ui';
|
||||
import dayjs from 'dayjs';
|
||||
import { ColumnsType } from '@c-x/ui/dist/Table';
|
||||
import { DomainListLoginHistoryResp } from '@/api/types';
|
||||
import User from '@/components/user';
|
||||
|
||||
type LoginHistory = NonNullable<
|
||||
DomainListLoginHistoryResp['login_histories']
|
||||
@@ -18,7 +19,13 @@ const LoginHistory = () => {
|
||||
title: '账号',
|
||||
dataIndex: 'user',
|
||||
render: (user, record) => {
|
||||
return record?.user?.username;
|
||||
return (
|
||||
<User
|
||||
username={record.user!.username!}
|
||||
id={record.user!.id!}
|
||||
email={record.user!.email!}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -54,7 +61,7 @@ const LoginHistory = () => {
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Card>
|
||||
<Card sx={{ flex: 1, height: 'calc(100% - 258px)' }}>
|
||||
<Stack
|
||||
direction='row'
|
||||
justifyContent='space-between'
|
||||
@@ -64,6 +71,7 @@ const LoginHistory = () => {
|
||||
<Box sx={{ fontWeight: 700 }}>登录历史</Box>
|
||||
</Stack>
|
||||
<Table
|
||||
height='calc(100% - 40px)'
|
||||
columns={columns}
|
||||
dataSource={data?.login_histories || []}
|
||||
pagination={false}
|
||||
|
||||
@@ -25,6 +25,7 @@ import dayjs from 'dayjs';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import ErrorRoundedIcon from '@mui/icons-material/ErrorRounded';
|
||||
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
||||
import User from '@/components/user';
|
||||
|
||||
const ResetPasswordModal = ({
|
||||
open,
|
||||
@@ -251,22 +252,13 @@ const MemberManage = () => {
|
||||
title: '账号',
|
||||
dataIndex: 'username',
|
||||
render: (text, record) => {
|
||||
return <>
|
||||
<Stack>
|
||||
<Link onClick={() => navigate(`/dashboard/member/${record.id}`)} sx={{
|
||||
'&:hover': {
|
||||
color: 'info.main',
|
||||
},
|
||||
cursor: 'pointer'
|
||||
}}>
|
||||
<Stack direction={'row'}>
|
||||
<AccountCircleIcon fontSize='small' sx={{ mr: 1, lineHeight: 24 }}/>
|
||||
<Typography>{text}</Typography>
|
||||
</Stack>
|
||||
</Link>
|
||||
<Typography color='text.auxiliary'>{record.email}</Typography>
|
||||
</Stack>
|
||||
</>;
|
||||
return (
|
||||
<User
|
||||
id={record.id!}
|
||||
username={record.username!}
|
||||
email={record.email!}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -282,7 +274,9 @@ const MemberManage = () => {
|
||||
dataIndex: 'last_active_at',
|
||||
width: 120,
|
||||
render: (text, record) => {
|
||||
return record.last_active_at === 0 ? '从未使用' : dayjs.unix(text).fromNow();
|
||||
return record.last_active_at === 0
|
||||
? '从未使用'
|
||||
: dayjs.unix(text).fromNow();
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -305,9 +299,8 @@ const MemberManage = () => {
|
||||
},
|
||||
},
|
||||
];
|
||||
console.log(currentUser);
|
||||
return (
|
||||
<Card>
|
||||
<Card sx={{ flex: 1, height: '100%' }}>
|
||||
<Menu anchorEl={anchorEl} open={!!anchorEl} onClose={handleClose}>
|
||||
{currentUser?.status === ConstsUserStatus.UserStatusActive && (
|
||||
<MenuItem onClick={onLockUser}>解锁成员</MenuItem>
|
||||
@@ -337,20 +330,7 @@ const MemberManage = () => {
|
||||
>
|
||||
<Box sx={{ fontWeight: 700 }}>成员列表</Box>
|
||||
<Stack direction='row' gap={1}>
|
||||
<TextField
|
||||
label='搜索'
|
||||
size='small'
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
'.MuiInputLabel-root': {
|
||||
fontSize: 14,
|
||||
},
|
||||
'.MuiInputBase-root': {
|
||||
height: 36.5,
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TextField label='搜索' size='small' />
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
@@ -362,6 +342,7 @@ const MemberManage = () => {
|
||||
</Stack>
|
||||
<Table
|
||||
columns={columns}
|
||||
height='calc(100% - 53px)'
|
||||
dataSource={data?.users || []}
|
||||
sx={{ mx: -2 }}
|
||||
pagination={false}
|
||||
|
||||
Reference in New Issue
Block a user