| @@ -38,6 +38,12 @@ export default [ | |||||
| key: 'workspace', | key: 'workspace', | ||||
| component: './Workspace/index', | component: './Workspace/index', | ||||
| }, | }, | ||||
| { | |||||
| name: '消息中心', | |||||
| path: 'message', | |||||
| key: 'message', | |||||
| component: './Message/index', | |||||
| }, | |||||
| ], | ], | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -582,6 +588,11 @@ export default [ | |||||
| }, | }, | ||||
| ], | ], | ||||
| }, | }, | ||||
| { | |||||
| name: '审核', | |||||
| path: 'approval', | |||||
| component: './System/Approval', | |||||
| }, | |||||
| ], | ], | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -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; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,46 @@ | |||||
| import RedPointImg from '@/assets/img/message/red-point.png'; | |||||
| import { getMessageCountReq } from '@/services/message'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import { useModel, useNavigate } from '@umijs/max'; | |||||
| import { useCallback, useEffect, useState } from 'react'; | |||||
| import styles from './index.less'; | |||||
| function MessageBroadcast() { | |||||
| const { initialState } = useModel('@@initialState'); | |||||
| const { currentUser } = initialState || {}; | |||||
| const { userId } = currentUser || {}; | |||||
| const [total, setTotal] = useState<number>(0); | |||||
| const navigate = useNavigate(); | |||||
| const getMessageCount = useCallback(async () => { | |||||
| const params: Record<string, any> = { | |||||
| receiver: userId, | |||||
| type: -1, | |||||
| }; | |||||
| const [res] = await to(getMessageCountReq(params)); | |||||
| if (res && res.data) { | |||||
| const { unread_total } = res.data; | |||||
| setTotal(unread_total); | |||||
| } | |||||
| }, [userId]); | |||||
| useEffect(() => { | |||||
| const interval = setInterval(() => { | |||||
| getMessageCount(); | |||||
| }, 60 * 1000); | |||||
| getMessageCount(); | |||||
| return () => { | |||||
| clearInterval(interval); | |||||
| }; | |||||
| }, [getMessageCount]); | |||||
| return ( | |||||
| <div className={styles['message-broadcast']} onClick={() => navigate('/workspace/message')}> | |||||
| {total > 0 && ( | |||||
| <img className={styles['message-broadcast__red-point']} src={RedPointImg}></img> | |||||
| )} | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default MessageBroadcast; | |||||
| @@ -1,8 +1,9 @@ | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import { ProBreadcrumb } from '@ant-design/pro-components'; | import { ProBreadcrumb } from '@ant-design/pro-components'; | ||||
| import { useModel } from '@umijs/max'; | import { useModel } from '@umijs/max'; | ||||
| import { Button } from 'antd'; | |||||
| import { Button, Flex } from 'antd'; | |||||
| import React from 'react'; | import React from 'react'; | ||||
| import MessageBroadcast from '../MessageBroadcast'; | |||||
| import Avatar from './AvatarDropdown'; | import Avatar from './AvatarDropdown'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| // import { SelectLang } from '@umijs/max'; | // import { SelectLang } from '@umijs/max'; | ||||
| @@ -43,7 +44,15 @@ const GlobalHeaderRight: React.FC = () => { | |||||
| <ProBreadcrumb></ProBreadcrumb> | <ProBreadcrumb></ProBreadcrumb> | ||||
| <Avatar menu={true} /> | |||||
| <Flex | |||||
| align="center" | |||||
| style={{ marginLeft: 'auto', marginRight: 0, background: '#3a3da5', padding: '0 30px' }} | |||||
| > | |||||
| <MessageBroadcast /> | |||||
| <Avatar menu={true} /> | |||||
| </Flex> | |||||
| {/* <SelectLang className={actionClassName} /> */} | {/* <SelectLang className={actionClassName} /> */} | ||||
| </div> | </div> | ||||
| ); | ); | ||||
| @@ -171,3 +171,16 @@ export enum ComponentType { | |||||
| Map = 'map', | Map = 'map', | ||||
| Str = 'str', | Str = 'str', | ||||
| } | } | ||||
| // 消息类型 | |||||
| export enum MessageType { | |||||
| System = 1, | |||||
| Mine = 2, | |||||
| } | |||||
| // 消息状态 | |||||
| export enum MessageStatus { | |||||
| All = -1, | |||||
| UnRead = 1, | |||||
| Readed = 2, | |||||
| } | |||||
| @@ -10,6 +10,7 @@ import { useCallback, useEffect, useState } from 'react'; | |||||
| let globalTimeOffset: number | undefined = undefined; | let globalTimeOffset: number | undefined = undefined; | ||||
| // 获取服务器时间偏移 | |||||
| export const globalGetSeverTime = async () => { | export const globalGetSeverTime = async () => { | ||||
| const requestStartTime = Date.now(); | const requestStartTime = Date.now(); | ||||
| const [res] = await to(getSeverTimeReq()); | 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)); | return new Date(Date.now() + (globalTimeOffset ?? 0)); | ||||
| }; | }; | ||||
| @@ -1,7 +1,7 @@ | |||||
| import miniHeaderImage from '@/assets/img/home/header-bg-mini.png'; | import miniHeaderImage from '@/assets/img/home/header-bg-mini.png'; | ||||
| import headerImage from '@/assets/img/home/header-bg.png'; | import headerImage from '@/assets/img/home/header-bg.png'; | ||||
| import { convertRemToPx } from '@/utils'; | import { convertRemToPx } from '@/utils'; | ||||
| import { useNavigate } from '@umijs/max'; | |||||
| import { gotoPageIfLogin } from '@/utils/ui'; | |||||
| import { | import { | ||||
| motion, | motion, | ||||
| useMotionTemplate, | useMotionTemplate, | ||||
| @@ -18,7 +18,6 @@ import styles from './index.less'; | |||||
| function IntroBlock() { | function IntroBlock() { | ||||
| const [backgroundImage1, setBackgroundImage1] = useState(undefined); | const [backgroundImage1, setBackgroundImage1] = useState(undefined); | ||||
| const [backgroundImage2, setBackgroundImage2] = useState(headerImage); | const [backgroundImage2, setBackgroundImage2] = useState(headerImage); | ||||
| const navigate = useNavigate(); | |||||
| const { scrollY } = useScroll(); | const { scrollY } = useScroll(); | ||||
| const springValue = useSpring(scrollY, { | const springValue = useSpring(scrollY, { | ||||
| stiffness: 100, | stiffness: 100, | ||||
| @@ -70,7 +69,7 @@ function IntroBlock() { | |||||
| 智能材料科研平台是用于材料研究和开发的技术平台,它旨在提供实验数据收集、分析和可视化等功能, | 智能材料科研平台是用于材料研究和开发的技术平台,它旨在提供实验数据收集、分析和可视化等功能, | ||||
| 以支持材料工程师、科学家和研究人员在材料设计、性能评估和工艺优化方面的工作。 | 以支持材料工程师、科学家和研究人员在材料设计、性能评估和工艺优化方面的工作。 | ||||
| </div> | </div> | ||||
| <div className={styles['intro__button']} onClick={() => navigate('/workspace')}> | |||||
| <div className={styles['intro__button']} onClick={() => gotoPageIfLogin('/workspace')}> | |||||
| 开始使用 | 开始使用 | ||||
| </div> | </div> | ||||
| <StatisticsBlock></StatisticsBlock> | <StatisticsBlock></StatisticsBlock> | ||||
| @@ -9,14 +9,14 @@ import BlockTitle from '../BlockTitle'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| const modelVariants: Variants = { | const modelVariants: Variants = { | ||||
| offscreen: (index: number) => ({ | |||||
| offscreen: { | |||||
| y: 0, | y: 0, | ||||
| opacity: 1, | opacity: 1, | ||||
| transition: { | transition: { | ||||
| ease: 'linear', | ease: 'linear', | ||||
| duration: 0.1, | |||||
| duration: 0.3, | |||||
| }, | }, | ||||
| }), | |||||
| }, | |||||
| onscreen: { | onscreen: { | ||||
| y: [0, 200, 0], | y: [0, 200, 0], | ||||
| opacity: [0, 0, 1], | opacity: [0, 0, 1], | ||||
| @@ -56,7 +56,7 @@ function ModelBlock() { | |||||
| return ( | return ( | ||||
| <motion.div | <motion.div | ||||
| variants={modelVariants} | variants={modelVariants} | ||||
| initial={'offscreen'} | |||||
| initial={false} | |||||
| whileInView={'onscreen'} | whileInView={'onscreen'} | ||||
| custom={index} | custom={index} | ||||
| className={styles['model__item']} | className={styles['model__item']} | ||||
| @@ -0,0 +1,125 @@ | |||||
| .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; | |||||
| 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; | |||||
| } | |||||
| } | |||||
| } | |||||
| &__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 { | |||||
| display: flex; | |||||
| flex: 1; | |||||
| flex-direction: column; | |||||
| 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); | |||||
| } | |||||
| } | |||||
| &__time { | |||||
| display: block; | |||||
| 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; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,348 @@ | |||||
| import KFEmpty, { EmptyType } from '@/components/KFEmpty'; | |||||
| import { MessageStatus, MessageType } from '@/enums'; | |||||
| import { useCheck } from '@/hooks/useCheck'; | |||||
| import { deleteMessagesReq, getMessageListReq, readMessagesReq } from '@/services/message'; | |||||
| import { ago } from '@/utils/date'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import { modalConfirm } from '@/utils/ui'; | |||||
| import { useModel } from '@umijs/max'; | |||||
| import { | |||||
| Button, | |||||
| Checkbox, | |||||
| Pagination, | |||||
| PaginationProps, | |||||
| type TablePaginationConfig, | |||||
| Typography, | |||||
| } from 'antd'; | |||||
| import classNames from 'classnames'; | |||||
| import { useCallback, useEffect, useMemo, useState } from 'react'; | |||||
| 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 = { | |||||
| messageType: MessageType; | |||||
| }; | |||||
| function MessageContent({ messageType }: MessageContentProps) { | |||||
| const { initialState } = useModel('@@initialState'); | |||||
| const { currentUser } = initialState || {}; | |||||
| const { userId } = currentUser || {}; | |||||
| const [pagination, setPagination] = useState<TablePaginationConfig>({ | |||||
| current: 1, | |||||
| pageSize: 20, | |||||
| }); | |||||
| const [messages, setMessages] = useState<Message[] | undefined>(undefined); | |||||
| const [allTotal, setAllTotal] = useState<number>(0); | |||||
| const [unreadTotal, setUnreadTotal] = useState<number | undefined>(undefined); | |||||
| const [messageStatus, setMessageStatus] = useState(MessageStatus.UnRead); | |||||
| 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 tabs = useMemo( | |||||
| () => [ | |||||
| { | |||||
| title: '未读', | |||||
| status: MessageStatus.UnRead, | |||||
| total: unreadTotal, | |||||
| }, | |||||
| { | |||||
| title: '全部', | |||||
| status: MessageStatus.All, | |||||
| }, | |||||
| ], | |||||
| [unreadTotal], | |||||
| ); | |||||
| const getMessages = useCallback(async () => { | |||||
| const params: Record<string, any> = { | |||||
| receiver: userId, | |||||
| status: messageStatus, | |||||
| type: messageType, | |||||
| page: pagination.current! - 1, | |||||
| 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 (ids?: number[]) => { | |||||
| const params: Record<string, any> = { | |||||
| ids: ids, | |||||
| }; | |||||
| const [res] = await to(readMessagesReq(params)); | |||||
| if (res && res.data) { | |||||
| getMessages(); | |||||
| } | |||||
| }; | |||||
| // 删除 | |||||
| const deleteMessages = async (ids?: number[]) => { | |||||
| const params: Record<string, any> = { | |||||
| ids: ids, | |||||
| }; | |||||
| const [res] = await to(deleteMessagesReq(params)); | |||||
| if (res && res.data) { | |||||
| getMessages(); | |||||
| } | |||||
| }; | |||||
| // 取消批量删除 | |||||
| const cancelBatchDelete = useCallback(() => { | |||||
| setIsDelete(false); | |||||
| setSelectedMessages([]); | |||||
| }, [setSelectedMessages]); | |||||
| useEffect(() => { | |||||
| getMessages(); | |||||
| }, [getMessages]); | |||||
| useEffect(() => { | |||||
| cancelBatchDelete(); | |||||
| }, [messageType, messageStatus, cancelBatchDelete]); | |||||
| // 批量删除 | |||||
| const handleBatchDelete = () => { | |||||
| modalConfirm({ | |||||
| title: '删除后,消息不可恢复', | |||||
| content: '是否确认删除?', | |||||
| onOk: () => { | |||||
| setIsDelete(false); | |||||
| setSelectedMessages([]); | |||||
| }, | |||||
| }); | |||||
| }; | |||||
| // 分页切换 | |||||
| const handlePageChange: PaginationProps['onChange'] = (page, pageSize) => { | |||||
| setPagination({ | |||||
| current: page, | |||||
| pageSize: pageSize, | |||||
| }); | |||||
| }; | |||||
| return ( | |||||
| <div className={styles['message-content']}> | |||||
| <div className={styles['message-content__tabs']}> | |||||
| {tabs.map((item) => ( | |||||
| <div | |||||
| key={item.status} | |||||
| className={classNames(styles['message-content__tabs__item'], { | |||||
| [styles['message-content__tabs__item--selected']]: item.status === messageStatus, | |||||
| })} | |||||
| onClick={() => { | |||||
| setMessageStatus(item.status); | |||||
| }} | |||||
| > | |||||
| <span>{item.title + (item.total ? `(${item.total})` : '')}</span> | |||||
| </div> | |||||
| ))} | |||||
| {isDelete ? ( | |||||
| <> | |||||
| <Button | |||||
| color="default" | |||||
| variant="link" | |||||
| style={{ marginLeft: 'auto', marginRight: 10 }} | |||||
| onClick={cancelBatchDelete} | |||||
| > | |||||
| 取消 | |||||
| </Button> | |||||
| <Button | |||||
| color="danger" | |||||
| variant="link" | |||||
| style={{ marginLeft: messageType === MessageType.Mine ? 0 : 'auto', marginRight: 0 }} | |||||
| onClick={handleBatchDelete} | |||||
| > | |||||
| 删除 | |||||
| </Button> | |||||
| </> | |||||
| ) : ( | |||||
| <> | |||||
| {messageType === MessageType.Mine && allTotal > 0 && ( | |||||
| <Button | |||||
| color="primary" | |||||
| variant="link" | |||||
| style={{ marginLeft: 'auto', marginRight: 10 }} | |||||
| onClick={() => setIsDelete(true)} | |||||
| > | |||||
| 批量删除 | |||||
| </Button> | |||||
| )} | |||||
| <Button | |||||
| color="primary" | |||||
| variant="link" | |||||
| style={{ marginLeft: messageType === MessageType.Mine ? 0 : 'auto', marginRight: 0 }} | |||||
| onClick={() => readMessages()} | |||||
| > | |||||
| 一键已读 | |||||
| </Button> | |||||
| </> | |||||
| )} | |||||
| </div> | |||||
| {isDelete && ( | |||||
| <div className={styles['message-content__check-container']}> | |||||
| <Checkbox | |||||
| style={{ marginRight: 10 }} | |||||
| indeterminate={messagesIndeterminate} | |||||
| checked={messagesAllChecked} | |||||
| onChange={checkAllMessages} | |||||
| > | |||||
| 全选 | |||||
| </Checkbox> | |||||
| <span> | |||||
| 已选 | |||||
| <span className={styles['message-content__check-container__count']}> | |||||
| {selectedMessages.length} | |||||
| </span> | |||||
| 项 | |||||
| </span> | |||||
| </div> | |||||
| )} | |||||
| {messages && messages.length > 0 && ( | |||||
| <> | |||||
| <div className={styles['message-content__list']}> | |||||
| {messages.map((message) => ( | |||||
| <div className={styles['message-content__list__item']} key={message.id}> | |||||
| {messageType === MessageType.Mine && isDelete && ( | |||||
| <Checkbox | |||||
| style={{ marginRight: 10 }} | |||||
| checked={isSingleMessagesChecked(message.id)} | |||||
| onChange={(e) => { | |||||
| e.stopPropagation(); | |||||
| checkSingleMessages(message.id); | |||||
| }} | |||||
| ></Checkbox> | |||||
| )} | |||||
| {messageStatus === MessageStatus.All && ( | |||||
| <div | |||||
| className={classNames( | |||||
| styles['message-content__list__item__status'], | |||||
| message.status === MessageStatus.UnRead | |||||
| ? styles['message-content__list__item__status--unread'] | |||||
| : styles['message-content__list__item__status--readed'], | |||||
| )} | |||||
| > | |||||
| {message.status === MessageStatus.UnRead ? '未读' : '已读'} | |||||
| </div> | |||||
| )} | |||||
| <Typography.Text ellipsis={{ tooltip: message.content }}> | |||||
| {message.content} | |||||
| </Typography.Text> | |||||
| <div className={styles['message-content__list__item__time']}> | |||||
| {ago(message.created_at)} | |||||
| </div> | |||||
| {message.status === MessageStatus.UnRead && ( | |||||
| <Button | |||||
| className={styles['message-content__list__item__button']} | |||||
| type="link" | |||||
| onClick={() => readMessages([message.id])} | |||||
| > | |||||
| 标记已读 | |||||
| </Button> | |||||
| )} | |||||
| {messageType === MessageType.Mine && ( | |||||
| <Button | |||||
| type="link" | |||||
| className={styles['message-content__list__item__button']} | |||||
| onClick={() => deleteMessages([message.id])} | |||||
| > | |||||
| 删除 | |||||
| </Button> | |||||
| )} | |||||
| </div> | |||||
| ))} | |||||
| </div> | |||||
| <Pagination | |||||
| align="end" | |||||
| total={allTotal} | |||||
| showSizeChanger | |||||
| defaultPageSize={20} | |||||
| pageSizeOptions={[20, 40, 60, 80, 100]} | |||||
| showQuickJumper | |||||
| onChange={handlePageChange} | |||||
| {...pagination} | |||||
| /> | |||||
| </> | |||||
| )} | |||||
| {messages && messages.length === 0 && ( | |||||
| <KFEmpty | |||||
| className={styles['message-content__empty']} | |||||
| type={EmptyType.NoData} | |||||
| title="暂无数据" | |||||
| content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} | |||||
| hasFooter={true} | |||||
| onButtonClick={getMessages} | |||||
| /> | |||||
| )} | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default MessageContent; | |||||
| @@ -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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -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; | |||||
| onChange: (type: MessageType) => void; | |||||
| }; | |||||
| function MessageMenu({ messageType: currentType, onChange }: MessageMenuProps) { | |||||
| return ( | |||||
| <div className={styles['message-menu']}> | |||||
| <div className={styles['message-menu__title']}>消息列表</div> | |||||
| {menus.map((item) => ( | |||||
| <div | |||||
| key={item.type} | |||||
| className={classNames(styles['message-menu__item'], { | |||||
| [styles['message-menu__item--selected']]: item.type === currentType, | |||||
| })} | |||||
| onClick={() => onChange(item.type)} | |||||
| > | |||||
| <img className={styles['message-menu__item__icon']} src={item.icon} /> | |||||
| <img className={styles['message-menu__item__icon--hover']} src={item.hoverIcon} /> | |||||
| <span>{item.title}</span> | |||||
| </div> | |||||
| ))} | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default MessageMenu; | |||||
| @@ -0,0 +1,8 @@ | |||||
| .message { | |||||
| display: flex; | |||||
| flex-direction: row; | |||||
| gap: 0 20px; | |||||
| height: 100%; | |||||
| padding: 75px 260px 30px; | |||||
| .backgroundFullImage(url(@/assets/img/message/message-bg.png)); | |||||
| } | |||||
| @@ -0,0 +1,20 @@ | |||||
| import { MessageType } from '@/enums'; | |||||
| import { useState } from 'react'; | |||||
| import MessageContent from './components/Content'; | |||||
| import MessageMenu from './components/Menu'; | |||||
| import styles from './index.less'; | |||||
| function MessagePage() { | |||||
| const [messageType, setMessageType] = useState(MessageType.System); | |||||
| return ( | |||||
| <div className={styles['message']}> | |||||
| <MessageMenu | |||||
| onChange={(type) => setMessageType(type)} | |||||
| messageType={messageType} | |||||
| ></MessageMenu> | |||||
| <MessageContent messageType={messageType}></MessageContent> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default MessagePage; | |||||
| @@ -0,0 +1,22 @@ | |||||
| .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%; | |||||
| } | |||||
| &__table { | |||||
| height: calc(100% - 60px); | |||||
| padding: 20px 30px 0; | |||||
| background-color: white; | |||||
| border-radius: 10px; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,407 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-04-16 13:58:08 | |||||
| * @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 { to } from '@/utils/promise'; | |||||
| import SessionStorage from '@/utils/sessionStorage'; | |||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||||
| import { modalConfirm } from '@/utils/ui'; | |||||
| import { useNavigate } from '@umijs/max'; | |||||
| import { | |||||
| App, | |||||
| Button, | |||||
| ConfigProvider, | |||||
| Table, | |||||
| type TablePaginationConfig, | |||||
| type TableProps, | |||||
| } from 'antd'; | |||||
| import classNames from 'classnames'; | |||||
| import { useCallback, useState } from 'react'; | |||||
| // import CreateMirrorModal from '../components/CreateMirrorModal'; | |||||
| // import EditorStatusCell from '../components/EditorStatusCell'; | |||||
| import styles from './index.less'; | |||||
| export type EditorData = { | |||||
| id: number; | |||||
| name: string; | |||||
| status: string; | |||||
| computing_resource: string; | |||||
| update_by: string; | |||||
| create_time: string; | |||||
| url: string; | |||||
| computing_resource_id: number; | |||||
| dataset?: string | DatasetData; | |||||
| model?: string | ModelData; | |||||
| image?: string; | |||||
| code_config?: string | CodeConfigData; | |||||
| }; | |||||
| function ApprovalList() { | |||||
| const navigate = useNavigate(); | |||||
| const [cacheState, setCacheState] = useCacheState(); | |||||
| const { message } = App.useApp(); | |||||
| const [tableData, setTableData] = useState<EditorData[]>([]); | |||||
| const [total, setTotal] = useState(0); | |||||
| const [pagination, setPagination] = useState<TablePaginationConfig>( | |||||
| cacheState?.pagination ?? { | |||||
| current: 1, | |||||
| pageSize: 10, | |||||
| }, | |||||
| ); | |||||
| const getResourceDescription = useSystemResource(); | |||||
| // 获取编辑器列表 | |||||
| const getEditorList = useCallback(async () => { | |||||
| const params: Record<string, any> = { | |||||
| page: pagination.current! - 1, | |||||
| size: 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)); | |||||
| if (res) { | |||||
| message.success('删除成功'); | |||||
| // 如果是一页的唯一数据,删除后,请求第一页的数据 | |||||
| // 否则直接刷新这一页的数据 | |||||
| setPagination((prev) => { | |||||
| return { | |||||
| ...prev, | |||||
| current: tableData.length === 1 ? Math.max(1, prev.current! - 1) : prev.current, | |||||
| }; | |||||
| }); | |||||
| } | |||||
| }; | |||||
| // 启动编辑器 | |||||
| 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(); | |||||
| } | |||||
| }, | |||||
| }); | |||||
| }; | |||||
| // 制作镜像 | |||||
| // const createMirror = (id: number) => { | |||||
| // const { close } = openAntdModal(CreateMirrorModal, { | |||||
| // envId: id, | |||||
| // onOk: () => { | |||||
| // close(); | |||||
| // }, | |||||
| // }); | |||||
| // }; | |||||
| // 处理删除 | |||||
| const handleEditorDelete = (record: EditorData) => { | |||||
| modalConfirm({ | |||||
| title: '删除后,该编辑器将不可恢复', | |||||
| content: '是否确认删除?', | |||||
| onOk: () => { | |||||
| deleteEditor(record.id); | |||||
| }, | |||||
| }); | |||||
| }; | |||||
| // 创建编辑器 | |||||
| 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'] = ( | |||||
| pagination, | |||||
| _filters, | |||||
| _sorter, | |||||
| { action }, | |||||
| ) => { | |||||
| if (action === 'paginate') { | |||||
| setPagination(pagination); | |||||
| } | |||||
| }; | |||||
| 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), | |||||
| }, | |||||
| { | |||||
| 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: ['model', 'showValue'], | |||||
| key: 'model', | |||||
| width: '11%', | |||||
| render: tableCellRender(true, TableCellValueType.Link, { | |||||
| onClick: gotoModel, | |||||
| }), | |||||
| }, | |||||
| { | |||||
| title: '代码配置', | |||||
| dataIndex: ['code_config', 'showValue'], | |||||
| key: 'code_config', | |||||
| width: '11%', | |||||
| render: tableCellRender(true, TableCellValueType.Link, { | |||||
| onClick: gotoCodeConfig, | |||||
| }), | |||||
| }, | |||||
| { | |||||
| title: '镜像', | |||||
| dataIndex: ['image', 'showValue'], | |||||
| key: 'image', | |||||
| width: '11%', | |||||
| render: tableCellRender(true), | |||||
| }, | |||||
| { | |||||
| title: '创建者', | |||||
| dataIndex: 'update_by', | |||||
| key: 'update_by', | |||||
| width: '11%', | |||||
| 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: EditorStatusCell, | |||||
| // }, | |||||
| { | |||||
| title: '操作', | |||||
| dataIndex: 'operation', | |||||
| width: 270, | |||||
| key: 'operation', | |||||
| render: (_: any, record: EditorData) => ( | |||||
| <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)} | |||||
| > | |||||
| 启动 | |||||
| </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> | |||||
| </div> | |||||
| ), | |||||
| }, | |||||
| ]; | |||||
| return ( | |||||
| <div className={styles['approval-list']}> | |||||
| <div className={styles['approval-list__header']}> | |||||
| <div>审核</div> | |||||
| <Button | |||||
| style={{ marginLeft: '20px' }} | |||||
| type="default" | |||||
| onClick={getEditorList} | |||||
| icon={<KFIcon type="icon-shuaxin" />} | |||||
| > | |||||
| 刷新 | |||||
| </Button> | |||||
| </div> | |||||
| <div className={classNames('vertical-scroll-table', styles['approval-list__table'])}> | |||||
| <Table | |||||
| dataSource={tableData} | |||||
| columns={columns} | |||||
| scroll={{ y: 'calc(100% - 55px)' }} | |||||
| pagination={{ | |||||
| ...pagination, | |||||
| total: total, | |||||
| showSizeChanger: true, | |||||
| showQuickJumper: true, | |||||
| showTotal: () => `共${total}条`, | |||||
| }} | |||||
| onChange={handleTableChange} | |||||
| rowKey="id" | |||||
| /> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default ApprovalList; | |||||
| @@ -0,0 +1,40 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2025-08-28 10:18:27 | |||||
| * @Description: 消息 | |||||
| */ | |||||
| import { request } from '@umijs/max'; | |||||
| // 获取消息列表 | |||||
| export function getMessageListReq(params: any) { | |||||
| return request(`/api/reader/gns/notification/gitlink/list`, { | |||||
| method: 'GET', | |||||
| params, | |||||
| }); | |||||
| } | |||||
| // 获取消息数量 | |||||
| export function getMessageCountReq(params: any) { | |||||
| return request(`/api/reader/gns/notification/gitlink/count`, { | |||||
| method: 'GET', | |||||
| params, | |||||
| skipLoading: true, | |||||
| }); | |||||
| } | |||||
| // 标记已读 | |||||
| export function readMessagesReq(data: any) { | |||||
| return request(`/api/reader/gns/notification/gitlink/count`, { | |||||
| method: 'POST', | |||||
| data, | |||||
| }); | |||||
| } | |||||
| // 删除消息 | |||||
| export function deleteMessagesReq(data: any) { | |||||
| return request(`/api/reader/gns/notification/gitlink/count`, { | |||||
| method: 'POST', | |||||
| data, | |||||
| }); | |||||
| } | |||||
| @@ -1,3 +1,4 @@ | |||||
| import { serverNow } from '@/hooks/useServerTime'; | |||||
| import dayjs from 'dayjs'; | import dayjs from 'dayjs'; | ||||
| /** | /** | ||||
| @@ -59,6 +60,60 @@ export const elapsedTime = (begin?: string | Date | null, end?: string | Date | | |||||
| return elspsedArray.slice(0, 2).join(''); | return elspsedArray.slice(0, 2).join(''); | ||||
| }; | }; | ||||
| /** | |||||
| * 计算相比现在的时间过去了多久 | |||||
| * | |||||
| * @param {string | null | undefined} date - The starting date. | |||||
| * @return {string} The formatted elapsed time string. | |||||
| */ | |||||
| export const ago = (date?: string | Date | null): string => { | |||||
| if (date === undefined || date === null) { | |||||
| return '--'; | |||||
| } | |||||
| const beginDate = dayjs(date); | |||||
| if (!beginDate.isValid()) { | |||||
| return '--'; | |||||
| } | |||||
| const endDate = dayjs(serverNow()); | |||||
| const timestamp = endDate.valueOf() - beginDate.valueOf(); | |||||
| if (timestamp <= 0) { | |||||
| return '刚刚'; | |||||
| } | |||||
| const duration = dayjs.duration(timestamp); | |||||
| const years = duration.years(); | |||||
| const months = duration.months(); | |||||
| const days = duration.days(); | |||||
| const hours = duration.hours(); | |||||
| const minutes = duration.minutes(); | |||||
| const seconds = duration.seconds(); | |||||
| const elspsedArray = []; | |||||
| if (years !== 0) { | |||||
| elspsedArray.push(`${years}年`); | |||||
| } | |||||
| if (months !== 0) { | |||||
| elspsedArray.push(`${months}个月`); | |||||
| } | |||||
| if (days !== 0) { | |||||
| elspsedArray.push(`${days}天`); | |||||
| } | |||||
| if (hours !== 0) { | |||||
| elspsedArray.push(`${hours}小时`); | |||||
| } | |||||
| if (minutes !== 0) { | |||||
| elspsedArray.push(`${minutes}分`); | |||||
| } | |||||
| if (seconds !== 0) { | |||||
| elspsedArray.push(`${seconds}秒`); | |||||
| } | |||||
| if (elspsedArray.length === 0) { | |||||
| return '刚刚'; | |||||
| } | |||||
| return elspsedArray[0] + '前'; | |||||
| }; | |||||
| /** | /** | ||||
| * 是否是有效的日期 | * 是否是有效的日期 | ||||
| * | * | ||||