diff --git a/react-ui/config/routes.ts b/react-ui/config/routes.ts index b2c41f14..a45883ab 100644 --- a/react-ui/config/routes.ts +++ b/react-ui/config/routes.ts @@ -345,7 +345,6 @@ export default [ }, ], }, - { name: '应用开发', path: '/appsDeployment', @@ -498,6 +497,18 @@ export default [ }, ], }, + { + name: '算力积分', + path: '/points', + routes: [ + { + name: '算力积分', + path: '', + key: 'points', + component: './Points/index', + }, + ], + }, { path: '*', layout: false, diff --git a/react-ui/public/fonts/DingTalk-JinBuTi.ttf b/react-ui/public/fonts/DingTalk-JinBuTi.ttf new file mode 100644 index 00000000..c4efa55a Binary files /dev/null and b/react-ui/public/fonts/DingTalk-JinBuTi.ttf differ diff --git a/react-ui/public/fonts/DingTalk-JinBuTi.woff b/react-ui/public/fonts/DingTalk-JinBuTi.woff new file mode 100644 index 00000000..5a8efa8e Binary files /dev/null and b/react-ui/public/fonts/DingTalk-JinBuTi.woff differ diff --git a/react-ui/public/fonts/DingTalk-JinBuTi.woff2 b/react-ui/public/fonts/DingTalk-JinBuTi.woff2 new file mode 100644 index 00000000..c8d272a5 Binary files /dev/null and b/react-ui/public/fonts/DingTalk-JinBuTi.woff2 differ diff --git a/react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.otf b/react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.otf new file mode 100644 index 00000000..21ba9622 Binary files /dev/null and b/react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.otf differ diff --git a/react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.ttf b/react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.ttf new file mode 100644 index 00000000..31ab5e30 Binary files /dev/null and b/react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.ttf differ diff --git a/react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.woff b/react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.woff new file mode 100644 index 00000000..0abc0f0e Binary files /dev/null and b/react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.woff differ diff --git a/react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.woff2 b/react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.woff2 new file mode 100644 index 00000000..f4433e2e Binary files /dev/null and b/react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.woff2 differ diff --git a/react-ui/public/fonts/font.css b/react-ui/public/fonts/font.css index ea5ffc5c..06af2fdb 100644 --- a/react-ui/public/fonts/font.css +++ b/react-ui/public/fonts/font.css @@ -1,5 +1,23 @@ @font-face { - font-family: Alibaba; - src: url('./ALIBABA-PUHUITI-MEDIUM.TTF'); - font-display: swap; - } \ No newline at end of file + font-family: Alibaba; + src: url('./ALIBABA-PUHUITI-MEDIUM.TTF'); + font-display: swap; +} + +@font-face { + font-family: 'TaoBaoMaiCaiTi'; + src: url('./TaoBaoMaiCaiTi-Regular.woff2') format('woff2'), /* 最优先使用 woff2 */ + url('./TaoBaoMaiCaiTi-Regular.woff') format('woff'), /* 兼容性较好的 woff */ + url('./TaoBaoMaiCaiTi-Regular.ttf') format('truetype'), /* ttf 作为备选 */ + url('./TaoBaoMaiCaiTi-Regular.otf') format('opentype'); /* otf 作为最后选项 */ + font-display: swap; /* 优化页面加载时的字体显示 */ +} + + +@font-face { + font-family: 'DingTalk-JinBuTi'; + src: url('./DingTalk-JinBuTi.woff2') format('woff2'), /* 最优先使用 woff2 */ + url('./DingTalk-JinBuTi.woff') format('woff'), /* 兼容性较好的 woff */ + url('./DingTalk-JinBuTi.ttf') format('truetype'); /* ttf 作为备选 */ + font-display: swap; /* 优化页面加载时的字体显示 */ +} \ No newline at end of file diff --git a/react-ui/src/assets/img/user-points-bg.png b/react-ui/src/assets/img/user-points-bg.png new file mode 100644 index 00000000..3f25b128 Binary files /dev/null and b/react-ui/src/assets/img/user-points-bg.png differ diff --git a/react-ui/src/assets/img/workspace-experiment.png b/react-ui/src/assets/img/workspace-experiment.png index bcd69981..0bca4327 100644 Binary files a/react-ui/src/assets/img/workspace-experiment.png and b/react-ui/src/assets/img/workspace-experiment.png differ diff --git a/react-ui/src/assets/img/workspace-pipeline.png b/react-ui/src/assets/img/workspace-pipeline.png index fbbc3ed7..f551cb9c 100644 Binary files a/react-ui/src/assets/img/workspace-pipeline.png and b/react-ui/src/assets/img/workspace-pipeline.png differ diff --git a/react-ui/src/pages/Points/components/Statistics/index.less b/react-ui/src/pages/Points/components/Statistics/index.less new file mode 100644 index 00000000..4ca97f96 --- /dev/null +++ b/react-ui/src/pages/Points/components/Statistics/index.less @@ -0,0 +1,31 @@ +.statistics { + display: flex; + align-items: center; + width: 100%; + padding: 20px 0; + background-color: @background-color; + border-radius: 8px; + + &__item { + display: flex; + flex: 1; + flex-direction: column; + align-items: center; + + &--border { + border-right: 1px solid @border-color; + } + + &__value { + margin-bottom: 8px; + color: @text-color; + font-weight: bold; + font-size: 30px; + } + + &__title { + color: #999999; + font-size: @font-size-input; + } + } +} diff --git a/react-ui/src/pages/Points/components/Statistics/index.tsx b/react-ui/src/pages/Points/components/Statistics/index.tsx new file mode 100644 index 00000000..e979b99d --- /dev/null +++ b/react-ui/src/pages/Points/components/Statistics/index.tsx @@ -0,0 +1,38 @@ +import classNames from 'classnames'; +import styles from './index.less'; + +type StatisticsProps = { + remaining?: number; + consuming?: number; +}; + +function Statistics({ remaining, consuming }: StatisticsProps) { + const items = [ + { + title: '当前可用算力积分(分)', + value: remaining ?? '-', + }, + { + title: '总消耗算力积分(分)', + value: consuming ?? '-', + }, + ]; + + return ( +
+ {items.map((item, index) => ( +
+ {item.value} + {item.title} +
+ ))} +
+ ); +} + +export default Statistics; diff --git a/react-ui/src/pages/Points/index.less b/react-ui/src/pages/Points/index.less new file mode 100644 index 00000000..e97785e2 --- /dev/null +++ b/react-ui/src/pages/Points/index.less @@ -0,0 +1,19 @@ +.points-detail { + height: 100%; + &__content { + height: calc(100% - 60px); + margin-top: 10px; + padding: 20px 30px 0; + background-color: white; + border-radius: 10px; + + &__top { + width: 100%; + } + + &__table { + height: calc(100% - 117px - 28px); + margin-top: 28px; + } + } +} diff --git a/react-ui/src/pages/Points/index.tsx b/react-ui/src/pages/Points/index.tsx new file mode 100644 index 00000000..20843820 --- /dev/null +++ b/react-ui/src/pages/Points/index.tsx @@ -0,0 +1,256 @@ +/* + * @Author: 赵伟 + * @Date: 2024-04-16 13:58:08 + * @Description: 模型部署服务列表 + */ +import PageTitle from '@/components/PageTitle'; +import { getPointsConsumptionReq, getPointsStatisticsReq } from '@/services/points'; +import themes from '@/styles/theme.less'; +import { to } from '@/utils/promise'; +import statusTableCell from '@/utils/statusTableCell'; +import tableCellRender, { TableCellValueType } from '@/utils/table'; +import { Link } from '@umijs/max'; +import { Table, type TablePaginationConfig, type TableProps } from 'antd'; +import classNames from 'classnames'; +import { useEffect, useState } from 'react'; +import Statistics from './components/Statistics'; +import styles from './index.less'; + +enum TaskType { + DevEnvironment = 'dev_environment', + Workflow = 'workflow', + Ray = 'ray', + Service = 'service', +} + +const taskTypeOptions = [ + { + value: 'dev_environment', + label: '开发环境', + }, + { + value: 'workflow', + label: '实验', + }, + { + value: 'ray', + label: '超参数自动寻优', + }, + { + value: 'service', + label: '服务', + }, +]; + +export type PointsData = { + computing_resource_id: number; + credit_per_hour: number; // 每小时消耗的积分 + deduce_credit: number; + deduce_last_time: string; + description: string; + id: number; + node_id: string; + start_time: string; + state: number; + workflow_id?: number; // 流水线id + task_id: number; // 实验id + task_ins_id: number; // 实例id + task_type: string; + user_id: number; +}; + +export type PointsStatistics = { + deduceCredit: number; + userCredit: number; +}; + +// 格式化任务 +const formatTask = (value: string, record: PointsData) => { + let url; + switch (record.task_type) { + case TaskType.DevEnvironment: + url = `/developmentEnvironment`; + break; + case TaskType.Workflow: + if (record.workflow_id && record.task_ins_id) { + url = `/pipeline/experiment/instance/${record.workflow_id}/${record.task_ins_id}`; + } + break; + case TaskType.Ray: + if (record.task_id && record.task_ins_id) { + url = `/pipeline/hyperparameter/instance/${record.task_id}/${record.task_ins_id}`; + } + break; + case TaskType.Service: + if (record.task_id && record.task_ins_id) { + url = `/dataset/modelDeployment/serviceInfo/${record.task_id}/versionInfo/${record.task_ins_id}`; + } + break; + default: + break; + } + + if (url) { + return {value}; + } else { + return {value}; + } +}; + +function PointsDetail() { + const [tableData, setTableData] = useState([]); + const [total, setTotal] = useState(0); + const [statistics, setStatistics] = useState(); + const [pagination, setPagination] = useState({ + current: 1, + pageSize: 10, + }); + + useEffect(() => { + // 获取积分统计 + const getPointsStatistics = async () => { + const [res] = await to(getPointsStatisticsReq()); + if (res && res.data) { + setStatistics(res.data); + } + }; + + getPointsStatistics(); + }, []); + + useEffect(() => { + // 获取积分消费明细 + const getPointsConsumption = async () => { + const params: Record = { + page: pagination.current! - 1, + size: pagination.pageSize, + }; + const [res] = await to(getPointsConsumptionReq(params)); + if (res && res.data) { + const { content = [], totalElements = 0 } = res.data; + setTableData(content); + setTotal(totalElements); + } + }; + + getPointsConsumption(); + }, [pagination]); + + // 分页切换 + const handleTableChange: TableProps['onChange'] = ( + pagination, + _filters, + _sorter, + { action }, + ) => { + if (action === 'paginate') { + setPagination(pagination); + } + }; + + const columns: TableProps['columns'] = [ + { + title: '序号', + dataIndex: 'index', + key: 'index', + width: 80, + render: tableCellRender(false, TableCellValueType.Index, { + page: pagination.current! - 1, + pageSize: pagination.pageSize!, + }), + }, + { + title: '任务', + dataIndex: 'task_name', + key: 'task_name', + width: '15%', + render: formatTask, + }, + { + title: '任务类型', + dataIndex: 'task_type', + key: 'task_type', + width: '10%', + render: statusTableCell(taskTypeOptions), + }, + { + title: '积分/小时', + dataIndex: 'credit_per_hour', + key: 'credit_per_hour', + width: '10%', + render: tableCellRender(), + }, + { + title: '消耗的积分(分)', + dataIndex: 'deduce_credit', + key: 'deduce_credit', + width: '12%', + render: tableCellRender(), + }, + { + title: '描述', + dataIndex: 'description', + key: 'description', + render: tableCellRender(true), + }, + { + title: '开始时间', + dataIndex: 'start_time', + key: 'start_time', + width: '15%', + render: tableCellRender(false, TableCellValueType.Date), + }, + { + title: '状态', + dataIndex: 'state', + key: 'state', + width: '8%', + render: statusTableCell([ + { + value: 0, + label: '已结束', + color: themes['textColor'], + }, + { + value: 1, + label: '进行中', + color: themes['primaryColor'], + }, + ]), + }, + ]; + + return ( +
+ +
+
+ +
+
+ `共${total}条`, + }} + onChange={handleTableChange} + rowKey="id" + /> + + + + ); +} + +export default PointsDetail; diff --git a/react-ui/src/pages/System/User/edit.tsx b/react-ui/src/pages/System/User/edit.tsx index dd23b6e0..3c8f045b 100644 --- a/react-ui/src/pages/System/User/edit.tsx +++ b/react-ui/src/pages/System/User/edit.tsx @@ -2,6 +2,7 @@ import { DictValueEnumObj } from '@/components/DictTag'; import KFModal from '@/components/KFModal'; import { ProForm, + ProFormDigit, ProFormRadio, ProFormSelect, ProFormText, @@ -64,6 +65,7 @@ const UserForm: React.FC = (props) => { remark: props.values.remark, gitLinkUsername: props.values.gitLinkUsername, gitLinkPassword: props.values.gitLinkPassword, + credit: props.values.credit, }); }, [form, props, statusOptions]); @@ -219,12 +221,7 @@ const UserForm: React.FC = (props) => { autoComplete: 'new-password', }} allowClear - rules={[ - { - required: true, - message: , - }, - ]} + rules={props.values.userId ? [] : [{ required: true, message: '请输入密码!' }]} /> = (props) => { }} rules={props.values.userId ? [] : [{ required: true, message: '请输入 Git 密码!' }]} /> + { dataIndex: 'phonenumber', valueType: 'text', }, + { + title: , + dataIndex: 'credit', + valueType: 'text', + }, { title: , dataIndex: 'status', diff --git a/react-ui/src/pages/Workspace/components/ExperimentChart/index.less b/react-ui/src/pages/Workspace/components/ExperimentChart/index.less index a723c650..3af052da 100644 --- a/react-ui/src/pages/Workspace/components/ExperimentChart/index.less +++ b/react-ui/src/pages/Workspace/components/ExperimentChart/index.less @@ -1,4 +1,5 @@ .experiment-chart { + flex: none; width: 295px; min-width: 295px; height: 140px; diff --git a/react-ui/src/pages/Workspace/components/ExperimentTable/index.less b/react-ui/src/pages/Workspace/components/ExperimentTable/index.less index fc83b21d..4b692883 100644 --- a/react-ui/src/pages/Workspace/components/ExperimentTable/index.less +++ b/react-ui/src/pages/Workspace/components/ExperimentTable/index.less @@ -1,8 +1,8 @@ .experiment-table { flex: 1; - min-width: 500px; + min-width: 378px; height: 140px; - padding: 12px 24px; + padding: 12px; background: @workspace-background; border-radius: 4px; @@ -32,15 +32,15 @@ } &__status { - width: 20%; + width: 15%; } &__duration { - width: 30%; + width: 25%; } &__date { - width: calc(50% - 60px); + width: calc(60% - 60px); } &__operation { diff --git a/react-ui/src/pages/Workspace/components/TotalStatistics/index.less b/react-ui/src/pages/Workspace/components/TotalStatistics/index.less index d8943328..0fdef0b0 100644 --- a/react-ui/src/pages/Workspace/components/TotalStatistics/index.less +++ b/react-ui/src/pages/Workspace/components/TotalStatistics/index.less @@ -1,16 +1,12 @@ .total-statistics { display: flex; align-items: center; - justify-content: center; - width: 400px; height: 140px; - background: @workspace-background; - border-radius: 4px; + padding: 0 16px; &__icon { - width: 85px; - height: 80px; - margin-right: 40px; + width: 63px; + margin-right: 15px; } &__title { @@ -20,18 +16,10 @@ font-size: @font-size-content; } - &__title-shadow { - position: absolute; - bottom: 6px; - left: 0; - width: 79px; - height: 6px; - background: linear-gradient(87.07deg, rgba(22, 100, 255, 0.6) 0%, rgba(22, 100, 255, 0) 100%); - } - &__count { color: @text-color; font-weight: 700; font-size: 25px; + font-family: DingTalk-JinBuTi; } } diff --git a/react-ui/src/pages/Workspace/components/TotalStatistics/index.tsx b/react-ui/src/pages/Workspace/components/TotalStatistics/index.tsx index c6834cfb..ef7f79de 100644 --- a/react-ui/src/pages/Workspace/components/TotalStatistics/index.tsx +++ b/react-ui/src/pages/Workspace/components/TotalStatistics/index.tsx @@ -1,3 +1,4 @@ +import { Flex } from 'antd'; import styles from './index.less'; type TotalStatisticsProps = { @@ -11,13 +12,12 @@ function TotalStatistics({ icon = '', title = '', count = 0, style }: TotalStati return (
-
+
{title} -
{count ?? '--'}
-
+
); } diff --git a/react-ui/src/pages/Workspace/components/UserPoints/index.less b/react-ui/src/pages/Workspace/components/UserPoints/index.less new file mode 100644 index 00000000..4a7554bf --- /dev/null +++ b/react-ui/src/pages/Workspace/components/UserPoints/index.less @@ -0,0 +1,36 @@ +.user-points { + display: flex; + flex: none; + flex-direction: column; + align-items: center; + + width: 326px; + height: 228px; + .backgroundFullImage(url(@/assets/img/user-points-bg.png)); + + &__label { + margin-top: 60px; + color: @text-color; + font-size: @font-size-title; + font-family: DingTalk-JinBuTi; + } + + &__value { + margin-top: 8px; + margin-bottom: 12px; + color: @primary-color; + font-size: 36px; + font-family: DingTalk-JinBuTi; + line-height: 43px; + } + + &__button { + padding: 8px 20px; + color: @primary-color; + font-size: @font-size-content; + background: rgba(255, 255, 255, 0.3); + border: 1px solid #ffffff; + border-radius: 8px; + cursor: pointer; + } +} diff --git a/react-ui/src/pages/Workspace/components/UserPoints/index.tsx b/react-ui/src/pages/Workspace/components/UserPoints/index.tsx new file mode 100644 index 00000000..3ea37e23 --- /dev/null +++ b/react-ui/src/pages/Workspace/components/UserPoints/index.tsx @@ -0,0 +1,40 @@ +import { PointsStatistics } from '@/pages/Points/index'; +import { getPointsStatisticsReq } from '@/services/points'; +import { to } from '@/utils/promise'; +import { useNavigate } from '@umijs/max'; +import { useEffect, useState } from 'react'; +import styles from './index.less'; + +function UserPoints() { + const [statistics, setStatistics] = useState(); + const navigate = useNavigate(); + + useEffect(() => { + // 获取积分统计 + const getPointsStatistics = async () => { + const [res] = await to(getPointsStatisticsReq()); + if (res && res.data) { + setStatistics(res.data); + } + }; + + getPointsStatistics(); + }, []); + + return ( +
+ 当前可用算力积分 + {statistics?.userCredit ?? '--'} +
{ + navigate('/points'); + }} + > + 查看详情 +
+
+ ); +} + +export default UserPoints; diff --git a/react-ui/src/pages/Workspace/index.less b/react-ui/src/pages/Workspace/index.less index 3fbcc8e8..d498e652 100644 --- a/react-ui/src/pages/Workspace/index.less +++ b/react-ui/src/pages/Workspace/index.less @@ -6,6 +6,7 @@ background: linear-gradient(#ecf2fe, #f9fafb); &__overview { + flex: 1; gap: 15px; margin-bottom: 16px; padding: 20px 30px; @@ -21,11 +22,19 @@ &__content { display: flex; + + flex-wrap: wrap; gap: 15px; align-items: center; - @media screen and (max-width: 1800px) { - flex-wrap: wrap; + &__statistics { + flex: none; + background: linear-gradient( + 123.08deg, + rgba(138, 138, 138, 0.06) 1.32%, + rgba(22, 100, 255, 0.02) 58.35% + ); + border-radius: 4px; } } } diff --git a/react-ui/src/pages/Workspace/index.tsx b/react-ui/src/pages/Workspace/index.tsx index 691ca550..efda7e70 100644 --- a/react-ui/src/pages/Workspace/index.tsx +++ b/react-ui/src/pages/Workspace/index.tsx @@ -2,6 +2,7 @@ import { useDraggable } from '@/hooks/draggable'; import { getWorkspaceOverviewReq } from '@/services/workspace'; import { ExperimentInstance } from '@/types'; import { to } from '@/utils/promise'; +import { Divider, Flex } from 'antd'; import { useEffect, useState } from 'react'; import Draggable from 'react-draggable'; import AssetsManagement from './components/AssetsManagement'; @@ -10,6 +11,7 @@ import ExperitableTable from './components/ExperimentTable'; import QuickStart from './components/QuickStart'; import RobotFrame from './components/RobotFrame'; import TotalStatistics from './components/TotalStatistics'; +import UserPoints from './components/UserPoints'; import UserSpace from './components/UserSpace'; import WorkspaceIntro from './components/WorkspaceIntro'; import styles from './index.less'; @@ -28,6 +30,7 @@ function Workspace() { setRobotFrameVisible((prev) => !prev), ); const users: number[] = new Array(8).fill(0); + useEffect(() => { getWorkspaceOverview(); }, []); @@ -43,27 +46,38 @@ function Workspace() { return (
-
-
运行概览
-
- - - - {overviewData?.experimentInsStatus && ( - - )} + +
+
运行概览
+
+ + + + + + + + {overviewData?.experimentInsStatus && ( + + )} +
-
+ +
diff --git a/react-ui/src/services/points/index.ts b/react-ui/src/services/points/index.ts new file mode 100644 index 00000000..6b5cc0d4 --- /dev/null +++ b/react-ui/src/services/points/index.ts @@ -0,0 +1,22 @@ +/* + * @Author: 赵伟 + * @Date: 2025-03-20 13:48:53 + * @Description: 积分 + */ + +import { request } from '@umijs/max'; + +// 分页查询积分消费明细 +export function getPointsConsumptionReq(params: any) { + return request(`/api/mmp/computingResource/resouceOccupy`, { + method: 'GET', + params, + }); +} + +// 分页查询积分消费明细 +export function getPointsStatisticsReq() { + return request(`/api/mmp/computingResource/credit`, { + method: 'GET', + }); +} diff --git a/react-ui/src/styles/theme.less b/react-ui/src/styles/theme.less index d044889f..89b955c8 100644 --- a/react-ui/src/styles/theme.less +++ b/react-ui/src/styles/theme.less @@ -69,6 +69,14 @@ -webkit-line-clamp: @line; } +// 背景 +.backgroundFullImage(@url) { + background-image: @url; + background-repeat: no-repeat; + background-position: top center; + background-size: 100% 100%; +} + // 导出变量 :export { primaryColor: @primary-color; diff --git a/react-ui/src/types/system/user.d.ts b/react-ui/src/types/system/user.d.ts index a74ee0e8..0eca095a 100644 --- a/react-ui/src/types/system/user.d.ts +++ b/react-ui/src/types/system/user.d.ts @@ -21,6 +21,7 @@ declare namespace API.System { remark: string; gitLinkUsername?: string; gitLinkPassword?: string; + credit?: number; } export interface UserListParams { diff --git a/react-ui/src/utils/statusTableCell.tsx b/react-ui/src/utils/statusTableCell.tsx new file mode 100644 index 00000000..8efa5803 --- /dev/null +++ b/react-ui/src/utils/statusTableCell.tsx @@ -0,0 +1,23 @@ +/* + * @Author: 赵伟 + * @Date: 2025-03-20 14:33:01 + * @Description: 通用的 Table 状态单元格 + */ + +export type StatusInfo = { + value: number | string; + label: string; + color?: string; +}; + +function statusTableCell(infos: StatusInfo[]) { + return function (status?: string | number | null) { + const info = infos.find((item) => item.value === status); + if (status === null || status === undefined || !info) { + return --; + } + return {info.label}; + }; +} + +export default statusTableCell; diff --git a/react-ui/src/utils/table.tsx b/react-ui/src/utils/table.tsx index 1a1e84c4..757471fe 100644 --- a/react-ui/src/utils/table.tsx +++ b/react-ui/src/utils/table.tsx @@ -1,7 +1,7 @@ /* * @Author: 赵伟 * @Date: 2024-06-26 10:05:52 - * @Description: Table cell 自定义 render + * @Description: Table Cell 自定义 render */ import { isEmpty } from '@/utils';