Merge pull request #53 from guanweiwang/main

feat: 优化用户展示, 更多贡献榜
This commit is contained in:
Yoko
2025-07-04 11:41:58 +08:00
committed by GitHub
12 changed files with 280 additions and 113 deletions

View File

@@ -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>
/>
);
};

View 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;

View 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;

View File

@@ -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: '最近活跃时间',

View File

@@ -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!} />;
},
},
{

View File

@@ -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!}
/>
);
},
},
{

View File

@@ -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!}
/>
);
},
},
{

View 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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}

View File

@@ -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}