diff --git a/react-ui/config/routes.ts b/react-ui/config/routes.ts index d1f7c046..133cbbc1 100644 --- a/react-ui/config/routes.ts +++ b/react-ui/config/routes.ts @@ -589,7 +589,7 @@ export default [ ], }, { - name: '审核', + name: '审核管理', path: 'approval', component: './System/Approval', }, diff --git a/react-ui/src/app.tsx b/react-ui/src/app.tsx index 0f0777ca..779ee3b3 100644 --- a/react-ui/src/app.tsx +++ b/react-ui/src/app.tsx @@ -192,6 +192,9 @@ export const antd: RuntimeAntdConfig = (memo) => { 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'], @@ -204,15 +207,15 @@ export const antd: RuntimeAntdConfig = (memo) => { memo.theme.components ??= {}; memo.theme.components.Tabs = {}; memo.theme.components.Button = { - // defaultBg: 'rgba(22, 100, 255, 0.06)', - // defaultBorderColor: 'rgba(22, 100, 255, 0.11)', - // defaultColor: themes['textColor'], - // defaultHoverBg: 'rgba(22, 100, 255, 0.06)', - // defaultHoverBorderColor: 'rgba(22, 100, 255, 0.5)', - // defaultHoverColor: '#3F7FFF', - // defaultActiveBg: 'rgba(22, 100, 255, 0.12)', - // defaultActiveBorderColor: 'rgba(22, 100, 255, 0.75)', - // defaultActiveColor: themes['primaryColor'], + defaultBg: 'rgba(22, 100, 255, 0.06)', + defaultBorderColor: 'rgba(22, 100, 255, 0.11)', + defaultColor: themes['textColor'], + defaultHoverBg: 'rgba(22, 100, 255, 0.06)', + defaultHoverBorderColor: 'rgba(22, 100, 255, 0.5)', + defaultHoverColor: '#3F7FFF', + defaultActiveBg: 'rgba(22, 100, 255, 0.12)', + defaultActiveBorderColor: 'rgba(22, 100, 255, 0.75)', + defaultActiveColor: themes['primaryColor'], contentFontSize: parseInt(themes['fontSize']), }; memo.theme.components.Input = { diff --git a/react-ui/src/assets/img/home/model-item-bg-hover2.png b/react-ui/src/assets/img/home/model-item-bg-hover2.png new file mode 100644 index 00000000..70f48227 Binary files /dev/null and b/react-ui/src/assets/img/home/model-item-bg-hover2.png differ diff --git a/react-ui/src/components/MessageBroadcast/index.tsx b/react-ui/src/components/MessageBroadcast/index.tsx index 72b6b884..71d03d53 100644 --- a/react-ui/src/components/MessageBroadcast/index.tsx +++ b/react-ui/src/components/MessageBroadcast/index.tsx @@ -13,6 +13,7 @@ function MessageBroadcast() { const navigate = useNavigate(); const getMessageCount = useCallback(async () => { + if (!userId) return; const params: Record = { receiver: userId, type: -1, diff --git a/react-ui/src/enums/index.ts b/react-ui/src/enums/index.ts index 5f522c50..f016b4f8 100644 --- a/react-ui/src/enums/index.ts +++ b/react-ui/src/enums/index.ts @@ -184,3 +184,10 @@ export enum MessageStatus { UnRead = 1, Readed = 2, } + +// 审核状态 +export enum ApprovalStatus { + Pending = 0, + Agree = 1, + Reject = 2, +} diff --git a/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx b/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx index 544d040a..774fb30d 100644 --- a/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx @@ -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) || '--'} )} +
{ /> {info.praises_count}
+ {version ? ( diff --git a/react-ui/src/pages/Dataset/config.tsx b/react-ui/src/pages/Dataset/config.tsx index 2f43bda5..04e9ee7b 100644 --- a/react-ui/src/pages/Dataset/config.tsx +++ b/react-ui/src/pages/Dataset/config.tsx @@ -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; // 获取详情 compareVersion: (params: any) => Promise; // 版本对比 getNextVersion: (params: any) => Promise; // 获取下一个版本 + publish: (params: any) => Promise; // 发布 name: string; // 名称 typeParamKey: 'data_type' | 'model_type'; // 类型参数名称,获取资源列表接口使用 tagParamKey: 'data_tag' | 'model_tag'; // 标签参数名称,获取资源列表接口使用 @@ -76,6 +79,7 @@ export const resourceConfig: Record = { getInfo: getDatasetInfo, compareVersion: compareDatasetVersion, getNextVersion: getDatasetNextVersionReq, + publish: publishDatasetReq, name: '数据集', typeParamKey: 'data_type', tagParamKey: 'data_tag', @@ -116,6 +120,7 @@ export const resourceConfig: Record = { getInfo: getModelInfo, compareVersion: compareModelVersion, getNextVersion: getModelNextVersionReq, + publish: publishModelReq, name: '模型', typeParamKey: 'model_type', tagParamKey: 'model_tag', 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 abea0841..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: { - y: 0, - opacity: 1, + offscreen: (down: boolean) => ({ + y: 100, + opacity: 0, transition: { ease: 'linear', - duration: 0.3, + 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 () => { @@ -56,11 +64,11 @@ function ModelBlock() { return ( { 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 index 7f35d759..b99e753f 100644 --- a/react-ui/src/pages/Message/components/Content/index.less +++ b/react-ui/src/pages/Message/components/Content/index.less @@ -8,6 +8,7 @@ &__tabs { display: flex; + flex: none; align-items: center; height: 76px; padding: 0 30px; @@ -21,6 +22,14 @@ &--selected, &:hover { color: @text-color; + font-weight: 500; + } + } + + :global { + .ant-btn:first-of-type { + margin-right: 10px; + margin-left: auto; } } } @@ -40,9 +49,7 @@ } &__list { - display: flex; flex: 1; - flex-direction: column; width: 100%; overflow-y: auto; @@ -73,8 +80,14 @@ } } + &__content { + flex: 1; + margin-right: 10px; + } + &__time { display: block; + flex: none; margin-left: auto; color: @text-color-secondary; } diff --git a/react-ui/src/pages/Message/components/Content/index.tsx b/react-ui/src/pages/Message/components/Content/index.tsx index df52fcc8..8b157ff9 100644 --- a/react-ui/src/pages/Message/components/Content/index.tsx +++ b/react-ui/src/pages/Message/components/Content/index.tsx @@ -1,89 +1,49 @@ +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 } from '@umijs/max'; +import { useModel, useNavigate } from '@umijs/max'; import { Button, Checkbox, Pagination, PaginationProps, - type TablePaginationConfig, Typography, + message, + type TablePaginationConfig, } from 'antd'; import classNames from 'classnames'; import { useCallback, useEffect, useMemo, useState } from 'react'; import styles from './index.less'; -// { -// "id": 673396, -// "status": 1, -// "content": "陈志航 已加入项目 复杂智能软件系统研究/模型训练项目UI设计", -// "notification_url": "https://www.gitlink.org.cn/ci4s/UIdesign", -// "source": "ProjectMemberJoined", -// "created_at": "2025-08-06 08:54:54", -// "time_ago": "23天前", -// "type": "notification" -// } - -// receiver:消息接收者ID (必传) -// status:值未-1时,获取全部信息;值为1时,只获取未读消息;值为2时,获取已读消息 -// type:值为-1时,获取全部信息;值为1时,获取系统消息;值为2时,获取@我消息 -// sources:消息来源 -// page:页码:值为-1时(默认值),不开启分页; -// size:页大小页码,默认20 - -// 统计数量的接口 -// receiver:消息接收者ID (必传) -// type:值为-1时,获取全部信息;值为1时,获取系统消息;值为2时,获取@我消息 - -// 消息列表接口返回类型 -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; -} - export type MessageContentProps = { messageType: MessageType; + messageStatus: MessageStatus; + pagination: TablePaginationConfig; + onStatusChange: (status: MessageStatus) => void; + onPaginationChange: (pagination: TablePaginationConfig) => void; }; -function MessageContent({ messageType }: MessageContentProps) { +function MessageContent({ + messageType, + messageStatus, + pagination, + onStatusChange, + onPaginationChange, +}: MessageContentProps) { const { initialState } = useModel('@@initialState'); const { currentUser } = initialState || {}; const { userId } = currentUser || {}; - const [pagination, setPagination] = useState({ - current: 1, - pageSize: 20, - }); + const setCacheState = useCacheState()[1]; const [messages, setMessages] = useState(undefined); const [allTotal, setAllTotal] = useState(0); const [unreadTotal, setUnreadTotal] = useState(undefined); - const [messageStatus, setMessageStatus] = useState(MessageStatus.UnRead); const [isDelete, setIsDelete] = useState(false); const messageIds = useMemo(() => messages?.map((v) => v.id), [messages]); const [ @@ -95,6 +55,7 @@ function MessageContent({ messageType }: MessageContentProps) { isSingleMessagesChecked, checkSingleMessages, ] = useCheck(messageIds ?? []); + const navigate = useNavigate(); const tabs = useMemo( () => [ @@ -112,11 +73,13 @@ function MessageContent({ messageType }: MessageContentProps) { ); const getMessages = useCallback(async () => { + if (!userId) return; + const params: Record = { receiver: userId, status: messageStatus, type: messageType, - page: pagination.current! - 1, + page: pagination.current, size: pagination.pageSize, }; const [res] = await to(getMessageListReq(params)); @@ -129,25 +92,71 @@ function MessageContent({ messageType }: MessageContentProps) { } }, [pagination, userId, messageStatus, messageType]); - // 标记已读 - const readMessages = async (ids?: number[]) => { - const params: Record = { - ids: ids, - }; - const [res] = await to(readMessagesReq(params)); - if (res && res.data) { + // 标记为已读 + 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[]) => { + const deleteMessages = async (ids: number[]) => { + if (ids.length <= 0) { + message.error('请选择要删除的消息'); + return; + } + const params: Record = { - ids: ids, + notificationIds: ids.join(','), + receiver: userId, + type: messageType, }; const [res] = await to(deleteMessagesReq(params)); - if (res && res.data) { - getMessages(); + if (res) { + cancelBatchDelete(); + // 如果是一页的唯一数据,删除后,请求前一页的数据 + // 否则直接刷新这一页的数据 + onPaginationChange({ + ...pagination, + current: + ids.length === messages?.length + ? Math.max(1, pagination.current! - 1) + : pagination.current, + }); } }; @@ -161,25 +170,46 @@ function MessageContent({ messageType }: MessageContentProps) { getMessages(); }, [getMessages]); + // 重置批量删除状态、分页 useEffect(() => { cancelBatchDelete(); }, [messageType, messageStatus, cancelBatchDelete]); // 批量删除 const handleBatchDelete = () => { + if (selectedMessages.length <= 0) { + message.error('请选择要删除的消息'); + return; + } + modalConfirm({ title: '删除后,消息不可恢复', content: '是否确认删除?', onOk: () => { - setIsDelete(false); - setSelectedMessages([]); + 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) => { - setPagination({ + onPaginationChange({ current: page, pageSize: pageSize, }); @@ -195,52 +225,34 @@ function MessageContent({ messageType }: MessageContentProps) { [styles['message-content__tabs__item--selected']]: item.status === messageStatus, })} onClick={() => { - setMessageStatus(item.status); + onStatusChange(item.status); }} > - {item.title + (item.total ? `(${item.total})` : '')} + {item.title + (item.total !== undefined ? `(${item.total})` : '')} ))} {isDelete ? ( <> - - + ) : ( <> {messageType === MessageType.Mine && allTotal > 0 && ( - + + )} + {allTotal > 0 && ( + readMessages()}> + 一键已读 + )} - )} @@ -269,15 +281,19 @@ function MessageContent({ messageType }: MessageContentProps) { <>
{messages.map((message) => ( -
+
hanldeMessageClick(message)} + > {messageType === MessageType.Mine && isDelete && ( { - e.stopPropagation(); + onChange={() => { checkSingleMessages(message.id); }} + onClick={(e) => e.stopPropagation()} > )} {messageStatus === MessageStatus.All && ( @@ -292,9 +308,13 @@ function MessageContent({ messageType }: MessageContentProps) { {message.status === MessageStatus.UnRead ? '未读' : '已读'}
)} - - {message.content} + /g, '') }} + > + +
{ago(message.created_at)}
@@ -302,16 +322,22 @@ function MessageContent({ messageType }: MessageContentProps) { )} {messageType === MessageType.Mine && ( @@ -336,8 +362,8 @@ function MessageContent({ messageType }: MessageContentProps) { className={styles['message-content__empty']} type={EmptyType.NoData} title="暂无数据" - content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} - hasFooter={true} + content={`没有${messageStatus === MessageStatus.UnRead ? '未读' : ''}消息`} + hasFooter onButtonClick={getMessages} /> )} diff --git a/react-ui/src/pages/Message/components/Menu/index.tsx b/react-ui/src/pages/Message/components/Menu/index.tsx index 351c1873..3f5b0513 100644 --- a/react-ui/src/pages/Message/components/Menu/index.tsx +++ b/react-ui/src/pages/Message/components/Menu/index.tsx @@ -23,10 +23,10 @@ const menus = [ export type MessageMenuProps = { messageType: MessageType; - onChange: (type: MessageType) => void; + onTypeChange: (type: MessageType) => void; }; -function MessageMenu({ messageType: currentType, onChange }: MessageMenuProps) { +function MessageMenu({ messageType: currentType, onTypeChange }: MessageMenuProps) { return (
消息列表
@@ -36,7 +36,7 @@ function MessageMenu({ messageType: currentType, onChange }: MessageMenuProps) { className={classNames(styles['message-menu__item'], { [styles['message-menu__item--selected']]: item.type === currentType, })} - onClick={() => onChange(item.type)} + onClick={() => onTypeChange(item.type)} > diff --git a/react-ui/src/pages/Message/index.less b/react-ui/src/pages/Message/index.less index c8232486..745c538c 100644 --- a/react-ui/src/pages/Message/index.less +++ b/react-ui/src/pages/Message/index.less @@ -3,6 +3,6 @@ flex-direction: row; gap: 0 20px; height: 100%; - padding: 75px 260px 30px; + 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 index 10bcdcf3..aba242cd 100644 --- a/react-ui/src/pages/Message/index.tsx +++ b/react-ui/src/pages/Message/index.tsx @@ -1,18 +1,79 @@ -import { MessageType } from '@/enums'; +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 [messageType, setMessageType] = useState(MessageType.System); + 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)} + onTypeChange={(type) => { + setMessageType(type); + resetToFirstPage(); + }} messageType={messageType} > - + { + setMessageStatus(status); + resetToFirstPage(); + }} + pagination={pagination} + onPaginationChange={setPagination} + >
); } 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.tsx b/react-ui/src/pages/System/Approval/index.tsx index 20a874e4..5691d65a 100644 --- a/react-ui/src/pages/System/Approval/index.tsx +++ b/react-ui/src/pages/System/Approval/index.tsx @@ -4,217 +4,82 @@ * @Description: 开发环境列表 */ -import { CodeConfigData } from '@/components/CodeSelectorModal'; -import KFIcon from '@/components/KFIcon'; -import { DevEditorStatus } from '@/enums'; -import { useCacheState } from '@/hooks/useCacheState'; -import { useSystemResource } from '@/hooks/useComputingResource'; -import { DatasetData, ModelData } from '@/pages/Dataset/config'; -import { - deleteEditorReq, - getEditorListReq, - startEditorReq, - stopEditorReq, -} from '@/services/developmentEnvironment'; -import themes from '@/styles/theme.less'; -import { parseJsonText } from '@/utils'; -import { formatCodeConfig, formatDataset, formatModel } from '@/utils/format'; +import { ApprovalStatus } from '@/enums'; +import { getApprovalListReq } from '@/services/message'; +import { openAntdModal } from '@/utils/modal'; import { to } from '@/utils/promise'; -import SessionStorage from '@/utils/sessionStorage'; import tableCellRender, { TableCellValueType } from '@/utils/table'; -import { modalConfirm } from '@/utils/ui'; -import { useNavigate } from '@umijs/max'; -import { - App, - Button, - ConfigProvider, - Table, - type TablePaginationConfig, - type TableProps, -} from 'antd'; +import { Button, Table, Typography, type TablePaginationConfig, type TableProps } from 'antd'; import classNames from 'classnames'; -import { useCallback, useState } from 'react'; -// import CreateMirrorModal from '../components/CreateMirrorModal'; -// import EditorStatusCell from '../components/EditorStatusCell'; +import { useCallback, useEffect, useState } from 'react'; +import ApprovalModal from './components/ApprovalModal'; +import StatusCell from './components/StatusCell'; import styles from './index.less'; -export type EditorData = { +export interface ApprovalData { id: number; - name: string; - status: string; - computing_resource: string; - update_by: string; - create_time: string; + 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; - computing_resource_id: number; - dataset?: string | DatasetData; - model?: string | ModelData; - image?: string; - code_config?: string | CodeConfigData; -}; +} + +export enum ApprovalType { + Dataset = 'DATASET', + Model = 'MODEL', +} + +const approvalTypeOptions = [ + { label: '数据集', value: ApprovalType.Dataset }, + { label: '模型', value: ApprovalType.Model }, +]; function ApprovalList() { - const navigate = useNavigate(); - const [cacheState, setCacheState] = useCacheState(); - const { message } = App.useApp(); - const [tableData, setTableData] = useState([]); + const [tableData, setTableData] = useState([]); const [total, setTotal] = useState(0); - const [pagination, setPagination] = useState( - cacheState?.pagination ?? { - current: 1, - pageSize: 10, - }, - ); - const getResourceDescription = useSystemResource(); + const [pagination, setPagination] = useState({ + current: 1, + pageSize: 10, + }); - // 获取编辑器列表 - const getEditorList = useCallback(async () => { + // 获取审核列表 + const getApprovalList = useCallback(async () => { const params: Record = { - page: pagination.current! - 1, - size: pagination.pageSize, + current: pagination.current, + pageSize: pagination.pageSize, }; - const [res] = await to(getEditorListReq(params)); - if (res && res.data) { - const { content = [], totalElements = 0 } = res.data; - content.forEach((item: EditorData) => { - item.dataset = typeof item.dataset === 'string' ? parseJsonText(item.dataset) : null; - item.model = typeof item.model === 'string' ? parseJsonText(item.model) : null; - item.image = typeof item.image === 'string' ? parseJsonText(item.image) : null; - item.code_config = - typeof item.code_config === 'string' ? parseJsonText(item.code_config) : null; - }); - setTableData(content); - setTotal(totalElements); - } - }, [pagination]); - - // useEffect(() => { - // getEditorList(); - // }, [getEditorList]); - - // 删除编辑器 - const deleteEditor = async (id: number) => { - const [res] = await to(deleteEditorReq(id)); + const [res] = await to(getApprovalListReq(params)); if (res) { - message.success('删除成功'); - // 如果是一页的唯一数据,删除后,请求第一页的数据 - // 否则直接刷新这一页的数据 - setPagination((prev) => { - return { - ...prev, - current: tableData.length === 1 ? Math.max(1, prev.current! - 1) : prev.current, - }; - }); + const { rows = [], total = 0 } = res; + setTableData(rows); + setTotal(total); } - }; - - // 启动编辑器 - const startEditor = async (id: number) => { - const [res] = await to(startEditorReq(id)); - if (res) { - message.success('操作成功'); - getEditorList(); - } - }; - - // 停止编辑器 - const stopEditor = async (id: number) => { - modalConfirm({ - title: '停止后,该编辑器将不可使用', - content: '是否确认停止?', - isDelete: false, - onOk: async () => { - const [res] = await to(stopEditorReq(id)); - if (res) { - message.success('操作成功'); - getEditorList(); - } - }, - }); - }; + }, [pagination]); - // 制作镜像 - // const createMirror = (id: number) => { - // const { close } = openAntdModal(CreateMirrorModal, { - // envId: id, - // onOk: () => { - // close(); - // }, - // }); - // }; + useEffect(() => { + getApprovalList(); + }, [getApprovalList]); - // 处理删除 - const handleEditorDelete = (record: EditorData) => { - modalConfirm({ - title: '删除后,该编辑器将不可恢复', - content: '是否确认删除?', + // 审核 + const approval = (record: ApprovalData) => { + const { close } = openAntdModal(ApprovalModal, { + record: record, onOk: () => { - deleteEditor(record.id); + close(); + getApprovalList(); }, }); }; - // 创建编辑器 - const createEditor = () => { - navigate(`/developmentEnvironment/create`); - setCacheState({ - pagination, - }); - }; - - // 跳转编辑器页面 - const gotoEditorPage = (record: EditorData, e: React.MouseEvent) => { - e.stopPropagation(); - - setCacheState({ - pagination, - }); - - SessionStorage.setItem(SessionStorage.editorUrlKey, record.url); - navigate(`/developmentEnvironment/editor`); - }; - - // 去数据集 - const gotoDataset = (record: EditorData, e: React.MouseEvent) => { - e.stopPropagation(); - - const dataset = record.dataset as DatasetData; - const link = formatDataset(dataset)?.link; - if (link) { - setCacheState({ - pagination, - }); - navigate(link); - } - }; - - // 去模型 - const gotoModel = (record: EditorData, e: React.MouseEvent) => { - e.stopPropagation(); - - const model = record.model as ModelData; - const link = formatModel(model)?.link; - if (link) { - setCacheState({ - pagination, - }); - navigate(link); - } - }; - - // 打开代码配置仓库 - const gotoCodeConfig = (record: EditorData, e: React.MouseEvent) => { - e.stopPropagation(); - - const codeConfig = record.code_config as CodeConfigData; - const url = formatCodeConfig(codeConfig)?.url; - if (url) { - window.open(url, '_blank'); - } - }; - // 分页切换 - const handleTableChange: TableProps['onChange'] = ( + const handleTableChange: TableProps['onChange'] = ( pagination, _filters, _sorter, @@ -225,147 +90,69 @@ function ApprovalList() { } }; - const columns: TableProps['columns'] = [ - { - title: '编辑器名称', - dataIndex: 'name', - key: 'name', - width: '12%', - render: (text, record, index) => - record.url && record.status === DevEditorStatus.Running - ? tableCellRender(true, TableCellValueType.Link, { - onClick: gotoEditorPage, - })(text, record, index) - : tableCellRender(true, TableCellValueType.Text)(text, record, index), - }, + const columns: TableProps['columns'] = [ { - title: '计算资源', - dataIndex: 'computing_resource', - key: 'computing_resource', - width: '11%', - render: tableCellRender(), - }, - { - title: '资源规格', - dataIndex: 'computing_resource_id', - key: 'computing_resource_id', - width: '11%', - render: tableCellRender(true, TableCellValueType.Custom, { - format: getResourceDescription, - }), - }, - { - title: '数据集', - dataIndex: ['dataset', 'showValue'], - key: 'dataset', - width: '11%', - render: tableCellRender(true, TableCellValueType.Link, { - onClick: gotoDataset, - }), + title: '内容', + dataIndex: 'title', + key: 'title', + render: (title) => ( + /g, '') }} + > + + + ), }, { - title: '模型', - dataIndex: ['model', 'showValue'], - key: 'model', - width: '11%', - render: tableCellRender(true, TableCellValueType.Link, { - onClick: gotoModel, + title: '类型', + dataIndex: 'type', + key: 'type', + width: 100, + render: tableCellRender(true, TableCellValueType.Enum, { + options: approvalTypeOptions, }), }, { - title: '代码配置', - dataIndex: ['code_config', 'showValue'], - key: 'code_config', - width: '11%', - render: tableCellRender(true, TableCellValueType.Link, { - onClick: gotoCodeConfig, - }), + title: '申请者', + dataIndex: 'applicant_name', + key: 'applicant_name', + width: 180, + render: tableCellRender(true), }, { - title: '镜像', - dataIndex: ['image', 'showValue'], - key: 'image', - width: '11%', - render: tableCellRender(true), + title: '申请时间', + dataIndex: 'applicant_time', + key: 'applicant_time', + width: 180, + render: tableCellRender(true, TableCellValueType.Date), }, { - title: '创建者', - dataIndex: 'update_by', - key: 'update_by', - width: '11%', + title: '审核意见', + dataIndex: 'result', + key: 'result', + width: 200, render: tableCellRender(true), }, { - title: '创建时间', - dataIndex: 'create_time', - key: 'create_time', - width: '11%', - render: tableCellRender(true, TableCellValueType.Date), + title: '状态', + dataIndex: 'status', + key: 'status', + width: 100, + render: StatusCell, }, - // { - // title: '状态', - // dataIndex: 'status', - // key: 'status', - // width: 100, - // render: EditorStatusCell, - // }, { title: '操作', dataIndex: 'operation', - width: 270, + width: 150, key: 'operation', - render: (_: any, record: EditorData) => ( + render: (_: any, record: ApprovalData) => (
- {record.status === DevEditorStatus.Pending || - record.status === DevEditorStatus.Running ? ( - - ) : ( - - )} - {/* {record.status !== DevEditorStatus.Running ? ( - - ) : null} */} - - - + ) : null}
), }, @@ -374,15 +161,7 @@ function ApprovalList() { return (
-
审核
- +
审核管理
{ { title: , dataIndex: 'option', - width: '220px', + width: '240px', valueType: 'option', render: (_, record) => [