navigate('/workspace')}>
+
gotoPageIfLogin('/workspace')}>
开始使用
diff --git a/react-ui/src/pages/Home/components/Model/index.less b/react-ui/src/pages/Home/components/Model/index.less
index 7f3898ef..bf2e8ba4 100644
--- a/react-ui/src/pages/Home/components/Model/index.less
+++ b/react-ui/src/pages/Home/components/Model/index.less
@@ -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 {
diff --git a/react-ui/src/pages/Home/components/Model/index.tsx b/react-ui/src/pages/Home/components/Model/index.tsx
index c3b0e621..1d7c8c6a 100644
--- a/react-ui/src/pages/Home/components/Model/index.tsx
+++ b/react-ui/src/pages/Home/components/Model/index.tsx
@@ -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
([]);
+ 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}`,
diff --git a/react-ui/src/pages/Home/components/ScrollReveal/index.tsx b/react-ui/src/pages/Home/components/ScrollReveal/index.tsx
deleted file mode 100644
index a043234e..00000000
--- a/react-ui/src/pages/Home/components/ScrollReveal/index.tsx
+++ /dev/null
@@ -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(null);
- const isInView = useInView(ref, { amount: 'all' });
-
- return (
-
- {children}
-
- );
-}
-
-export default ScrollReveal;
diff --git a/react-ui/src/pages/Home/components/Service/index.tsx b/react-ui/src/pages/Home/components/Service/index.tsx
index 2f28a5f0..828d0635 100644
--- a/react-ui/src/pages/Home/components/Service/index.tsx
+++ b/react-ui/src/pages/Home/components/Service/index.tsx
@@ -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() {
diff --git a/react-ui/src/pages/Home/components/Statistics/index.less b/react-ui/src/pages/Home/components/Statistics/index.less
index 848e713d..8c6b3369 100644
--- a/react-ui/src/pages/Home/components/Statistics/index.less
+++ b/react-ui/src/pages/Home/components/Statistics/index.less
@@ -9,6 +9,7 @@
&__item {
display: flex;
align-items: center;
+ width: 9rem;
&__icon {
width: 3.75rem;
diff --git a/react-ui/src/pages/Message/components/Content/index.less b/react-ui/src/pages/Message/components/Content/index.less
new file mode 100644
index 00000000..b99e753f
--- /dev/null
+++ b/react-ui/src/pages/Message/components/Content/index.less
@@ -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;
+ }
+}
diff --git a/react-ui/src/pages/Message/components/Content/index.tsx b/react-ui/src/pages/Message/components/Content/index.tsx
new file mode 100644
index 00000000..8b157ff9
--- /dev/null
+++ b/react-ui/src/pages/Message/components/Content/index.tsx
@@ -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(undefined);
+ const [allTotal, setAllTotal] = useState(0);
+ const [unreadTotal, setUnreadTotal] = useState(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 = {
+ 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 = 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 = {
+ 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 (
+
+
+ {tabs.map((item) => (
+
{
+ onStatusChange(item.status);
+ }}
+ >
+ {item.title + (item.total !== undefined ? `(${item.total})` : '')}
+
+ ))}
+
+ {isDelete ? (
+ <>
+
+ 取消
+
+
+ 删除
+
+ >
+ ) : (
+ <>
+ {messageType === MessageType.Mine && allTotal > 0 && (
+
setIsDelete(true)}>
+ 批量删除
+
+ )}
+ {allTotal > 0 && (
+
readMessages()}>
+ 一键已读
+
+ )}
+ >
+ )}
+
+
+ {isDelete && (
+
+
+ 全选
+
+
+ 已选
+
+ {selectedMessages.length}
+
+ 项
+
+
+ )}
+
+ {messages && messages.length > 0 && (
+ <>
+
+ {messages.map((message) => (
+
hanldeMessageClick(message)}
+ >
+ {messageType === MessageType.Mine && isDelete && (
+
{
+ checkSingleMessages(message.id);
+ }}
+ onClick={(e) => e.stopPropagation()}
+ >
+ )}
+ {messageStatus === MessageStatus.All && (
+
+ {message.status === MessageStatus.UnRead ? '未读' : '已读'}
+
+ )}
+
/g, '') }}
+ >
+
+
+
+
+ {ago(message.created_at)}
+
+ {message.status === MessageStatus.UnRead && (
+
+ )}
+ {messageType === MessageType.Mine && (
+
+ )}
+
+ ))}
+
+
+ >
+ )}
+ {messages && messages.length === 0 && (
+
+ )}
+
+ );
+}
+
+export default MessageContent;
diff --git a/react-ui/src/pages/Message/components/Menu/index.less b/react-ui/src/pages/Message/components/Menu/index.less
new file mode 100644
index 00000000..dbfba954
--- /dev/null
+++ b/react-ui/src/pages/Message/components/Menu/index.less
@@ -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;
+ }
+ }
+}
diff --git a/react-ui/src/pages/Message/components/Menu/index.tsx b/react-ui/src/pages/Message/components/Menu/index.tsx
new file mode 100644
index 00000000..3f5b0513
--- /dev/null
+++ b/react-ui/src/pages/Message/components/Menu/index.tsx
@@ -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 (
+
+
消息列表
+ {menus.map((item) => (
+
onTypeChange(item.type)}
+ >
+

+

+
{item.title}
+
+ ))}
+
+ );
+}
+
+export default MessageMenu;
diff --git a/react-ui/src/pages/Message/index.less b/react-ui/src/pages/Message/index.less
new file mode 100644
index 00000000..745c538c
--- /dev/null
+++ b/react-ui/src/pages/Message/index.less
@@ -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));
+}
diff --git a/react-ui/src/pages/Message/index.tsx b/react-ui/src/pages/Message/index.tsx
new file mode 100644
index 00000000..aba242cd
--- /dev/null
+++ b/react-ui/src/pages/Message/index.tsx
@@ -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(
+ cacheState?.pagination ?? {
+ current: 1,
+ pageSize: 20,
+ },
+ );
+ const [messageStatus, setMessageStatus] = useState(
+ cacheState?.messageStatus ?? MessageStatus.UnRead,
+ );
+
+ // 重置页面为第一页
+ const resetToFirstPage = () => {
+ setPagination((prev) => ({
+ ...prev,
+ current: 1,
+ }));
+ };
+
+ return (
+
+ {
+ setMessageType(type);
+ resetToFirstPage();
+ }}
+ messageType={messageType}
+ >
+ {
+ setMessageStatus(status);
+ resetToFirstPage();
+ }}
+ pagination={pagination}
+ onPaginationChange={setPagination}
+ >
+
+ );
+}
+
+export default MessagePage;
diff --git a/react-ui/src/pages/System/Approval/components/ApprovalModal/index.less b/react-ui/src/pages/System/Approval/components/ApprovalModal/index.less
new file mode 100644
index 00000000..1494eb96
--- /dev/null
+++ b/react-ui/src/pages/System/Approval/components/ApprovalModal/index.less
@@ -0,0 +1,9 @@
+.approval-info {
+ margin-bottom: 20px;
+
+ &__item {
+ display: flex;
+ align-items: center;
+ margin-bottom: 10px;
+ }
+}
diff --git a/react-ui/src/pages/System/Approval/components/ApprovalModal/index.tsx b/react-ui/src/pages/System/Approval/components/ApprovalModal/index.tsx
new file mode 100644
index 00000000..6e629a90
--- /dev/null
+++ b/react-ui/src/pages/System/Approval/components/ApprovalModal/index.tsx
@@ -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 {
+ 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 (
+
+ 审批通过
+ ,
+ ,
+ ]}
+ >
+
+
+
+
+
+
+
+ );
+}
+
+export default ApprovalModal;
diff --git a/react-ui/src/pages/System/Approval/components/StatusCell/index.less b/react-ui/src/pages/System/Approval/components/StatusCell/index.less
new file mode 100644
index 00000000..17a5e5ef
--- /dev/null
+++ b/react-ui/src/pages/System/Approval/components/StatusCell/index.less
@@ -0,0 +1,15 @@
+.status-cell {
+ color: @text-color;
+
+ &--agree {
+ color: @success-color;
+ }
+
+ &--reject {
+ color: @error-color;
+ }
+
+ &--pending {
+ color: @text-color;
+ }
+}
diff --git a/react-ui/src/pages/System/Approval/components/StatusCell/index.tsx b/react-ui/src/pages/System/Approval/components/StatusCell/index.tsx
new file mode 100644
index 00000000..23adf66e
--- /dev/null
+++ b/react-ui/src/pages/System/Approval/components/StatusCell/index.tsx
@@ -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.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 --;
+ }
+ return {statusInfo[status].text};
+}
+
+export default StatusCell;
diff --git a/react-ui/src/pages/System/Approval/index.less b/react-ui/src/pages/System/Approval/index.less
new file mode 100644
index 00000000..16adf456
--- /dev/null
+++ b/react-ui/src/pages/System/Approval/index.less
@@ -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;
+ }
+}
diff --git a/react-ui/src/pages/System/Approval/index.tsx b/react-ui/src/pages/System/Approval/index.tsx
new file mode 100644
index 00000000..47dcd88c
--- /dev/null
+++ b/react-ui/src/pages/System/Approval/index.tsx
@@ -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([]);
+ const [total, setTotal] = useState(0);
+ const [status, setStatus] = useState('');
+ const [pagination, setPagination] = useState({
+ current: 1,
+ pageSize: 10,
+ });
+
+ // 获取审核列表
+ const getApprovalList = useCallback(async () => {
+ const params: Record = {
+ 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['onChange'] = (
+ pagination,
+ _filters,
+ _sorter,
+ { action },
+ ) => {
+ if (action === 'paginate') {
+ setPagination(pagination);
+ }
+ };
+
+ const columns: TableProps['columns'] = [
+ {
+ title: '内容',
+ dataIndex: 'title',
+ key: 'title',
+ render: (title) => (
+ /g, '') }}
+ >
+
+
+ ),
+ },
+ {
+ 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) => (
+
+ {record.status === ApprovalStatus.Pending ? (
+
+ ) : null}
+
+ ),
+ },
+ ];
+
+ return (
+
+
+
+
+ 状态:
+
+
+
+
`共${total}条`,
+ }}
+ onChange={handleTableChange}
+ rowKey="id"
+ />
+
+
+
+ );
+}
+
+export default ApprovalList;
diff --git a/react-ui/src/pages/System/Role/index.tsx b/react-ui/src/pages/System/Role/index.tsx
index c7a4e9cf..23215104 100644
--- a/react-ui/src/pages/System/Role/index.tsx
+++ b/react-ui/src/pages/System/Role/index.tsx
@@ -254,7 +254,7 @@ const RoleTableList: React.FC = () => {
{
title: ,
dataIndex: 'option',
- width: '220px',
+ width: '240px',
valueType: 'option',
render: (_, record) => [