Browse Source

Merge pull request '合并到dev-zw' (#285) from dev-zw-notification into dev-zw

pull/286/head
cp3hnu 7 months ago
parent
commit
87dab0a293
52 changed files with 1716 additions and 64 deletions
  1. +1
    -2
      react-ui/config/defaultSettings.ts
  2. +11
    -0
      react-ui/config/routes.ts
  3. +3
    -0
      react-ui/package.json
  4. +7
    -0
      react-ui/src/app.tsx
  5. BIN
      react-ui/src/assets/img/home/model-item-bg-hover2.png
  6. BIN
      react-ui/src/assets/img/message/at-hover.png
  7. BIN
      react-ui/src/assets/img/message/at.png
  8. BIN
      react-ui/src/assets/img/message/content-bg.png
  9. BIN
      react-ui/src/assets/img/message/menu-bg.png
  10. BIN
      react-ui/src/assets/img/message/message-bg.png
  11. BIN
      react-ui/src/assets/img/message/red-point.png
  12. BIN
      react-ui/src/assets/img/message/system-hover.png
  13. BIN
      react-ui/src/assets/img/message/system.png
  14. BIN
      react-ui/src/assets/img/message/trumpet-hover.png
  15. BIN
      react-ui/src/assets/img/message/trumpet.png
  16. +25
    -0
      react-ui/src/components/KFButton/index.less
  17. +75
    -0
      react-ui/src/components/KFButton/index.tsx
  18. +19
    -0
      react-ui/src/components/MessageBroadcast/index.less
  19. +47
    -0
      react-ui/src/components/MessageBroadcast/index.tsx
  20. +11
    -2
      react-ui/src/components/RightContent/index.tsx
  21. +26
    -0
      react-ui/src/enums/index.ts
  22. +3
    -1
      react-ui/src/hooks/useServerTime.ts
  23. +20
    -1
      react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx
  24. +1
    -0
      react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx
  25. +5
    -0
      react-ui/src/pages/Dataset/config.tsx
  26. +2
    -3
      react-ui/src/pages/Home/components/Intro/index.tsx
  27. +1
    -1
      react-ui/src/pages/Home/components/Model/index.less
  28. +19
    -11
      react-ui/src/pages/Home/components/Model/index.tsx
  29. +0
    -25
      react-ui/src/pages/Home/components/ScrollReveal/index.tsx
  30. +3
    -4
      react-ui/src/pages/Home/components/Service/index.tsx
  31. +1
    -0
      react-ui/src/pages/Home/components/Statistics/index.less
  32. +138
    -0
      react-ui/src/pages/Message/components/Content/index.less
  33. +374
    -0
      react-ui/src/pages/Message/components/Content/index.tsx
  34. +69
    -0
      react-ui/src/pages/Message/components/Menu/index.less
  35. +50
    -0
      react-ui/src/pages/Message/components/Menu/index.tsx
  36. +8
    -0
      react-ui/src/pages/Message/index.less
  37. +81
    -0
      react-ui/src/pages/Message/index.tsx
  38. +9
    -0
      react-ui/src/pages/System/Approval/components/ApprovalModal/index.less
  39. +166
    -0
      react-ui/src/pages/System/Approval/components/ApprovalModal/index.tsx
  40. +15
    -0
      react-ui/src/pages/System/Approval/components/StatusCell/index.less
  41. +36
    -0
      react-ui/src/pages/System/Approval/components/StatusCell/index.tsx
  42. +29
    -0
      react-ui/src/pages/System/Approval/index.less
  43. +210
    -0
      react-ui/src/pages/System/Approval/index.tsx
  44. +1
    -1
      react-ui/src/pages/System/Role/index.tsx
  45. +1
    -1
      react-ui/src/pages/System/User/index.tsx
  46. +4
    -4
      react-ui/src/pages/Workspace/components/AssetsManagement/index.less
  47. +1
    -1
      react-ui/src/pages/Workspace/components/UserSpace/index.less
  48. +17
    -0
      react-ui/src/services/dataset/index.js
  49. +66
    -0
      react-ui/src/services/message/index.ts
  50. +9
    -7
      react-ui/src/styles/theme.less
  51. +97
    -0
      react-ui/src/utils/color.ts
  52. +55
    -0
      react-ui/src/utils/date.ts

+ 1
- 2
react-ui/config/defaultSettings.ts View File

@@ -9,8 +9,7 @@ const Settings: ProLayoutProps & {
} = {
locale: 'zh-CN',
navTheme: 'light',
// 拂晓蓝
colorPrimary: '#1664ff',
colorPrimary: '#514cf9',
// layout: 'mix',
contentWidth: 'Fluid',
fixedHeader: false,


+ 11
- 0
react-ui/config/routes.ts View File

@@ -38,6 +38,12 @@ export default [
key: 'workspace',
component: './Workspace/index',
},
{
name: '消息中心',
path: 'message',
key: 'message',
component: './Message/index',
},
],
},
{
@@ -582,6 +588,11 @@ export default [
},
],
},
{
name: '审核管理',
path: 'approval',
component: './System/Approval',
},
],
},
{


+ 3
- 0
react-ui/package.json View File

@@ -60,14 +60,17 @@
"not ie <= 10"
],
"dependencies": {
"@ant-design/colors": "~7.2.1",
"@ant-design/icons": "^5.0.0",
"@ant-design/pro-components": "^2.4.4",
"@ant-design/use-emotion-css": "1.0.4",
"@antv/g6": "^4.8.24",
"@antv/hierarchy": "^0.6.12",
"@ctrl/tinycolor": "~4.1.0",
"@types/crypto-js": "^4.2.2",
"@umijs/route-utils": "^4.0.1",
"antd": "~5.21.4",
"antd-style": "~3.7.1",
"caniuse-lite": "~1.0.30001707",
"classnames": "^2.3.2",
"crypto-js": "^4.2.0",


+ 7
- 0
react-ui/src/app.tsx View File

@@ -190,11 +190,18 @@ export const antd: RuntimeAntdConfig = (memo) => {
memo.theme ??= {};
memo.theme.token = {
colorPrimary: themes['primaryColor'],
colorPrimaryHover: themes['primaryHoverColor'],
colorPrimaryActive: themes['primaryActiveColor'],
colorPrimaryText: themes['primaryColor'],
colorPrimaryTextHover: themes['primaryHoverColor'],
colorPrimaryTextActive: themes['primaryActiveColor'],
// colorPrimaryBg: 'rgba(81, 76, 249, 0.07)',
colorSuccess: themes['successColor'],
colorError: themes['errorColor'],
colorWarning: themes['warningColor'],
colorLink: themes['primaryColor'],
colorText: themes['textColor'],
fontSize: parseInt(themes['fontSize']),
controlHeightLG: 46,
};
memo.theme.components ??= {};


BIN
react-ui/src/assets/img/home/model-item-bg-hover2.png View File

Before After
Width: 894  |  Height: 416  |  Size: 142 kB

BIN
react-ui/src/assets/img/message/at-hover.png View File

Before After
Width: 36  |  Height: 36  |  Size: 2.0 kB

BIN
react-ui/src/assets/img/message/at.png View File

Before After
Width: 36  |  Height: 36  |  Size: 1.9 kB

BIN
react-ui/src/assets/img/message/content-bg.png View File

Before After
Width: 2368  |  Height: 1830  |  Size: 514 kB

BIN
react-ui/src/assets/img/message/menu-bg.png View File

Before After
Width: 392  |  Height: 1830  |  Size: 107 kB

BIN
react-ui/src/assets/img/message/message-bg.png View File

Before After
Width: 3840  |  Height: 2040  |  Size: 1.6 MB

BIN
react-ui/src/assets/img/message/red-point.png View File

Before After
Width: 11  |  Height: 11  |  Size: 343 B

BIN
react-ui/src/assets/img/message/system-hover.png View File

Before After
Width: 36  |  Height: 36  |  Size: 1.2 kB

BIN
react-ui/src/assets/img/message/system.png View File

Before After
Width: 36  |  Height: 36  |  Size: 1.3 kB

BIN
react-ui/src/assets/img/message/trumpet-hover.png View File

Before After
Width: 66  |  Height: 66  |  Size: 1.8 kB

BIN
react-ui/src/assets/img/message/trumpet.png View File

Before After
Width: 66  |  Height: 66  |  Size: 1.7 kB

+ 25
- 0
react-ui/src/components/KFButton/index.less View File

@@ -0,0 +1,25 @@
body {
.kf-primary-button.ant-btn-color-primary.ant-btn-variant-link {
background-color: .addAlpha(@primary-color, 0.07) [] !important;
}

.kf-default-button.ant-btn-color-default.ant-btn-variant-link {
background-color: .addAlpha(@text-color-secondary, 0.07) [] !important;
}

.kf-danger-button.ant-btn-color-danger.ant-btn-variant-link {
background-color: .addAlpha(@error-color, 0.07) [] !important;
}

.ant-btn-color-default.ant-btn-variant-link.kf-default-button:not(:disabled):not(
.ant-btn-disabled
):hover {
color: .addAlpha(@text-color-secondary, 0.5) [];
}

.ant-btn-color-default.ant-btn-variant-link.kf-default-button:not(:disabled):not(
.ant-btn-disabled
):active {
color: @text-color-secondary;
}
}

+ 75
- 0
react-ui/src/components/KFButton/index.tsx View File

@@ -0,0 +1,75 @@
import themes from '@/styles/theme.less';
import { addAlpha, derivePrimaryStates } from '@/utils/color';
import { Button, ButtonProps } from 'antd';
import { createStyles } from 'antd-style';
import './index.less';

type KFColor = 'primary' | 'default' | 'danger';

export interface KFButtonProps extends ButtonProps {
kfColor?: KFColor;
}

const useStyles = createStyles(({ token, css }) => ({
primary: css`
color: ${token.colorPrimary} !important;
background-color: ${addAlpha(themes['primaryColor'], 0.07)} !important;

&:hover {
color: ${token.colorPrimaryHover} !important;
}

&:active {
color: ${token.colorPrimaryActive} !important;
}
`,
default: css`
color: ${themes['textColorSecondary']} !important;
background-color: ${addAlpha(themes['textColorSecondary'], 0.07)} !important;

&:hover {
color: ${derivePrimaryStates(themes['textColorSecondary']).colorPrimaryHover} !important;
}

&:active {
color: ${derivePrimaryStates(themes['textColorSecondary']).colorPrimaryActive} !important;
}
`,
danger: css`
color: ${themes['errorColor']} !important;
background-color: ${addAlpha(themes['errorColor'], 0.07)} !important;

&:hover {
color: ${derivePrimaryStates(themes['errorColor']).colorPrimaryHover} !important;
}

&:active {
color: ${derivePrimaryStates(themes['errorColor']).colorPrimaryActive} !important;
}
`,
}));

function KFButton({ kfColor = 'default', className, ...rest }: KFButtonProps) {
const { styles, cx } = useStyles();

let style = '';
switch (kfColor) {
case 'primary':
style = styles.primary;
break;
case 'default':
style = styles.default;
break;
case 'danger':
style = styles.danger;
break;
default:
break;
}

return (
<Button {...rest} className={cx(className, style)} color={kfColor} variant="link"></Button>
);
}

export default KFButton;

+ 19
- 0
react-ui/src/components/MessageBroadcast/index.less View File

@@ -0,0 +1,19 @@
.message-broadcast {
position: relative;
width: 32px;
height: 32px;
margin-right: 8px;
cursor: pointer;
.backgroundFullImage(url(@/assets/img/message/trumpet.png));

&:hover {
background-image: url(@/assets/img/message/trumpet-hover.png);
}

&__red-point {
position: absolute;
top: 8px;
left: 18px;
width: 6px;
}
}

+ 47
- 0
react-ui/src/components/MessageBroadcast/index.tsx View File

@@ -0,0 +1,47 @@
import RedPointImg from '@/assets/img/message/red-point.png';
import { getMessageCountReq } from '@/services/message';
import { to } from '@/utils/promise';
import { useModel, useNavigate } from '@umijs/max';
import { useCallback, useEffect, useState } from 'react';
import styles from './index.less';

function MessageBroadcast() {
const { initialState } = useModel('@@initialState');
const { currentUser } = initialState || {};
const { userId } = currentUser || {};
const [total, setTotal] = useState<number>(0);
const navigate = useNavigate();

const getMessageCount = useCallback(async () => {
if (!userId) return;
const params: Record<string, any> = {
receiver: userId,
type: -1,
};
const [res] = await to(getMessageCountReq(params));
if (res && res.data) {
const { unread_total } = res.data;
setTotal(unread_total);
}
}, [userId]);

useEffect(() => {
const interval = setInterval(() => {
getMessageCount();
}, 60 * 1000);
getMessageCount();
return () => {
clearInterval(interval);
};
}, [getMessageCount]);

return (
<div className={styles['message-broadcast']} onClick={() => navigate('/workspace/message')}>
{total > 0 && (
<img className={styles['message-broadcast__red-point']} src={RedPointImg}></img>
)}
</div>
);
}

export default MessageBroadcast;

+ 11
- 2
react-ui/src/components/RightContent/index.tsx View File

@@ -1,8 +1,9 @@
import KFIcon from '@/components/KFIcon';
import { ProBreadcrumb } from '@ant-design/pro-components';
import { useModel } from '@umijs/max';
import { Button } from 'antd';
import { Button, Flex } from 'antd';
import React from 'react';
import MessageBroadcast from '../MessageBroadcast';
import Avatar from './AvatarDropdown';
import styles from './index.less';
// import { SelectLang } from '@umijs/max';
@@ -43,7 +44,15 @@ const GlobalHeaderRight: React.FC = () => {

<ProBreadcrumb></ProBreadcrumb>

<Avatar menu={true} />
<Flex
align="center"
style={{ marginLeft: 'auto', marginRight: 0, background: '#3a3da5', padding: '0 30px' }}
>
<MessageBroadcast />

<Avatar menu={true} />
</Flex>

{/* <SelectLang className={actionClassName} /> */}
</div>
);


+ 26
- 0
react-ui/src/enums/index.ts View File

@@ -171,3 +171,29 @@ export enum ComponentType {
Map = 'map',
Str = 'str',
}

// 消息类型
export enum MessageType {
System = 1,
Mine = 2,
}

// 消息状态
export enum MessageStatus {
All = -1,
UnRead = 1,
Readed = 2,
}

// 审核状态
export enum ApprovalStatus {
Pending = 0,
Agree = 1,
Reject = 2,
}

export const approvalStatusOptions = [
{ label: '待审核', value: ApprovalStatus.Pending },
{ label: '通过', value: ApprovalStatus.Agree },
{ label: '拒绝', value: ApprovalStatus.Reject },
];

+ 3
- 1
react-ui/src/hooks/useServerTime.ts View File

@@ -10,6 +10,7 @@ import { useCallback, useEffect, useState } from 'react';

let globalTimeOffset: number | undefined = undefined;

// 获取服务器时间偏移
export const globalGetSeverTime = async () => {
const requestStartTime = Date.now();
const [res] = await to(getSeverTimeReq());
@@ -23,7 +24,8 @@ export const globalGetSeverTime = async () => {
}
};

export const now = () => {
// 服务器的当前时间
export const serverNow = () => {
return new Date(Date.now() + (globalTimeOffset ?? 0));
};



+ 20
- 1
react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx View File

@@ -64,7 +64,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {

// 获取详情
const getResourceDetail = useCallback(
async (version: string | undefined) => {
async (version?: string) => {
const params = {
id: resourceId,
owner,
@@ -223,6 +223,21 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
}
};

// 处理发布
const handlePublish = async () => {
const request = config.publish;
const params = {
id: resourceId,
owner,
name,
identifier,
};
const [res] = await to(request(params));
if (res) {
message.success('操作成功');
}
};

const items = [
{
key: ResourceInfoTabKeys.Introduction,
@@ -282,6 +297,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
{(info[tagPropertyName] as string) || '--'}
</div>
)}

<div
className={classNames(styles['resource-info__top__praise'], {
[styles['resource-info__top__praise--praised']]: info.praised,
@@ -295,6 +311,9 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
/>
<span>{info.praises_count}</span>
</div>
<Button type="default" onClick={handlePublish}>
发布
</Button>
</Flex>
{version ? (
<Flex align="center">


+ 1
- 0
react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx View File

@@ -34,6 +34,7 @@ function ResourceVersion({ resourceType, info }: ResourceVersionProps) {
id: info.id,
version: info.version,
identifier: info.identifier,
owner: info.owner,
is_public: info.is_public,
});
};


+ 5
- 0
react-ui/src/pages/Dataset/config.tsx View File

@@ -19,6 +19,8 @@ import {
getModelList,
getModelNextVersionReq,
getModelVersionList,
publishDatasetReq,
publishModelReq,
} from '@/services/dataset/index.js';
import { limitUploadFileType } from '@/utils/ui';
import type { TabsProps, UploadFile } from 'antd';
@@ -45,6 +47,7 @@ type ResourceTypeInfo = {
getInfo: (params: any) => Promise<any>; // 获取详情
compareVersion: (params: any) => Promise<any>; // 版本对比
getNextVersion: (params: any) => Promise<any>; // 获取下一个版本
publish: (params: any) => Promise<any>; // 发布
name: string; // 名称
typeParamKey: 'data_type' | 'model_type'; // 类型参数名称,获取资源列表接口使用
tagParamKey: 'data_tag' | 'model_tag'; // 标签参数名称,获取资源列表接口使用
@@ -76,6 +79,7 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = {
getInfo: getDatasetInfo,
compareVersion: compareDatasetVersion,
getNextVersion: getDatasetNextVersionReq,
publish: publishDatasetReq,
name: '数据集',
typeParamKey: 'data_type',
tagParamKey: 'data_tag',
@@ -116,6 +120,7 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = {
getInfo: getModelInfo,
compareVersion: compareModelVersion,
getNextVersion: getModelNextVersionReq,
publish: publishModelReq,
name: '模型',
typeParamKey: 'model_type',
tagParamKey: 'model_tag',


+ 2
- 3
react-ui/src/pages/Home/components/Intro/index.tsx View File

@@ -1,7 +1,7 @@
import miniHeaderImage from '@/assets/img/home/header-bg-mini.png';
import headerImage from '@/assets/img/home/header-bg.png';
import { convertRemToPx } from '@/utils';
import { useNavigate } from '@umijs/max';
import { gotoPageIfLogin } from '@/utils/ui';
import {
motion,
useMotionTemplate,
@@ -18,7 +18,6 @@ import styles from './index.less';
function IntroBlock() {
const [backgroundImage1, setBackgroundImage1] = useState(undefined);
const [backgroundImage2, setBackgroundImage2] = useState(headerImage);
const navigate = useNavigate();
const { scrollY } = useScroll();
const springValue = useSpring(scrollY, {
stiffness: 100,
@@ -70,7 +69,7 @@ function IntroBlock() {
智能材料科研平台是用于材料研究和开发的技术平台,它旨在提供实验数据收集、分析和可视化等功能,
以支持材料工程师、科学家和研究人员在材料设计、性能评估和工艺优化方面的工作。
</div>
<div className={styles['intro__button']} onClick={() => navigate('/workspace')}>
<div className={styles['intro__button']} onClick={() => gotoPageIfLogin('/workspace')}>
开始使用
</div>
<StatisticsBlock></StatisticsBlock>


+ 1
- 1
react-ui/src/pages/Home/components/Model/index.less View File

@@ -30,7 +30,7 @@

&:hover {
color: white;
.backgroundFullImage(url(@/assets/img/home/model-item-bg-hover.png));
.backgroundFullImage(url(@/assets/img/home/model-item-bg-hover2.png));
}

&__hot {


+ 19
- 11
react-ui/src/pages/Home/components/Model/index.tsx View File

@@ -3,34 +3,42 @@ import { getPublicModelsReq } from '@/services/home';
import { to } from '@/utils/promise';
import { gotoPageIfLogin } from '@/utils/ui';
import { Divider, Flex } from 'antd';
import { motion, type Variants } from 'motion/react';
import { motion, useMotionValueEvent, useScroll, type Variants } from 'motion/react';
import { useEffect, useState } from 'react';
import BlockTitle from '../BlockTitle';
import styles from './index.less';

const modelVariants: Variants = {
offscreen: (index: number) => ({
y: 0,
opacity: 1,
offscreen: (down: boolean) => ({
y: 100,
opacity: 0,
transition: {
ease: 'linear',
duration: 0.1,
duration: 0.5,
},
}),
onscreen: {
y: [0, 200, 0],
opacity: [0, 0, 1],
y: 0,
opacity: 1,
transition: {
ease: 'easeOut',
duration: 0.3,
times: [0, 0, 1],
delay: 0.5,
duration: 0.5,
// times: [0, 0, 1],
},
},
};

function ModelBlock() {
const [modelData, setModelData] = useState<ModelData[]>([]);
const [isDowning, setIsDowning] = useState(true);
const { scrollYProgress } = useScroll();
useMotionValueEvent(scrollYProgress, 'change', (value) => {
setIsDowning((scrollYProgress.getPrevious() ?? 0) - value < 0);
});

useEffect(() => {
console.log(isDowning);
}, [isDowning]);

useEffect(() => {
const getPublicModels = async () => {
@@ -58,9 +66,9 @@ function ModelBlock() {
variants={modelVariants}
initial={'offscreen'}
whileInView={'onscreen'}
custom={index}
className={styles['model__item']}
key={item.id}
custom={isDowning}
onClick={() => {
gotoPageIfLogin(
`/dataset/model/info/${item.id}?name=${item.name}&owner=${item.owner}&identifier=${item.identifier}&is_public=${item.is_public}`,


+ 0
- 25
react-ui/src/pages/Home/components/ScrollReveal/index.tsx View File

@@ -1,25 +0,0 @@
import { motion, useInView, Variants } from 'motion/react';
import { ReactNode, useRef } from 'react';

type ScrollRevealProps = {
children: ReactNode;
variants: Variants;
};

function ScrollReveal({ children, variants }: ScrollRevealProps) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { amount: 'all' });

return (
<motion.div
variants={variants}
ref={ref}
initial="offscreen"
animate={isInView ? 'onscreen' : 'offscreen'}
>
{children}
</motion.div>
);
}

export default ScrollReveal;

+ 3
- 4
react-ui/src/pages/Home/components/Service/index.tsx View File

@@ -15,22 +15,21 @@ import styles from './index.less';

const serviceVariants: Variants = {
offscreen: {
y: -100,
y: -200,
opacity: 0,
transition: {
ease: 'linear',
duration: 0,
},
},
onscreen: (index: number) => ({
onscreen: {
y: 0,
opacity: 1,
transition: {
type: 'spring',
duration: 1,
delay: index * 0.3,
},
}),
},
};

function ServiceBlock() {


+ 1
- 0
react-ui/src/pages/Home/components/Statistics/index.less View File

@@ -9,6 +9,7 @@
&__item {
display: flex;
align-items: center;
width: 9rem;

&__icon {
width: 3.75rem;


+ 138
- 0
react-ui/src/pages/Message/components/Content/index.less View File

@@ -0,0 +1,138 @@
.message-content {
display: flex;
flex: 1;
flex-direction: column;
min-width: 0;
height: 100%;
.backgroundFullImage(url(@/assets/img/message/content-bg.png));

&__tabs {
display: flex;
flex: none;
align-items: center;
height: 76px;
padding: 0 30px;
border-bottom: 1px dashed rgba(130, 132, 164, 0.18);

&__item {
margin-right: 20px;
color: @text-color-secondary;
font-size: @font-size;

&--selected,
&:hover {
color: @text-color;
font-weight: 500;
}
}

:global {
.ant-btn:first-of-type {
margin-right: 10px;
margin-left: auto;
}
}
}

&__check-container {
display: flex;
align-items: center;
margin: 16px 0 10px;
padding-left: 30px;
color: @text-color-secondary;
font-size: @font-size;

&__count {
margin: 0 2px;
color: @primary-color;
}
}

&__list {
flex: 1;
width: 100%;
overflow-y: auto;

&__item {
display: flex;
align-items: center;
width: 100%;
height: 56px;
padding: 0 30px;
color: @text-color;
font-size: @font-size;

&__status {
flex: none;
margin-right: 10px;
padding: 2px 4px;
font-size: 12px;
border-radius: 4px;

&--unread {
color: #d7312a;
background-color: rgba(215, 49, 42, 0.07);
}

&--readed {
color: #2a814b;
background-color: rgba(42, 129, 75, 0.07);
}
}

&__content {
flex: 1;
margin-right: 10px;
}

&__time {
display: block;
flex: none;
margin-left: auto;
color: @text-color-secondary;
}

&__button {
display: none;
flex: none;
padding-right: 0;
padding-left: 0;
color: @primary-color-hover;
font-size: @font-size;

&:hover {
color: @primary-color !important;
}

&:first-of-type {
margin-right: 10px;
margin-left: auto;
}
}

&:hover {
color: @primary-color;
background-color: .addAlpha(@primary-color, 0.05) [];
}

&:hover &__button {
display: block;
}

&:hover &__time {
display: none;
}
}
}

:global {
.ant-pagination {
margin-right: 30px;
margin-bottom: 40px;
}
}

&__empty {
flex: 1;
}
}

+ 374
- 0
react-ui/src/pages/Message/components/Content/index.tsx View File

@@ -0,0 +1,374 @@
import KFButton from '@/components/KFButton';
import KFEmpty, { EmptyType } from '@/components/KFEmpty';
import { MessageStatus, MessageType } from '@/enums';
import { useCacheState } from '@/hooks/useCacheState';
import { useCheck } from '@/hooks/useCheck';
import { Message, MessageResponse } from '@/pages/Message';
import { deleteMessagesReq, getMessageListReq, readMessagesReq } from '@/services/message';
import { ago } from '@/utils/date';
import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui';
import { useModel, useNavigate } from '@umijs/max';
import {
Button,
Checkbox,
Pagination,
PaginationProps,
Typography,
message,
type TablePaginationConfig,
} from 'antd';
import classNames from 'classnames';
import { useCallback, useEffect, useMemo, useState } from 'react';
import styles from './index.less';

export type MessageContentProps = {
messageType: MessageType;
messageStatus: MessageStatus;
pagination: TablePaginationConfig;
onStatusChange: (status: MessageStatus) => void;
onPaginationChange: (pagination: TablePaginationConfig) => void;
};

function MessageContent({
messageType,
messageStatus,
pagination,
onStatusChange,
onPaginationChange,
}: MessageContentProps) {
const { initialState } = useModel('@@initialState');
const { currentUser } = initialState || {};
const { userId } = currentUser || {};
const setCacheState = useCacheState()[1];
const [messages, setMessages] = useState<Message[] | undefined>(undefined);
const [allTotal, setAllTotal] = useState<number>(0);
const [unreadTotal, setUnreadTotal] = useState<number | undefined>(undefined);
const [isDelete, setIsDelete] = useState(false);
const messageIds = useMemo(() => messages?.map((v) => v.id), [messages]);
const [
selectedMessages,
setSelectedMessages,
messagesAllChecked,
messagesIndeterminate,
checkAllMessages,
isSingleMessagesChecked,
checkSingleMessages,
] = useCheck(messageIds ?? []);
const navigate = useNavigate();

const tabs = useMemo(
() => [
{
title: '未读',
status: MessageStatus.UnRead,
total: unreadTotal,
},
{
title: '全部',
status: MessageStatus.All,
},
],
[unreadTotal],
);

const getMessages = useCallback(async () => {
if (!userId) return;

const params: Record<string, any> = {
receiver: userId,
status: messageStatus,
type: messageType,
page: pagination.current,
size: pagination.pageSize,
};
const [res] = await to(getMessageListReq(params));
if (res && res.data) {
const { records, records_count, unread_notification, unread_atme } =
res.data as MessageResponse;
setMessages(records);
setAllTotal(records_count);
setUnreadTotal(messageType === MessageType.System ? unread_notification : unread_atme);
}
}, [pagination, userId, messageStatus, messageType]);

// 标记为已读
const readMessages = async (
message?: Message,
skipLoading: boolean = false,
skipResult: boolean = false,
) => {
const params: Record<string, any> = message
? {
notificationIds: message.id,
status: MessageStatus.Readed,
receiver: message.receiver,
type: message.type,
}
: {
notificationIds: -1,
status: MessageStatus.Readed,
receiver: userId,
type: messageType,
};
const [res] = await to(readMessagesReq(params, skipLoading));

// 点击消息置为已读时,不需要修改数据
if (!skipResult && res) {
// 如果当前是【未读】状态
// 【一键已读】后,设置分页为第一页
// 如果是一页的唯一数据,设置为前一页
if (messageStatus === MessageStatus.UnRead) {
onPaginationChange({
...pagination,
current: message
? messages?.length === 1
? Math.max(1, pagination.current! - 1)
: pagination.current
: 1,
});
}
} else {
getMessages();
}
};

// 删除
const deleteMessages = async (ids: number[]) => {
if (ids.length <= 0) {
message.error('请选择要删除的消息');
return;
}

const params: Record<string, any> = {
notificationIds: ids.join(','),
receiver: userId,
type: messageType,
};
const [res] = await to(deleteMessagesReq(params));
if (res) {
cancelBatchDelete();
// 如果是一页的唯一数据,删除后,请求前一页的数据
// 否则直接刷新这一页的数据
onPaginationChange({
...pagination,
current:
ids.length === messages?.length
? Math.max(1, pagination.current! - 1)
: pagination.current,
});
}
};

// 取消批量删除
const cancelBatchDelete = useCallback(() => {
setIsDelete(false);
setSelectedMessages([]);
}, [setSelectedMessages]);

useEffect(() => {
getMessages();
}, [getMessages]);

// 重置批量删除状态、分页
useEffect(() => {
cancelBatchDelete();
}, [messageType, messageStatus, cancelBatchDelete]);

// 批量删除
const handleBatchDelete = () => {
if (selectedMessages.length <= 0) {
message.error('请选择要删除的消息');
return;
}

modalConfirm({
title: '删除后,消息不可恢复',
content: '是否确认删除?',
onOk: () => {
deleteMessages(selectedMessages);
},
});
};

// 点击消息
const hanldeMessageClick = (message: Message) => {
if (message.status === MessageStatus.UnRead) {
readMessages(message, true, true);
}

if (message.notification_url) {
navigate(message.notification_url);
setCacheState({
messageType,
pagination,
messageStatus,
});
}
};

// 分页切换
const handlePageChange: PaginationProps['onChange'] = (page, pageSize) => {
onPaginationChange({
current: page,
pageSize: pageSize,
});
};

return (
<div className={styles['message-content']}>
<div className={styles['message-content__tabs']}>
{tabs.map((item) => (
<div
key={item.status}
className={classNames(styles['message-content__tabs__item'], {
[styles['message-content__tabs__item--selected']]: item.status === messageStatus,
})}
onClick={() => {
onStatusChange(item.status);
}}
>
<span>{item.title + (item.total !== undefined ? `(${item.total})` : '')}</span>
</div>
))}

{isDelete ? (
<>
<KFButton kfColor="default" variant="link" onClick={cancelBatchDelete}>
取消
</KFButton>
<KFButton kfColor="danger" variant="link" onClick={handleBatchDelete}>
删除
</KFButton>
</>
) : (
<>
{messageType === MessageType.Mine && allTotal > 0 && (
<KFButton kfColor="primary" variant="link" onClick={() => setIsDelete(true)}>
批量删除
</KFButton>
)}
{allTotal > 0 && (
<KFButton kfColor="primary" variant="link" onClick={() => readMessages()}>
一键已读
</KFButton>
)}
</>
)}
</div>

{isDelete && (
<div className={styles['message-content__check-container']}>
<Checkbox
style={{ marginRight: 10 }}
indeterminate={messagesIndeterminate}
checked={messagesAllChecked}
onChange={checkAllMessages}
>
全选
</Checkbox>
<span>
已选
<span className={styles['message-content__check-container__count']}>
{selectedMessages.length}
</span>
</span>
</div>
)}

{messages && messages.length > 0 && (
<>
<div className={styles['message-content__list']}>
{messages.map((message) => (
<div
className={styles['message-content__list__item']}
key={message.id}
onClick={() => hanldeMessageClick(message)}
>
{messageType === MessageType.Mine && isDelete && (
<Checkbox
style={{ marginRight: 10 }}
checked={isSingleMessagesChecked(message.id)}
onChange={() => {
checkSingleMessages(message.id);
}}
onClick={(e) => e.stopPropagation()}
></Checkbox>
)}
{messageStatus === MessageStatus.All && (
<div
className={classNames(
styles['message-content__list__item__status'],
message.status === MessageStatus.UnRead
? styles['message-content__list__item__status--unread']
: styles['message-content__list__item__status--readed'],
)}
>
{message.status === MessageStatus.UnRead ? '未读' : '已读'}
</div>
)}
<Typography.Text
className={styles['message-content__list__item__content']}
ellipsis={{ tooltip: message.content.replace(/<\/?b>/g, '') }}
>
<span dangerouslySetInnerHTML={{ __html: message.content }}></span>
</Typography.Text>

<div className={styles['message-content__list__item__time']}>
{ago(message.created_at)}
</div>
{message.status === MessageStatus.UnRead && (
<Button
className={styles['message-content__list__item__button']}
type="link"
onClick={(e) => {
e.stopPropagation();
readMessages(message);
}}
>
标记为已读
</Button>
)}
{messageType === MessageType.Mine && (
<Button
type="link"
className={styles['message-content__list__item__button']}
onClick={(e) => {
e.stopPropagation();
deleteMessages([message.id]);
}}
>
删除
</Button>
)}
</div>
))}
</div>
<Pagination
align="end"
total={allTotal}
showSizeChanger
defaultPageSize={20}
pageSizeOptions={[20, 40, 60, 80, 100]}
showQuickJumper
onChange={handlePageChange}
{...pagination}
/>
</>
)}
{messages && messages.length === 0 && (
<KFEmpty
className={styles['message-content__empty']}
type={EmptyType.NoData}
title="暂无数据"
content={`没有${messageStatus === MessageStatus.UnRead ? '未读' : ''}消息`}
hasFooter
onButtonClick={getMessages}
/>
)}
</div>
);
}

export default MessageContent;

+ 69
- 0
react-ui/src/pages/Message/components/Menu/index.less View File

@@ -0,0 +1,69 @@
.message-menu {
flex: none;
width: 196px;
height: 100%;
.backgroundFullImage(url(@/assets/img/message/menu-bg.png));

&__title {
position: relative;
margin-bottom: 25px;
padding: 20px 20px 10px;
color: @text-color;
font-size: @font-size;

&::after {
position: absolute;
right: 20px;
bottom: 0;
left: 20px;
border-bottom: 1px dashed rgba(130, 132, 164, 0.18);
content: '';
}
}

&__item {
display: flex;
align-items: center;
margin-bottom: 4px;
padding: 10px 0 10px 18px;
color: @text-color-secondary;
font-size: @font-size;
border-left: 2px solid transparent;

&--selected,
&:hover {
color: @primary-color;
background-image: linear-gradient(
101.08deg,
rgba(81, 76, 249, 0.09) 0%,
rgba(255, 255, 255, 0) 100%
);
border-left: 2px solid @primary-color;
}

&__icon,
&__icon--hover {
width: 18px;
height: 18px;
margin-right: 10px;
}

&__icon {
display: block;
}

&__icon--hover {
display: none;
}

&--selected &__icon,
&__item:hover &__icon {
display: none;
}

&--selected &__icon--hover,
&__item:hover &__icon--hover {
display: block;
}
}
}

+ 50
- 0
react-ui/src/pages/Message/components/Menu/index.tsx View File

@@ -0,0 +1,50 @@
import AtHoverIcon from '@/assets/img/message/at-hover.png';
import AtIcon from '@/assets/img/message/at.png';
import SystemHoverIcon from '@/assets/img/message/system-hover.png';
import SystemIcon from '@/assets/img/message/system.png';
import { MessageType } from '@/enums';
import classNames from 'classnames';
import styles from './index.less';

const menus = [
{
title: '系统消息',
icon: SystemIcon,
hoverIcon: SystemHoverIcon,
type: MessageType.System,
},
{
title: '@我的',
icon: AtIcon,
hoverIcon: AtHoverIcon,
type: MessageType.Mine,
},
];

export type MessageMenuProps = {
messageType: MessageType;
onTypeChange: (type: MessageType) => void;
};

function MessageMenu({ messageType: currentType, onTypeChange }: MessageMenuProps) {
return (
<div className={styles['message-menu']}>
<div className={styles['message-menu__title']}>消息列表</div>
{menus.map((item) => (
<div
key={item.type}
className={classNames(styles['message-menu__item'], {
[styles['message-menu__item--selected']]: item.type === currentType,
})}
onClick={() => onTypeChange(item.type)}
>
<img className={styles['message-menu__item__icon']} src={item.icon} />
<img className={styles['message-menu__item__icon--hover']} src={item.hoverIcon} />
<span>{item.title}</span>
</div>
))}
</div>
);
}

export default MessageMenu;

+ 8
- 0
react-ui/src/pages/Message/index.less View File

@@ -0,0 +1,8 @@
.message {
display: flex;
flex-direction: row;
gap: 0 20px;
height: 100%;
padding: 30px 60px 30px;
.backgroundFullImage(url(@/assets/img/message/message-bg.png));
}

+ 81
- 0
react-ui/src/pages/Message/index.tsx View File

@@ -0,0 +1,81 @@
import { MessageStatus, MessageType } from '@/enums';
import { useCacheState } from '@/hooks/useCacheState';
import { type TablePaginationConfig } from 'antd';
import { useState } from 'react';
import MessageContent from './components/Content';
import MessageMenu from './components/Menu';
import styles from './index.less';

// 消息列表接口返回类型
export interface MessageResponse {
receiver: number;
type: MessageType;
unread_total: number;
unread_notification: number;
unread_atme: number;
records: Message[];
records_count: number;
page_num: number;
total_page_count: number;
page_size: number;
}

// 消息数据
export interface Message {
id: number;
sender: number;
receiver: number;
content: string;
status: MessageStatus;
type: MessageType;
source: string;
extra: string;
notification_url: string;
created_at: Date;
}

function MessagePage() {
const [cacheState] = useCacheState();
const [messageType, setMessageType] = useState(cacheState?.messageType ?? MessageType.System);
const [pagination, setPagination] = useState<TablePaginationConfig>(
cacheState?.pagination ?? {
current: 1,
pageSize: 20,
},
);
const [messageStatus, setMessageStatus] = useState(
cacheState?.messageStatus ?? MessageStatus.UnRead,
);

// 重置页面为第一页
const resetToFirstPage = () => {
setPagination((prev) => ({
...prev,
current: 1,
}));
};

return (
<div className={styles['message']}>
<MessageMenu
onTypeChange={(type) => {
setMessageType(type);
resetToFirstPage();
}}
messageType={messageType}
></MessageMenu>
<MessageContent
messageType={messageType}
messageStatus={messageStatus}
onStatusChange={(status) => {
setMessageStatus(status);
resetToFirstPage();
}}
pagination={pagination}
onPaginationChange={setPagination}
></MessageContent>
</div>
);
}

export default MessagePage;

+ 9
- 0
react-ui/src/pages/System/Approval/components/ApprovalModal/index.less View File

@@ -0,0 +1,9 @@
.approval-info {
margin-bottom: 20px;

&__item {
display: flex;
align-items: center;
margin-bottom: 10px;
}
}

+ 166
- 0
react-ui/src/pages/System/Approval/components/ApprovalModal/index.tsx View File

@@ -0,0 +1,166 @@
import BasicInfo from '@/components/BasicInfo';
import KFModal from '@/components/KFModal';
import SubAreaTitle from '@/components/SubAreaTitle';
import { ApprovalData, ApprovalType } from '@/pages/System/Approval';
import { agreeApprovalReq, rejectApprovalReq } from '@/services/message';
import { parseJsonText } from '@/utils';
import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui';
import { Button, Form, Input, message, type ModalProps } from 'antd';

interface ApprovalModalProps extends Omit<ModalProps, 'onOk'> {
record: ApprovalData;
onOk: () => void;
}

function ApprovalModal({ record, onOk, ...rest }: ApprovalModalProps) {
const [form] = Form.useForm();
const content = parseJsonText(record.content);
const recordTypeName = record.type === ApprovalType.Dataset ? '数据集' : '模型';

const items =
record.type === ApprovalType.Dataset
? [
{
label: '数据集名称',
value: content.name,
},
{
label: '数据集分类',
value: content.dataType,
},
{
label: '研究方向',
value: content.dataTag,
},
{
label: '数据集描述',
value: content.description,
},
]
: [
{
label: '模型名称',
value: content.name,
},
{
label: '模型框架',
value: content.model_type,
},
{
label: '模型能力',
value: content.model_tag,
},
{
label: '模型描述',
value: content.description,
},
];

// 审批通过
const agreeApproval = async (remark?: string) => {
const [res] = await to(
agreeApprovalReq({
id: record.id,
result: remark,
}),
);
if (res) {
onOk?.();
}
};

// 审批拒绝
const rejectApproval = async (remark: string) => {
const [res] = await to(
rejectApprovalReq({
id: record.id,
result: remark,
}),
);
if (res) {
onOk?.();
}
};

const handleAgree = () => {
const remark = form.getFieldValue('remark') as string | undefined;
const remarkTrim = remark?.trim();
modalConfirm({
isDelete: false,
title: `审批通过后,将发布该${recordTypeName}`,
content: '是否确认通过?',
onOk: () => {
agreeApproval(remarkTrim);
},
});
};

const handleReject = () => {
const remark = form.getFieldValue('remark') as string | undefined;
const remarkTrim = remark?.trim();
if (!remarkTrim) {
message.error('请输入审批意见');
return;
}

modalConfirm({
isDelete: false,
title: `审批拒绝后,将不发布该${recordTypeName}`,
content: '是否确认拒绝?',
onOk: () => {
rejectApproval(remarkTrim);
},
});
};

return (
<KFModal
{...rest}
title="审核"
width={825}
footer={[
<Button key="agree" type="primary" onClick={handleAgree}>
审批通过
</Button>,
<Button key="reject" type="primary" danger onClick={handleReject}>
审批拒绝
</Button>,
]}
>
<SubAreaTitle
title="基本信息"
image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '15px' }}
></SubAreaTitle>
<BasicInfo
datas={items}
labelWidth={80}
labelAlign="justify"
style={{ marginBottom: 20, width: '100%' }}
></BasicInfo>
<Form name="form" layout="vertical" form={form} autoComplete="off">
<Form.Item
label="审核意见"
name="remark"
rules={[
{
required: false,
message: '请输入审核意见',
},
]}
>
<Input.TextArea
placeholder="请输入审核意见"
autoSize={{ minRows: 3, maxRows: 6 }}
maxLength={128}
showCount
allowClear
/>
</Form.Item>
</Form>
</KFModal>
);
}

export default ApprovalModal;

+ 15
- 0
react-ui/src/pages/System/Approval/components/StatusCell/index.less View File

@@ -0,0 +1,15 @@
.status-cell {
color: @text-color;

&--agree {
color: @success-color;
}

&--reject {
color: @error-color;
}

&--pending {
color: @text-color;
}
}

+ 36
- 0
react-ui/src/pages/System/Approval/components/StatusCell/index.tsx View File

@@ -0,0 +1,36 @@
/*
* @Author: 赵伟
* @Date: 2024-04-18 18:35:41
* @Description: 编辑器状态组件
*/
import { ApprovalStatus } from '@/enums';
import styles from './index.less';

export type DevEditorStatusInfo = {
text: string;
classname: string;
};

export const statusInfo: Record<ApprovalStatus, DevEditorStatusInfo> = {
[ApprovalStatus.Pending]: {
classname: styles['status-cell--pending'],
text: '待审核',
},
[ApprovalStatus.Agree]: {
classname: styles['status-cell--agree'],
text: '通过',
},
[ApprovalStatus.Reject]: {
classname: styles['status-cell--reject'],
text: '已拒绝',
},
};

function StatusCell(status?: ApprovalStatus | null) {
if (status === null || status === undefined || !statusInfo[status]) {
return <span>--</span>;
}
return <span className={statusInfo[status].classname}>{statusInfo[status].text}</span>;
}

export default StatusCell;

+ 29
- 0
react-ui/src/pages/System/Approval/index.less View File

@@ -0,0 +1,29 @@
.approval-list {
height: 100%;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
height: 50px;
margin-bottom: 10px;
padding: 0 30px;
background-image: url(@/assets/img/page-title-bg.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;
}

&__content {
height: calc(100% - 60px);
padding: 20px 30px 0;
background-color: white;
border-radius: 10px;
}

&__table {
height: calc(100% - 32px - 28px);
margin-top: 28px;
background-color: white;
border-radius: 10px;
}
}

+ 210
- 0
react-ui/src/pages/System/Approval/index.tsx View File

@@ -0,0 +1,210 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 审核列表
*/

import { ApprovalStatus, approvalStatusOptions } from '@/enums';
import { getApprovalListReq } from '@/services/message';
import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import {
Button,
Select,
Table,
Typography,
type TablePaginationConfig,
type TableProps,
} from 'antd';
import classNames from 'classnames';
import { useCallback, useEffect, useState } from 'react';
import ApprovalModal from './components/ApprovalModal';
import StatusCell from './components/StatusCell';
import styles from './index.less';

export interface ApprovalData {
id: number;
status: number;
result: null;
content: string;
applicant_id: number;
applicant_name: null;
applicant_time: Date;
approver_id: number;
approver_time: Date;
title: string;
type: ApprovalType;
url: string;
}

export enum ApprovalType {
Dataset = 'DATASET',
Model = 'MODEL',
}

const approvalTypeOptions = [
{ label: '数据集', value: ApprovalType.Dataset },
{ label: '模型', value: ApprovalType.Model },
];

const statusOptions = [{ label: '全部', value: '' }, ...approvalStatusOptions];

function ApprovalList() {
const [tableData, setTableData] = useState<ApprovalData[]>([]);
const [total, setTotal] = useState(0);
const [status, setStatus] = useState('');
const [pagination, setPagination] = useState<TablePaginationConfig>({
current: 1,
pageSize: 10,
});

// 获取审核列表
const getApprovalList = useCallback(async () => {
const params: Record<string, any> = {
current: pagination.current,
pageSize: pagination.pageSize,
status: status,
};
const [res] = await to(getApprovalListReq(params));
if (res) {
const { rows = [], total = 0 } = res;
setTableData(rows);
setTotal(total);
}
}, [pagination, status]);

useEffect(() => {
getApprovalList();
}, [getApprovalList]);

// 审核
const approval = (record: ApprovalData) => {
const { close } = openAntdModal(ApprovalModal, {
record: record,
onOk: () => {
close();
getApprovalList();
},
});
};

// 分页切换
const handleTableChange: TableProps<ApprovalData>['onChange'] = (
pagination,
_filters,
_sorter,
{ action },
) => {
if (action === 'paginate') {
setPagination(pagination);
}
};

const columns: TableProps<ApprovalData>['columns'] = [
{
title: '内容',
dataIndex: 'title',
key: 'title',
render: (title) => (
<Typography.Text
style={{ width: '100%' }}
ellipsis={{ tooltip: title.replace(/<\/?b>/g, '') }}
>
<span dangerouslySetInnerHTML={{ __html: title }}></span>
</Typography.Text>
),
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
width: 100,
render: tableCellRender(true, TableCellValueType.Enum, {
options: approvalTypeOptions,
}),
},
{
title: '申请者',
dataIndex: 'applicant_name',
key: 'applicant_name',
width: 180,
render: tableCellRender(true),
},
{
title: '申请时间',
dataIndex: 'applicant_time',
key: 'applicant_time',
width: 180,
render: tableCellRender(true, TableCellValueType.Date),
},
{
title: '审核意见',
dataIndex: 'result',
key: 'result',
width: 200,
render: tableCellRender(true),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: StatusCell,
},
{
title: '操作',
dataIndex: 'operation',
width: 150,
key: 'operation',
render: (_: any, record: ApprovalData) => (
<div>
{record.status === ApprovalStatus.Pending ? (
<Button type="link" size="small" key="stop" onClick={() => approval(record)}>
审核
</Button>
) : null}
</div>
),
},
];

return (
<div className={styles['approval-list']}>
<div className={styles['approval-list__header']}>
<div>审核管理</div>
</div>
<div className={styles['approval-list__content']}>
<div className={styles['approval-list__filter']}>
<span>状态:</span>
<Select
style={{ width: 100 }}
placeholder="请选择"
onChange={(value) => setStatus(value ?? '')}
options={statusOptions}
value={status}
allowClear
></Select>
</div>
<div className={classNames('vertical-scroll-table', styles['approval-list__table'])}>
<Table
dataSource={tableData}
columns={columns}
scroll={{ y: 'calc(100% - 55px)' }}
pagination={{
...pagination,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: () => `共${total}条`,
}}
onChange={handleTableChange}
rowKey="id"
/>
</div>
</div>
</div>
);
}

export default ApprovalList;

+ 1
- 1
react-ui/src/pages/System/Role/index.tsx View File

@@ -254,7 +254,7 @@ const RoleTableList: React.FC = () => {
{
title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
dataIndex: 'option',
width: '220px',
width: '240px',
valueType: 'option',
render: (_, record) => [
<Button


+ 1
- 1
react-ui/src/pages/System/User/index.tsx View File

@@ -281,7 +281,7 @@ const UserTableList: React.FC = () => {
{
title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
dataIndex: 'option',
width: '220px',
width: '240px',
valueType: 'option',
render: (_, record) => [
<Button


+ 4
- 4
react-ui/src/pages/Workspace/components/AssetsManagement/index.less View File

@@ -28,7 +28,7 @@
margin-top: 12px;
margin-bottom: 30px;
padding: 2px 7px;
color: @primary-color-secondary;
color: @primary-color;
font-size: 13px;
background-color: rgba(187, 210, 255, 0.29);
border-radius: 2px;
@@ -40,14 +40,14 @@
width: 40%;
text-align: left;

&:nth-child(3n+2) {
&:nth-child(3n + 2) {
width: 30%;
text-align: center;
width: 30%;
}

&:nth-child(3n) {
width: 30%;
text-align: right;
width: 30%;
}

&__title {


+ 1
- 1
react-ui/src/pages/Workspace/components/UserSpace/index.less View File

@@ -33,7 +33,7 @@
&__role {
display: inline-block;
padding: 1px 7px;
color: @primary-color-secondary;
color: @primary-color;
font-size: 13px;
background-color: rgba(187, 210, 255, 0.29);
border-radius: 2px;


+ 17
- 0
react-ui/src/services/dataset/index.js View File

@@ -98,6 +98,14 @@ export function getDatasetNextVersionReq(data) {
});
}

// 发布数据集
export function publishDatasetReq(data) {
return request(`/api/mmp/newdataset/publish`, {
method: 'POST',
data,
});
}


// ----------------------------模型---------------------------------

@@ -237,3 +245,12 @@ export function unpraiseResourceReq(id) {
});
}

// 发布模型
export function publishModelReq(data) {
return request(`/api/mmp/newmodel/publish`, {
method: 'POST',
data,
});
}



+ 66
- 0
react-ui/src/services/message/index.ts View File

@@ -0,0 +1,66 @@
/*
* @Author: 赵伟
* @Date: 2025-08-28 10:18:27
* @Description: 消息
*/

import { request } from '@umijs/max';

// 获取消息列表
export function getMessageListReq(params: any) {
return request(`/api/reader/gns/notification/gitlink/list`, {
method: 'GET',
params,
});
}

// 获取消息数量
export function getMessageCountReq(params: any) {
return request(`/api/reader/gns/notification/gitlink/count`, {
method: 'GET',
params,
skipLoading: true,
skipErrorHandler: true,
});
}

// 单条消息标记为已读
export function readMessagesReq(data: any, skipLoading: boolean) {
return request(`/api/writer/gns/notification/gitlink`, {
method: 'PUT',
data,
skipLoading,
});
}

// 删除消息
export function deleteMessagesReq(data: any) {
return request(`/api/writer/gns/notification/gitlink`, {
method: 'DELETE',
data,
});
}

// 获取审核列表
export function getApprovalListReq(params: any) {
return request(`/api/mmp/sysApproval/my-approve`, {
method: 'GET',
params,
});
}

// 审核通过
export function agreeApprovalReq(data: any) {
return request(`/api/mmp/sysApproval/approve`, {
method: 'POST',
data,
});
}

// 审核拒绝
export function rejectApprovalReq(data: any) {
return request(`/api/mmp/sysApproval/reject`, {
method: 'POST',
data,
});
}

+ 9
- 7
react-ui/src/styles/theme.less View File

@@ -5,13 +5,13 @@
*/

// 颜色
@primary-color: #1664ff; // 主色调
@primary-color-secondary: #4e89ff;
@primary-color-hover: #69b1ff;
@primary-color: #514cf9; // 主色调
@primary-color-hover: #7c79ff;
@primary-color-active: #2735fe;
@sider-background-color: #f2f5f7; // 侧边栏背景颜色
@background-color: #f9fafb; // 页面背景颜色
@text-color: #1d1d20;
@text-color-secondary: #575757;
@text-color: #0c0d0e;
@text-color-secondary: #494a6f;
@text-color-tertiary: #8a8a8a;
@text-placeholder-color: rgba(0, 0, 0, 0.25);
@text-disabled-color: rgba(0, 0, 0, 0.25);
@@ -20,10 +20,10 @@
@warning-color: #f98e1b;
@abort-color: #8a8a8a;
@pending-color: #ecb934;
@underline-color: #5d93ff;
@underline-color: #514cf9;
@border-color: #eaeaea;

@link-hover-color: #69b1ff;
@link-hover-color: #514cf9;
@heading-color: rgba(0, 0, 0, 0.85);
@input-icon-hover-color: rgba(0, 0, 0, 0.85);

@@ -92,6 +92,8 @@
// 导出变量
:export {
primaryColor: @primary-color;
primaryHoverColor: @primary-color-hover;
primaryActiveColor: @primary-color-active;
successColor: @success-color;
errorColor: @error-color;
warningColor: @warning-color;


+ 97
- 0
react-ui/src/utils/color.ts View File

@@ -0,0 +1,97 @@
/*
* @Author: 赵伟
* @Date: 2025-09-02 09:52:50
* @Description: 兼容 Antd 颜色设置
*/

// 安装依赖:npm i @ant-design/colors
import { generate } from '@ant-design/colors';
import { TinyColor } from '@ctrl/tinycolor';

type Options = {
/** 是否使用暗色算法(需传背景色,默认按 AntD 暗色背景) */
dark?: boolean;
backgroundColor?: string; // 仅 dark 为 true 时生效
};

export function derivePrimaryStates(seed: string, opts: Options = {}) {
const { dark = false, backgroundColor = '#141414' } = opts;
const palette = generate(seed, dark ? { theme: 'dark', backgroundColor } : undefined);

return {
// 基础主色
colorPrimary: palette[5], // 主色(第 6 阶)

// 状态
colorPrimaryHover: palette[4], // hover(第 5 阶)
colorPrimaryActive: palette[6], // active(第 7 阶)
colorPrimaryTextHover: palette[4], // 文本 hover
colorPrimaryTextActive: palette[6], // 文本 active

// 背景
colorPrimaryBg: palette[0], // 浅背景(第 1 阶)
colorPrimaryBgHover: palette[1], // 背景 hover(第 2 阶)

// 边框
colorPrimaryBorder: palette[2], // 普通边框(第 3 阶)
colorPrimaryBorderHover: palette[3], // 边框 hover(第 4 阶)

// 文本
colorPrimaryText: palette[5], // 主文本(等同于 colorPrimary)

// 备用(有时用于选中态阴影/描边)
colorPrimaryShadow: palette[6],
};
}

export function derivePrimaryStates2(color: string) {
const base = new TinyColor(color);
return {
colorPrimaryHover: base.lighten(10).toHexString(),
colorPrimaryActive: base.darken(10).toHexString(),
};
}

/**
* 给颜色值添加透明度.
*
* @param color - 颜色值, #ff0000、#888 或者 rgb(255, 0, 0)
* @param alpha - 透明的,0~1
* @return 颜色值,rgba
*/

export function addAlpha(color: string, alpha: number): string {
const alphaFixed = Math.max(0, Math.min(1, alpha)); // 保证在 [0,1]

// #rrggbb 或 #rgb
if (color.startsWith('#')) {
let r: number, g: number, b: number;

if (color.length === 4) {
// #rgb
r = parseInt(color[1] + color[1], 16);
g = parseInt(color[2] + color[2], 16);
b = parseInt(color[3] + color[3], 16);
} else if (color.length === 7) {
// #rrggbb
r = parseInt(color.slice(1, 3), 16);
g = parseInt(color.slice(3, 5), 16);
b = parseInt(color.slice(5, 7), 16);
} else {
throw new Error('Invalid hex color format');
}

return `rgba(${r}, ${g}, ${b}, ${alphaFixed})`;
}

// rgb(r,g,b)
const rgbMatch = color.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/i);
if (rgbMatch) {
const r = parseInt(rgbMatch[1], 10);
const g = parseInt(rgbMatch[2], 10);
const b = parseInt(rgbMatch[3], 10);
return `rgba(${r}, ${g}, ${b}, ${alphaFixed})`;
}

throw new Error('Unsupported color format');
}

+ 55
- 0
react-ui/src/utils/date.ts View File

@@ -1,3 +1,4 @@
import { serverNow } from '@/hooks/useServerTime';
import dayjs from 'dayjs';

/**
@@ -59,6 +60,60 @@ export const elapsedTime = (begin?: string | Date | null, end?: string | Date |
return elspsedArray.slice(0, 2).join('');
};

/**
* 计算相比现在的时间过去了多久
*
* @param {string | null | undefined} date - The starting date.
* @return {string} The formatted elapsed time string.
*/
export const ago = (date?: string | Date | null): string => {
if (date === undefined || date === null) {
return '--';
}

const beginDate = dayjs(date);
if (!beginDate.isValid()) {
return '--';
}

const endDate = dayjs(serverNow());
const timestamp = endDate.valueOf() - beginDate.valueOf();
if (timestamp <= 0) {
return '刚刚';
}
const duration = dayjs.duration(timestamp);
const years = duration.years();
const months = duration.months();
const days = duration.days();
const hours = duration.hours();
const minutes = duration.minutes();
const seconds = duration.seconds();
const elspsedArray = [];
if (years !== 0) {
elspsedArray.push(`${years}年`);
}
if (months !== 0) {
elspsedArray.push(`${months}个月`);
}
if (days !== 0) {
elspsedArray.push(`${days}天`);
}
if (hours !== 0) {
elspsedArray.push(`${hours}小时`);
}
if (minutes !== 0) {
elspsedArray.push(`${minutes}分`);
}
if (seconds !== 0) {
elspsedArray.push(`${seconds}秒`);
}

if (elspsedArray.length === 0) {
return '刚刚';
}
return elspsedArray[0] + '前';
};

/**
* 是否是有效的日期
*


Loading…
Cancel
Save