| @@ -8,7 +8,7 @@ | |||
| "build": "max build", | |||
| "deploy": "npm run build && npm run gh-pages", | |||
| "dev": "npm run start:dev", | |||
| "dev-no-sso": "cross-env NO_SSO=true npm run start:mock", | |||
| "dev-no-sso": "cross-env NO_SSO=true PORT=8001 npm run start:mock", | |||
| "docker-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./", | |||
| "docker-prod:build": "docker-compose -f ./docker/docker-compose.yml build", | |||
| "docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up", | |||
| @@ -75,6 +75,7 @@ | |||
| "classnames": "^2.3.2", | |||
| "crypto-js": "^4.2.0", | |||
| "echarts": "^5.5.0", | |||
| "echarts-gl": "~2.0.9", | |||
| "fabric": "^5.3.0", | |||
| "highlight.js": "^11.7.0", | |||
| "lodash": "^4.17.21", | |||
| @@ -0,0 +1,29 @@ | |||
| import { useEffect, useState } from 'react'; | |||
| /** | |||
| * 获取 root font size | |||
| * | |||
| * @param initialValue - 状态的初始值 | |||
| * @return 包含状态值、状态设置函数和可变引用对象的数组 | |||
| */ | |||
| export function useRootFontSize() { | |||
| const fontSizeStr = document.documentElement.style.fontSize; | |||
| const fontSize = parseFloat(fontSizeStr); | |||
| const [size, setSize] = useState(fontSize); | |||
| useEffect(() => { | |||
| const setFontSize = () => { | |||
| setTimeout(() => { | |||
| const fontSizeStr = document.documentElement.style.fontSize; | |||
| const fontSize = parseFloat(fontSizeStr); | |||
| setSize(fontSize); | |||
| }, 300); | |||
| }; | |||
| window.addEventListener('resize', setFontSize); | |||
| return () => { | |||
| window.removeEventListener('resize', setFontSize); | |||
| }; | |||
| }, []); | |||
| return size; | |||
| } | |||
| @@ -1,65 +1,56 @@ | |||
| .assets-management { | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| width: 100%; | |||
| padding: 20px 20px 0; | |||
| background-color: white; | |||
| border-radius: 4px; | |||
| height: 10.875rem; | |||
| color: white; | |||
| .backgroundFullImage(url(@/assets/img/workspace/statistics-bg.png)); | |||
| :global { | |||
| .ant-select-filled { | |||
| background-color: rgba(138, 138, 138, 0.12); | |||
| border-radius: 2px; | |||
| .ant-select-selection-item { | |||
| color: @text-color-secondary !important; | |||
| font-size: 13px; | |||
| } | |||
| } | |||
| &__title { | |||
| flex: none; | |||
| padding: 1.25rem; | |||
| font-weight: 700; | |||
| font-size: 1rem; | |||
| } | |||
| &__title { | |||
| color: @text-color; | |||
| font-weight: 500; | |||
| font-size: @font-size-title; | |||
| &__line { | |||
| flex: none; | |||
| width: 64.9375rem; | |||
| height: 1px; | |||
| padding-left: 1.25rem; | |||
| background-image: linear-gradient( | |||
| 135.57deg, | |||
| rgba(255, 255, 255, 0.3) 0%, | |||
| rgba(255, 255, 255, 0) 100% | |||
| ); | |||
| } | |||
| &__increase { | |||
| display: inline-block; | |||
| margin-top: 12px; | |||
| margin-bottom: 30px; | |||
| padding: 2px 7px; | |||
| color: @primary-color; | |||
| font-size: 13px; | |||
| background-color: rgba(187, 210, 255, 0.29); | |||
| border-radius: 2px; | |||
| &__content { | |||
| display: flex; | |||
| flex: 1; | |||
| align-items: center; | |||
| padding: 0 1.25rem; | |||
| } | |||
| &__summary { | |||
| display: flex; | |||
| flex-direction: column; | |||
| width: 40%; | |||
| text-align: left; | |||
| align-items: center; | |||
| width: 8.125rem; | |||
| border-right: 1px solid rgba(255, 255, 255, 0.2); | |||
| &:nth-child(3n + 2) { | |||
| width: 30%; | |||
| text-align: center; | |||
| &:last-child { | |||
| border-right: none; | |||
| } | |||
| &:nth-child(3n) { | |||
| width: 30%; | |||
| text-align: right; | |||
| &__value { | |||
| margin-bottom: 4px; | |||
| font-weight: 700; | |||
| font-size: 1.25rem; | |||
| } | |||
| &__title { | |||
| margin-bottom: 12px; | |||
| color: @text-color-secondary; | |||
| font-size: @font-size; | |||
| } | |||
| &__value { | |||
| color: @text-color; | |||
| font-weight: 500; | |||
| font-size: 22px; | |||
| font-size: 0.875rem; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,12 +1,15 @@ | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { getWorkspaceAssetCountReq } from '@/services/workspace'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Flex, Select } from 'antd'; | |||
| import { Flex, Typography } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import styles from './index.less'; | |||
| function AssetsManagement() { | |||
| const [type, setType] = useState(CommonTabKeys.Public); | |||
| const [assetCounts, setAssetCounts] = useState<{ title: string; value: number }[]>([]); | |||
| const [assetCounts, setAssetCounts] = useState<{ title: string; value: number; icon: string }[]>( | |||
| [], | |||
| ); | |||
| useEffect(() => { | |||
| // 获取工作空间资产数量 | |||
| @@ -21,34 +24,37 @@ function AssetsManagement() { | |||
| { | |||
| title: '数据集', | |||
| value: dataset, | |||
| icon: 'dataset.png', | |||
| }, | |||
| { | |||
| title: '模型', | |||
| value: model, | |||
| icon: 'model.png', | |||
| }, | |||
| { | |||
| title: '代码配置', | |||
| value: 0, | |||
| icon: 'code.png', | |||
| }, | |||
| { | |||
| title: '镜像', | |||
| value: image, | |||
| icon: 'image.png', | |||
| }, | |||
| // { | |||
| // title: '组件', | |||
| // value: component, | |||
| // }, | |||
| // { | |||
| // title: '代码配置', | |||
| // value: 0, | |||
| // }, | |||
| { | |||
| title: '流水线模版', | |||
| value: workflow, | |||
| title: 'APP', | |||
| value: service, | |||
| icon: 'app.png', | |||
| }, | |||
| { | |||
| title: '代码配置', | |||
| value: codeConfig, | |||
| title: '流水线模板', | |||
| value: workflow, | |||
| icon: 'experiment.png', | |||
| }, | |||
| { | |||
| title: '服务', | |||
| value: service, | |||
| title: 'NOTEBOOKS', | |||
| value: codeConfig, | |||
| icon: 'train.png', | |||
| }, | |||
| ]; | |||
| setAssetCounts(items); | |||
| @@ -60,26 +66,18 @@ function AssetsManagement() { | |||
| return ( | |||
| <div className={styles['assets-management']}> | |||
| <Flex justify="space-between"> | |||
| <div className={styles['assets-management__title']}>多形态资源库</div> | |||
| <Select | |||
| size="small" | |||
| value={type} | |||
| style={{ width: 70 }} | |||
| onChange={setType} | |||
| variant="filled" | |||
| options={[ | |||
| { value: CommonTabKeys.Public, label: '公开' }, | |||
| { value: CommonTabKeys.Private, label: '个人' }, | |||
| ]} | |||
| /> | |||
| </Flex> | |||
| {/* <div className={styles['assets-management__increase']}>今日新增数量:5</div> */} | |||
| <Flex gap="22px 0" wrap="wrap" style={{ marginTop: '40px', padding: '0 8px' }}> | |||
| <div className={styles['assets-management__title']}>个人资源库统计</div> | |||
| <div className={styles['assets-management__line']}></div> | |||
| <Flex className={styles['assets-management__content']}> | |||
| {assetCounts.map((item, index) => ( | |||
| <div className={styles['assets-management__summary']} key={index}> | |||
| <div key={index} className={styles['assets-management__summary']}> | |||
| <Typography.Text | |||
| className={styles['assets-management__summary__value']} | |||
| ellipsis={{ tooltip: item.value }} | |||
| > | |||
| {item.value ?? '-'} | |||
| </Typography.Text> | |||
| <div className={styles['assets-management__summary__title']}>{item.title}</div> | |||
| <div className={styles['assets-management__summary__value']}>{item.value ?? '-'}</div> | |||
| </div> | |||
| ))} | |||
| </Flex> | |||
| @@ -0,0 +1,27 @@ | |||
| .block-title { | |||
| display: flex; | |||
| align-items: center; | |||
| padding: 0.9375rem 1.25rem; | |||
| color: @text-color; | |||
| font-weight: 700; | |||
| font-size: 1rem; | |||
| .backgroundFullImage(url(@/assets/img/workspace/block-title-bg.png)); | |||
| &__icon { | |||
| width: 1.875rem; | |||
| height: 1.875rem; | |||
| margin-right: 0.9375rem; | |||
| } | |||
| &__right { | |||
| margin-right: 0; | |||
| margin-left: auto; | |||
| color: @text-color-secondary; | |||
| font-weight: normal; | |||
| font-size: 0.875rem; | |||
| &:hover { | |||
| color: @primary-color; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,28 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { Flex } from 'antd'; | |||
| import styles from './index.less'; | |||
| export type BlockTitleProps = { | |||
| title: string; | |||
| viewMore?: () => void; | |||
| }; | |||
| function BlockTitle({ title, viewMore }: BlockTitleProps) { | |||
| return ( | |||
| <div className={styles['block-title']}> | |||
| <img | |||
| src={require('@/assets/img/workspace/title-icon.png')} | |||
| className={styles['block-title__icon']} | |||
| ></img> | |||
| <span>{title}</span> | |||
| {viewMore && ( | |||
| <Flex className={styles['block-title__right']} onClick={viewMore}> | |||
| <span>查看全部</span> | |||
| <KFIcon type="icon-a-zu14930" font={12} style={{ marginLeft: 8 }} /> | |||
| </Flex> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| export default BlockTitle; | |||
| @@ -0,0 +1,285 @@ | |||
| /** | |||
| * 绘制3d图 | |||
| * @param pieData 总数据 | |||
| * @param internalDiameterRatio:透明的空心占比 | |||
| * @param distance 视角到主体的距离 | |||
| * @param alpha 旋转角度 | |||
| * @param pieHeight 立体的高度 | |||
| * @param opacity 饼或者环的透明度 | |||
| */ | |||
| const setSeriesItem = (item, k) => { | |||
| const seriesItem = { | |||
| name: | |||
| typeof item.name === 'undefined' | |||
| ? `series${item}` | |||
| : item.name, | |||
| type: 'surface', | |||
| parametric: true, | |||
| wireframe: { | |||
| show: false | |||
| }, | |||
| pieData: item, | |||
| pieStatus: { | |||
| selected: false, | |||
| hovered: false, | |||
| k: k | |||
| }, | |||
| center: ['10%', '50%'] | |||
| } | |||
| if ( item.color) { | |||
| seriesItem.itemStyle = { color: item.color } | |||
| } | |||
| return seriesItem | |||
| } | |||
| export const getPie3D = (pieData, internalDiameterRatio, distance, alpha, pieHeight, opacity = 1) => { | |||
| const series = [] | |||
| let sumValue = 0 | |||
| let startValue = 0 | |||
| let endValue = 0 | |||
| let legendData = [] | |||
| let legendBfb = [] | |||
| const k = 1 - internalDiameterRatio | |||
| pieData.sort((a, b) => { | |||
| return b.value - a.value | |||
| }) | |||
| // 为每一个饼图数据,生成一个 series-surface 配置 | |||
| for (let i = 0; i < pieData.length; i++) { | |||
| sumValue += pieData[i].value | |||
| const seriesItem = setSeriesItem(pieData[i], k) | |||
| // if (typeof pieData[i].itemStyle !== 'undefined') { | |||
| // const itemStyle = {} | |||
| // itemStyle.color = | |||
| // typeof pieData[i].itemStyle.color !== 'undefined' | |||
| // ? pieData[i].itemStyle.color | |||
| // : opacity | |||
| // itemStyle.opacity = | |||
| // typeof pieData[i].itemStyle.opacity !== 'undefined' | |||
| // ? pieData[i].itemStyle.opacity | |||
| // : opacity | |||
| // seriesItem.itemStyle = itemStyle | |||
| // } | |||
| series.push(seriesItem); | |||
| } | |||
| if (sumValue === 0) { | |||
| series.length = 0 | |||
| series.push(setSeriesItem({ value: 100, color: '#d3d3d3', name: '暂无数据' })) | |||
| } | |||
| // 使用上一次遍历时,计算出的数据和 sumValue,调用 getParametricEquation 函数, | |||
| // 向每个 series-surface 传入不同的参数方程 series-surface.parametricEquation,也就是实现每一个扇形。 | |||
| legendData = [] | |||
| legendBfb = [] | |||
| for (let i = 0; i < series.length; i++) { | |||
| endValue = startValue + series[i].pieData.value | |||
| series[i].pieData.startRatio = startValue / sumValue | |||
| series[i].pieData.endRatio = endValue / sumValue | |||
| series[i].parametricEquation = getParametricEquation( | |||
| series[i].pieData.startRatio, | |||
| series[i].pieData.endRatio, | |||
| false, | |||
| false, | |||
| k, | |||
| series[i].pieData.value | |||
| ) | |||
| startValue = endValue | |||
| const bfb = fomatFloat(series[i].pieData.value / sumValue, 4) | |||
| legendData.push({ | |||
| name: series[i].name, | |||
| value: bfb | |||
| }) | |||
| legendBfb.push({ | |||
| name: series[i].name, | |||
| value: bfb | |||
| }) | |||
| } | |||
| const boxHeight = getHeight3D(series, pieHeight) // 通过pieHeight设定3d饼/环的高度,单位是px | |||
| // 准备待返回的配置项,把准备好的 legendData、series 传入。 | |||
| const option = { | |||
| legend: { | |||
| show: false, | |||
| data: legendData, | |||
| orient: 'vertical', | |||
| left: 10, | |||
| top: 10, | |||
| itemGap: 10, | |||
| textStyle: { | |||
| color: '#A1E2FF' | |||
| }, | |||
| icon: 'rect', | |||
| formatter: function (param) { | |||
| const item = legendBfb.filter(item => item.name === param)[0] | |||
| const bfs = fomatFloat(item.value * 100, 2) + '%' | |||
| return `${item.name}` | |||
| } | |||
| }, | |||
| tooltip: { | |||
| backgroundColor: '#033b77', | |||
| borderColor: '#21f2c4', | |||
| textStyle: { | |||
| color: '#fff', | |||
| fontSize: 13 | |||
| }, | |||
| formatter: params => { | |||
| if ( | |||
| params.seriesName !== 'mouseoutSeries' && | |||
| params.seriesName !== 'pie2d' | |||
| ) { | |||
| const bfb = ( | |||
| (option.series[params.seriesIndex].pieData.endRatio - | |||
| option.series[params.seriesIndex].pieData.startRatio) * | |||
| 100 | |||
| ).toFixed(2) | |||
| if (bfb === 'NaN') { | |||
| return ( | |||
| `${params.seriesName}` | |||
| ) | |||
| } else { | |||
| return ( | |||
| `${params.seriesName}<br/>` + | |||
| `<span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${params.color};"></span>` + | |||
| `${bfb}%` | |||
| ) | |||
| } | |||
| } | |||
| } | |||
| }, | |||
| xAxis3D: { | |||
| min: -1, | |||
| max: 1 | |||
| }, | |||
| yAxis3D: { | |||
| min: -1, | |||
| max: 1 | |||
| }, | |||
| zAxis3D: { | |||
| min: -1, | |||
| max: 1 | |||
| }, | |||
| grid3D: { | |||
| top: -20, | |||
| show: false, | |||
| boxHeight: 8, // 圆环的高度 | |||
| viewControl: { | |||
| // 3d效果可以放大、旋转等,请自己去查看官方配置 | |||
| alpha, // 角度 | |||
| distance, // 调整视角到主体的距离,类似调整zoom | |||
| rotateSensitivity: 0, // 设置为0无法旋转 | |||
| zoomSensitivity: 0, // 设置为0无法缩放 | |||
| panSensitivity: 0, // 设置为0无法平移 | |||
| autoRotate: false // 自动旋转 | |||
| } | |||
| }, | |||
| series:series | |||
| } | |||
| return option | |||
| } | |||
| /** | |||
| * 生成扇形的曲面参数方程,用于 series-surface.parametricEquation | |||
| */ | |||
| export const getParametricEquation = (startRatio, endRatio, isSelected, isHovered, k, h) => { | |||
| // 计算 | |||
| const midRatio = (startRatio + endRatio) / 2 | |||
| const startRadian = startRatio * Math.PI * 2 | |||
| const endRadian = endRatio * Math.PI * 2 | |||
| const midRadian = midRatio * Math.PI * 2 | |||
| // 如果只有一个扇形,则不实现选中效果。 | |||
| if (startRatio === 0 && endRatio === 1) { | |||
| isSelected = false | |||
| } | |||
| // 通过扇形内径/外径的值,换算出辅助参数 k(默认值 1/3) | |||
| k = typeof k !== 'undefined' ? k : 1 / 3 | |||
| // 计算选中效果分别在 x 轴、y 轴方向上的位移(未选中,则位移均为 0) | |||
| const offsetX = isSelected ? Math.cos(midRadian) * 0.1 : 0 | |||
| const offsetY = isSelected ? Math.sin(midRadian) * 0.1 : 0 | |||
| // 计算高亮效果的放大比例(未高亮,则比例为 1) | |||
| const hoverRate = isHovered ? 1.05 : 1 | |||
| // 返回曲面参数方程 | |||
| return { | |||
| u: { | |||
| min: -Math.PI, | |||
| max: Math.PI * 3, | |||
| step: Math.PI / 32 | |||
| }, | |||
| v: { | |||
| min: 0, | |||
| max: Math.PI * 2, | |||
| step: Math.PI / 20 | |||
| }, | |||
| x: function (u, v) { | |||
| if (u < startRadian) { | |||
| return ( | |||
| offsetX + | |||
| Math.cos(startRadian) * (1 + Math.cos(v) * k) * hoverRate | |||
| ) | |||
| } | |||
| if (u > endRadian) { | |||
| return ( | |||
| offsetX + Math.cos(endRadian) * (1 + Math.cos(v) * k) * hoverRate | |||
| ) | |||
| } | |||
| return offsetX + Math.cos(u) * (1 + Math.cos(v) * k) * hoverRate | |||
| }, | |||
| y: function (u, v) { | |||
| if (u < startRadian) { | |||
| return ( | |||
| offsetY + | |||
| Math.sin(startRadian) * (1 + Math.cos(v) * k) * hoverRate | |||
| ) | |||
| } | |||
| if (u > endRadian) { | |||
| return ( | |||
| offsetY + Math.sin(endRadian) * (1 + Math.cos(v) * k) * hoverRate | |||
| ) | |||
| } | |||
| return offsetY + Math.sin(u) * (1 + Math.cos(v) * k) * hoverRate | |||
| }, | |||
| z: function (u, v) { | |||
| if (u < -Math.PI * 0.5) { | |||
| return Math.sin(u) | |||
| } | |||
| if (u > Math.PI * 2.5) { | |||
| return Math.sin(u) * 1 | |||
| } | |||
| return Math.sin(v) > 0 ? 1 : -1 | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * 获取3d丙图的最高扇区的高度 | |||
| */ | |||
| const getHeight3D = (series, height) => { | |||
| series.sort((a, b) => { | |||
| return b.pieData.value - a.pieData.value | |||
| }) | |||
| return (height * 25) / series[0].pieData.value | |||
| } | |||
| /** | |||
| * 格式化浮点数 | |||
| */ | |||
| const fomatFloat = (num, n) => { | |||
| let f = parseFloat(num) | |||
| if (isNaN(f)) { | |||
| return false | |||
| } | |||
| f = Math.round(num * Math.pow(10, n)) / Math.pow(10, n) // n 幂 | |||
| let s = f.toString() | |||
| let rs = s.indexOf('.') | |||
| // 判定如果是整数,增加小数点再补0 | |||
| if (rs < 0) { | |||
| rs = s.length | |||
| s += '.' | |||
| } | |||
| while (s.length <= rs + n) { | |||
| s += '0' | |||
| } | |||
| return s | |||
| } | |||
| @@ -1,8 +1,14 @@ | |||
| .experiment-chart { | |||
| flex: none; | |||
| width: 295px; | |||
| min-width: 295px; | |||
| height: 140px; | |||
| background: @workspace-background; | |||
| border-radius: 4px; | |||
| display: flex; | |||
| flex-direction: column; | |||
| width: 100%; | |||
| height: 15rem; | |||
| margin-top: 1.25rem; | |||
| &__title { | |||
| margin-bottom: 1.25rem; | |||
| color: @text-color; | |||
| font-weight: 700; | |||
| font-size: 1rem; | |||
| } | |||
| } | |||
| @@ -1,3 +1,4 @@ | |||
| import { useRootFontSize } from '@/hooks/useRootFontSize'; | |||
| import themes from '@/styles/theme.less'; | |||
| import * as echarts from 'echarts'; | |||
| import React, { useEffect, useMemo, useRef } from 'react'; | |||
| @@ -87,6 +88,8 @@ type ExperimentChartProps = { | |||
| }; | |||
| function ExperimentChart({ chartData, style }: ExperimentChartProps) { | |||
| const fontSize = useRootFontSize(); | |||
| const chartRef = useRef<HTMLDivElement>(null); | |||
| const total = | |||
| chartData.Failed + | |||
| @@ -99,21 +102,21 @@ function ExperimentChart({ chartData, style }: ExperimentChartProps) { | |||
| () => ({ | |||
| title: { | |||
| show: true, | |||
| left: '29%', | |||
| top: 'center', | |||
| left: '50%', | |||
| top: 3.5 * fontSize, | |||
| textAlign: 'center', | |||
| text: [`{a|${total}}`, '{b|实验状态}'].join('\n'), | |||
| textStyle: { | |||
| rich: { | |||
| a: { | |||
| color: themes['textColor'], | |||
| fontSize: 20, | |||
| fontSize: 1.25 * fontSize, | |||
| fontWeight: 700, | |||
| lineHeight: 28, | |||
| lineHeight: 1.75 * fontSize, | |||
| }, | |||
| b: { | |||
| color: themes['textColorSecondary'], | |||
| fontSize: 10, | |||
| fontSize: 0.625 * fontSize, | |||
| fontWeight: 'normal', | |||
| }, | |||
| }, | |||
| @@ -123,20 +126,22 @@ function ExperimentChart({ chartData, style }: ExperimentChartProps) { | |||
| trigger: 'item', | |||
| }, | |||
| legend: { | |||
| top: 'center', | |||
| right: '5%', | |||
| orient: 'vertical', | |||
| bottom: '0', | |||
| left: 'center', | |||
| orient: 'horizontal', | |||
| icon: 'circle', | |||
| itemWidth: 6, | |||
| itemGap: 20, | |||
| height: 100, | |||
| itemWidth: 0.75 * fontSize, | |||
| itemGap: 1.25 * fontSize, | |||
| textStyle: { | |||
| fontSize: 0.75 * fontSize, | |||
| }, | |||
| }, | |||
| color: [color1, color2, color3, color4, color5], | |||
| series: [ | |||
| { | |||
| type: 'pie', | |||
| radius: ['70%', '80%'], | |||
| center: ['30%', '50%'], | |||
| center: ['50%', '40%'], | |||
| avoidLabelOverlap: false, | |||
| padAngle: 3, | |||
| itemStyle: { | |||
| @@ -165,7 +170,7 @@ function ExperimentChart({ chartData, style }: ExperimentChartProps) { | |||
| { | |||
| type: 'pie', | |||
| radius: '60%', | |||
| center: ['30%', '50%'], | |||
| center: ['50%', '40%'], | |||
| avoidLabelOverlap: false, | |||
| label: { | |||
| show: false, | |||
| @@ -192,7 +197,7 @@ function ExperimentChart({ chartData, style }: ExperimentChartProps) { | |||
| }, | |||
| ], | |||
| }), | |||
| [chartData, total], | |||
| [chartData, total, fontSize], | |||
| ); | |||
| useEffect(() => { | |||
| @@ -202,16 +207,24 @@ function ExperimentChart({ chartData, style }: ExperimentChartProps) { | |||
| // 设置图表实例的配置项和数据 | |||
| chart.setOption(options); | |||
| // 改变大小 | |||
| const chartResize = () => { | |||
| chart.resize(); | |||
| }; | |||
| window.addEventListener('resize', chartResize); | |||
| // 组件卸载 | |||
| return () => { | |||
| // myChart.dispose() 销毁实例 | |||
| window.removeEventListener('resize', chartResize); | |||
| chart.dispose(); | |||
| }; | |||
| }, [options]); | |||
| return ( | |||
| <div className={styles['experiment-chart']} style={style}> | |||
| <div style={{ width: '100%', height: '100%' }} ref={chartRef}></div> | |||
| <div className={styles['experiment-chart__title']}>实验统计</div> | |||
| <div style={{ width: '100%', flex: 1 }} ref={chartRef}></div> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,125 @@ | |||
| import * as echarts from 'echarts'; | |||
| import React from 'react'; | |||
| import styles from './index.less'; | |||
| import ThreePieChart from './threePieChart'; | |||
| const color1 = new echarts.graphic.LinearGradient( | |||
| 0, | |||
| 0, | |||
| 0, | |||
| 1, | |||
| [ | |||
| { offset: 0, color: '#c73131' }, | |||
| { offset: 1, color: '#ff7e96' }, | |||
| ], | |||
| false, | |||
| ); | |||
| const color2 = new echarts.graphic.LinearGradient( | |||
| 0, | |||
| 0, | |||
| 0, | |||
| 1, | |||
| [ | |||
| { offset: 0, color: '#6ac21d' }, | |||
| { offset: 1, color: '#96e850' }, | |||
| ], | |||
| false, | |||
| ); | |||
| const color3 = new echarts.graphic.LinearGradient( | |||
| 0, | |||
| 0, | |||
| 0, | |||
| 1, | |||
| [ | |||
| { offset: 0, color: '#8c8c8c' }, | |||
| { offset: 1, color: '#c8c6c6' }, | |||
| ], | |||
| false, | |||
| ); | |||
| const color4 = new echarts.graphic.LinearGradient( | |||
| 0, | |||
| 0, | |||
| 0, | |||
| 1, | |||
| [ | |||
| { offset: 0, color: '#ecb934' }, | |||
| { offset: 1, color: '#f0864d' }, | |||
| ], | |||
| false, | |||
| ); | |||
| const color5 = new echarts.graphic.LinearGradient( | |||
| 0, | |||
| 0, | |||
| 0, | |||
| 1, | |||
| [ | |||
| { offset: 0, color: '#7ea9fe' }, | |||
| { offset: 1, color: '#1664ff' }, | |||
| ], | |||
| false, | |||
| ); | |||
| const color6 = new echarts.graphic.LinearGradient( | |||
| 0, | |||
| 0, | |||
| 0, | |||
| 1, | |||
| [ | |||
| { offset: 0, color: 'rgba(255, 255, 255, 0.62)' }, | |||
| { offset: 1, color: '#ebf2ff ' }, | |||
| ], | |||
| false, | |||
| ); | |||
| export type ExperimentStatistics = { | |||
| Failed: number; | |||
| Pending: number; | |||
| Running: number; | |||
| Succeeded: number; | |||
| Terminated: number; | |||
| }; | |||
| type ExperimentChartProps = { | |||
| style?: React.CSSProperties; | |||
| chartData: ExperimentStatistics; | |||
| }; | |||
| function ExperimentChart({ chartData, style }: ExperimentChartProps) { | |||
| // useEffect(() => { | |||
| // // 创建一个echarts实例,返回echarts实例 | |||
| // const chart = echarts.init(chartRef.current); | |||
| // // 设置图表实例的配置项和数据 | |||
| // chart.setOption(options); | |||
| // // 组件卸载 | |||
| // return () => { | |||
| // // myChart.dispose() 销毁实例 | |||
| // chart.dispose(); | |||
| // }; | |||
| // }, [options]); | |||
| const pieData = [ | |||
| { name: '销售业务', value: 77.56, color: '#f00' }, | |||
| { name: '技术服务', value: 18.21, color: '#0f0' }, | |||
| { name: '配送业务', value: 3.67, color: '#00f' }, | |||
| { name: '其他业务', value: 0.17, color: '#ff0' }, | |||
| ]; | |||
| return ( | |||
| <div className={styles['experiment-chart']} style={style}> | |||
| <div className={styles['experiment-chart__title']}>实验统计</div> | |||
| <div style={{ width: '100%', height: '100%' }}> | |||
| <ThreePieChart | |||
| id="test" | |||
| data={pieData} | |||
| style={{ width: '100%', height: '200px' }} | |||
| ></ThreePieChart> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default ExperimentChart; | |||
| @@ -0,0 +1,84 @@ | |||
| import * as echarts from 'echarts'; | |||
| import { useEffect } from 'react'; | |||
| import { getPie3D } from './charts'; | |||
| function PieEchart({ id, data, style }) { | |||
| useEffect(() => { | |||
| if (data && data.length > 0) { | |||
| init(setLabel(data)); | |||
| } | |||
| }, [data]); | |||
| const init = (data) => { | |||
| let huan_val = document.getElementById(id); | |||
| let myEcharts = echarts.init(huan_val); | |||
| let options = getOption(data); | |||
| console.log(options); | |||
| options.color = ['#4154f1', '#ffa900', '#00aa82', '#a296fd', '#40acfc']; | |||
| options.legend = { | |||
| orient: 'horizontal', | |||
| right: 'center', | |||
| bottom: 40, | |||
| width: '100%', | |||
| itemWidth: 11, | |||
| itemHeight: 11, | |||
| }; | |||
| options.grid3D.top = -50; | |||
| options.series.map((item) => { | |||
| item.radius = [30, 120]; | |||
| item.height = 20; | |||
| }); | |||
| console.log(options); | |||
| myEcharts.setOption(options); | |||
| }; | |||
| const getOption = (data) => { | |||
| console.log(data); | |||
| let option = getPie3D(data, 0.86, 150, 20, 20, 1); | |||
| return option; | |||
| }; | |||
| const setLabel = (data) => { | |||
| console.log(data, 'data'); | |||
| data.forEach((item, index) => { | |||
| item.itemStyle = { | |||
| color: item.color, | |||
| }; | |||
| item.label = { | |||
| normal: { | |||
| show: true, | |||
| color: item.color, | |||
| rich: { | |||
| b: { | |||
| color: '#fff', | |||
| lineHeight: 25, | |||
| align: 'left', | |||
| }, | |||
| c: { | |||
| fontSize: 22, | |||
| color: '#fff', | |||
| textShadowColor: '#1c90a6', | |||
| textShadowOffsetX: 0, | |||
| textShadowOffsetY: 2, | |||
| textShadowBlur: 5, | |||
| }, | |||
| d: { | |||
| color: item.color, | |||
| align: 'left', | |||
| }, | |||
| }, | |||
| }, | |||
| }; | |||
| item.labelLine = { | |||
| normal: { | |||
| lineStyle: { | |||
| width: 1, | |||
| color: 'rgba(255,255,255,0.7)', | |||
| }, | |||
| }, | |||
| }; | |||
| }); | |||
| return data; | |||
| }; | |||
| return <div id={id} style={style}></div>; | |||
| } | |||
| export default PieEchart; | |||
| @@ -0,0 +1,61 @@ | |||
| .experiments { | |||
| width: calc(40% - 1.25rem); | |||
| margin-left: 1.25rem; | |||
| background-color: white; | |||
| border-radius: 10px; | |||
| box-shadow: 0rem 0.1875rem 0.75rem rgba(151, 150, 180, 0.08); | |||
| &__content { | |||
| width: 100%; | |||
| padding: 1.25rem; | |||
| } | |||
| &__item { | |||
| margin-bottom: 0.875rem; | |||
| padding: 1rem 1.25rem; | |||
| background-color: #f7f9fd; | |||
| border: 1px solid transparent; | |||
| border-radius: 0.5rem; | |||
| cursor: pointer; | |||
| &:last-child { | |||
| margin-bottom: 0; | |||
| } | |||
| &:hover { | |||
| background-image: linear-gradient(173.59deg, #e7e8ff 0%, #f7f9fd 20.63%, #f7f9fd 100%); | |||
| border: 1px solid @primary-color; | |||
| } | |||
| &__title { | |||
| flex: 0 1 fit-content; | |||
| color: @text-color; | |||
| font-size: 1rem; | |||
| } | |||
| &__status { | |||
| margin-left: 1rem; | |||
| padding: 0.125rem 0.25rem; | |||
| color: #1664ff; | |||
| font-size: 0.75rem; | |||
| background-color: rgba(22, 100, 255, 0.07); | |||
| border-radius: 4px; | |||
| } | |||
| &__time { | |||
| margin-right: 0; | |||
| margin-left: auto; | |||
| color: @text-color-secondary; | |||
| font-size: 0.8125rem; | |||
| } | |||
| &:hover &__title { | |||
| color: @primary-color; | |||
| } | |||
| &__desc { | |||
| margin-top: 0.875rem; | |||
| font-size: 0.875rem; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,82 @@ | |||
| import { ExperimentInstance } from '@/types'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { useNavigate } from '@umijs/max'; | |||
| import { Flex, Typography } from 'antd'; | |||
| import BlockTitle from '../BlockTitle'; | |||
| import styles from './index.less'; | |||
| type ExperimentsProps = { | |||
| tableData: ExperimentInstance[]; | |||
| }; | |||
| const testData = [ | |||
| { | |||
| name: '基于pytorch的XXXX开发', | |||
| description: '基于pytorch的XXXX开发基于pytorch的XXXX开发', | |||
| status: 'Running', | |||
| date: new Date(), | |||
| }, | |||
| { | |||
| name: '基于pytorch的XXXX开发', | |||
| description: '基于pytorch的XXXX开发基于pytorch的XXXX开发', | |||
| status: 'Running', | |||
| date: new Date(), | |||
| }, | |||
| { | |||
| name: '基于pytorch的XXXX开发', | |||
| description: '基于pytorch的XXXX开发基于pytorch的XXXX开发', | |||
| status: 'Running', | |||
| date: new Date(), | |||
| }, | |||
| { | |||
| name: '基于pytorch的XXXX开发', | |||
| description: '基于pytorch的XXXX开发基于pytorch的XXXX开发', | |||
| status: 'Running', | |||
| date: new Date(), | |||
| }, | |||
| ]; | |||
| function Experiments({ tableData }: ExperimentsProps) { | |||
| const navigate = useNavigate(); | |||
| const gotoExperiment = (record: ExperimentInstance) => { | |||
| navigate(`/pipeline/experiment/instance/${record.workflow_id}/${record.id}`); | |||
| }; | |||
| return ( | |||
| <div className={styles.experiments}> | |||
| <BlockTitle title="正在运行的训练" viewMore={() => {}}></BlockTitle> | |||
| <div className={styles['experiments__content']}> | |||
| {testData.map((item) => { | |||
| return ( | |||
| <div | |||
| className={styles['experiments__item']} | |||
| key={item.id} | |||
| onClick={() => gotoExperiment(item)} | |||
| > | |||
| <Flex align="center"> | |||
| <Typography.Text | |||
| className={styles['experiments__item__title']} | |||
| ellipsis={{ tooltip: item.name }} | |||
| > | |||
| {item.name} | |||
| </Typography.Text> | |||
| <div className={styles['experiments__item__status']}>运行中</div> | |||
| <div className={styles['experiments__item__time']}> | |||
| {formatDate(item.date, 'YYYY-MM-DD')} | |||
| </div> | |||
| </Flex> | |||
| <Typography.Text | |||
| className={styles['experiments__item__desc']} | |||
| ellipsis={{ tooltip: item.description }} | |||
| > | |||
| {item.description} | |||
| </Typography.Text> | |||
| </div> | |||
| ); | |||
| })} | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default Experiments; | |||
| @@ -0,0 +1,62 @@ | |||
| .messages { | |||
| flex: 1; | |||
| margin-top: 1.25rem; | |||
| &__title { | |||
| color: @text-color; | |||
| font-weight: 700; | |||
| font-size: 1rem; | |||
| } | |||
| &__view-more { | |||
| margin-right: 0; | |||
| margin-left: auto; | |||
| color: @text-color-secondary; | |||
| font-weight: normal; | |||
| font-size: 0.875rem; | |||
| &:hover { | |||
| color: @primary-color; | |||
| } | |||
| } | |||
| &__content { | |||
| width: 100%; | |||
| margin-top: 1.25rem; | |||
| padding: 1rem 1.25rem; | |||
| overflow-y: auto; | |||
| background-color: #ffffff; | |||
| border-radius: 10px; | |||
| box-shadow: 0px 3px 6px rgba(151, 150, 180, 0.08); | |||
| } | |||
| &__item { | |||
| display: flex; | |||
| align-self: center; | |||
| padding: 0.625rem 0; | |||
| border-bottom: 1px dashed rgba(130, 132, 164, 0.18); | |||
| &__title { | |||
| flex: 1; | |||
| margin-right: 0.625rem; | |||
| color: @text-color-secondary; | |||
| font-size: 0.875rem; | |||
| &:hover { | |||
| color: @primary-color; | |||
| } | |||
| } | |||
| &__time { | |||
| flex: none; | |||
| margin-right: 0; | |||
| margin-left: auto; | |||
| color: @text-color-secondary; | |||
| font-size: 0.8125rem; | |||
| } | |||
| &:hover &__title { | |||
| color: @primary-color; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,106 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { MessageStatus, MessageType } from '@/enums'; | |||
| import { Message, MessageResponse } from '@/pages/Message'; | |||
| import { getMessageListReq, readMessagesReq } from '@/services/message'; | |||
| import { ago } from '@/utils/date'; | |||
| import { to } from '@/utils/promise'; | |||
| import { useModel, useNavigate } from '@umijs/max'; | |||
| import { Empty, Flex, Typography } from 'antd'; | |||
| import { useCallback, useEffect, useState } from 'react'; | |||
| import styles from './index.less'; | |||
| function Messages() { | |||
| const { initialState } = useModel('@@initialState'); | |||
| const { currentUser } = initialState || {}; | |||
| const { userId } = currentUser || {}; | |||
| const navigate = useNavigate(); | |||
| const [messages, setMessages] = useState<Message[]>([]); | |||
| const getMessages = useCallback(async () => { | |||
| if (!userId) return; | |||
| const params: Record<string, any> = { | |||
| receiver: userId, | |||
| status: MessageStatus.All, | |||
| type: MessageType.System, | |||
| page: 1, | |||
| size: 10, | |||
| }; | |||
| const [res] = await to(getMessageListReq(params)); | |||
| if (res && res.data) { | |||
| const { records } = res.data as MessageResponse; | |||
| setMessages(records); | |||
| } | |||
| }, [userId]); | |||
| useEffect(() => { | |||
| getMessages(); | |||
| }, [getMessages]); | |||
| // 标记为已读 | |||
| const readMessages = async (message: Message) => { | |||
| const params: Record<string, any> = { | |||
| notificationIds: message.id, | |||
| status: MessageStatus.Readed, | |||
| receiver: message.receiver, | |||
| type: message.type, | |||
| }; | |||
| readMessagesReq(params, true); | |||
| }; | |||
| // 点击消息中心 | |||
| const gotoMessageCenter = () => { | |||
| navigate(`/workspace/message`); | |||
| }; | |||
| // 点击消息 | |||
| const hanldeMessageClick = (message: Message) => { | |||
| if (message.status === MessageStatus.UnRead) { | |||
| readMessages(message); | |||
| } | |||
| if (message.notification_url) { | |||
| navigate(message.notification_url); | |||
| } | |||
| }; | |||
| return ( | |||
| <div className={styles.messages}> | |||
| <Flex align="center" justify="space-between"> | |||
| <div className={styles['messages__title']}>消息通知</div> | |||
| <Flex align="center" className={styles['messages__view-more']} onClick={gotoMessageCenter}> | |||
| <span>消息中心</span> | |||
| <KFIcon type="icon-a-zu14930" font={12} style={{ marginLeft: 8 }} /> | |||
| </Flex> | |||
| </Flex> | |||
| <div className={styles['messages__content']}> | |||
| {messages.length > 0 ? ( | |||
| messages.map((item) => { | |||
| return ( | |||
| <div | |||
| className={styles['messages__item']} | |||
| key={item.id} | |||
| onClick={() => hanldeMessageClick(item)} | |||
| > | |||
| <Typography.Text | |||
| className={styles['messages__item__content']} | |||
| ellipsis={{ tooltip: item.content }} | |||
| > | |||
| {item.content} | |||
| </Typography.Text> | |||
| <div className={styles['messages__item__time']}>{ago(item.created_at)}</div> | |||
| </div> | |||
| ); | |||
| }) | |||
| ) : ( | |||
| <Empty | |||
| image={Empty.PRESENTED_IMAGE_SIMPLE} | |||
| // imageStyle={{ height: '40px', marginTop: '-10px' }} | |||
| /> | |||
| )} | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default Messages; | |||
| @@ -0,0 +1,120 @@ | |||
| .notebooks { | |||
| width: 60%; | |||
| background-color: white; | |||
| border-radius: 10px; | |||
| box-shadow: 0rem 0.1875rem 0.75rem rgba(151, 150, 180, 0.08); | |||
| &__content { | |||
| display: flex; | |||
| flex-wrap: wrap; | |||
| gap: 1.25rem; | |||
| width: 100%; | |||
| padding: 1.25rem; | |||
| } | |||
| &__item { | |||
| width: calc((100% - 1.25rem) / 2); | |||
| padding: 1.25rem; | |||
| background-image: linear-gradient(173.59deg, #e7e8ff 0%, #f7f9fd 20.63%, #f7f9fd 100%); | |||
| border: 1px solid transparent; | |||
| border-radius: 0.5rem; | |||
| cursor: pointer; | |||
| &:hover { | |||
| background-image: linear-gradient(203.57deg, #e7e8ff 0%, #f7f9fd 20.63%, #f7f9fd 100%); | |||
| border: 1px solid @primary-color; | |||
| outline: 4px solid .addAlpha(@primary-color, 0.1) []; | |||
| } | |||
| &__title-icon { | |||
| flex: none; | |||
| width: 1.75rem; | |||
| height: 1.75rem; | |||
| margin-right: 0.75rem; | |||
| } | |||
| &__title { | |||
| flex: 0 1 fit-content; | |||
| color: @text-color; | |||
| font-size: 1rem; | |||
| } | |||
| &:hover &__title { | |||
| color: @primary-color; | |||
| } | |||
| &__arrow { | |||
| position: relative; | |||
| top: 0.125rem; | |||
| display: none; | |||
| width: 0.75rem; | |||
| height: 0.75rem; | |||
| margin-left: 0.5rem; | |||
| color: @primary-color; | |||
| } | |||
| &:hover &__arrow { | |||
| display: block; | |||
| } | |||
| &__desc { | |||
| margin-top: 0.625rem; | |||
| margin-bottom: 1.25rem; | |||
| font-size: 0.875rem; | |||
| } | |||
| &__user { | |||
| color: @text-color; | |||
| font-size: 0.875rem; | |||
| } | |||
| &__user-divider { | |||
| flex: none; | |||
| height: 0.625rem; | |||
| margin-right: 0.75rem; | |||
| margin-left: 0.75rem; | |||
| background-color: @home-divider-color; | |||
| } | |||
| &__timestamp { | |||
| flex: none; | |||
| color: @text-color-secondary; | |||
| font-size: 0.8125rem; | |||
| } | |||
| &:hover &__user-divider { | |||
| background-color: #d7d9e1; | |||
| } | |||
| &__category { | |||
| padding: 0.25rem 0.625rem; | |||
| color: #646a73; | |||
| font-size: 0.875rem; | |||
| background-color: .addAlpha(@primary-color, 0.07) []; | |||
| border-radius: 0.25rem; | |||
| } | |||
| &:hover &__category { | |||
| color: @primary-color; | |||
| } | |||
| &__info-btn { | |||
| display: none; | |||
| width: 100%; | |||
| height: 2rem; | |||
| } | |||
| &:hover &__info-btn { | |||
| display: block; | |||
| } | |||
| &__user-container { | |||
| display: flex; | |||
| height: 2rem; | |||
| } | |||
| &:hover &__user-container { | |||
| display: none; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,95 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { ModelData } from '@/pages/Dataset/config'; | |||
| import { getPublicModelsReq } from '@/services/home'; | |||
| import { to } from '@/utils/promise'; | |||
| import { gotoPageIfLogin } from '@/utils/ui'; | |||
| import { Button, Divider, Flex, Typography } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import BlockTitle from '../BlockTitle'; | |||
| import styles from './index.less'; | |||
| function NotebookComponent() { | |||
| const [modelData, setModelData] = useState<ModelData[]>([]); | |||
| useEffect(() => { | |||
| const getPublicModels = async () => { | |||
| const [res] = await to(getPublicModelsReq()); | |||
| if (res && res.data) { | |||
| const { content = [] } = res.data; | |||
| setModelData(content.slice(0, 4)); | |||
| } | |||
| }; | |||
| getPublicModels(); | |||
| }, []); | |||
| return ( | |||
| <div className={styles.notebooks}> | |||
| <BlockTitle title="最近使用notebooks" viewMore={() => {}}></BlockTitle> | |||
| <div className={styles['notebooks__content']}> | |||
| {modelData.map((item) => { | |||
| return ( | |||
| <div | |||
| className={styles['notebooks__item']} | |||
| key={item.id} | |||
| onClick={() => { | |||
| gotoPageIfLogin( | |||
| `/dataset/model/info/${item.id}?name=${item.name}&owner=${item.owner}&identifier=${item.identifier}&is_public=${item.is_public}`, | |||
| ); | |||
| }} | |||
| > | |||
| <Flex align="center"> | |||
| <img | |||
| src={require('@/assets/img/workspace/notebook-icon.png')} | |||
| className={styles['notebooks__item__title-icon']} | |||
| ></img> | |||
| <Typography.Text | |||
| className={styles['notebooks__item__title']} | |||
| ellipsis={{ tooltip: item.name }} | |||
| > | |||
| {item.name} | |||
| </Typography.Text> | |||
| <KFIcon | |||
| type="icon-a-zu14930" | |||
| font={12} | |||
| className={styles['notebooks__item__arrow']} | |||
| /> | |||
| </Flex> | |||
| <Typography.Text | |||
| className={styles['notebooks__item__desc']} | |||
| ellipsis={{ tooltip: item.description }} | |||
| > | |||
| {item.description} | |||
| </Typography.Text> | |||
| <Flex align="center" gap={'0 0.625rem'} style={{ marginBottom: '1rem' }}> | |||
| <div className={styles['notebooks__item__category']}>电池开发</div> | |||
| <div className={styles['notebooks__item__category']}>材料研发</div> | |||
| </Flex> | |||
| <Button type="primary" className={styles['notebooks__item__info-btn']}> | |||
| 查看详情 | |||
| </Button> | |||
| <Flex align="center" className={styles['notebooks__item__user-container']}> | |||
| <Typography.Text | |||
| className={styles['notebooks__item__user']} | |||
| ellipsis={{ tooltip: item.create_by }} | |||
| > | |||
| {item.create_by} | |||
| </Typography.Text> | |||
| <Divider | |||
| type="vertical" | |||
| className={styles['notebooks__item__user-divider']} | |||
| ></Divider> | |||
| <div className={styles['notebooks__item__timestamp']}>{item.time_ago}更新</div> | |||
| </Flex> | |||
| </div> | |||
| ); | |||
| })} | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default NotebookComponent; | |||
| @@ -0,0 +1,111 @@ | |||
| .workspace-search { | |||
| padding: 0 170px; | |||
| color: @text-color; | |||
| font-size: 18px; | |||
| &__icon { | |||
| width: 36px; | |||
| height: 36px; | |||
| margin-right: 12px; | |||
| padding: 4px 5px; | |||
| background-color: white; | |||
| border-radius: 6px; | |||
| } | |||
| &__star { | |||
| position: relative; | |||
| top: -10px; | |||
| width: 36px; | |||
| height: 36px; | |||
| } | |||
| &__textarea { | |||
| position: relative; | |||
| margin-top: 15px; | |||
| margin-bottom: 20px; | |||
| :global { | |||
| .ant-input-affix-wrapper { | |||
| border: 2px solid rgba(22, 100, 255, 0.1); | |||
| border-radius: 20px; | |||
| box-shadow: 0px 0px 12px rgba(33, 73, 212, 0.04); | |||
| &:hover { | |||
| border: 2px solid @primary-color; | |||
| outline: 4px solid .addAlpha(@primary-color, 0.1) []; | |||
| } | |||
| } | |||
| .ant-input { | |||
| padding: 20px; | |||
| } | |||
| .ant-btn { | |||
| position: absolute; | |||
| right: 20px; | |||
| bottom: 20px; | |||
| z-index: 100; | |||
| padding: 6px 13px; | |||
| font-size: 14px; | |||
| background: @text-color !important; | |||
| &:hover { | |||
| outline: 4px solid .addAlpha(@text-color, 0.15) []; | |||
| } | |||
| &:active { | |||
| opacity: 0.8; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| &__question { | |||
| display: flex; | |||
| align-items: center; | |||
| height: 40px; | |||
| padding: 0 14px; | |||
| color: @text-color-secondary; | |||
| font-size: 14px; | |||
| border: 1px solid .addAlpha(@text-color-secondary, 0.2) []; | |||
| border-radius: 15px; | |||
| box-shadow: 0px 0px 12px rgba(33, 73, 212, 0.04); | |||
| &:hover { | |||
| color: @primary-color; | |||
| background-color: rgba(81, 76, 249, 0.05); | |||
| border: 1px solid .addAlpha(@primary-color, 0.24) []; | |||
| box-shadow: 0px 0px 12px rgba(33, 73, 212, 0.04); | |||
| } | |||
| &__icon { | |||
| width: 19px; | |||
| height: 19px; | |||
| margin-right: 8px; | |||
| } | |||
| } | |||
| &__try { | |||
| margin-top: 24px; | |||
| margin-bottom: 34px; | |||
| color: @text-color-secondary; | |||
| font-size: 14px; | |||
| :global { | |||
| .ant-btn { | |||
| display: flex; | |||
| align-items: center; | |||
| height: 28px; | |||
| margin-left: 10px; | |||
| padding: 0 15px; | |||
| color: @text-color-secondary; | |||
| font-size: 14px; | |||
| background: white !important; | |||
| &:hover { | |||
| color: @primary-color !important; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,80 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { Button, Flex, Input } from 'antd'; | |||
| import styles from './index.less'; | |||
| const hotQuestions = [ | |||
| { | |||
| icon: '', | |||
| title: '高分子材料有哪些?', | |||
| }, | |||
| { | |||
| icon: '', | |||
| title: '高分子材料有哪些?', | |||
| }, | |||
| { | |||
| icon: '', | |||
| title: '高分子材料有哪些?', | |||
| }, | |||
| { | |||
| icon: '', | |||
| title: '高分子材料有哪些?', | |||
| }, | |||
| ]; | |||
| function WorkspaceSearch() { | |||
| return ( | |||
| <div className={styles['workspace-search']}> | |||
| <Flex align="center"> | |||
| <img | |||
| src={require('@/assets/img/workspace/search-icon.png')} | |||
| className={styles['workspace-search__icon']} | |||
| ></img> | |||
| <span>今天你想问一些什么科学的问题</span> | |||
| <img | |||
| src={require('@/assets/img/workspace/search-star.gif')} | |||
| className={styles['workspace-search__star']} | |||
| ></img> | |||
| </Flex> | |||
| <div className={styles['workspace-search__textarea']}> | |||
| <Input.TextArea | |||
| autoSize={{ minRows: 5, maxRows: 5 }} | |||
| placeholder="请输入科学问题进行搜索" | |||
| allowClear | |||
| /> | |||
| <Button | |||
| icon={<KFIcon type="icon-a-lianhe173" />} | |||
| color="default" | |||
| variant="solid" | |||
| shape="round" | |||
| > | |||
| 开始搜索 | |||
| </Button> | |||
| </div> | |||
| <Flex align="center" gap={'20px'} wrap> | |||
| {hotQuestions.map((item, index) => ( | |||
| <div key={index} className={styles['workspace-search__question']}> | |||
| <img | |||
| src={require('@/assets/img/workspace/hot-icon.png')} | |||
| className={styles['workspace-search__question__icon']} | |||
| ></img> | |||
| <span>{item.title}</span> | |||
| </div> | |||
| ))} | |||
| </Flex> | |||
| <Flex className={styles['workspace-search__try']} align="center" justify="center"> | |||
| <div>试一试主题:梅洛 · 庞蒂的“身体现象学”</div> | |||
| <Button | |||
| icon={<KFIcon type="icon-shuaxin" font={12} />} | |||
| color="default" | |||
| variant="solid" | |||
| shape="round" | |||
| size="middle" | |||
| > | |||
| 换一换 | |||
| </Button> | |||
| </Flex> | |||
| </div> | |||
| ); | |||
| } | |||
| export default WorkspaceSearch; | |||
| @@ -1,40 +1,34 @@ | |||
| .user-points { | |||
| display: flex; | |||
| flex: none; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| width: 326px; | |||
| height: 228px; | |||
| padding: 0 20px; | |||
| .backgroundFullImage(url(@/assets/img/user-points-bg.png)); | |||
| margin-bottom: 16px; | |||
| &__label { | |||
| margin-top: 60px; | |||
| &__title { | |||
| margin-bottom: 1.25rem; | |||
| color: @text-color; | |||
| font-size: @font-size-title; | |||
| font-family: DingTalk-JinBuTi; | |||
| font-weight: 700; | |||
| font-size: 1rem; | |||
| } | |||
| &__value { | |||
| width: 100%; | |||
| margin-top: 8px; | |||
| margin-bottom: 12px; | |||
| color: @primary-color; | |||
| font-size: 36px; | |||
| font-family: DingTalk-JinBuTi; | |||
| line-height: 43px; | |||
| text-align: center; | |||
| .singleLine(); | |||
| } | |||
| &__content { | |||
| display: flex; | |||
| gap: 0 1.25rem; | |||
| padding: 1.25rem; | |||
| background-color: white; | |||
| border-radius: 0.625rem; | |||
| &__title { | |||
| margin-bottom: 0.625rem; | |||
| color: @text-color-secondary; | |||
| font-size: 0.875rem; | |||
| } | |||
| &__value { | |||
| color: @text-color; | |||
| font-weight: 700; | |||
| font-size: 1.25rem; | |||
| } | |||
| &__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; | |||
| &__unit { | |||
| position: relative; | |||
| top: -0.25rem; | |||
| margin-left: 0.25rem; | |||
| } | |||
| } | |||
| } | |||
| @@ -3,7 +3,7 @@ import { getPointsStatisticsReq } from '@/services/points'; | |||
| import { formatNumber } from '@/utils/format'; | |||
| import { to } from '@/utils/promise'; | |||
| import { useNavigate } from '@umijs/max'; | |||
| import { Typography } from 'antd'; | |||
| import { Flex, Typography } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import styles from './index.less'; | |||
| @@ -27,20 +27,32 @@ function UserPoints() { | |||
| return ( | |||
| <div className={styles['user-points']}> | |||
| <div className={styles['user-points__label']}>当前可用算力积分</div> | |||
| <Typography.Paragraph | |||
| className={styles['user-points__value']} | |||
| ellipsis={{ tooltip: userCredit }} | |||
| > | |||
| {userCredit} | |||
| </Typography.Paragraph> | |||
| <div | |||
| className={styles['user-points__button']} | |||
| onClick={() => { | |||
| navigate('/points'); | |||
| }} | |||
| > | |||
| 查看详情 | |||
| <div className={styles['user-points__title']}>算力积分明细</div> | |||
| <div className={styles['user-points__content']}> | |||
| <div style={{ width: '50%' }}> | |||
| <div className={styles['user-points__content__title']}>当前可以积分</div> | |||
| <Flex align="flex-end"> | |||
| <Typography.Text | |||
| className={styles['user-points__content__value']} | |||
| ellipsis={{ tooltip: userCredit }} | |||
| > | |||
| {userCredit} | |||
| </Typography.Text> | |||
| <span className={styles['user-points__content__unit']}> 分</span> | |||
| </Flex> | |||
| </div> | |||
| <div style={{ width: '50%' }}> | |||
| <div className={styles['user-points__content__title']}>总消耗积分</div> | |||
| <Flex align="flex-end"> | |||
| <Typography.Text | |||
| className={styles['user-points__content__value']} | |||
| ellipsis={{ tooltip: userCredit }} | |||
| > | |||
| {userCredit} | |||
| </Typography.Text> | |||
| <span className={styles['user-points__content__unit']}> 分</span> | |||
| </Flex> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| @@ -1,71 +1,15 @@ | |||
| .workspace { | |||
| position: relative; | |||
| height: 100%; | |||
| padding: 20px 30px 10px; | |||
| padding: 80px 24px 70px; | |||
| overflow-y: auto; | |||
| background: linear-gradient(#ecf2fe, #f9fafb); | |||
| .backgroundFullImage(url(@/assets/img/workspace/workspace-bg.png)); | |||
| &__overview { | |||
| flex: 1; | |||
| gap: 15px; | |||
| padding: 20px 30px; | |||
| background-color: white; | |||
| border-radius: 4px; | |||
| &__title { | |||
| margin-bottom: 20px; | |||
| color: @text-color; | |||
| font-weight: 500; | |||
| font-size: @font-size-title; | |||
| } | |||
| &__content { | |||
| display: flex; | |||
| flex-wrap: wrap; | |||
| gap: 15px; | |||
| align-items: center; | |||
| &__statistics { | |||
| flex: none; | |||
| // min-width: 500px; | |||
| background: linear-gradient( | |||
| 123.08deg, | |||
| rgba(138, 138, 138, 0.06) 1.32%, | |||
| rgba(22, 100, 255, 0.02) 58.35% | |||
| ); | |||
| border-radius: 4px; | |||
| // // 媒体查询 | |||
| // @media screen and (max-width: 1600px) { | |||
| // flex: 1; | |||
| // } | |||
| } | |||
| } | |||
| } | |||
| &__quick-start { | |||
| display: flex; | |||
| gap: 15px; | |||
| align-items: flex-start; | |||
| width: 100%; | |||
| margin-top: 16px; | |||
| } | |||
| &__user { | |||
| &__right { | |||
| display: flex; | |||
| flex-direction: column; | |||
| width: 326px; | |||
| min-width: 326px; | |||
| height: 700px; | |||
| } | |||
| &__robot-img { | |||
| position: fixed; | |||
| right: 20px; | |||
| bottom: 90px; | |||
| z-index: 99; | |||
| width: 56px; | |||
| height: 56px; | |||
| cursor: pointer; | |||
| width: calc(25% - 1.25rem); | |||
| padding: 1.25rem; | |||
| .backgroundFullImage(url(@/assets/img/workspace/points-bg.png)); | |||
| } | |||
| } | |||
| @@ -2,17 +2,18 @@ | |||
| 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'; | |||
| import ExperimentChart, { type ExperimentStatistics } from './components/ExperimentChart'; | |||
| import ExperitableTable from './components/ExperimentTable'; | |||
| import QuickStart from './components/QuickStart'; | |||
| import { type ExperimentStatistics } from './components/ExperimentChart'; | |||
| // import RobotFrame from './components/RobotFrame'; | |||
| import TotalStatistics from './components/TotalStatistics'; | |||
| import { Flex } from 'antd'; | |||
| import AssetsManagement from './components/AssetsManagement'; | |||
| import ExperimentChart from './components/ExperimentChart'; | |||
| import Experiments from './components/Experiments'; | |||
| import Messages from './components/Messages'; | |||
| import Notebooks from './components/Notebooks'; | |||
| import WorkspaceSearch from './components/Search'; | |||
| import UserPoints from './components/UserPoints'; | |||
| import WorkspaceIntro from './components/WorkspaceIntro'; | |||
| import styles from './index.less'; | |||
| type OverviewData = { | |||
| @@ -24,11 +25,6 @@ type OverviewData = { | |||
| function Workspace() { | |||
| const [overviewData, setOverviewData] = useState<OverviewData>(); | |||
| // const [robotFrameVisible, setRobotFrameVisible] = useState(false); | |||
| // const { handleStart, handleStop, handleDrag, handleClick } = useDraggable(() => | |||
| // setRobotFrameVisible((prev) => !prev), | |||
| // ); | |||
| const users: number[] = new Array(8).fill(0); | |||
| useEffect(() => { | |||
| getWorkspaceOverview(); | |||
| @@ -44,59 +40,23 @@ function Workspace() { | |||
| return ( | |||
| <div className={styles.workspace}> | |||
| <WorkspaceIntro></WorkspaceIntro> | |||
| <Flex gap={'15px'} wrap> | |||
| <div className={styles['workspace__overview']}> | |||
| <div className={styles['workspace__overview__title']}>运行概览</div> | |||
| <div className={styles['workspace__overview__content']}> | |||
| <Flex align="center" className={styles['workspace__overview__content__statistics']}> | |||
| <TotalStatistics | |||
| icon={require('@/assets/img/workspace-pipeline.png')} | |||
| title="流水线总数" | |||
| count={overviewData?.workflowCount} | |||
| /> | |||
| <Divider | |||
| type="vertical" | |||
| dashed | |||
| style={{ color: '1px dashed rgba(96, 107, 122, 0.19)', height: 68, margin: 0 }} | |||
| /> | |||
| <TotalStatistics | |||
| icon={require('@/assets/img/workspace-experiment.png')} | |||
| title="正在运行实例总数" | |||
| count={overviewData?.runningExperimentInsCount} | |||
| /> | |||
| </Flex> | |||
| <ExperitableTable | |||
| tableData={overviewData?.latestExperimentInsList || []} | |||
| ></ExperitableTable> | |||
| {overviewData?.experimentInsStatus && ( | |||
| <ExperimentChart chartData={overviewData?.experimentInsStatus}></ExperimentChart> | |||
| )} | |||
| </div> | |||
| </div> | |||
| </Flex> | |||
| <div className={styles['workspace__quick-start']}> | |||
| <QuickStart></QuickStart> | |||
| <div className={styles['workspace__user']}> | |||
| <UserPoints /> | |||
| {/* <UserSpace users={users}></UserSpace> */} | |||
| <WorkspaceSearch></WorkspaceSearch> | |||
| <Flex gap={'0 1.25rem'} justify="stretch"> | |||
| <div style={{ width: '75%' }}> | |||
| <AssetsManagement></AssetsManagement> | |||
| <Flex style={{ marginTop: 24 }} justify="stretch"> | |||
| <Notebooks></Notebooks> | |||
| <Experiments></Experiments> | |||
| </Flex> | |||
| </div> | |||
| </div> | |||
| {/* <Draggable onStart={handleStart} onStop={handleStop} onDrag={handleDrag} bounds="body"> | |||
| <img | |||
| className={styles['workspace__robot-img']} | |||
| src={require('@/assets/img/robot.png')} | |||
| onClick={handleClick} | |||
| draggable={false} | |||
| alt="" | |||
| ></img> | |||
| </Draggable> | |||
| <RobotFrame | |||
| visible={robotFrameVisible} | |||
| onClose={() => setRobotFrameVisible(false)} | |||
| ></RobotFrame> */} | |||
| <div className={styles['workspace__right']}> | |||
| <UserPoints></UserPoints> | |||
| {overviewData?.experimentInsStatus && ( | |||
| <ExperimentChart chartData={overviewData?.experimentInsStatus}></ExperimentChart> | |||
| )} | |||
| <Messages></Messages> | |||
| </div> | |||
| </Flex> | |||
| </div> | |||
| ); | |||
| } | |||