| @@ -589,7 +589,7 @@ export default [ | |||||
| ], | ], | ||||
| }, | }, | ||||
| { | { | ||||
| name: '审核', | |||||
| name: '审核管理', | |||||
| path: 'approval', | path: 'approval', | ||||
| component: './System/Approval', | component: './System/Approval', | ||||
| }, | }, | ||||
| @@ -192,6 +192,9 @@ export const antd: RuntimeAntdConfig = (memo) => { | |||||
| colorPrimary: themes['primaryColor'], | colorPrimary: themes['primaryColor'], | ||||
| colorPrimaryHover: themes['primaryHoverColor'], | colorPrimaryHover: themes['primaryHoverColor'], | ||||
| colorPrimaryActive: themes['primaryActiveColor'], | colorPrimaryActive: themes['primaryActiveColor'], | ||||
| colorPrimaryText: themes['primaryColor'], | |||||
| colorPrimaryTextHover: themes['primaryHoverColor'], | |||||
| colorPrimaryTextActive: themes['primaryActiveColor'], | |||||
| // colorPrimaryBg: 'rgba(81, 76, 249, 0.07)', | // colorPrimaryBg: 'rgba(81, 76, 249, 0.07)', | ||||
| colorSuccess: themes['successColor'], | colorSuccess: themes['successColor'], | ||||
| colorError: themes['errorColor'], | colorError: themes['errorColor'], | ||||
| @@ -204,15 +207,15 @@ export const antd: RuntimeAntdConfig = (memo) => { | |||||
| memo.theme.components ??= {}; | memo.theme.components ??= {}; | ||||
| memo.theme.components.Tabs = {}; | memo.theme.components.Tabs = {}; | ||||
| memo.theme.components.Button = { | 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']), | contentFontSize: parseInt(themes['fontSize']), | ||||
| }; | }; | ||||
| memo.theme.components.Input = { | memo.theme.components.Input = { | ||||
| @@ -13,6 +13,7 @@ function MessageBroadcast() { | |||||
| const navigate = useNavigate(); | const navigate = useNavigate(); | ||||
| const getMessageCount = useCallback(async () => { | const getMessageCount = useCallback(async () => { | ||||
| if (!userId) return; | |||||
| const params: Record<string, any> = { | const params: Record<string, any> = { | ||||
| receiver: userId, | receiver: userId, | ||||
| type: -1, | type: -1, | ||||
| @@ -184,3 +184,10 @@ export enum MessageStatus { | |||||
| UnRead = 1, | UnRead = 1, | ||||
| Readed = 2, | Readed = 2, | ||||
| } | } | ||||
| // 审核状态 | |||||
| export enum ApprovalStatus { | |||||
| Pending = 0, | |||||
| Agree = 1, | |||||
| Reject = 2, | |||||
| } | |||||
| @@ -64,7 +64,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||||
| // 获取详情 | // 获取详情 | ||||
| const getResourceDetail = useCallback( | const getResourceDetail = useCallback( | ||||
| async (version: string | undefined) => { | |||||
| async (version?: string) => { | |||||
| const params = { | const params = { | ||||
| id: resourceId, | id: resourceId, | ||||
| owner, | 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 = [ | const items = [ | ||||
| { | { | ||||
| key: ResourceInfoTabKeys.Introduction, | key: ResourceInfoTabKeys.Introduction, | ||||
| @@ -282,6 +297,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||||
| {(info[tagPropertyName] as string) || '--'} | {(info[tagPropertyName] as string) || '--'} | ||||
| </div> | </div> | ||||
| )} | )} | ||||
| <div | <div | ||||
| className={classNames(styles['resource-info__top__praise'], { | className={classNames(styles['resource-info__top__praise'], { | ||||
| [styles['resource-info__top__praise--praised']]: info.praised, | [styles['resource-info__top__praise--praised']]: info.praised, | ||||
| @@ -295,6 +311,9 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||||
| /> | /> | ||||
| <span>{info.praises_count}</span> | <span>{info.praises_count}</span> | ||||
| </div> | </div> | ||||
| <Button type="default" onClick={handlePublish}> | |||||
| 发布 | |||||
| </Button> | |||||
| </Flex> | </Flex> | ||||
| {version ? ( | {version ? ( | ||||
| <Flex align="center"> | <Flex align="center"> | ||||
| @@ -19,6 +19,8 @@ import { | |||||
| getModelList, | getModelList, | ||||
| getModelNextVersionReq, | getModelNextVersionReq, | ||||
| getModelVersionList, | getModelVersionList, | ||||
| publishDatasetReq, | |||||
| publishModelReq, | |||||
| } from '@/services/dataset/index.js'; | } from '@/services/dataset/index.js'; | ||||
| import { limitUploadFileType } from '@/utils/ui'; | import { limitUploadFileType } from '@/utils/ui'; | ||||
| import type { TabsProps, UploadFile } from 'antd'; | import type { TabsProps, UploadFile } from 'antd'; | ||||
| @@ -45,6 +47,7 @@ type ResourceTypeInfo = { | |||||
| getInfo: (params: any) => Promise<any>; // 获取详情 | getInfo: (params: any) => Promise<any>; // 获取详情 | ||||
| compareVersion: (params: any) => Promise<any>; // 版本对比 | compareVersion: (params: any) => Promise<any>; // 版本对比 | ||||
| getNextVersion: (params: any) => Promise<any>; // 获取下一个版本 | getNextVersion: (params: any) => Promise<any>; // 获取下一个版本 | ||||
| publish: (params: any) => Promise<any>; // 发布 | |||||
| name: string; // 名称 | name: string; // 名称 | ||||
| typeParamKey: 'data_type' | 'model_type'; // 类型参数名称,获取资源列表接口使用 | typeParamKey: 'data_type' | 'model_type'; // 类型参数名称,获取资源列表接口使用 | ||||
| tagParamKey: 'data_tag' | 'model_tag'; // 标签参数名称,获取资源列表接口使用 | tagParamKey: 'data_tag' | 'model_tag'; // 标签参数名称,获取资源列表接口使用 | ||||
| @@ -76,6 +79,7 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = { | |||||
| getInfo: getDatasetInfo, | getInfo: getDatasetInfo, | ||||
| compareVersion: compareDatasetVersion, | compareVersion: compareDatasetVersion, | ||||
| getNextVersion: getDatasetNextVersionReq, | getNextVersion: getDatasetNextVersionReq, | ||||
| publish: publishDatasetReq, | |||||
| name: '数据集', | name: '数据集', | ||||
| typeParamKey: 'data_type', | typeParamKey: 'data_type', | ||||
| tagParamKey: 'data_tag', | tagParamKey: 'data_tag', | ||||
| @@ -116,6 +120,7 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = { | |||||
| getInfo: getModelInfo, | getInfo: getModelInfo, | ||||
| compareVersion: compareModelVersion, | compareVersion: compareModelVersion, | ||||
| getNextVersion: getModelNextVersionReq, | getNextVersion: getModelNextVersionReq, | ||||
| publish: publishModelReq, | |||||
| name: '模型', | name: '模型', | ||||
| typeParamKey: 'model_type', | typeParamKey: 'model_type', | ||||
| tagParamKey: 'model_tag', | tagParamKey: 'model_tag', | ||||
| @@ -30,7 +30,7 @@ | |||||
| &:hover { | &:hover { | ||||
| color: white; | color: white; | ||||
| .backgroundFullImage(url(@/assets/img/home/model-item-bg-hover.png)); | |||||
| .backgroundFullImage(url(@/assets/img/home/model-item-bg-hover2.png)); | |||||
| } | } | ||||
| &__hot { | &__hot { | ||||
| @@ -3,34 +3,42 @@ import { getPublicModelsReq } from '@/services/home'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { gotoPageIfLogin } from '@/utils/ui'; | import { gotoPageIfLogin } from '@/utils/ui'; | ||||
| import { Divider, Flex } from 'antd'; | 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 { useEffect, useState } from 'react'; | ||||
| import BlockTitle from '../BlockTitle'; | import BlockTitle from '../BlockTitle'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| const modelVariants: Variants = { | const modelVariants: Variants = { | ||||
| offscreen: { | |||||
| y: 0, | |||||
| opacity: 1, | |||||
| offscreen: (down: boolean) => ({ | |||||
| y: 100, | |||||
| opacity: 0, | |||||
| transition: { | transition: { | ||||
| ease: 'linear', | ease: 'linear', | ||||
| duration: 0.3, | |||||
| duration: 0.5, | |||||
| }, | }, | ||||
| }, | |||||
| }), | |||||
| onscreen: { | onscreen: { | ||||
| y: [0, 200, 0], | |||||
| opacity: [0, 0, 1], | |||||
| y: 0, | |||||
| opacity: 1, | |||||
| transition: { | transition: { | ||||
| ease: 'easeOut', | ease: 'easeOut', | ||||
| duration: 0.3, | |||||
| times: [0, 0, 1], | |||||
| delay: 0.5, | |||||
| duration: 0.5, | |||||
| // times: [0, 0, 1], | |||||
| }, | }, | ||||
| }, | }, | ||||
| }; | }; | ||||
| function ModelBlock() { | function ModelBlock() { | ||||
| const [modelData, setModelData] = useState<ModelData[]>([]); | 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(() => { | useEffect(() => { | ||||
| const getPublicModels = async () => { | const getPublicModels = async () => { | ||||
| @@ -56,11 +64,11 @@ function ModelBlock() { | |||||
| return ( | return ( | ||||
| <motion.div | <motion.div | ||||
| variants={modelVariants} | variants={modelVariants} | ||||
| initial={false} | |||||
| initial={'offscreen'} | |||||
| whileInView={'onscreen'} | whileInView={'onscreen'} | ||||
| custom={index} | |||||
| className={styles['model__item']} | className={styles['model__item']} | ||||
| key={item.id} | key={item.id} | ||||
| custom={isDowning} | |||||
| onClick={() => { | onClick={() => { | ||||
| gotoPageIfLogin( | gotoPageIfLogin( | ||||
| `/dataset/model/info/${item.id}?name=${item.name}&owner=${item.owner}&identifier=${item.identifier}&is_public=${item.is_public}`, | `/dataset/model/info/${item.id}?name=${item.name}&owner=${item.owner}&identifier=${item.identifier}&is_public=${item.is_public}`, | ||||
| @@ -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; | |||||
| @@ -15,22 +15,21 @@ import styles from './index.less'; | |||||
| const serviceVariants: Variants = { | const serviceVariants: Variants = { | ||||
| offscreen: { | offscreen: { | ||||
| y: -100, | |||||
| y: -200, | |||||
| opacity: 0, | opacity: 0, | ||||
| transition: { | transition: { | ||||
| ease: 'linear', | ease: 'linear', | ||||
| duration: 0, | duration: 0, | ||||
| }, | }, | ||||
| }, | }, | ||||
| onscreen: (index: number) => ({ | |||||
| onscreen: { | |||||
| y: 0, | y: 0, | ||||
| opacity: 1, | opacity: 1, | ||||
| transition: { | transition: { | ||||
| type: 'spring', | type: 'spring', | ||||
| duration: 1, | duration: 1, | ||||
| delay: index * 0.3, | |||||
| }, | }, | ||||
| }), | |||||
| }, | |||||
| }; | }; | ||||
| function ServiceBlock() { | function ServiceBlock() { | ||||
| @@ -9,6 +9,7 @@ | |||||
| &__item { | &__item { | ||||
| display: flex; | display: flex; | ||||
| align-items: center; | align-items: center; | ||||
| width: 9rem; | |||||
| &__icon { | &__icon { | ||||
| width: 3.75rem; | width: 3.75rem; | ||||
| @@ -8,6 +8,7 @@ | |||||
| &__tabs { | &__tabs { | ||||
| display: flex; | display: flex; | ||||
| flex: none; | |||||
| align-items: center; | align-items: center; | ||||
| height: 76px; | height: 76px; | ||||
| padding: 0 30px; | padding: 0 30px; | ||||
| @@ -21,6 +22,14 @@ | |||||
| &--selected, | &--selected, | ||||
| &:hover { | &:hover { | ||||
| color: @text-color; | color: @text-color; | ||||
| font-weight: 500; | |||||
| } | |||||
| } | |||||
| :global { | |||||
| .ant-btn:first-of-type { | |||||
| margin-right: 10px; | |||||
| margin-left: auto; | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -40,9 +49,7 @@ | |||||
| } | } | ||||
| &__list { | &__list { | ||||
| display: flex; | |||||
| flex: 1; | flex: 1; | ||||
| flex-direction: column; | |||||
| width: 100%; | width: 100%; | ||||
| overflow-y: auto; | overflow-y: auto; | ||||
| @@ -73,8 +80,14 @@ | |||||
| } | } | ||||
| } | } | ||||
| &__content { | |||||
| flex: 1; | |||||
| margin-right: 10px; | |||||
| } | |||||
| &__time { | &__time { | ||||
| display: block; | display: block; | ||||
| flex: none; | |||||
| margin-left: auto; | margin-left: auto; | ||||
| color: @text-color-secondary; | color: @text-color-secondary; | ||||
| } | } | ||||
| @@ -1,89 +1,49 @@ | |||||
| import KFButton from '@/components/KFButton'; | |||||
| import KFEmpty, { EmptyType } from '@/components/KFEmpty'; | import KFEmpty, { EmptyType } from '@/components/KFEmpty'; | ||||
| import { MessageStatus, MessageType } from '@/enums'; | import { MessageStatus, MessageType } from '@/enums'; | ||||
| import { useCacheState } from '@/hooks/useCacheState'; | |||||
| import { useCheck } from '@/hooks/useCheck'; | import { useCheck } from '@/hooks/useCheck'; | ||||
| import { Message, MessageResponse } from '@/pages/Message'; | |||||
| import { deleteMessagesReq, getMessageListReq, readMessagesReq } from '@/services/message'; | import { deleteMessagesReq, getMessageListReq, readMessagesReq } from '@/services/message'; | ||||
| import { ago } from '@/utils/date'; | import { ago } from '@/utils/date'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { modalConfirm } from '@/utils/ui'; | import { modalConfirm } from '@/utils/ui'; | ||||
| import { useModel } from '@umijs/max'; | |||||
| import { useModel, useNavigate } from '@umijs/max'; | |||||
| import { | import { | ||||
| Button, | Button, | ||||
| Checkbox, | Checkbox, | ||||
| Pagination, | Pagination, | ||||
| PaginationProps, | PaginationProps, | ||||
| type TablePaginationConfig, | |||||
| Typography, | Typography, | ||||
| message, | |||||
| type TablePaginationConfig, | |||||
| } from 'antd'; | } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useCallback, useEffect, useMemo, useState } from 'react'; | import { useCallback, useEffect, useMemo, useState } from 'react'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| // { | |||||
| // "id": 673396, | |||||
| // "status": 1, | |||||
| // "content": "<b>陈志航</b> 已加入项目 <b>复杂智能软件系统研究/模型训练项目UI设计</b>", | |||||
| // "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 = { | export type MessageContentProps = { | ||||
| messageType: MessageType; | 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 { initialState } = useModel('@@initialState'); | ||||
| const { currentUser } = initialState || {}; | const { currentUser } = initialState || {}; | ||||
| const { userId } = currentUser || {}; | const { userId } = currentUser || {}; | ||||
| const [pagination, setPagination] = useState<TablePaginationConfig>({ | |||||
| current: 1, | |||||
| pageSize: 20, | |||||
| }); | |||||
| const setCacheState = useCacheState()[1]; | |||||
| const [messages, setMessages] = useState<Message[] | undefined>(undefined); | const [messages, setMessages] = useState<Message[] | undefined>(undefined); | ||||
| const [allTotal, setAllTotal] = useState<number>(0); | const [allTotal, setAllTotal] = useState<number>(0); | ||||
| const [unreadTotal, setUnreadTotal] = useState<number | undefined>(undefined); | const [unreadTotal, setUnreadTotal] = useState<number | undefined>(undefined); | ||||
| const [messageStatus, setMessageStatus] = useState(MessageStatus.UnRead); | |||||
| const [isDelete, setIsDelete] = useState(false); | const [isDelete, setIsDelete] = useState(false); | ||||
| const messageIds = useMemo(() => messages?.map((v) => v.id), [messages]); | const messageIds = useMemo(() => messages?.map((v) => v.id), [messages]); | ||||
| const [ | const [ | ||||
| @@ -95,6 +55,7 @@ function MessageContent({ messageType }: MessageContentProps) { | |||||
| isSingleMessagesChecked, | isSingleMessagesChecked, | ||||
| checkSingleMessages, | checkSingleMessages, | ||||
| ] = useCheck(messageIds ?? []); | ] = useCheck(messageIds ?? []); | ||||
| const navigate = useNavigate(); | |||||
| const tabs = useMemo( | const tabs = useMemo( | ||||
| () => [ | () => [ | ||||
| @@ -112,11 +73,13 @@ function MessageContent({ messageType }: MessageContentProps) { | |||||
| ); | ); | ||||
| const getMessages = useCallback(async () => { | const getMessages = useCallback(async () => { | ||||
| if (!userId) return; | |||||
| const params: Record<string, any> = { | const params: Record<string, any> = { | ||||
| receiver: userId, | receiver: userId, | ||||
| status: messageStatus, | status: messageStatus, | ||||
| type: messageType, | type: messageType, | ||||
| page: pagination.current! - 1, | |||||
| page: pagination.current, | |||||
| size: pagination.pageSize, | size: pagination.pageSize, | ||||
| }; | }; | ||||
| const [res] = await to(getMessageListReq(params)); | const [res] = await to(getMessageListReq(params)); | ||||
| @@ -129,25 +92,71 @@ function MessageContent({ messageType }: MessageContentProps) { | |||||
| } | } | ||||
| }, [pagination, userId, messageStatus, messageType]); | }, [pagination, userId, messageStatus, messageType]); | ||||
| // 标记已读 | |||||
| const readMessages = async (ids?: number[]) => { | |||||
| const params: Record<string, any> = { | |||||
| 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<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(); | getMessages(); | ||||
| } | } | ||||
| }; | }; | ||||
| // 删除 | // 删除 | ||||
| const deleteMessages = async (ids?: number[]) => { | |||||
| const deleteMessages = async (ids: number[]) => { | |||||
| if (ids.length <= 0) { | |||||
| message.error('请选择要删除的消息'); | |||||
| return; | |||||
| } | |||||
| const params: Record<string, any> = { | const params: Record<string, any> = { | ||||
| ids: ids, | |||||
| notificationIds: ids.join(','), | |||||
| receiver: userId, | |||||
| type: messageType, | |||||
| }; | }; | ||||
| const [res] = await to(deleteMessagesReq(params)); | 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(); | ||||
| }, [getMessages]); | }, [getMessages]); | ||||
| // 重置批量删除状态、分页 | |||||
| useEffect(() => { | useEffect(() => { | ||||
| cancelBatchDelete(); | cancelBatchDelete(); | ||||
| }, [messageType, messageStatus, cancelBatchDelete]); | }, [messageType, messageStatus, cancelBatchDelete]); | ||||
| // 批量删除 | // 批量删除 | ||||
| const handleBatchDelete = () => { | const handleBatchDelete = () => { | ||||
| if (selectedMessages.length <= 0) { | |||||
| message.error('请选择要删除的消息'); | |||||
| return; | |||||
| } | |||||
| modalConfirm({ | modalConfirm({ | ||||
| title: '删除后,消息不可恢复', | title: '删除后,消息不可恢复', | ||||
| content: '是否确认删除?', | content: '是否确认删除?', | ||||
| onOk: () => { | 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) => { | const handlePageChange: PaginationProps['onChange'] = (page, pageSize) => { | ||||
| setPagination({ | |||||
| onPaginationChange({ | |||||
| current: page, | current: page, | ||||
| pageSize: pageSize, | pageSize: pageSize, | ||||
| }); | }); | ||||
| @@ -195,52 +225,34 @@ function MessageContent({ messageType }: MessageContentProps) { | |||||
| [styles['message-content__tabs__item--selected']]: item.status === messageStatus, | [styles['message-content__tabs__item--selected']]: item.status === messageStatus, | ||||
| })} | })} | ||||
| onClick={() => { | onClick={() => { | ||||
| setMessageStatus(item.status); | |||||
| onStatusChange(item.status); | |||||
| }} | }} | ||||
| > | > | ||||
| <span>{item.title + (item.total ? `(${item.total})` : '')}</span> | |||||
| <span>{item.title + (item.total !== undefined ? `(${item.total})` : '')}</span> | |||||
| </div> | </div> | ||||
| ))} | ))} | ||||
| {isDelete ? ( | {isDelete ? ( | ||||
| <> | <> | ||||
| <Button | |||||
| color="default" | |||||
| variant="link" | |||||
| style={{ marginLeft: 'auto', marginRight: 10 }} | |||||
| onClick={cancelBatchDelete} | |||||
| > | |||||
| <KFButton kfColor="default" variant="link" onClick={cancelBatchDelete}> | |||||
| 取消 | 取消 | ||||
| </Button> | |||||
| <Button | |||||
| color="danger" | |||||
| variant="link" | |||||
| style={{ marginLeft: messageType === MessageType.Mine ? 0 : 'auto', marginRight: 0 }} | |||||
| onClick={handleBatchDelete} | |||||
| > | |||||
| </KFButton> | |||||
| <KFButton kfColor="danger" variant="link" onClick={handleBatchDelete}> | |||||
| 删除 | 删除 | ||||
| </Button> | |||||
| </KFButton> | |||||
| </> | </> | ||||
| ) : ( | ) : ( | ||||
| <> | <> | ||||
| {messageType === MessageType.Mine && allTotal > 0 && ( | {messageType === MessageType.Mine && allTotal > 0 && ( | ||||
| <Button | |||||
| color="primary" | |||||
| variant="link" | |||||
| style={{ marginLeft: 'auto', marginRight: 10 }} | |||||
| onClick={() => setIsDelete(true)} | |||||
| > | |||||
| <KFButton kfColor="primary" variant="link" onClick={() => setIsDelete(true)}> | |||||
| 批量删除 | 批量删除 | ||||
| </Button> | |||||
| </KFButton> | |||||
| )} | |||||
| {allTotal > 0 && ( | |||||
| <KFButton kfColor="primary" variant="link" onClick={() => readMessages()}> | |||||
| 一键已读 | |||||
| </KFButton> | |||||
| )} | )} | ||||
| <Button | |||||
| color="primary" | |||||
| variant="link" | |||||
| style={{ marginLeft: messageType === MessageType.Mine ? 0 : 'auto', marginRight: 0 }} | |||||
| onClick={() => readMessages()} | |||||
| > | |||||
| 一键已读 | |||||
| </Button> | |||||
| </> | </> | ||||
| )} | )} | ||||
| </div> | </div> | ||||
| @@ -269,15 +281,19 @@ function MessageContent({ messageType }: MessageContentProps) { | |||||
| <> | <> | ||||
| <div className={styles['message-content__list']}> | <div className={styles['message-content__list']}> | ||||
| {messages.map((message) => ( | {messages.map((message) => ( | ||||
| <div className={styles['message-content__list__item']} key={message.id}> | |||||
| <div | |||||
| className={styles['message-content__list__item']} | |||||
| key={message.id} | |||||
| onClick={() => hanldeMessageClick(message)} | |||||
| > | |||||
| {messageType === MessageType.Mine && isDelete && ( | {messageType === MessageType.Mine && isDelete && ( | ||||
| <Checkbox | <Checkbox | ||||
| style={{ marginRight: 10 }} | style={{ marginRight: 10 }} | ||||
| checked={isSingleMessagesChecked(message.id)} | checked={isSingleMessagesChecked(message.id)} | ||||
| onChange={(e) => { | |||||
| e.stopPropagation(); | |||||
| onChange={() => { | |||||
| checkSingleMessages(message.id); | checkSingleMessages(message.id); | ||||
| }} | }} | ||||
| onClick={(e) => e.stopPropagation()} | |||||
| ></Checkbox> | ></Checkbox> | ||||
| )} | )} | ||||
| {messageStatus === MessageStatus.All && ( | {messageStatus === MessageStatus.All && ( | ||||
| @@ -292,9 +308,13 @@ function MessageContent({ messageType }: MessageContentProps) { | |||||
| {message.status === MessageStatus.UnRead ? '未读' : '已读'} | {message.status === MessageStatus.UnRead ? '未读' : '已读'} | ||||
| </div> | </div> | ||||
| )} | )} | ||||
| <Typography.Text ellipsis={{ tooltip: message.content }}> | |||||
| {message.content} | |||||
| <Typography.Text | |||||
| className={styles['message-content__list__item__content']} | |||||
| ellipsis={{ tooltip: message.content.replace(/<\/?b>/g, '') }} | |||||
| > | |||||
| <span dangerouslySetInnerHTML={{ __html: message.content }}></span> | |||||
| </Typography.Text> | </Typography.Text> | ||||
| <div className={styles['message-content__list__item__time']}> | <div className={styles['message-content__list__item__time']}> | ||||
| {ago(message.created_at)} | {ago(message.created_at)} | ||||
| </div> | </div> | ||||
| @@ -302,16 +322,22 @@ function MessageContent({ messageType }: MessageContentProps) { | |||||
| <Button | <Button | ||||
| className={styles['message-content__list__item__button']} | className={styles['message-content__list__item__button']} | ||||
| type="link" | type="link" | ||||
| onClick={() => readMessages([message.id])} | |||||
| onClick={(e) => { | |||||
| e.stopPropagation(); | |||||
| readMessages(message); | |||||
| }} | |||||
| > | > | ||||
| 标记已读 | |||||
| 标记为已读 | |||||
| </Button> | </Button> | ||||
| )} | )} | ||||
| {messageType === MessageType.Mine && ( | {messageType === MessageType.Mine && ( | ||||
| <Button | <Button | ||||
| type="link" | type="link" | ||||
| className={styles['message-content__list__item__button']} | className={styles['message-content__list__item__button']} | ||||
| onClick={() => deleteMessages([message.id])} | |||||
| onClick={(e) => { | |||||
| e.stopPropagation(); | |||||
| deleteMessages([message.id]); | |||||
| }} | |||||
| > | > | ||||
| 删除 | 删除 | ||||
| </Button> | </Button> | ||||
| @@ -336,8 +362,8 @@ function MessageContent({ messageType }: MessageContentProps) { | |||||
| className={styles['message-content__empty']} | className={styles['message-content__empty']} | ||||
| type={EmptyType.NoData} | type={EmptyType.NoData} | ||||
| title="暂无数据" | title="暂无数据" | ||||
| content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} | |||||
| hasFooter={true} | |||||
| content={`没有${messageStatus === MessageStatus.UnRead ? '未读' : ''}消息`} | |||||
| hasFooter | |||||
| onButtonClick={getMessages} | onButtonClick={getMessages} | ||||
| /> | /> | ||||
| )} | )} | ||||
| @@ -23,10 +23,10 @@ const menus = [ | |||||
| export type MessageMenuProps = { | export type MessageMenuProps = { | ||||
| messageType: MessageType; | messageType: MessageType; | ||||
| onChange: (type: MessageType) => void; | |||||
| onTypeChange: (type: MessageType) => void; | |||||
| }; | }; | ||||
| function MessageMenu({ messageType: currentType, onChange }: MessageMenuProps) { | |||||
| function MessageMenu({ messageType: currentType, onTypeChange }: MessageMenuProps) { | |||||
| return ( | return ( | ||||
| <div className={styles['message-menu']}> | <div className={styles['message-menu']}> | ||||
| <div className={styles['message-menu__title']}>消息列表</div> | <div className={styles['message-menu__title']}>消息列表</div> | ||||
| @@ -36,7 +36,7 @@ function MessageMenu({ messageType: currentType, onChange }: MessageMenuProps) { | |||||
| className={classNames(styles['message-menu__item'], { | className={classNames(styles['message-menu__item'], { | ||||
| [styles['message-menu__item--selected']]: item.type === currentType, | [styles['message-menu__item--selected']]: item.type === currentType, | ||||
| })} | })} | ||||
| onClick={() => onChange(item.type)} | |||||
| onClick={() => onTypeChange(item.type)} | |||||
| > | > | ||||
| <img className={styles['message-menu__item__icon']} src={item.icon} /> | <img className={styles['message-menu__item__icon']} src={item.icon} /> | ||||
| <img className={styles['message-menu__item__icon--hover']} src={item.hoverIcon} /> | <img className={styles['message-menu__item__icon--hover']} src={item.hoverIcon} /> | ||||
| @@ -3,6 +3,6 @@ | |||||
| flex-direction: row; | flex-direction: row; | ||||
| gap: 0 20px; | gap: 0 20px; | ||||
| height: 100%; | height: 100%; | ||||
| padding: 75px 260px 30px; | |||||
| padding: 30px 60px 30px; | |||||
| .backgroundFullImage(url(@/assets/img/message/message-bg.png)); | .backgroundFullImage(url(@/assets/img/message/message-bg.png)); | ||||
| } | } | ||||
| @@ -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 { useState } from 'react'; | ||||
| import MessageContent from './components/Content'; | import MessageContent from './components/Content'; | ||||
| import MessageMenu from './components/Menu'; | import MessageMenu from './components/Menu'; | ||||
| import styles from './index.less'; | 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() { | function MessagePage() { | ||||
| const [messageType, setMessageType] = useState(MessageType.System); | |||||
| 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 ( | return ( | ||||
| <div className={styles['message']}> | <div className={styles['message']}> | ||||
| <MessageMenu | <MessageMenu | ||||
| onChange={(type) => setMessageType(type)} | |||||
| onTypeChange={(type) => { | |||||
| setMessageType(type); | |||||
| resetToFirstPage(); | |||||
| }} | |||||
| messageType={messageType} | messageType={messageType} | ||||
| ></MessageMenu> | ></MessageMenu> | ||||
| <MessageContent messageType={messageType}></MessageContent> | |||||
| <MessageContent | |||||
| messageType={messageType} | |||||
| messageStatus={messageStatus} | |||||
| onStatusChange={(status) => { | |||||
| setMessageStatus(status); | |||||
| resetToFirstPage(); | |||||
| }} | |||||
| pagination={pagination} | |||||
| onPaginationChange={setPagination} | |||||
| ></MessageContent> | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -0,0 +1,9 @@ | |||||
| .approval-info { | |||||
| margin-bottom: 20px; | |||||
| &__item { | |||||
| display: flex; | |||||
| align-items: center; | |||||
| margin-bottom: 10px; | |||||
| } | |||||
| } | |||||
| @@ -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; | |||||
| @@ -0,0 +1,15 @@ | |||||
| .status-cell { | |||||
| color: @text-color; | |||||
| &--agree { | |||||
| color: @success-color; | |||||
| } | |||||
| &--reject { | |||||
| color: @error-color; | |||||
| } | |||||
| &--pending { | |||||
| color: @text-color; | |||||
| } | |||||
| } | |||||
| @@ -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; | |||||
| @@ -4,217 +4,82 @@ | |||||
| * @Description: 开发环境列表 | * @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 { to } from '@/utils/promise'; | ||||
| import SessionStorage from '@/utils/sessionStorage'; | |||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | 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 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'; | import styles from './index.less'; | ||||
| export type EditorData = { | |||||
| export interface ApprovalData { | |||||
| id: number; | 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; | 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() { | function ApprovalList() { | ||||
| const navigate = useNavigate(); | |||||
| const [cacheState, setCacheState] = useCacheState(); | |||||
| const { message } = App.useApp(); | |||||
| const [tableData, setTableData] = useState<EditorData[]>([]); | |||||
| const [tableData, setTableData] = useState<ApprovalData[]>([]); | |||||
| const [total, setTotal] = useState(0); | const [total, setTotal] = useState(0); | ||||
| const [pagination, setPagination] = useState<TablePaginationConfig>( | |||||
| cacheState?.pagination ?? { | |||||
| current: 1, | |||||
| pageSize: 10, | |||||
| }, | |||||
| ); | |||||
| const getResourceDescription = useSystemResource(); | |||||
| const [pagination, setPagination] = useState<TablePaginationConfig>({ | |||||
| current: 1, | |||||
| pageSize: 10, | |||||
| }); | |||||
| // 获取编辑器列表 | |||||
| const getEditorList = useCallback(async () => { | |||||
| // 获取审核列表 | |||||
| const getApprovalList = useCallback(async () => { | |||||
| const params: Record<string, any> = { | const params: Record<string, any> = { | ||||
| 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) { | 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: () => { | 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<EditorData>['onChange'] = ( | |||||
| const handleTableChange: TableProps<ApprovalData>['onChange'] = ( | |||||
| pagination, | pagination, | ||||
| _filters, | _filters, | ||||
| _sorter, | _sorter, | ||||
| @@ -225,147 +90,69 @@ function ApprovalList() { | |||||
| } | } | ||||
| }; | }; | ||||
| const columns: TableProps<EditorData>['columns'] = [ | |||||
| { | |||||
| title: '编辑器名称', | |||||
| dataIndex: 'name', | |||||
| key: 'name', | |||||
| width: '12%', | |||||
| render: (text, record, index) => | |||||
| record.url && record.status === DevEditorStatus.Running | |||||
| ? tableCellRender<EditorData>(true, TableCellValueType.Link, { | |||||
| onClick: gotoEditorPage, | |||||
| })(text, record, index) | |||||
| : tableCellRender<EditorData>(true, TableCellValueType.Text)(text, record, index), | |||||
| }, | |||||
| const columns: TableProps<ApprovalData>['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) => ( | |||||
| <Typography.Text | |||||
| style={{ width: '100%' }} | |||||
| ellipsis={{ tooltip: title.replace(/<\/?b>/g, '') }} | |||||
| > | |||||
| <span dangerouslySetInnerHTML={{ __html: title }}></span> | |||||
| </Typography.Text> | |||||
| ), | |||||
| }, | }, | ||||
| { | { | ||||
| 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), | 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: '操作', | title: '操作', | ||||
| dataIndex: 'operation', | dataIndex: 'operation', | ||||
| width: 270, | |||||
| width: 150, | |||||
| key: 'operation', | key: 'operation', | ||||
| render: (_: any, record: EditorData) => ( | |||||
| render: (_: any, record: ApprovalData) => ( | |||||
| <div> | <div> | ||||
| {record.status === DevEditorStatus.Pending || | |||||
| record.status === DevEditorStatus.Running ? ( | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="stop" | |||||
| icon={<KFIcon type="icon-tingzhi" />} | |||||
| onClick={() => stopEditor(record.id)} | |||||
| > | |||||
| 停止 | |||||
| </Button> | |||||
| ) : ( | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="debug" | |||||
| icon={<KFIcon type="icon-tiaoshi" />} | |||||
| onClick={() => startEditor(record.id)} | |||||
| > | |||||
| 启动 | |||||
| {record.status === ApprovalStatus.Pending ? ( | |||||
| <Button type="link" size="small" key="stop" onClick={() => approval(record)}> | |||||
| 审核 | |||||
| </Button> | </Button> | ||||
| )} | |||||
| {/* {record.status !== DevEditorStatus.Running ? ( | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="jingxiang" | |||||
| icon={<KFIcon type="icon-jingxiang" />} | |||||
| onClick={() => createMirror(record.id)} | |||||
| > | |||||
| 制作镜像 | |||||
| </Button> | |||||
| ) : null} */} | |||||
| <ConfigProvider | |||||
| theme={{ | |||||
| token: { | |||||
| colorLink: themes['warningColor'], | |||||
| }, | |||||
| }} | |||||
| > | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="remove" | |||||
| icon={<KFIcon type="icon-shanchu" />} | |||||
| onClick={() => handleEditorDelete(record)} | |||||
| > | |||||
| 删除 | |||||
| </Button> | |||||
| </ConfigProvider> | |||||
| ) : null} | |||||
| </div> | </div> | ||||
| ), | ), | ||||
| }, | }, | ||||
| @@ -374,15 +161,7 @@ function ApprovalList() { | |||||
| return ( | return ( | ||||
| <div className={styles['approval-list']}> | <div className={styles['approval-list']}> | ||||
| <div className={styles['approval-list__header']}> | <div className={styles['approval-list__header']}> | ||||
| <div>审核</div> | |||||
| <Button | |||||
| style={{ marginLeft: '20px' }} | |||||
| type="default" | |||||
| onClick={getEditorList} | |||||
| icon={<KFIcon type="icon-shuaxin" />} | |||||
| > | |||||
| 刷新 | |||||
| </Button> | |||||
| <div>审核管理</div> | |||||
| </div> | </div> | ||||
| <div className={classNames('vertical-scroll-table', styles['approval-list__table'])}> | <div className={classNames('vertical-scroll-table', styles['approval-list__table'])}> | ||||
| <Table | <Table | ||||
| @@ -254,7 +254,7 @@ const RoleTableList: React.FC = () => { | |||||
| { | { | ||||
| title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />, | title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />, | ||||
| dataIndex: 'option', | dataIndex: 'option', | ||||
| width: '220px', | |||||
| width: '240px', | |||||
| valueType: 'option', | valueType: 'option', | ||||
| render: (_, record) => [ | render: (_, record) => [ | ||||
| <Button | <Button | ||||
| @@ -281,7 +281,7 @@ const UserTableList: React.FC = () => { | |||||
| { | { | ||||
| title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />, | title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />, | ||||
| dataIndex: 'option', | dataIndex: 'option', | ||||
| width: '220px', | |||||
| width: '240px', | |||||
| valueType: 'option', | valueType: 'option', | ||||
| render: (_, record) => [ | render: (_, record) => [ | ||||
| <Button | <Button | ||||
| @@ -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, | |||||
| }); | |||||
| } | |||||
| @@ -20,20 +20,46 @@ export function getMessageCountReq(params: any) { | |||||
| method: 'GET', | method: 'GET', | ||||
| params, | params, | ||||
| skipLoading: true, | skipLoading: true, | ||||
| skipErrorHandler: true, | |||||
| }); | }); | ||||
| } | } | ||||
| // 标记已读 | |||||
| export function readMessagesReq(data: any) { | |||||
| return request(`/api/reader/gns/notification/gitlink/count`, { | |||||
| method: 'POST', | |||||
| // 单条消息标记为已读 | |||||
| export function readMessagesReq(data: any, skipLoading: boolean) { | |||||
| return request(`/api/writer/gns/notification/gitlink`, { | |||||
| method: 'PUT', | |||||
| data, | data, | ||||
| skipLoading, | |||||
| }); | }); | ||||
| } | } | ||||
| // 删除消息 | // 删除消息 | ||||
| export function deleteMessagesReq(data: any) { | export function deleteMessagesReq(data: any) { | ||||
| return request(`/api/reader/gns/notification/gitlink/count`, { | |||||
| 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', | method: 'POST', | ||||
| data, | data, | ||||
| }); | }); | ||||