diff --git a/react-ui/config/defaultSettings.ts b/react-ui/config/defaultSettings.ts index c4c59d2f..cf0e5a16 100644 --- a/react-ui/config/defaultSettings.ts +++ b/react-ui/config/defaultSettings.ts @@ -9,8 +9,7 @@ const Settings: ProLayoutProps & { } = { locale: 'zh-CN', navTheme: 'light', - // 拂晓蓝 - colorPrimary: '#1664ff', + colorPrimary: '#514cf9', // layout: 'mix', contentWidth: 'Fluid', fixedHeader: false, diff --git a/react-ui/config/routes.ts b/react-ui/config/routes.ts index ce40e189..133cbbc1 100644 --- a/react-ui/config/routes.ts +++ b/react-ui/config/routes.ts @@ -38,6 +38,12 @@ export default [ key: 'workspace', component: './Workspace/index', }, + { + name: '消息中心', + path: 'message', + key: 'message', + component: './Message/index', + }, ], }, { @@ -582,6 +588,11 @@ export default [ }, ], }, + { + name: '审核管理', + path: 'approval', + component: './System/Approval', + }, ], }, { diff --git a/react-ui/package.json b/react-ui/package.json index 5f317010..4b28d7cf 100644 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -60,14 +60,17 @@ "not ie <= 10" ], "dependencies": { + "@ant-design/colors": "~7.2.1", "@ant-design/icons": "^5.0.0", "@ant-design/pro-components": "^2.4.4", "@ant-design/use-emotion-css": "1.0.4", "@antv/g6": "^4.8.24", "@antv/hierarchy": "^0.6.12", + "@ctrl/tinycolor": "~4.1.0", "@types/crypto-js": "^4.2.2", "@umijs/route-utils": "^4.0.1", "antd": "~5.21.4", + "antd-style": "~3.7.1", "caniuse-lite": "~1.0.30001707", "classnames": "^2.3.2", "crypto-js": "^4.2.0", diff --git a/react-ui/src/app.tsx b/react-ui/src/app.tsx index 3eda3284..779ee3b3 100644 --- a/react-ui/src/app.tsx +++ b/react-ui/src/app.tsx @@ -190,11 +190,18 @@ export const antd: RuntimeAntdConfig = (memo) => { memo.theme ??= {}; memo.theme.token = { colorPrimary: themes['primaryColor'], + colorPrimaryHover: themes['primaryHoverColor'], + colorPrimaryActive: themes['primaryActiveColor'], + colorPrimaryText: themes['primaryColor'], + colorPrimaryTextHover: themes['primaryHoverColor'], + colorPrimaryTextActive: themes['primaryActiveColor'], + // colorPrimaryBg: 'rgba(81, 76, 249, 0.07)', colorSuccess: themes['successColor'], colorError: themes['errorColor'], colorWarning: themes['warningColor'], colorLink: themes['primaryColor'], colorText: themes['textColor'], + fontSize: parseInt(themes['fontSize']), controlHeightLG: 46, }; memo.theme.components ??= {}; 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/assets/img/message/at-hover.png b/react-ui/src/assets/img/message/at-hover.png new file mode 100644 index 00000000..57aa81db Binary files /dev/null and b/react-ui/src/assets/img/message/at-hover.png differ diff --git a/react-ui/src/assets/img/message/at.png b/react-ui/src/assets/img/message/at.png new file mode 100644 index 00000000..e1cd2875 Binary files /dev/null and b/react-ui/src/assets/img/message/at.png differ diff --git a/react-ui/src/assets/img/message/content-bg.png b/react-ui/src/assets/img/message/content-bg.png new file mode 100644 index 00000000..dc31d537 Binary files /dev/null and b/react-ui/src/assets/img/message/content-bg.png differ diff --git a/react-ui/src/assets/img/message/menu-bg.png b/react-ui/src/assets/img/message/menu-bg.png new file mode 100644 index 00000000..c762354b Binary files /dev/null and b/react-ui/src/assets/img/message/menu-bg.png differ diff --git a/react-ui/src/assets/img/message/message-bg.png b/react-ui/src/assets/img/message/message-bg.png new file mode 100644 index 00000000..6a54cd26 Binary files /dev/null and b/react-ui/src/assets/img/message/message-bg.png differ diff --git a/react-ui/src/assets/img/message/red-point.png b/react-ui/src/assets/img/message/red-point.png new file mode 100644 index 00000000..bfd49b2e Binary files /dev/null and b/react-ui/src/assets/img/message/red-point.png differ diff --git a/react-ui/src/assets/img/message/system-hover.png b/react-ui/src/assets/img/message/system-hover.png new file mode 100644 index 00000000..31efbf60 Binary files /dev/null and b/react-ui/src/assets/img/message/system-hover.png differ diff --git a/react-ui/src/assets/img/message/system.png b/react-ui/src/assets/img/message/system.png new file mode 100644 index 00000000..4e23e3ec Binary files /dev/null and b/react-ui/src/assets/img/message/system.png differ diff --git a/react-ui/src/assets/img/message/trumpet-hover.png b/react-ui/src/assets/img/message/trumpet-hover.png new file mode 100644 index 00000000..82439bd6 Binary files /dev/null and b/react-ui/src/assets/img/message/trumpet-hover.png differ diff --git a/react-ui/src/assets/img/message/trumpet.png b/react-ui/src/assets/img/message/trumpet.png new file mode 100644 index 00000000..720aeb04 Binary files /dev/null and b/react-ui/src/assets/img/message/trumpet.png differ diff --git a/react-ui/src/components/KFButton/index.less b/react-ui/src/components/KFButton/index.less new file mode 100644 index 00000000..36a74d2c --- /dev/null +++ b/react-ui/src/components/KFButton/index.less @@ -0,0 +1,25 @@ +body { + .kf-primary-button.ant-btn-color-primary.ant-btn-variant-link { + background-color: .addAlpha(@primary-color, 0.07) [] !important; + } + + .kf-default-button.ant-btn-color-default.ant-btn-variant-link { + background-color: .addAlpha(@text-color-secondary, 0.07) [] !important; + } + + .kf-danger-button.ant-btn-color-danger.ant-btn-variant-link { + background-color: .addAlpha(@error-color, 0.07) [] !important; + } + + .ant-btn-color-default.ant-btn-variant-link.kf-default-button:not(:disabled):not( + .ant-btn-disabled + ):hover { + color: .addAlpha(@text-color-secondary, 0.5) []; + } + + .ant-btn-color-default.ant-btn-variant-link.kf-default-button:not(:disabled):not( + .ant-btn-disabled + ):active { + color: @text-color-secondary; + } +} diff --git a/react-ui/src/components/KFButton/index.tsx b/react-ui/src/components/KFButton/index.tsx new file mode 100644 index 00000000..f28bbcc2 --- /dev/null +++ b/react-ui/src/components/KFButton/index.tsx @@ -0,0 +1,75 @@ +import themes from '@/styles/theme.less'; +import { addAlpha, derivePrimaryStates } from '@/utils/color'; +import { Button, ButtonProps } from 'antd'; +import { createStyles } from 'antd-style'; +import './index.less'; + +type KFColor = 'primary' | 'default' | 'danger'; + +export interface KFButtonProps extends ButtonProps { + kfColor?: KFColor; +} + +const useStyles = createStyles(({ token, css }) => ({ + primary: css` + color: ${token.colorPrimary} !important; + background-color: ${addAlpha(themes['primaryColor'], 0.07)} !important; + + &:hover { + color: ${token.colorPrimaryHover} !important; + } + + &:active { + color: ${token.colorPrimaryActive} !important; + } + `, + default: css` + color: ${themes['textColorSecondary']} !important; + background-color: ${addAlpha(themes['textColorSecondary'], 0.07)} !important; + + &:hover { + color: ${derivePrimaryStates(themes['textColorSecondary']).colorPrimaryHover} !important; + } + + &:active { + color: ${derivePrimaryStates(themes['textColorSecondary']).colorPrimaryActive} !important; + } + `, + danger: css` + color: ${themes['errorColor']} !important; + background-color: ${addAlpha(themes['errorColor'], 0.07)} !important; + + &:hover { + color: ${derivePrimaryStates(themes['errorColor']).colorPrimaryHover} !important; + } + + &:active { + color: ${derivePrimaryStates(themes['errorColor']).colorPrimaryActive} !important; + } + `, +})); + +function KFButton({ kfColor = 'default', className, ...rest }: KFButtonProps) { + const { styles, cx } = useStyles(); + + let style = ''; + switch (kfColor) { + case 'primary': + style = styles.primary; + break; + case 'default': + style = styles.default; + break; + case 'danger': + style = styles.danger; + break; + default: + break; + } + + return ( + + ); +} + +export default KFButton; diff --git a/react-ui/src/components/MessageBroadcast/index.less b/react-ui/src/components/MessageBroadcast/index.less new file mode 100644 index 00000000..d3fda321 --- /dev/null +++ b/react-ui/src/components/MessageBroadcast/index.less @@ -0,0 +1,19 @@ +.message-broadcast { + position: relative; + width: 32px; + height: 32px; + margin-right: 8px; + cursor: pointer; + .backgroundFullImage(url(@/assets/img/message/trumpet.png)); + + &:hover { + background-image: url(@/assets/img/message/trumpet-hover.png); + } + + &__red-point { + position: absolute; + top: 8px; + left: 18px; + width: 6px; + } +} diff --git a/react-ui/src/components/MessageBroadcast/index.tsx b/react-ui/src/components/MessageBroadcast/index.tsx new file mode 100644 index 00000000..71d03d53 --- /dev/null +++ b/react-ui/src/components/MessageBroadcast/index.tsx @@ -0,0 +1,47 @@ +import RedPointImg from '@/assets/img/message/red-point.png'; +import { getMessageCountReq } from '@/services/message'; +import { to } from '@/utils/promise'; +import { useModel, useNavigate } from '@umijs/max'; +import { useCallback, useEffect, useState } from 'react'; +import styles from './index.less'; + +function MessageBroadcast() { + const { initialState } = useModel('@@initialState'); + const { currentUser } = initialState || {}; + const { userId } = currentUser || {}; + const [total, setTotal] = useState(0); + const navigate = useNavigate(); + + const getMessageCount = useCallback(async () => { + if (!userId) return; + const params: Record = { + receiver: userId, + type: -1, + }; + const [res] = await to(getMessageCountReq(params)); + if (res && res.data) { + const { unread_total } = res.data; + setTotal(unread_total); + } + }, [userId]); + + useEffect(() => { + const interval = setInterval(() => { + getMessageCount(); + }, 60 * 1000); + getMessageCount(); + return () => { + clearInterval(interval); + }; + }, [getMessageCount]); + + return ( +
navigate('/workspace/message')}> + {total > 0 && ( + + )} +
+ ); +} + +export default MessageBroadcast; diff --git a/react-ui/src/components/RightContent/index.tsx b/react-ui/src/components/RightContent/index.tsx index 9b84950a..fcdf9f10 100644 --- a/react-ui/src/components/RightContent/index.tsx +++ b/react-ui/src/components/RightContent/index.tsx @@ -1,8 +1,9 @@ import KFIcon from '@/components/KFIcon'; import { ProBreadcrumb } from '@ant-design/pro-components'; import { useModel } from '@umijs/max'; -import { Button } from 'antd'; +import { Button, Flex } from 'antd'; import React from 'react'; +import MessageBroadcast from '../MessageBroadcast'; import Avatar from './AvatarDropdown'; import styles from './index.less'; // import { SelectLang } from '@umijs/max'; @@ -43,7 +44,15 @@ const GlobalHeaderRight: React.FC = () => { - + + + + + + {/* */} ); diff --git a/react-ui/src/enums/index.ts b/react-ui/src/enums/index.ts index 848016f0..1b077b81 100644 --- a/react-ui/src/enums/index.ts +++ b/react-ui/src/enums/index.ts @@ -171,3 +171,29 @@ export enum ComponentType { Map = 'map', Str = 'str', } + +// 消息类型 +export enum MessageType { + System = 1, + Mine = 2, +} + +// 消息状态 +export enum MessageStatus { + All = -1, + UnRead = 1, + Readed = 2, +} + +// 审核状态 +export enum ApprovalStatus { + Pending = 0, + Agree = 1, + Reject = 2, +} + +export const approvalStatusOptions = [ + { label: '待审核', value: ApprovalStatus.Pending }, + { label: '通过', value: ApprovalStatus.Agree }, + { label: '拒绝', value: ApprovalStatus.Reject }, +]; diff --git a/react-ui/src/hooks/useServerTime.ts b/react-ui/src/hooks/useServerTime.ts index a5c6f229..fcf8469d 100644 --- a/react-ui/src/hooks/useServerTime.ts +++ b/react-ui/src/hooks/useServerTime.ts @@ -10,6 +10,7 @@ import { useCallback, useEffect, useState } from 'react'; let globalTimeOffset: number | undefined = undefined; +// 获取服务器时间偏移 export const globalGetSeverTime = async () => { const requestStartTime = Date.now(); const [res] = await to(getSeverTimeReq()); @@ -23,7 +24,8 @@ export const globalGetSeverTime = async () => { } }; -export const now = () => { +// 服务器的当前时间 +export const serverNow = () => { return new Date(Date.now() + (globalTimeOffset ?? 0)); }; 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/components/ResourceVersion/index.tsx b/react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx index 18fbc3ee..a20c3065 100644 --- a/react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx @@ -34,6 +34,7 @@ function ResourceVersion({ resourceType, info }: ResourceVersionProps) { id: info.id, version: info.version, identifier: info.identifier, + owner: info.owner, is_public: info.is_public, }); }; 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/Intro/index.tsx b/react-ui/src/pages/Home/components/Intro/index.tsx index 527aa2e8..7ea944c1 100644 --- a/react-ui/src/pages/Home/components/Intro/index.tsx +++ b/react-ui/src/pages/Home/components/Intro/index.tsx @@ -1,7 +1,7 @@ import miniHeaderImage from '@/assets/img/home/header-bg-mini.png'; import headerImage from '@/assets/img/home/header-bg.png'; import { convertRemToPx } from '@/utils'; -import { useNavigate } from '@umijs/max'; +import { gotoPageIfLogin } from '@/utils/ui'; import { motion, useMotionTemplate, @@ -18,7 +18,6 @@ import styles from './index.less'; function IntroBlock() { const [backgroundImage1, setBackgroundImage1] = useState(undefined); const [backgroundImage2, setBackgroundImage2] = useState(headerImage); - const navigate = useNavigate(); const { scrollY } = useScroll(); const springValue = useSpring(scrollY, { stiffness: 100, @@ -70,7 +69,7 @@ function IntroBlock() { 智能材料科研平台是用于材料研究和开发的技术平台,它旨在提供实验数据收集、分析和可视化等功能, 以支持材料工程师、科学家和研究人员在材料设计、性能评估和工艺优化方面的工作。 -
navigate('/workspace')}> +
gotoPageIfLogin('/workspace')}> 开始使用
diff --git a/react-ui/src/pages/Home/components/Model/index.less b/react-ui/src/pages/Home/components/Model/index.less index 7f3898ef..bf2e8ba4 100644 --- a/react-ui/src/pages/Home/components/Model/index.less +++ b/react-ui/src/pages/Home/components/Model/index.less @@ -30,7 +30,7 @@ &:hover { color: white; - .backgroundFullImage(url(@/assets/img/home/model-item-bg-hover.png)); + .backgroundFullImage(url(@/assets/img/home/model-item-bg-hover2.png)); } &__hot { diff --git a/react-ui/src/pages/Home/components/Model/index.tsx b/react-ui/src/pages/Home/components/Model/index.tsx index c3b0e621..1d7c8c6a 100644 --- a/react-ui/src/pages/Home/components/Model/index.tsx +++ b/react-ui/src/pages/Home/components/Model/index.tsx @@ -3,34 +3,42 @@ import { getPublicModelsReq } from '@/services/home'; import { to } from '@/utils/promise'; import { gotoPageIfLogin } from '@/utils/ui'; import { Divider, Flex } from 'antd'; -import { motion, type Variants } from 'motion/react'; +import { motion, useMotionValueEvent, useScroll, type Variants } from 'motion/react'; import { useEffect, useState } from 'react'; import BlockTitle from '../BlockTitle'; import styles from './index.less'; const modelVariants: Variants = { - offscreen: (index: number) => ({ - y: 0, - opacity: 1, + offscreen: (down: boolean) => ({ + y: 100, + opacity: 0, transition: { ease: 'linear', - duration: 0.1, + duration: 0.5, }, }), onscreen: { - y: [0, 200, 0], - opacity: [0, 0, 1], + y: 0, + opacity: 1, transition: { ease: 'easeOut', - duration: 0.3, - times: [0, 0, 1], - delay: 0.5, + duration: 0.5, + // times: [0, 0, 1], }, }, }; function ModelBlock() { const [modelData, setModelData] = useState([]); + const [isDowning, setIsDowning] = useState(true); + const { scrollYProgress } = useScroll(); + useMotionValueEvent(scrollYProgress, 'change', (value) => { + setIsDowning((scrollYProgress.getPrevious() ?? 0) - value < 0); + }); + + useEffect(() => { + console.log(isDowning); + }, [isDowning]); useEffect(() => { const getPublicModels = async () => { @@ -58,9 +66,9 @@ function ModelBlock() { variants={modelVariants} initial={'offscreen'} whileInView={'onscreen'} - custom={index} className={styles['model__item']} key={item.id} + custom={isDowning} onClick={() => { gotoPageIfLogin( `/dataset/model/info/${item.id}?name=${item.name}&owner=${item.owner}&identifier=${item.identifier}&is_public=${item.is_public}`, diff --git a/react-ui/src/pages/Home/components/ScrollReveal/index.tsx b/react-ui/src/pages/Home/components/ScrollReveal/index.tsx deleted file mode 100644 index a043234e..00000000 --- a/react-ui/src/pages/Home/components/ScrollReveal/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { motion, useInView, Variants } from 'motion/react'; -import { ReactNode, useRef } from 'react'; - -type ScrollRevealProps = { - children: ReactNode; - variants: Variants; -}; - -function ScrollReveal({ children, variants }: ScrollRevealProps) { - const ref = useRef(null); - const isInView = useInView(ref, { amount: 'all' }); - - return ( - - {children} - - ); -} - -export default ScrollReveal; diff --git a/react-ui/src/pages/Home/components/Service/index.tsx b/react-ui/src/pages/Home/components/Service/index.tsx index 2f28a5f0..828d0635 100644 --- a/react-ui/src/pages/Home/components/Service/index.tsx +++ b/react-ui/src/pages/Home/components/Service/index.tsx @@ -15,22 +15,21 @@ import styles from './index.less'; const serviceVariants: Variants = { offscreen: { - y: -100, + y: -200, opacity: 0, transition: { ease: 'linear', duration: 0, }, }, - onscreen: (index: number) => ({ + onscreen: { y: 0, opacity: 1, transition: { type: 'spring', duration: 1, - delay: index * 0.3, }, - }), + }, }; function ServiceBlock() { diff --git a/react-ui/src/pages/Home/components/Statistics/index.less b/react-ui/src/pages/Home/components/Statistics/index.less index 848e713d..8c6b3369 100644 --- a/react-ui/src/pages/Home/components/Statistics/index.less +++ b/react-ui/src/pages/Home/components/Statistics/index.less @@ -9,6 +9,7 @@ &__item { display: flex; align-items: center; + width: 9rem; &__icon { width: 3.75rem; diff --git a/react-ui/src/pages/Message/components/Content/index.less b/react-ui/src/pages/Message/components/Content/index.less new file mode 100644 index 00000000..b99e753f --- /dev/null +++ b/react-ui/src/pages/Message/components/Content/index.less @@ -0,0 +1,138 @@ +.message-content { + display: flex; + flex: 1; + flex-direction: column; + min-width: 0; + height: 100%; + .backgroundFullImage(url(@/assets/img/message/content-bg.png)); + + &__tabs { + display: flex; + flex: none; + align-items: center; + height: 76px; + padding: 0 30px; + border-bottom: 1px dashed rgba(130, 132, 164, 0.18); + + &__item { + margin-right: 20px; + color: @text-color-secondary; + font-size: @font-size; + + &--selected, + &:hover { + color: @text-color; + font-weight: 500; + } + } + + :global { + .ant-btn:first-of-type { + margin-right: 10px; + margin-left: auto; + } + } + } + + &__check-container { + display: flex; + align-items: center; + margin: 16px 0 10px; + padding-left: 30px; + color: @text-color-secondary; + font-size: @font-size; + + &__count { + margin: 0 2px; + color: @primary-color; + } + } + + &__list { + flex: 1; + width: 100%; + overflow-y: auto; + + &__item { + display: flex; + align-items: center; + width: 100%; + height: 56px; + padding: 0 30px; + color: @text-color; + font-size: @font-size; + + &__status { + flex: none; + margin-right: 10px; + padding: 2px 4px; + font-size: 12px; + border-radius: 4px; + + &--unread { + color: #d7312a; + background-color: rgba(215, 49, 42, 0.07); + } + + &--readed { + color: #2a814b; + background-color: rgba(42, 129, 75, 0.07); + } + } + + &__content { + flex: 1; + margin-right: 10px; + } + + &__time { + display: block; + flex: none; + margin-left: auto; + color: @text-color-secondary; + } + + &__button { + display: none; + flex: none; + padding-right: 0; + padding-left: 0; + color: @primary-color-hover; + font-size: @font-size; + + &:hover { + color: @primary-color !important; + } + + &:first-of-type { + margin-right: 10px; + margin-left: auto; + } + } + + &:hover { + color: @primary-color; + background-color: .addAlpha(@primary-color, 0.05) []; + } + + &:hover &__button { + display: block; + } + + &:hover &__time { + display: none; + } + } + } + + :global { + .ant-pagination { + margin-right: 30px; + margin-bottom: 40px; + } + } + + &__empty { + flex: 1; + } +} diff --git a/react-ui/src/pages/Message/components/Content/index.tsx b/react-ui/src/pages/Message/components/Content/index.tsx new file mode 100644 index 00000000..8b157ff9 --- /dev/null +++ b/react-ui/src/pages/Message/components/Content/index.tsx @@ -0,0 +1,374 @@ +import KFButton from '@/components/KFButton'; +import KFEmpty, { EmptyType } from '@/components/KFEmpty'; +import { MessageStatus, MessageType } from '@/enums'; +import { useCacheState } from '@/hooks/useCacheState'; +import { useCheck } from '@/hooks/useCheck'; +import { Message, MessageResponse } from '@/pages/Message'; +import { deleteMessagesReq, getMessageListReq, readMessagesReq } from '@/services/message'; +import { ago } from '@/utils/date'; +import { to } from '@/utils/promise'; +import { modalConfirm } from '@/utils/ui'; +import { useModel, useNavigate } from '@umijs/max'; +import { + Button, + Checkbox, + Pagination, + PaginationProps, + Typography, + message, + type TablePaginationConfig, +} from 'antd'; +import classNames from 'classnames'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import styles from './index.less'; + +export type MessageContentProps = { + messageType: MessageType; + messageStatus: MessageStatus; + pagination: TablePaginationConfig; + onStatusChange: (status: MessageStatus) => void; + onPaginationChange: (pagination: TablePaginationConfig) => void; +}; + +function MessageContent({ + messageType, + messageStatus, + pagination, + onStatusChange, + onPaginationChange, +}: MessageContentProps) { + const { initialState } = useModel('@@initialState'); + const { currentUser } = initialState || {}; + const { userId } = currentUser || {}; + const setCacheState = useCacheState()[1]; + const [messages, setMessages] = useState(undefined); + const [allTotal, setAllTotal] = useState(0); + const [unreadTotal, setUnreadTotal] = useState(undefined); + const [isDelete, setIsDelete] = useState(false); + const messageIds = useMemo(() => messages?.map((v) => v.id), [messages]); + const [ + selectedMessages, + setSelectedMessages, + messagesAllChecked, + messagesIndeterminate, + checkAllMessages, + isSingleMessagesChecked, + checkSingleMessages, + ] = useCheck(messageIds ?? []); + const navigate = useNavigate(); + + const tabs = useMemo( + () => [ + { + title: '未读', + status: MessageStatus.UnRead, + total: unreadTotal, + }, + { + title: '全部', + status: MessageStatus.All, + }, + ], + [unreadTotal], + ); + + const getMessages = useCallback(async () => { + if (!userId) return; + + const params: Record = { + receiver: userId, + status: messageStatus, + type: messageType, + page: pagination.current, + size: pagination.pageSize, + }; + const [res] = await to(getMessageListReq(params)); + if (res && res.data) { + const { records, records_count, unread_notification, unread_atme } = + res.data as MessageResponse; + setMessages(records); + setAllTotal(records_count); + setUnreadTotal(messageType === MessageType.System ? unread_notification : unread_atme); + } + }, [pagination, userId, messageStatus, messageType]); + + // 标记为已读 + const readMessages = async ( + message?: Message, + skipLoading: boolean = false, + skipResult: boolean = false, + ) => { + const params: Record = message + ? { + notificationIds: message.id, + status: MessageStatus.Readed, + receiver: message.receiver, + type: message.type, + } + : { + notificationIds: -1, + status: MessageStatus.Readed, + receiver: userId, + type: messageType, + }; + const [res] = await to(readMessagesReq(params, skipLoading)); + + // 点击消息置为已读时,不需要修改数据 + if (!skipResult && res) { + // 如果当前是【未读】状态 + // 【一键已读】后,设置分页为第一页 + // 如果是一页的唯一数据,设置为前一页 + if (messageStatus === MessageStatus.UnRead) { + onPaginationChange({ + ...pagination, + current: message + ? messages?.length === 1 + ? Math.max(1, pagination.current! - 1) + : pagination.current + : 1, + }); + } + } else { + getMessages(); + } + }; + + // 删除 + const deleteMessages = async (ids: number[]) => { + if (ids.length <= 0) { + message.error('请选择要删除的消息'); + return; + } + + const params: Record = { + notificationIds: ids.join(','), + receiver: userId, + type: messageType, + }; + const [res] = await to(deleteMessagesReq(params)); + if (res) { + cancelBatchDelete(); + // 如果是一页的唯一数据,删除后,请求前一页的数据 + // 否则直接刷新这一页的数据 + onPaginationChange({ + ...pagination, + current: + ids.length === messages?.length + ? Math.max(1, pagination.current! - 1) + : pagination.current, + }); + } + }; + + // 取消批量删除 + const cancelBatchDelete = useCallback(() => { + setIsDelete(false); + setSelectedMessages([]); + }, [setSelectedMessages]); + + useEffect(() => { + getMessages(); + }, [getMessages]); + + // 重置批量删除状态、分页 + useEffect(() => { + cancelBatchDelete(); + }, [messageType, messageStatus, cancelBatchDelete]); + + // 批量删除 + const handleBatchDelete = () => { + if (selectedMessages.length <= 0) { + message.error('请选择要删除的消息'); + return; + } + + modalConfirm({ + title: '删除后,消息不可恢复', + content: '是否确认删除?', + onOk: () => { + deleteMessages(selectedMessages); + }, + }); + }; + + // 点击消息 + const hanldeMessageClick = (message: Message) => { + if (message.status === MessageStatus.UnRead) { + readMessages(message, true, true); + } + + if (message.notification_url) { + navigate(message.notification_url); + setCacheState({ + messageType, + pagination, + messageStatus, + }); + } + }; + + // 分页切换 + const handlePageChange: PaginationProps['onChange'] = (page, pageSize) => { + onPaginationChange({ + current: page, + pageSize: pageSize, + }); + }; + + return ( +
+
+ {tabs.map((item) => ( +
{ + onStatusChange(item.status); + }} + > + {item.title + (item.total !== undefined ? `(${item.total})` : '')} +
+ ))} + + {isDelete ? ( + <> + + 取消 + + + 删除 + + + ) : ( + <> + {messageType === MessageType.Mine && allTotal > 0 && ( + setIsDelete(true)}> + 批量删除 + + )} + {allTotal > 0 && ( + readMessages()}> + 一键已读 + + )} + + )} +
+ + {isDelete && ( +
+ + 全选 + + + 已选 + + {selectedMessages.length} + + 项 + +
+ )} + + {messages && messages.length > 0 && ( + <> +
+ {messages.map((message) => ( +
hanldeMessageClick(message)} + > + {messageType === MessageType.Mine && isDelete && ( + { + checkSingleMessages(message.id); + }} + onClick={(e) => e.stopPropagation()} + > + )} + {messageStatus === MessageStatus.All && ( +
+ {message.status === MessageStatus.UnRead ? '未读' : '已读'} +
+ )} + /g, '') }} + > + + + +
+ {ago(message.created_at)} +
+ {message.status === MessageStatus.UnRead && ( + + )} + {messageType === MessageType.Mine && ( + + )} +
+ ))} +
+ + + )} + {messages && messages.length === 0 && ( + + )} +
+ ); +} + +export default MessageContent; diff --git a/react-ui/src/pages/Message/components/Menu/index.less b/react-ui/src/pages/Message/components/Menu/index.less new file mode 100644 index 00000000..dbfba954 --- /dev/null +++ b/react-ui/src/pages/Message/components/Menu/index.less @@ -0,0 +1,69 @@ +.message-menu { + flex: none; + width: 196px; + height: 100%; + .backgroundFullImage(url(@/assets/img/message/menu-bg.png)); + + &__title { + position: relative; + margin-bottom: 25px; + padding: 20px 20px 10px; + color: @text-color; + font-size: @font-size; + + &::after { + position: absolute; + right: 20px; + bottom: 0; + left: 20px; + border-bottom: 1px dashed rgba(130, 132, 164, 0.18); + content: ''; + } + } + + &__item { + display: flex; + align-items: center; + margin-bottom: 4px; + padding: 10px 0 10px 18px; + color: @text-color-secondary; + font-size: @font-size; + border-left: 2px solid transparent; + + &--selected, + &:hover { + color: @primary-color; + background-image: linear-gradient( + 101.08deg, + rgba(81, 76, 249, 0.09) 0%, + rgba(255, 255, 255, 0) 100% + ); + border-left: 2px solid @primary-color; + } + + &__icon, + &__icon--hover { + width: 18px; + height: 18px; + margin-right: 10px; + } + + &__icon { + display: block; + } + + &__icon--hover { + display: none; + } + + &--selected &__icon, + &__item:hover &__icon { + display: none; + } + + &--selected &__icon--hover, + &__item:hover &__icon--hover { + display: block; + } + } +} diff --git a/react-ui/src/pages/Message/components/Menu/index.tsx b/react-ui/src/pages/Message/components/Menu/index.tsx new file mode 100644 index 00000000..3f5b0513 --- /dev/null +++ b/react-ui/src/pages/Message/components/Menu/index.tsx @@ -0,0 +1,50 @@ +import AtHoverIcon from '@/assets/img/message/at-hover.png'; +import AtIcon from '@/assets/img/message/at.png'; +import SystemHoverIcon from '@/assets/img/message/system-hover.png'; +import SystemIcon from '@/assets/img/message/system.png'; +import { MessageType } from '@/enums'; +import classNames from 'classnames'; +import styles from './index.less'; + +const menus = [ + { + title: '系统消息', + icon: SystemIcon, + hoverIcon: SystemHoverIcon, + type: MessageType.System, + }, + { + title: '@我的', + icon: AtIcon, + hoverIcon: AtHoverIcon, + type: MessageType.Mine, + }, +]; + +export type MessageMenuProps = { + messageType: MessageType; + onTypeChange: (type: MessageType) => void; +}; + +function MessageMenu({ messageType: currentType, onTypeChange }: MessageMenuProps) { + return ( +
+
消息列表
+ {menus.map((item) => ( +
onTypeChange(item.type)} + > + + + {item.title} +
+ ))} +
+ ); +} + +export default MessageMenu; diff --git a/react-ui/src/pages/Message/index.less b/react-ui/src/pages/Message/index.less new file mode 100644 index 00000000..745c538c --- /dev/null +++ b/react-ui/src/pages/Message/index.less @@ -0,0 +1,8 @@ +.message { + display: flex; + flex-direction: row; + gap: 0 20px; + height: 100%; + padding: 30px 60px 30px; + .backgroundFullImage(url(@/assets/img/message/message-bg.png)); +} diff --git a/react-ui/src/pages/Message/index.tsx b/react-ui/src/pages/Message/index.tsx new file mode 100644 index 00000000..aba242cd --- /dev/null +++ b/react-ui/src/pages/Message/index.tsx @@ -0,0 +1,81 @@ +import { MessageStatus, MessageType } from '@/enums'; +import { useCacheState } from '@/hooks/useCacheState'; +import { type TablePaginationConfig } from 'antd'; +import { useState } from 'react'; +import MessageContent from './components/Content'; +import MessageMenu from './components/Menu'; +import styles from './index.less'; + +// 消息列表接口返回类型 +export interface MessageResponse { + receiver: number; + type: MessageType; + unread_total: number; + unread_notification: number; + unread_atme: number; + records: Message[]; + records_count: number; + page_num: number; + total_page_count: number; + page_size: number; +} + +// 消息数据 +export interface Message { + id: number; + sender: number; + receiver: number; + content: string; + status: MessageStatus; + type: MessageType; + source: string; + extra: string; + notification_url: string; + created_at: Date; +} + +function MessagePage() { + const [cacheState] = useCacheState(); + const [messageType, setMessageType] = useState(cacheState?.messageType ?? MessageType.System); + const [pagination, setPagination] = useState( + cacheState?.pagination ?? { + current: 1, + pageSize: 20, + }, + ); + const [messageStatus, setMessageStatus] = useState( + cacheState?.messageStatus ?? MessageStatus.UnRead, + ); + + // 重置页面为第一页 + const resetToFirstPage = () => { + setPagination((prev) => ({ + ...prev, + current: 1, + })); + }; + + return ( +
+ { + setMessageType(type); + resetToFirstPage(); + }} + messageType={messageType} + > + { + setMessageStatus(status); + resetToFirstPage(); + }} + pagination={pagination} + onPaginationChange={setPagination} + > +
+ ); +} + +export default MessagePage; diff --git a/react-ui/src/pages/System/Approval/components/ApprovalModal/index.less b/react-ui/src/pages/System/Approval/components/ApprovalModal/index.less new file mode 100644 index 00000000..1494eb96 --- /dev/null +++ b/react-ui/src/pages/System/Approval/components/ApprovalModal/index.less @@ -0,0 +1,9 @@ +.approval-info { + margin-bottom: 20px; + + &__item { + display: flex; + align-items: center; + margin-bottom: 10px; + } +} diff --git a/react-ui/src/pages/System/Approval/components/ApprovalModal/index.tsx b/react-ui/src/pages/System/Approval/components/ApprovalModal/index.tsx new file mode 100644 index 00000000..6e629a90 --- /dev/null +++ b/react-ui/src/pages/System/Approval/components/ApprovalModal/index.tsx @@ -0,0 +1,166 @@ +import BasicInfo from '@/components/BasicInfo'; +import KFModal from '@/components/KFModal'; +import SubAreaTitle from '@/components/SubAreaTitle'; +import { ApprovalData, ApprovalType } from '@/pages/System/Approval'; +import { agreeApprovalReq, rejectApprovalReq } from '@/services/message'; +import { parseJsonText } from '@/utils'; +import { to } from '@/utils/promise'; +import { modalConfirm } from '@/utils/ui'; +import { Button, Form, Input, message, type ModalProps } from 'antd'; + +interface ApprovalModalProps extends Omit { + record: ApprovalData; + onOk: () => void; +} + +function ApprovalModal({ record, onOk, ...rest }: ApprovalModalProps) { + const [form] = Form.useForm(); + const content = parseJsonText(record.content); + const recordTypeName = record.type === ApprovalType.Dataset ? '数据集' : '模型'; + + const items = + record.type === ApprovalType.Dataset + ? [ + { + label: '数据集名称', + value: content.name, + }, + { + label: '数据集分类', + value: content.dataType, + }, + { + label: '研究方向', + value: content.dataTag, + }, + { + label: '数据集描述', + value: content.description, + }, + ] + : [ + { + label: '模型名称', + value: content.name, + }, + { + label: '模型框架', + value: content.model_type, + }, + { + label: '模型能力', + value: content.model_tag, + }, + { + label: '模型描述', + value: content.description, + }, + ]; + + // 审批通过 + const agreeApproval = async (remark?: string) => { + const [res] = await to( + agreeApprovalReq({ + id: record.id, + result: remark, + }), + ); + if (res) { + onOk?.(); + } + }; + + // 审批拒绝 + const rejectApproval = async (remark: string) => { + const [res] = await to( + rejectApprovalReq({ + id: record.id, + result: remark, + }), + ); + if (res) { + onOk?.(); + } + }; + + const handleAgree = () => { + const remark = form.getFieldValue('remark') as string | undefined; + const remarkTrim = remark?.trim(); + modalConfirm({ + isDelete: false, + title: `审批通过后,将发布该${recordTypeName}`, + content: '是否确认通过?', + onOk: () => { + agreeApproval(remarkTrim); + }, + }); + }; + + const handleReject = () => { + const remark = form.getFieldValue('remark') as string | undefined; + const remarkTrim = remark?.trim(); + if (!remarkTrim) { + message.error('请输入审批意见'); + return; + } + + modalConfirm({ + isDelete: false, + title: `审批拒绝后,将不发布该${recordTypeName}`, + content: '是否确认拒绝?', + onOk: () => { + rejectApproval(remarkTrim); + }, + }); + }; + + return ( + + 审批通过 + , + , + ]} + > + + +
+ + + +
+
+ ); +} + +export default ApprovalModal; diff --git a/react-ui/src/pages/System/Approval/components/StatusCell/index.less b/react-ui/src/pages/System/Approval/components/StatusCell/index.less new file mode 100644 index 00000000..17a5e5ef --- /dev/null +++ b/react-ui/src/pages/System/Approval/components/StatusCell/index.less @@ -0,0 +1,15 @@ +.status-cell { + color: @text-color; + + &--agree { + color: @success-color; + } + + &--reject { + color: @error-color; + } + + &--pending { + color: @text-color; + } +} diff --git a/react-ui/src/pages/System/Approval/components/StatusCell/index.tsx b/react-ui/src/pages/System/Approval/components/StatusCell/index.tsx new file mode 100644 index 00000000..23adf66e --- /dev/null +++ b/react-ui/src/pages/System/Approval/components/StatusCell/index.tsx @@ -0,0 +1,36 @@ +/* + * @Author: 赵伟 + * @Date: 2024-04-18 18:35:41 + * @Description: 编辑器状态组件 + */ +import { ApprovalStatus } from '@/enums'; +import styles from './index.less'; + +export type DevEditorStatusInfo = { + text: string; + classname: string; +}; + +export const statusInfo: Record = { + [ApprovalStatus.Pending]: { + classname: styles['status-cell--pending'], + text: '待审核', + }, + [ApprovalStatus.Agree]: { + classname: styles['status-cell--agree'], + text: '通过', + }, + [ApprovalStatus.Reject]: { + classname: styles['status-cell--reject'], + text: '已拒绝', + }, +}; + +function StatusCell(status?: ApprovalStatus | null) { + if (status === null || status === undefined || !statusInfo[status]) { + return --; + } + return {statusInfo[status].text}; +} + +export default StatusCell; diff --git a/react-ui/src/pages/System/Approval/index.less b/react-ui/src/pages/System/Approval/index.less new file mode 100644 index 00000000..16adf456 --- /dev/null +++ b/react-ui/src/pages/System/Approval/index.less @@ -0,0 +1,29 @@ +.approval-list { + height: 100%; + &__header { + display: flex; + align-items: center; + justify-content: space-between; + height: 50px; + margin-bottom: 10px; + padding: 0 30px; + background-image: url(@/assets/img/page-title-bg.png); + background-repeat: no-repeat; + background-position: top center; + background-size: 100% 100%; + } + + &__content { + height: calc(100% - 60px); + padding: 20px 30px 0; + background-color: white; + border-radius: 10px; + } + + &__table { + height: calc(100% - 32px - 28px); + margin-top: 28px; + background-color: white; + border-radius: 10px; + } +} diff --git a/react-ui/src/pages/System/Approval/index.tsx b/react-ui/src/pages/System/Approval/index.tsx new file mode 100644 index 00000000..47dcd88c --- /dev/null +++ b/react-ui/src/pages/System/Approval/index.tsx @@ -0,0 +1,210 @@ +/* + * @Author: 赵伟 + * @Date: 2024-04-16 13:58:08 + * @Description: 审核列表 + */ + +import { ApprovalStatus, approvalStatusOptions } from '@/enums'; +import { getApprovalListReq } from '@/services/message'; +import { openAntdModal } from '@/utils/modal'; +import { to } from '@/utils/promise'; +import tableCellRender, { TableCellValueType } from '@/utils/table'; +import { + Button, + Select, + Table, + Typography, + type TablePaginationConfig, + type TableProps, +} from 'antd'; +import classNames from 'classnames'; +import { useCallback, useEffect, useState } from 'react'; +import ApprovalModal from './components/ApprovalModal'; +import StatusCell from './components/StatusCell'; +import styles from './index.less'; + +export interface ApprovalData { + id: number; + status: number; + result: null; + content: string; + applicant_id: number; + applicant_name: null; + applicant_time: Date; + approver_id: number; + approver_time: Date; + title: string; + type: ApprovalType; + url: string; +} + +export enum ApprovalType { + Dataset = 'DATASET', + Model = 'MODEL', +} + +const approvalTypeOptions = [ + { label: '数据集', value: ApprovalType.Dataset }, + { label: '模型', value: ApprovalType.Model }, +]; + +const statusOptions = [{ label: '全部', value: '' }, ...approvalStatusOptions]; + +function ApprovalList() { + const [tableData, setTableData] = useState([]); + const [total, setTotal] = useState(0); + const [status, setStatus] = useState(''); + const [pagination, setPagination] = useState({ + current: 1, + pageSize: 10, + }); + + // 获取审核列表 + const getApprovalList = useCallback(async () => { + const params: Record = { + current: pagination.current, + pageSize: pagination.pageSize, + status: status, + }; + const [res] = await to(getApprovalListReq(params)); + if (res) { + const { rows = [], total = 0 } = res; + setTableData(rows); + setTotal(total); + } + }, [pagination, status]); + + useEffect(() => { + getApprovalList(); + }, [getApprovalList]); + + // 审核 + const approval = (record: ApprovalData) => { + const { close } = openAntdModal(ApprovalModal, { + record: record, + onOk: () => { + close(); + getApprovalList(); + }, + }); + }; + + // 分页切换 + const handleTableChange: TableProps['onChange'] = ( + pagination, + _filters, + _sorter, + { action }, + ) => { + if (action === 'paginate') { + setPagination(pagination); + } + }; + + const columns: TableProps['columns'] = [ + { + title: '内容', + dataIndex: 'title', + key: 'title', + render: (title) => ( + /g, '') }} + > + + + ), + }, + { + title: '类型', + dataIndex: 'type', + key: 'type', + width: 100, + render: tableCellRender(true, TableCellValueType.Enum, { + options: approvalTypeOptions, + }), + }, + { + title: '申请者', + dataIndex: 'applicant_name', + key: 'applicant_name', + width: 180, + render: tableCellRender(true), + }, + { + title: '申请时间', + dataIndex: 'applicant_time', + key: 'applicant_time', + width: 180, + render: tableCellRender(true, TableCellValueType.Date), + }, + { + title: '审核意见', + dataIndex: 'result', + key: 'result', + width: 200, + render: tableCellRender(true), + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 100, + render: StatusCell, + }, + { + title: '操作', + dataIndex: 'operation', + width: 150, + key: 'operation', + render: (_: any, record: ApprovalData) => ( +
+ {record.status === ApprovalStatus.Pending ? ( + + ) : null} +
+ ), + }, + ]; + + return ( +
+
+
审核管理
+
+
+
+ 状态: + +
+
+ `共${total}条`, + }} + onChange={handleTableChange} + rowKey="id" + /> + + + + ); +} + +export default ApprovalList; diff --git a/react-ui/src/pages/System/Role/index.tsx b/react-ui/src/pages/System/Role/index.tsx index c7a4e9cf..23215104 100644 --- a/react-ui/src/pages/System/Role/index.tsx +++ b/react-ui/src/pages/System/Role/index.tsx @@ -254,7 +254,7 @@ const RoleTableList: React.FC = () => { { title: , dataIndex: 'option', - width: '220px', + width: '240px', valueType: 'option', render: (_, record) => [