diff --git a/.gitignore b/.gitignore index 21a12484..5510490a 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,5 @@ mvnw # web **/node_modules + +*storybook.log diff --git a/react-ui/.gitignore b/react-ui/.gitignore index 889039c2..9d2581cd 100644 --- a/react-ui/.gitignore +++ b/react-ui/.gitignore @@ -41,10 +41,5 @@ screenshot build pnpm-lock.yaml -/src/services/codeConfig/index.js -/src/pages/CodeConfig/components/AddCodeConfigModal/index.less -/src/pages/CodeConfig/List/index.less -/src/pages/Dataset/components/ResourceItem/index.less -/src/pages/CodeConfig/components/AddCodeConfigModal/index.tsx -/src/pages/CodeConfig/components/CodeConfigItem/index.tsx -/src/pages/Dataset/components/ResourceItem/index.tsx + +*storybook.log diff --git a/react-ui/.nvmrc b/react-ui/.nvmrc new file mode 100644 index 00000000..8ddbc0c6 --- /dev/null +++ b/react-ui/.nvmrc @@ -0,0 +1 @@ +v18.16.0 diff --git a/react-ui/.storybook/babel-plugin-auto-css-modules.js b/react-ui/.storybook/babel-plugin-auto-css-modules.js new file mode 100644 index 00000000..9c7709ff --- /dev/null +++ b/react-ui/.storybook/babel-plugin-auto-css-modules.js @@ -0,0 +1,16 @@ +export default function(babel) { + const { types: t } = babel; + return { + visitor: { + ImportDeclaration(path) { + const source = path.node.source.value; + // console.log("zzzz", source); + if (source.endsWith('.less')) { + if (path.node.specifiers.length > 0) { + path.node.source.value += "?modules"; + } + } + }, + }, + }; +}; \ No newline at end of file diff --git a/react-ui/.storybook/main.ts b/react-ui/.storybook/main.ts new file mode 100644 index 00000000..820a0eeb --- /dev/null +++ b/react-ui/.storybook/main.ts @@ -0,0 +1,117 @@ +import type { StorybookConfig } from '@storybook/react-webpack5'; +import path from 'path'; +import webpack from 'webpack'; + +const config: StorybookConfig = { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + addons: [ + // '@storybook/addon-webpack5-compiler-swc', + '@storybook/addon-webpack5-compiler-babel', + '@storybook/addon-onboarding', + '@storybook/addon-essentials', + '@chromatic-com/storybook', + '@storybook/addon-interactions', + ], + framework: { + name: '@storybook/react-webpack5', + options: {}, + }, + staticDirs: ['../public'], + docs: { + defaultName: 'Documentation', + }, + webpackFinal: async (config) => { + if (config.resolve) { + config.resolve.alias = { + ...config.resolve.alias, + '@': path.resolve(__dirname, '../src'), + '@umijs/max$': path.resolve(__dirname, './mock/umijs.mock.tsx'), + }; + } + if (config.module && config.module.rules) { + config.module.rules.push( + { + test: /\.less$/, + oneOf: [ + { + resourceQuery: /modules/, + use: [ + 'style-loader', + { + loader: 'css-loader', + options: { + importLoaders: 1, + import: true, + esModule: true, + modules: { + localIdentName: '[local]___[hash:base64:5]', + }, + }, + }, + { + loader: 'less-loader', + options: { + lessOptions: { + javascriptEnabled: true, // 如果需要支持 Ant Design 的 Less 变量,开启此项 + modifyVars: { + hack: 'true; @import "@/styles/theme.less";', + }, + }, + }, + }, + ], + include: path.resolve(__dirname, '../src'), // 限制范围,避免处理 node_modules + }, + { + use: [ + 'style-loader', + 'css-loader', + { + loader: 'less-loader', + options: { + lessOptions: { + javascriptEnabled: true, // 如果需要支持 Ant Design 的 Less 变量,开启此项 + modifyVars: { + hack: 'true; @import "@/styles/theme.less";', + }, + }, + }, + }, + ], + include: path.resolve(__dirname, '../src'), // 限制范围,避免处理 node_modules + }, + ], + }, + { + test: /\.(tsx?|jsx?)$/, + loader: 'ts-loader', + options: { + transpileOnly: true, + }, + include: [ + path.resolve(__dirname, '../src'), // 限制范围,避免处理 node_modules + path.resolve(__dirname, './'), + ], + }, + ); + } + if (config.plugins) { + config.plugins.push( + new webpack.ProvidePlugin({ + React: 'react', // 全局注入 React + }), + ); + } + + return config; + }, + babel: async (config: any) => { + if (!config.plugins) { + config.plugins = []; + } + + config.plugins.push(path.resolve(__dirname, './babel-plugin-auto-css-modules.js')); + return config; + }, +}; +export default config; diff --git a/react-ui/.storybook/mock/umijs.mock.tsx b/react-ui/.storybook/mock/umijs.mock.tsx new file mode 100644 index 00000000..ae8a7646 --- /dev/null +++ b/react-ui/.storybook/mock/umijs.mock.tsx @@ -0,0 +1,19 @@ +export const Link = ({ to, children, ...props }: any) => ( + + {children} + +); + +export const request = (url: string, options: any) => { + return fetch(url, options) + .then((res) => { + if (!res.ok) { + throw new Error(res.statusText); + } + return res; + }) + .then((res) => res.json()); +}; + +export { useNavigate, useParams, useSearchParams } from 'react-router-dom'; +export const history = window.history; diff --git a/react-ui/.storybook/preview.tsx b/react-ui/.storybook/preview.tsx new file mode 100644 index 00000000..af81cc5c --- /dev/null +++ b/react-ui/.storybook/preview.tsx @@ -0,0 +1,96 @@ +import '@/global.less'; +import '@/overrides.less'; +import themes from '@/styles/theme.less'; +import type { Preview } from '@storybook/react'; +import { App, ConfigProvider } from 'antd'; +import zhCN from 'antd/locale/zh_CN'; +import { initialize, mswLoader } from 'msw-storybook-addon'; +import './storybook.css'; + +/* + * Initializes MSW + * See https://github.com/mswjs/msw-storybook-addon#configuring-msw + * to learn how to customize it + */ +initialize(); + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + options: { + storySort: { + method: 'alphabetical', + }, + }, + }, + decorators: [ + (Story) => ( + + + + + + ), + ], + loaders: [mswLoader], // 👈 Add the MSW loader to all stories +}; + +export default preview; diff --git a/react-ui/.storybook/storybook.css b/react-ui/.storybook/storybook.css new file mode 100644 index 00000000..6c592a3c --- /dev/null +++ b/react-ui/.storybook/storybook.css @@ -0,0 +1,19 @@ +html, +body, +#root { + min-width: unset; + height: 100%; + margin: 0; + padding: 0; + overflow-y: visible; +} + +.ant-input-search-large .ant-input-affix-wrapper, .ant-input-search-large .ant-input-search-button { + height: 46px; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} diff --git a/react-ui/.storybook/tsconfig.json b/react-ui/.storybook/tsconfig.json new file mode 100644 index 00000000..601d7708 --- /dev/null +++ b/react-ui/.storybook/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "esnext", // 指定ECMAScript目标版本 + "lib": ["dom", "dom.iterable", "esnext"], // 要包含在编译中的库文件列表 + "allowJs": true, // 允许编译JavaScript文件 + "skipLibCheck": true, // 跳过所有声明文件的类型检查 + "esModuleInterop": true, // 禁用命名空间导入(import * as fs from "fs"),并启用CJS/AMD/UMD样式的导入(import fs from "fs") + "allowSyntheticDefaultImports": true, // 允许从没有默认导出的模块进行默认导入 + "strict": true, // 启用所有严格类型检查选项 + "forceConsistentCasingInFileNames": false, // 允许对同一文件的引用使用不一致的大小写 + "module": "esnext", // 指定模块代码生成 + "moduleResolution": "bundler", // 使用bundlers样式解析模块 + "isolatedModules": true, // 无条件地为未解析的文件发出导入 + "resolveJsonModule": true, // 包含.json扩展名的模块 + "noEmit": true, // 不发出输出(即不编译代码,只进行类型检查) + "jsx": "react-jsx", // 在.tsx文件中支持JSX + "sourceMap": true, // 生成相应的.map文件 + "declaration": true, // 生成相应的.d.ts文件 + "noUnusedLocals": true, // 报告未使用的局部变量错误 + "noUnusedParameters": true, // 报告未使用的参数错误 + "incremental": true, // 通过读写磁盘上的文件来启用增量编译 + "noFallthroughCasesInSwitch": true, // 报告switch语句中的fallthrough案例错误 + "strictNullChecks": true, // 启用严格的null检查 + "baseUrl": "./" + } +} diff --git a/react-ui/.storybook/typings.d.ts b/react-ui/.storybook/typings.d.ts new file mode 100644 index 00000000..742f70c6 --- /dev/null +++ b/react-ui/.storybook/typings.d.ts @@ -0,0 +1,20 @@ +declare module 'slash2'; +declare module '*.css'; +declare module '*.less'; +declare module '*.scss'; +declare module '*.sass'; +declare module '*.svg'; +declare module '*.png'; +declare module '*.jpg'; +declare module '*.jpeg'; +declare module '*.gif'; +declare module '*.bmp'; +declare module '*.tiff'; +declare module 'omit.js'; +declare module 'numeral'; +declare module '@antv/data-set'; +declare module 'mockjs'; +declare module 'react-fittext'; +declare module 'bizcharts-plugin-slider'; + +declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false; diff --git a/react-ui/package.json b/react-ui/package.json index dc5be1c5..c98f0286 100644 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -39,7 +39,9 @@ "test": "jest", "test:coverage": "npm run jest -- --coverage", "test:update": "npm run jest -- -u", - "tsc": "tsc --noEmit" + "tsc": "tsc --noEmit", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" }, "lint-staged": { "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js", @@ -83,6 +85,17 @@ }, "devDependencies": { "@ant-design/pro-cli": "^3.1.0", + "@chromatic-com/storybook": "~3.2.4", + "@storybook/addon-essentials": "~8.5.3", + "@storybook/addon-interactions": "~8.5.3", + "@storybook/addon-onboarding": "~8.5.3", + "@storybook/addon-styling-webpack": "~1.0.1", + "@storybook/addon-webpack5-compiler-babel": "~3.0.5", + "@storybook/addon-webpack5-compiler-swc": "~2.0.0", + "@storybook/blocks": "~8.5.3", + "@storybook/react": "~8.5.3", + "@storybook/react-webpack5": "~8.5.3", + "@storybook/test": "~8.5.3", "@testing-library/react": "^14.0.0", "@types/antd": "^1.0.0", "@types/express": "^4.17.14", @@ -96,15 +109,22 @@ "@umijs/max": "^4.0.66", "cross-env": "^7.0.3", "eslint": "^8.39.0", + "eslint-plugin-storybook": "~0.11.2", "express": "^4.18.2", "gh-pages": "^5.0.0", "husky": "^8.0.3", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", + "less": "~4.2.2", + "less-loader": "~12.2.0", "lint-staged": "^13.2.0", "mockjs": "^1.1.0", + "msw": "~2.7.0", + "msw-storybook-addon": "~2.0.4", "prettier": "^2.8.1", + "storybook": "~8.5.3", "swagger-ui-dist": "^4.18.2", + "ts-loader": "~9.5.2", "ts-node": "^10.9.1", "typescript": "^5.0.4", "umi-presets-pro": "^2.0.0" @@ -140,5 +160,10 @@ "CNAME", "create-umi" ] + }, + "msw": { + "workerDirectory": [ + "public" + ] } -} +} \ No newline at end of file diff --git a/react-ui/public/mockServiceWorker.js b/react-ui/public/mockServiceWorker.js new file mode 100644 index 00000000..ec47a9a5 --- /dev/null +++ b/react-ui/public/mockServiceWorker.js @@ -0,0 +1,307 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const PACKAGE_VERSION = '2.7.0' +const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, + }, + [responseClone.body], + ) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, + }, + [requestBuffer], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) + }) +} + +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} diff --git a/react-ui/src/app.tsx b/react-ui/src/app.tsx index 275e5e17..65b4440a 100644 --- a/react-ui/src/app.tsx +++ b/react-ui/src/app.tsx @@ -7,7 +7,6 @@ import defaultSettings from '../config/defaultSettings'; import '../public/fonts/font.css'; import { getAccessToken } from './access'; import './dayjsConfig'; -import './global.less'; import { removeAllPageCacheState } from './hooks/pageCacheState'; import { getRemoteMenu, @@ -41,7 +40,7 @@ export async function getInitialState(): Promise { roleNames: response.user.roles, } as API.CurrentUser; } catch (error) { - console.error('1111', error); + console.error('getInitialState', error); gotoLoginPage(); } return undefined; @@ -215,7 +214,7 @@ export const antd: RuntimeAntdConfig = (memo) => { defaultColor: themes['textColor'], defaultHoverBg: 'rgba(22, 100, 255, 0.06)', defaultHoverBorderColor: 'rgba(22, 100, 255, 0.5)', - defaultHoverColor: '#3F7FFF ', + defaultHoverColor: '#3F7FFF', defaultActiveBg: 'rgba(22, 100, 255, 0.12)', defaultActiveBorderColor: 'rgba(22, 100, 255, 0.75)', defaultActiveColor: themes['primaryColor'], diff --git a/react-ui/src/components/BasicInfo/BasicInfoItem.tsx b/react-ui/src/components/BasicInfo/BasicInfoItem.tsx index 776a7f94..86e63891 100644 --- a/react-ui/src/components/BasicInfo/BasicInfoItem.tsx +++ b/react-ui/src/components/BasicInfo/BasicInfoItem.tsx @@ -4,23 +4,37 @@ * @Description: 用于 BasicInfo 和 BasicTableInfo 组件的子组件 */ +import { Typography } from 'antd'; import React from 'react'; import BasicInfoItemValue from './BasicInfoItemValue'; import { type BasicInfoData, type BasicInfoLink } from './types'; type BasicInfoItemProps = { + /** 基础信息 */ data: BasicInfoData; + /** 标题宽度 */ labelWidth: number; + /** 自定义类名前缀 */ classPrefix: string; + /** 标题是否显示省略号 */ + labelEllipsis?: boolean; + /** 标签对齐方式 */ + labelAlign?: 'start' | 'end' | 'justify'; }; -function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemProps) { +function BasicInfoItem({ + data, + labelWidth, + classPrefix, + labelEllipsis = true, + labelAlign = 'start', +}: BasicInfoItemProps) { const { label, value, format, ellipsis } = data; const formatValue = format ? format(value) : value; const myClassName = `${classPrefix}__item`; let valueComponent = undefined; if (React.isValidElement(formatValue)) { - valueComponent = formatValue; + valueComponent =
{formatValue}
; } else if (Array.isArray(formatValue)) { valueComponent = (
@@ -53,8 +67,16 @@ function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemProps) { } return (
-
- {label} +
+ + {label} +
{valueComponent}
diff --git a/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx b/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx index eca6c80c..c5a993e4 100644 --- a/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx +++ b/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx @@ -4,23 +4,30 @@ * @Description: 用于 BasicInfoItem 的组件 */ +import { isEmpty } from '@/utils'; import { Link } from '@umijs/max'; import { Typography } from 'antd'; -import React from 'react'; type BasicInfoItemValueProps = { + /** 值是否显示省略号 */ ellipsis?: boolean; + /** 自定义类名前缀 */ classPrefix: string; - value: string | React.ReactNode; + /** 值 */ + value?: string; + /** 内部链接 */ link?: string; + /** 外部链接 */ url?: string; }; -function BasicInfoItemValue({ value, link, url, ellipsis, classPrefix }: BasicInfoItemValueProps) { - if (React.isValidElement(value)) { - return value; - } - +function BasicInfoItemValue({ + value, + link, + url, + classPrefix, + ellipsis = true, +}: BasicInfoItemValueProps) { const myClassName = `${classPrefix}__item__value`; let component = undefined; if (url && value) { @@ -36,12 +43,12 @@ function BasicInfoItemValue({ value, link, url, ellipsis, classPrefix }: BasicIn ); } else { - component = {value ?? '--'}; + component = {!isEmpty(value) ? value : '--'}; } return (
- + {component}
diff --git a/react-ui/src/components/BasicInfo/index.less b/react-ui/src/components/BasicInfo/index.less index e4570868..ced2d2ba 100644 --- a/react-ui/src/components/BasicInfo/index.less +++ b/react-ui/src/components/BasicInfo/index.less @@ -17,8 +17,6 @@ color: @text-color-secondary; font-size: @font-size-content; line-height: 1.6; - text-align: justify; - text-align-last: justify; &::after { position: absolute; @@ -31,10 +29,12 @@ flex: 1; flex-direction: column; gap: 5px 0; + min-width: 0; } &__value { flex: 1; + min-width: 0; margin-left: 16px; font-size: @font-size-content; line-height: 1.6; @@ -49,5 +49,29 @@ text-underline-offset: 3px; } } + + &__node { + flex: 1; + min-width: 0; + margin-left: 16px; + font-size: @font-size-content; + line-height: 1.6; + word-break: break-all; + } + } +} + +.kf-basic-info--three-columns { + width: 100%; + + .kf-basic-info__item { + width: calc((100% - 80px) / 3); + + &__label { + font-size: @font-size; + } + &__value { + font-size: @font-size; + } } } diff --git a/react-ui/src/components/BasicInfo/index.tsx b/react-ui/src/components/BasicInfo/index.tsx index 5f60b79a..11eacfde 100644 --- a/react-ui/src/components/BasicInfo/index.tsx +++ b/react-ui/src/components/BasicInfo/index.tsx @@ -5,22 +5,52 @@ import './index.less'; import type { BasicInfoData, BasicInfoLink } from './types'; export type { BasicInfoData, BasicInfoLink }; -type BasicInfoProps = { +export type BasicInfoProps = { + /** 基础信息 */ datas: BasicInfoData[]; + /** 标题宽度 */ + labelWidth: number; + /** 标题是否显示省略号 */ + labelEllipsis?: boolean; + /** 是否一行三列 */ + threeColumns?: boolean; + /** 标签对齐方式 */ + labelAlign?: 'start' | 'end' | 'justify'; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; - labelWidth: number; }; -export default function BasicInfo({ datas, className, style, labelWidth }: BasicInfoProps) { +/** + * 基础信息展示组件,用于展示基础信息,支持一行两列或一行三列,支持数据格式化 + */ +export default function BasicInfo({ + datas, + className, + style, + labelWidth, + labelEllipsis = true, + threeColumns = false, + labelAlign = 'start', +}: BasicInfoProps) { return ( -
+
{datas.map((item) => ( ))}
diff --git a/react-ui/src/components/BasicTableInfo/index.less b/react-ui/src/components/BasicTableInfo/index.less index 850af79c..479fe332 100644 --- a/react-ui/src/components/BasicTableInfo/index.less +++ b/react-ui/src/components/BasicTableInfo/index.less @@ -34,7 +34,6 @@ &__value { flex: 1; min-width: 0; - margin: 0 !important; padding: 12px 20px 4px; font-size: @font-size; word-break: break-all; @@ -56,5 +55,13 @@ text-underline-offset: 3px; } } + + &__node { + flex: 1; + min-width: 0; + padding: 12px 20px; + font-size: @font-size; + word-break: break-all; + } } } diff --git a/react-ui/src/components/BasicTableInfo/index.tsx b/react-ui/src/components/BasicTableInfo/index.tsx index f24f3dc9..68c350bd 100644 --- a/react-ui/src/components/BasicTableInfo/index.tsx +++ b/react-ui/src/components/BasicTableInfo/index.tsx @@ -1,29 +1,27 @@ import classNames from 'classnames'; +import { BasicInfoProps } from '../BasicInfo'; import BasicInfoItem from '../BasicInfo/BasicInfoItem'; import { type BasicInfoData, type BasicInfoLink } from '../BasicInfo/types'; import './index.less'; export type { BasicInfoData, BasicInfoLink }; -type BasicTableInfoProps = { - datas: BasicInfoData[]; - className?: string; - style?: React.CSSProperties; - labelWidth: number; -}; - +/** + * 表格基础信息展示组件,用于展示基础信息,一行四列,支持数据格式化 + */ export default function BasicTableInfo({ datas, className, style, labelWidth, -}: BasicTableInfoProps) { + labelEllipsis, +}: BasicInfoProps) { const remainder = datas.length % 4; const array = []; if (remainder > 0) { for (let i = 0; i < 4 - remainder; i++) { array.push({ label: '', - value: '', + value: false, // 用于区分是否是空数据,不能是空字符串、null、undefined }); } } @@ -36,6 +34,7 @@ export default function BasicTableInfo({ key={`${item.label}-${index}`} data={item} labelWidth={labelWidth} + labelEllipsis={labelEllipsis} classPrefix="kf-basic-table-info" /> ))} diff --git a/react-ui/src/pages/Pipeline/components/CodeConfigItem/index.less b/react-ui/src/components/CodeConfigItem/index.less similarity index 100% rename from react-ui/src/pages/Pipeline/components/CodeConfigItem/index.less rename to react-ui/src/components/CodeConfigItem/index.less diff --git a/react-ui/src/pages/Pipeline/components/CodeConfigItem/index.tsx b/react-ui/src/components/CodeConfigItem/index.tsx similarity index 100% rename from react-ui/src/pages/Pipeline/components/CodeConfigItem/index.tsx rename to react-ui/src/components/CodeConfigItem/index.tsx diff --git a/react-ui/src/components/CodeSelect/index.tsx b/react-ui/src/components/CodeSelect/index.tsx index 79401b25..242183e1 100644 --- a/react-ui/src/components/CodeSelect/index.tsx +++ b/react-ui/src/components/CodeSelect/index.tsx @@ -4,8 +4,8 @@ * @Description: 流水线选择代码配置表单 */ +import CodeSelectorModal from '@/components/CodeSelectorModal'; import KFIcon from '@/components/KFIcon'; -import CodeSelectorModal from '@/pages/Pipeline/components/CodeSelectorModal'; import { openAntdModal } from '@/utils/modal'; import { Button } from 'antd'; import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; @@ -15,6 +15,7 @@ export { requiredValidator, type ParameterInputObject } from '../ParameterInput' type CodeSelectProps = ParameterInputProps; +/** 代码配置选择表单组件 */ function CodeSelect({ value, onChange, disabled, ...rest }: CodeSelectProps) { const selectResource = () => { const { close } = openAntdModal(CodeSelectorModal, { diff --git a/react-ui/src/components/CodeSelectorModal/index.less b/react-ui/src/components/CodeSelectorModal/index.less new file mode 100644 index 00000000..82d73ff8 --- /dev/null +++ b/react-ui/src/components/CodeSelectorModal/index.less @@ -0,0 +1,49 @@ +.kf-code-selector-modal { + width: 100%; + height: 100%; + + &__search { + width: 100%; + } + + &__content { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 10px; + width: 100%; + max-height: 50vh; + margin-top: 24px; + margin-bottom: 30px; + overflow-x: hidden; + overflow-y: auto; + } + + &__empty { + padding-top: 40px; + } + + // 覆盖 antd 样式 + .ant-input-affix-wrapper { + border-radius: 23px !important; + .ant-input-prefix { + margin-inline-end: 12px; + } + .ant-input-suffix { + margin-inline-end: 12px; + } + .ant-input-clear-icon { + font-size: 16px; + } + } + + .ant-input-group-addon { + display: none; + } + + .ant-pagination { + .ant-select-single { + height: 32px !important; + } + } +} diff --git a/react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.tsx b/react-ui/src/components/CodeSelectorModal/index.tsx similarity index 84% rename from react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.tsx rename to react-ui/src/components/CodeSelectorModal/index.tsx index 13198a78..c983093e 100644 --- a/react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.tsx +++ b/react-ui/src/components/CodeSelectorModal/index.tsx @@ -4,16 +4,16 @@ * @Description: 选择代码 */ +import KFIcon from '@/components/KFIcon'; import KFModal from '@/components/KFModal'; import { type CodeConfigData } from '@/pages/CodeConfig/List'; import { getCodeConfigListReq } from '@/services/codeConfig'; import { to } from '@/utils/promise'; -import { Icon } from '@umijs/max'; import type { ModalProps, PaginationProps } from 'antd'; import { Empty, Input, Pagination } from 'antd'; import { useEffect, useState } from 'react'; import CodeConfigItem from '../CodeConfigItem'; -import styles from './index.less'; +import './index.less'; export { type CodeConfigData }; @@ -21,6 +21,7 @@ export interface CodeSelectorModalProps extends Omit { onOk?: (params: CodeConfigData | undefined) => void; } +/** 选择代码配置的弹窗,推荐使用函数的方式打开 */ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { const [dataList, setDataList] = useState([]); const [total, setTotal] = useState(0); @@ -79,9 +80,9 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { footer={null} destroyOnClose > -
+
+ } + // prefix={ + // + // } /> {dataList?.length !== 0 ? ( <> -
+
{dataList?.map((item) => ( ))} @@ -112,7 +116,7 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { /> ) : ( -
+
)} diff --git a/react-ui/src/components/ConfigInfo/index.tsx b/react-ui/src/components/ConfigInfo/index.tsx new file mode 100644 index 00000000..0f81d46f --- /dev/null +++ b/react-ui/src/components/ConfigInfo/index.tsx @@ -0,0 +1,41 @@ +import BasicInfo, { type BasicInfoData, type BasicInfoProps } from '@/components/BasicInfo'; +import InfoGroup from '@/components/InfoGroup'; +import classNames from 'classnames'; +export type { BasicInfoData }; + +interface ConfigInfoProps extends BasicInfoProps { + /** 标题 */ + title: string; + /** 子元素 */ + children?: React.ReactNode; +} + +/** 详情基本信息块,目前主要用于主动机器学习、超参数寻优、自主学习详情中 */ +function ConfigInfo({ + title, + datas, + labelWidth, + labelAlign = 'start', + labelEllipsis = true, + threeColumns = true, + className, + style, + children, +}: ConfigInfoProps) { + return ( + +
+ + {children} +
+
+ ); +} + +export default ConfigInfo; diff --git a/react-ui/src/components/DisabledInput/index.tsx b/react-ui/src/components/DisabledInput/index.tsx index c951324e..3a31def8 100644 --- a/react-ui/src/components/DisabledInput/index.tsx +++ b/react-ui/src/components/DisabledInput/index.tsx @@ -6,6 +6,9 @@ type DisabledInputProps = { valuePropName?: string; }; +/** + * 模拟禁用的输入框,但是完全显示内容 + */ function DisabledInput({ value, valuePropName }: DisabledInputProps) { const data = valuePropName ? value[valuePropName] : value; return ( diff --git a/react-ui/src/components/FullScreenFrame/index.tsx b/react-ui/src/components/FullScreenFrame/index.tsx index a5c3413a..800a727e 100644 --- a/react-ui/src/components/FullScreenFrame/index.tsx +++ b/react-ui/src/components/FullScreenFrame/index.tsx @@ -2,22 +2,30 @@ import classNames from 'classnames'; import './index.less'; type FullScreenFrameProps = { + /** URL */ url: string; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; - onload?: (e?: React.SyntheticEvent) => void; - onerror?: (e?: React.SyntheticEvent) => void; + /** 加载完成回调 */ + onLoad?: (e?: React.SyntheticEvent) => void; + /** 加载失败回调 */ + onError?: (e?: React.SyntheticEvent) => void; }; -function FullScreenFrame({ url, className, style, onload, onerror }: FullScreenFrameProps) { +/** + * 全屏 iframe,IFramePage 组件的子组件,开发中应该使用 IFramePage + */ +function FullScreenFrame({ url, className, style, onLoad, onError }: FullScreenFrameProps) { return (
{url && ( )}
diff --git a/react-ui/src/components/IFramePage/index.tsx b/react-ui/src/components/IFramePage/index.tsx index 9cce1a3b..060f3bc1 100644 --- a/react-ui/src/components/IFramePage/index.tsx +++ b/react-ui/src/components/IFramePage/index.tsx @@ -12,32 +12,36 @@ export enum IframePageType { DatasetAnnotation = 'DatasetAnnotation', // 数据标注 AppDevelopment = 'AppDevelopment', // 应用开发 DevEnv = 'DevEnv', // 开发环境 - GitLink = 'GitLink', + GitLink = 'GitLink', // git link } const getRequestAPI = (type: IframePageType): (() => Promise) => { switch (type) { - case IframePageType.DatasetAnnotation: + case IframePageType.DatasetAnnotation: // 数据标注 return () => Promise.resolve({ code: 200, data: 'http://172.20.32.181:18888/oauth/login' }); //getLabelStudioUrl; - case IframePageType.AppDevelopment: + case IframePageType.AppDevelopment: // 应用开发 return () => Promise.resolve({ code: 200, data: 'http://172.20.32.185:30080/' }); - case IframePageType.DevEnv: + case IframePageType.DevEnv: // 开发环境 return () => Promise.resolve({ code: 200, data: SessionStorage.getItem(SessionStorage.editorUrlKey) || '', }); - case IframePageType.GitLink: + case IframePageType.GitLink: // git link return () => Promise.resolve({ code: 200, data: 'http://172.20.32.201:4000' }); } }; type IframePageProps = { + /** 子系统 */ type: IframePageType; + /** 自定义样式类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; }; +/** 系统内嵌 iframe,目前系统有数据标注、应用开发、开发环境、GitLink 四个子系统,使用时可以添加其他子系统 */ function IframePage({ type, className, style }: IframePageProps) { const [iframeUrl, setIframeUrl] = useState(''); const [loading, setLoading] = useState(false); @@ -66,7 +70,7 @@ function IframePage({ type, className, style }: IframePageProps) { return (
{loading && createPortal(, document.body)} - +
); } diff --git a/react-ui/src/components/InfoGroupTitle/index.less b/react-ui/src/components/InfoGroup/InfoGroupTitle.less similarity index 93% rename from react-ui/src/components/InfoGroupTitle/index.less rename to react-ui/src/components/InfoGroup/InfoGroupTitle.less index ee3c9011..b8aff97c 100644 --- a/react-ui/src/components/InfoGroupTitle/index.less +++ b/react-ui/src/components/InfoGroup/InfoGroupTitle.less @@ -1,7 +1,7 @@ .kf-info-group-title { width: 100%; height: 56px; - padding-left: @content-padding; + padding: 0 @content-padding; background: linear-gradient( 179.03deg, rgba(199, 223, 255, 0.12) 0%, @@ -21,6 +21,7 @@ color: @text-color; font-weight: 500; font-size: @font-size-title; + .singleLine(); &::after { position: absolute; diff --git a/react-ui/src/components/InfoGroupTitle/index.tsx b/react-ui/src/components/InfoGroup/InfoGroupTitle.tsx similarity index 83% rename from react-ui/src/components/InfoGroupTitle/index.tsx rename to react-ui/src/components/InfoGroup/InfoGroupTitle.tsx index 7eec2ab8..bde57b3f 100644 --- a/react-ui/src/components/InfoGroupTitle/index.tsx +++ b/react-ui/src/components/InfoGroup/InfoGroupTitle.tsx @@ -1,13 +1,19 @@ import { Flex } from 'antd'; import classNames from 'classnames'; -import './index.less'; +import './InfoGroupTitle.less'; type InfoGroupTitleProps = { + /** 标题 */ title: string; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; }; +/** + * 信息组标题 + */ function InfoGroupTitle({ title, style, className }: InfoGroupTitleProps) { return ( diff --git a/react-ui/src/components/InfoGroup/index.tsx b/react-ui/src/components/InfoGroup/index.tsx index 8cf4b4e8..0f2a3b42 100644 --- a/react-ui/src/components/InfoGroup/index.tsx +++ b/react-ui/src/components/InfoGroup/index.tsx @@ -1,16 +1,25 @@ import classNames from 'classnames'; -import InfoGroupTitle from '../InfoGroupTitle'; +import InfoGroupTitle from './InfoGroupTitle'; import './index.less'; type InfoGroupProps = { + /** 标题 */ title: string; - height?: string | number; // 如果要纵向滚动,需要设置高度 - width?: string | number; // 如果要横向滚动,需要设置宽度 + /** 高度, 如果要纵向滚动,需要设置高度 */ + height?: string | number; + /** 宽度, 如果要横向滚动,需要设置宽度 */ + width?: string | number; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; + /** 子元素 */ children?: React.ReactNode; }; +/** + * 信息组,用于展示基本信息,支持横向、纵向滚动。自动机器学习、超参数寻优都是使用这个组件 + */ function InfoGroup({ title, height, width, className, style, children }: InfoGroupProps) { const contentStyle: React.CSSProperties = {}; if (height) { diff --git a/react-ui/src/components/KFEmpty/index.less b/react-ui/src/components/KFEmpty/index.less index 39f70281..3e5a85b4 100644 --- a/react-ui/src/components/KFEmpty/index.less +++ b/react-ui/src/components/KFEmpty/index.less @@ -33,7 +33,7 @@ margin-top: 20px; margin-bottom: 30px; - &__back-btn { + &__button { height: 32px; } } diff --git a/react-ui/src/components/KFEmpty/index.tsx b/react-ui/src/components/KFEmpty/index.tsx index e59b6f6b..b2a680f6 100644 --- a/react-ui/src/components/KFEmpty/index.tsx +++ b/react-ui/src/components/KFEmpty/index.tsx @@ -9,15 +9,24 @@ export enum EmptyType { } type EmptyProps = { - className?: string; - style?: React.CSSProperties; + /** 类型 */ type: EmptyType; + /** 标题 */ title?: string; + /** 内容 */ content?: string; + /** 是否有页脚,如果有默认是一个按钮 */ hasFooter?: boolean; - footer?: () => React.ReactNode; + /** 按钮标题,默认是"刷新" */ buttonTitle?: string; - onRefresh?: () => void; + /** 按钮点击回调 */ + onButtonClick?: () => void; + /** 自定义页脚内容 */ + footer?: () => React.ReactNode; + /** 自定义类名 */ + className?: string; + /** 自定义样式 */ + style?: React.CSSProperties; }; function getEmptyImage(type: EmptyType) { @@ -31,6 +40,7 @@ function getEmptyImage(type: EmptyType) { } } +/** 空状态 */ function KFEmpty({ className, style, @@ -40,7 +50,7 @@ function KFEmpty({ hasFooter = true, footer, buttonTitle = '刷新', - onRefresh, + onButtonClick, }: EmptyProps) { const image = getEmptyImage(type); @@ -54,7 +64,7 @@ function KFEmpty({ {footer ? ( footer() ) : ( - )} diff --git a/react-ui/src/components/KFIcon/index.tsx b/react-ui/src/components/KFIcon/index.tsx index d84257a7..38d3644c 100644 --- a/react-ui/src/components/KFIcon/index.tsx +++ b/react-ui/src/components/KFIcon/index.tsx @@ -14,14 +14,20 @@ const Icon = createFromIconfontCN({ type IconFontProps = Parameters[0]; interface KFIconProps extends IconFontProps { + /** 图标 */ type: string; + /** 字体大小 */ font?: number; + /** 字体颜色 */ color?: string; - style?: React.CSSProperties; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ + style?: React.CSSProperties; } -function KFIcon({ type, font = 15, color = '', style = {}, className, ...rest }: KFIconProps) { +/** 封装 iconfont 图标 */ +function KFIcon({ type, font = 15, color, className, style, ...rest }: KFIconProps) { const iconStyle = { ...style, fontSize: font, diff --git a/react-ui/src/components/ModalTitle/index.less b/react-ui/src/components/KFModal/KFModalTitle.less similarity index 100% rename from react-ui/src/components/ModalTitle/index.less rename to react-ui/src/components/KFModal/KFModalTitle.less diff --git a/react-ui/src/components/ModalTitle/index.tsx b/react-ui/src/components/KFModal/KFModalTitle.tsx similarity index 84% rename from react-ui/src/components/ModalTitle/index.tsx rename to react-ui/src/components/KFModal/KFModalTitle.tsx index 4c0179eb..d2ec3265 100644 --- a/react-ui/src/components/ModalTitle/index.tsx +++ b/react-ui/src/components/KFModal/KFModalTitle.tsx @@ -6,12 +6,16 @@ import classNames from 'classnames'; import React from 'react'; -import './index.less'; +import './KFModalTitle.less'; type ModalTitleProps = { + /** 标题 */ title: React.ReactNode; + /** 图片 */ image?: string; + /** 自定义样式 */ style?: React.CSSProperties; + /** 自定义类名 */ className?: string; }; diff --git a/react-ui/src/components/KFModal/index.tsx b/react-ui/src/components/KFModal/index.tsx index c073ab27..8156d6a9 100644 --- a/react-ui/src/components/KFModal/index.tsx +++ b/react-ui/src/components/KFModal/index.tsx @@ -4,19 +4,22 @@ * @Description: 自定义 Modal */ -import ModalTitle from '@/components/ModalTitle'; import { Modal, type ModalProps } from 'antd'; import classNames from 'classnames'; +import KFModalTitle from './KFModalTitle'; import './index.less'; export interface KFModalProps extends ModalProps { + /** 标题图片 */ image?: string; } + +/** 自定义 Modal,应用中的业务 Modal 应该使用它进行封装,推荐使用函数的方式打开 */ function KFModal({ title, image, children, - className = '', + className, centered, maskClosable, ...rest @@ -27,7 +30,7 @@ function KFModal({ {...rest} centered={centered ?? true} maskClosable={maskClosable ?? false} - title={} + title={} > {children} diff --git a/react-ui/src/components/KFRadio/index.tsx b/react-ui/src/components/KFRadio/index.tsx index 4bb4ccce..7191dae6 100644 --- a/react-ui/src/components/KFRadio/index.tsx +++ b/react-ui/src/components/KFRadio/index.tsx @@ -8,32 +8,40 @@ import classNames from 'classnames'; import './index.less'; export type KFRadioItem = { - key: string; title: string; + value: string; icon?: React.ReactNode; }; type KFRadioProps = { + /** 选项 */ items: KFRadioItem[]; + /** 当前选中项 */ value?: string; + /** 自定义样式 */ style?: React.CSSProperties; + /** 自定义类名 */ className?: string; + /** 选中回调 */ onChange?: (value: string) => void; }; +/** + * 自定义 Radio + */ function KFRadio({ items, value, style, className, onChange }: KFRadioProps) { return ( {items.map((item) => { return ( onChange?.(item.key)} + onClick={() => onChange?.(item.value)} > {item.icon} {item.title} diff --git a/react-ui/src/components/KFSpin/index.tsx b/react-ui/src/components/KFSpin/index.tsx index 519ab6ef..a3a6c3e5 100644 --- a/react-ui/src/components/KFSpin/index.tsx +++ b/react-ui/src/components/KFSpin/index.tsx @@ -5,13 +5,19 @@ */ import { Spin, SpinProps } from 'antd'; -import styles from './index.less'; +import './index.less'; -function KFSpin(props: SpinProps) { +interface KFSpinProps extends SpinProps { + /** 加载文本 */ + label: string; +} + +/** 自定义 Spin */ +function KFSpin({ label = '加载中', ...rest }: KFSpinProps) { return ( -
- -
加载中
+
+ +
{label}
); } diff --git a/react-ui/src/components/LabelValue/index.less b/react-ui/src/components/LabelValue/index.less deleted file mode 100644 index 5f1b9b0c..00000000 --- a/react-ui/src/components/LabelValue/index.less +++ /dev/null @@ -1,19 +0,0 @@ -.kf-label-value { - display: flex; - align-items: flex-start; - font-size: 16px; - line-height: 1.6; - - &__label { - flex: none; - width: 80px; - color: @text-color-secondary; - } - - &__value { - flex: 1; - color: @text-color; - white-space: pre-line; - word-break: break-all; - } -} diff --git a/react-ui/src/components/LabelValue/index.tsx b/react-ui/src/components/LabelValue/index.tsx deleted file mode 100644 index 22b9b3eb..00000000 --- a/react-ui/src/components/LabelValue/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import classNames from 'classnames'; -import './index.less'; - -type labelValueProps = { - label: string; - value?: any; - className?: string; - style?: React.CSSProperties; -}; - -function LabelValue({ label, value, className, style }: labelValueProps) { - return ( -
-
{label}
-
{value ?? '--'}
-
- ); -} - -export default LabelValue; diff --git a/react-ui/src/components/MenuIconSelector/index.less b/react-ui/src/components/MenuIconSelector/index.less index 77529762..5a64a8d3 100644 --- a/react-ui/src/components/MenuIconSelector/index.less +++ b/react-ui/src/components/MenuIconSelector/index.less @@ -1,5 +1,4 @@ .menu-icon-selector { - // grid 布局,每行显示 8 个图标 display: grid; grid-template-columns: repeat(4, 80px); gap: 20px; @@ -10,7 +9,7 @@ display: flex; align-items: center; justify-content: center; - width: 80x; + width: 80px; height: 80px; border: 1px solid transparent; border-radius: 4px; diff --git a/react-ui/src/components/MenuIconSelector/index.tsx b/react-ui/src/components/MenuIconSelector/index.tsx index dd57320e..fa38910b 100644 --- a/react-ui/src/components/MenuIconSelector/index.tsx +++ b/react-ui/src/components/MenuIconSelector/index.tsx @@ -12,7 +12,9 @@ import { useEffect, useState } from 'react'; import styles from './index.less'; interface MenuIconSelectorProps extends Omit { + /** 选中的图标 */ selectedIcon?: string; + /** 选择回调 */ onOk: (param: string) => void; } @@ -21,6 +23,7 @@ type IconObject = { font_class: string; }; +/** 菜单图标选择器 */ function MenuIconSelector({ open, selectedIcon, onOk, ...rest }: MenuIconSelectorProps) { const [icons, setIcons] = useState([]); useEffect(() => { diff --git a/react-ui/src/components/PageTitle/index.tsx b/react-ui/src/components/PageTitle/index.tsx index ca192454..2703e032 100644 --- a/react-ui/src/components/PageTitle/index.tsx +++ b/react-ui/src/components/PageTitle/index.tsx @@ -8,10 +8,17 @@ import React from 'react'; import './index.less'; type PageTitleProps = { - title: string; + /** 标题 */ + title: React.ReactNode; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; }; + +/** + * 页面标题 + */ function PageTitle({ title, style, className = '' }: PageTitleProps) { return (
diff --git a/react-ui/src/components/ParameterInput/index.tsx b/react-ui/src/components/ParameterInput/index.tsx index aa0c7492..99bb78af 100644 --- a/react-ui/src/components/ParameterInput/index.tsx +++ b/react-ui/src/components/ParameterInput/index.tsx @@ -1,7 +1,7 @@ /* * @Author: 赵伟 * @Date: 2024-04-16 08:42:57 - * @Description: 参数输入组件,支持手动输入,选择全局参数,选择数据集/模型/镜像 + * @Description: 参数输入表单组件,支持手动输入,也支持选择全局参数 */ import { CommonTabKeys } from '@/enums'; @@ -26,18 +26,34 @@ export type ParameterInputObject = { export type ParameterInputValue = ParameterInputObject | string; export interface ParameterInputProps { + /** 值,可以是字符串,也可以是 ParameterInputObject 对象 */ value?: ParameterInputValue; + /** + * 值变化时的回调 + * @param value 值,可以是字符串,也可以是 ParameterInputObject 对象 + */ onChange?: (value?: ParameterInputValue) => void; + /** 点击时的回调 */ onClick?: () => void; + /** 删除时的回调 */ onRemove?: () => void; + /** 是否可以手动输入 */ canInput?: boolean; + /** 是否是文本框 */ textArea?: boolean; + /** 占位符 */ placeholder?: string; + /** 是否允许清除 */ allowClear?: boolean; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; + /** 大小 */ size?: 'middle' | 'small' | 'large'; + /** 是否禁用 */ disabled?: boolean; + /** 元素 id */ id?: string; } diff --git a/react-ui/src/components/ResourceSelect/index.tsx b/react-ui/src/components/ResourceSelect/index.tsx index 6e0179d4..718d571a 100644 --- a/react-ui/src/components/ResourceSelect/index.tsx +++ b/react-ui/src/components/ResourceSelect/index.tsx @@ -20,15 +20,17 @@ import './index.less'; export { requiredValidator, type ParameterInputObject } from '../ParameterInput'; export { ResourceSelectorType, selectorTypeConfig, type ResourceSelectorResponse }; -type ResourceSelectProps = { +interface ResourceSelectProps extends ParameterInputProps { + /** 类型,数据集、模型、镜像 */ type: ResourceSelectorType; -} & ParameterInputProps; +} -// 获取选择数据集、模型后面按钮 icon +// 获取选择数据集、模型、镜像后面按钮 icon const getSelectBtnIcon = (type: ResourceSelectorType) => { return ; }; +/** 数据集、模型、镜像选择表单组件 */ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSelectProps) { const [selectedResource, setSelectedResource] = useState( undefined, diff --git a/react-ui/src/components/ResourceSelectorModal/index.tsx b/react-ui/src/components/ResourceSelectorModal/index.tsx index 28beedd0..24e97a4d 100644 --- a/react-ui/src/components/ResourceSelectorModal/index.tsx +++ b/react-ui/src/components/ResourceSelectorModal/index.tsx @@ -4,11 +4,11 @@ * @Description: 选择数据集、模型、镜像 */ +import KFIcon from '@/components/KFIcon'; import KFModal from '@/components/KFModal'; import { CommonTabKeys } from '@/enums'; import { ResourceFileData } from '@/pages/Dataset/config'; import { to } from '@/utils/promise'; -import { Icon } from '@umijs/max'; import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd'; import { Input, Tabs, Tree } from 'antd'; import React, { useEffect, useMemo, useRef, useState } from 'react'; @@ -16,7 +16,7 @@ import { ResourceSelectorType, selectorTypeConfig } from './config'; import styles from './index.less'; export { ResourceSelectorType, selectorTypeConfig }; -// 选择数据集\模型\镜像的返回类型 +// 选择数据集、模型、镜像的返回类型 export type ResourceSelectorResponse = { activeTab: CommonTabKeys; // 是我的还是公开的 id: string; // 数据集\模型\镜像 id @@ -28,10 +28,18 @@ export type ResourceSelectorResponse = { }; export interface ResourceSelectorModalProps extends Omit { - type: ResourceSelectorType; // 数据集\模型\镜像 + /** 类型,数据集、模型、镜像 */ + type: ResourceSelectorType; + /** 默认展开的节点 */ defaultExpandedKeys?: React.Key[]; + /** 默认展开的节点 */ defaultCheckedKeys?: React.Key[]; + /** 默认激活的 Tab */ defaultActiveTab?: CommonTabKeys; + /** + * 确认回调 + * @param params 选择的数据 + */ onOk?: (params: ResourceSelectorResponse | undefined) => void; } @@ -61,6 +69,7 @@ const getIdAndVersion = (versionKey: string) => { }; }; +/** 选择数据集、模型、镜像的弹框,推荐使用函数的方式打开 */ function ResourceSelectorModal({ type, defaultExpandedKeys = [], @@ -267,7 +276,14 @@ function ResourceSelectorModal({ variant="borderless" value={searchText} onChange={(e) => setSearchText(e.target.value)} - prefix={} + prefix={ + + } + // prefix={} /> { - {/* */} - {/* */}
diff --git a/react-ui/src/components/SubAreaTitle/index.tsx b/react-ui/src/components/SubAreaTitle/index.tsx index cd07b206..4c94deee 100644 --- a/react-ui/src/components/SubAreaTitle/index.tsx +++ b/react-ui/src/components/SubAreaTitle/index.tsx @@ -8,13 +8,20 @@ import classNames from 'classnames'; import './index.less'; type SubAreaTitleProps = { + /** 标题 */ title: string; + /** 图片 */ image?: string; - style?: React.CSSProperties; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ + style?: React.CSSProperties; }; -function SubAreaTitle({ title, image, style, className }: SubAreaTitleProps) { +/** + * 表单或者详情页的分区标题 + */ +function SubAreaTitle({ title, image, className, style }: SubAreaTitleProps) { return (
{image && ( diff --git a/react-ui/src/global.less b/react-ui/src/global.less index fbbfa34d..9944c70e 100644 --- a/react-ui/src/global.less +++ b/react-ui/src/global.less @@ -5,7 +5,7 @@ body, height: 100%; margin: 0; padding: 0; - overflow-y: hidden; + overflow-y: visible; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; diff --git a/react-ui/src/pages/404.tsx b/react-ui/src/pages/404.tsx index dbacba53..d6f45451 100644 --- a/react-ui/src/pages/404.tsx +++ b/react-ui/src/pages/404.tsx @@ -12,7 +12,7 @@ const NoFoundPage = () => { content={'很抱歉,您访问的页面地址有误,\n或者该页面不存在。'} hasFooter={true} buttonTitle="返回首页" - onRefresh={() => navigate('/')} + onButtonClick={() => navigate('/')} > ); }; diff --git a/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx index bb30e064..5f207b7d 100644 --- a/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx +++ b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx @@ -1,3 +1,4 @@ +import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; import { AutoMLTaskType, autoMLEnsembleClassOptions, autoMLTaskTypeOptions } from '@/enums'; import { AutoMLData } from '@/pages/AutoML/types'; import { experimentStatusInfo } from '@/pages/Experiment/status'; @@ -8,7 +9,6 @@ import { formatBoolean, formatDataset, formatDate, formatEnum } from '@/utils/fo import { Flex } from 'antd'; import classNames from 'classnames'; import { useMemo } from 'react'; -import ConfigInfo, { type BasicInfoData } from '../ConfigInfo'; import styles from './index.less'; // 格式化优化方向 @@ -46,28 +46,23 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB { label: '实验名称', value: info.ml_name, - ellipsis: true, }, { label: '实验描述', value: info.ml_description, - ellipsis: true, }, { label: '创建人', value: info.create_by, - ellipsis: true, }, { label: '创建时间', value: info.create_time, - ellipsis: true, format: formatDate, }, { label: '更新时间', value: info.update_time, - ellipsis: true, format: formatDate, }, ]; @@ -81,18 +76,15 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB { label: '任务类型', value: info.task_type, - ellipsis: true, format: formatEnum(autoMLTaskTypeOptions), }, { label: '特征预处理算法', value: info.include_feature_preprocessor, - ellipsis: true, }, { label: '排除的特征预处理算法', value: info.exclude_feature_preprocessor, - ellipsis: true, }, { label: info.task_type === AutoMLTaskType.Regression ? '回归算法' : '分类算法', @@ -100,7 +92,6 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB info.task_type === AutoMLTaskType.Regression ? info.include_regressor : info.include_classifier, - ellipsis: true, }, { label: info.task_type === AutoMLTaskType.Regression ? '排除的回归算法' : '排除的分类算法', @@ -108,91 +99,73 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB info.task_type === AutoMLTaskType.Regression ? info.exclude_regressor : info.exclude_classifier, - ellipsis: true, }, { label: '集成方式', value: info.ensemble_class, - ellipsis: true, format: formatEnum(autoMLEnsembleClassOptions), }, { label: '集成模型数量', value: info.ensemble_size, - ellipsis: true, }, { label: '集成最佳模型数量', value: info.ensemble_nbest, - ellipsis: true, }, { label: '最大数量', value: info.max_models_on_disc, - ellipsis: true, }, { label: '内存限制(MB)', value: info.memory_limit, - ellipsis: true, }, { label: '单次时间限制(秒)', value: info.per_run_time_limit, - ellipsis: true, }, { label: '搜索时间限制(秒)', value: info.time_left_for_this_task, - ellipsis: true, }, { label: '重采样策略', value: info.resampling_strategy, - ellipsis: true, }, { label: '交叉验证折数', value: info.folds, - ellipsis: true, }, { label: '是否打乱', value: info.shuffle, - ellipsis: true, format: formatBoolean, }, { label: '训练集比率', value: info.train_size, - ellipsis: true, }, { label: '测试集比率', value: info.test_size, - ellipsis: true, }, { label: '计算指标', value: info.scoring_functions, - ellipsis: true, }, { label: '随机种子', value: info.seed, - ellipsis: true, }, - { label: '数据集', value: info.dataset, - ellipsis: true, format: formatDataset, }, { label: '预测目标列', value: info.target_columns, - ellipsis: true, }, ]; }, [info]); @@ -205,18 +178,15 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB { label: '指标名称', value: info.metric_name, - ellipsis: true, }, { label: '优化方向', value: info.greater_is_better, - ellipsis: true, format: formatOptimizeMode, }, { label: '指标权重', value: info.metrics, - ellipsis: true, format: formatMetricsWeight, }, ]; @@ -231,12 +201,10 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB { label: '启动时间', value: formatDate(runStatus.startedAt), - ellipsis: true, }, { label: '执行时长', value: elapsedTime(runStatus.startedAt, runStatus.finishedAt), - ellipsis: true, }, { label: '状态', @@ -259,7 +227,6 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB
), - ellipsis: true, }, ]; }, [runStatus]); @@ -269,7 +236,7 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB {isInstance && runStatus && ( @@ -277,18 +244,18 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB {!isInstance && ( )} - +
); } diff --git a/react-ui/src/pages/AutoML/components/ConfigInfo/index.less b/react-ui/src/pages/AutoML/components/ConfigInfo/index.less deleted file mode 100644 index 33fb3314..00000000 --- a/react-ui/src/pages/AutoML/components/ConfigInfo/index.less +++ /dev/null @@ -1,20 +0,0 @@ -.config-info { - :global { - .kf-basic-info { - width: 100%; - - &__item { - width: calc((100% - 80px) / 3); - &__label { - font-size: @font-size; - text-align: left; - text-align-last: left; - } - &__value { - min-width: 0; - font-size: @font-size; - } - } - } - } -} diff --git a/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx b/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx deleted file mode 100644 index 72596581..00000000 --- a/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; -import InfoGroup from '@/components/InfoGroup'; -import classNames from 'classnames'; -import styles from './index.less'; -export type { BasicInfoData }; - -type ConfigInfoProps = { - title: string; - data: BasicInfoData[]; - labelWidth: number; - className?: string; - style?: React.CSSProperties; - children?: React.ReactNode; -}; - -function ConfigInfo({ title, data, labelWidth, className, style, children }: ConfigInfoProps) { - return ( - -
- - {children} -
-
- ); -} - -export default ConfigInfo; diff --git a/react-ui/src/pages/CodeConfig/List/index.tsx b/react-ui/src/pages/CodeConfig/List/index.tsx index 2efef04c..0c484e54 100644 --- a/react-ui/src/pages/CodeConfig/List/index.tsx +++ b/react-ui/src/pages/CodeConfig/List/index.tsx @@ -197,7 +197,7 @@ function CodeConfigList() { title="暂无数据" content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} hasFooter={true} - onRefresh={getDataList} + onButtonClick={getDataList} /> )}
diff --git a/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx b/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx index 807270a8..10d7d9d2 100644 --- a/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx @@ -19,50 +19,41 @@ const getDatasetDatas = (data: DatasetData): BasicInfoData[] => [ { label: '数据集名称', value: data.name, - ellipsis: true, }, { label: '版本', value: data.version, - ellipsis: true, }, { label: '创建人', value: data.create_by, - ellipsis: true, }, { label: '更新时间', value: data.update_time, - ellipsis: true, }, { label: '数据来源', value: data.dataset_source, format: formatSource, - ellipsis: true, }, { label: '训练任务', value: data.train_task, format: formatTrainTask, - ellipsis: true, }, { label: '处理代码', value: data.processing_code, format: formatCodeConfig, - ellipsis: true, }, { label: '数据集分类', value: data.data_type, - ellipsis: true, }, { label: '研究方向', value: data.data_tag, - ellipsis: true, }, ]; diff --git a/react-ui/src/pages/Dataset/components/ResourceList/index.tsx b/react-ui/src/pages/Dataset/components/ResourceList/index.tsx index 6f2c7523..9577ad41 100644 --- a/react-ui/src/pages/Dataset/components/ResourceList/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceList/index.tsx @@ -226,7 +226,7 @@ function ResourceList( title="暂无数据" content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} hasFooter={true} - onRefresh={getDataList} + onButtonClick={getDataList} /> )}
diff --git a/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx b/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx index 90ad4d15..1a0b9a18 100644 --- a/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx +++ b/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx @@ -36,13 +36,13 @@ enum ComputingResourceType { const EditorRadioItems: KFRadioItem[] = [ { - key: ComputingResourceType.GPU, title: '英伟达GPU', + value: ComputingResourceType.GPU, icon: , }, { - key: ComputingResourceType.NPU, title: '昇腾NPU', + value: ComputingResourceType.NPU, icon: , }, ]; diff --git a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx index 62f33d6d..eb00288b 100644 --- a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx @@ -1,6 +1,6 @@ +import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; import { hyperParameterOptimizedMode } from '@/enums'; import { useComputingResource } from '@/hooks/resource'; -import ConfigInfo, { type BasicInfoData } from '@/pages/AutoML/components/ConfigInfo'; import { experimentStatusInfo } from '@/pages/Experiment/status'; import { schedulerAlgorithms, @@ -61,28 +61,23 @@ function HyperParameterBasic({ { label: '实验名称', value: info.name, - ellipsis: true, }, { label: '实验描述', value: info.description, - ellipsis: true, }, { label: '创建人', value: info.create_by, - ellipsis: true, }, { label: '创建时间', value: info.create_time, - ellipsis: true, format: formatDate, }, { label: '更新时间', value: info.update_time, - ellipsis: true, format: formatDate, }, ]; @@ -96,75 +91,62 @@ function HyperParameterBasic({ { label: '代码', value: info.code, - ellipsis: true, format: formatCodeConfig, }, { label: '主函数代码文件', value: info.main_py, - ellipsis: true, }, { label: '镜像', value: info.image, format: formatMirror, - ellipsis: true, }, { label: '数据集', value: info.dataset, - ellipsis: true, format: formatDataset, }, { label: '模型', value: info.model, - ellipsis: true, format: formatModel, }, { label: '总实验次数', value: info.num_samples, - ellipsis: true, }, { label: '搜索算法', value: info.search_alg, format: formatEnum(searchAlgorithms), - ellipsis: true, }, { label: '调度算法', value: info.scheduler, format: formatEnum(schedulerAlgorithms), - ellipsis: true, }, { label: '单次试验最大时间', value: info.max_t, - ellipsis: true, }, { label: '最小试验数', value: info.min_samples_required, - ellipsis: true, }, { label: '优化方向', value: info.mode, - ellipsis: true, format: formatOptimizeMode, }, { label: '指标', value: info.metric, - ellipsis: true, }, { label: '资源规格', value: info.resource, format: formatResource, - ellipsis: true, }, ]; }, [info]); @@ -216,7 +198,7 @@ function HyperParameterBasic({ {isInstance && runStatus && ( @@ -224,14 +206,14 @@ function HyperParameterBasic({ {!isInstance && ( )} diff --git a/react-ui/src/pages/Mirror/Create/index.tsx b/react-ui/src/pages/Mirror/Create/index.tsx index c4e89ce2..01a22031 100644 --- a/react-ui/src/pages/Mirror/Create/index.tsx +++ b/react-ui/src/pages/Mirror/Create/index.tsx @@ -30,13 +30,13 @@ type FormData = { const mirrorRadioItems: KFRadioItem[] = [ { - key: CommonTabKeys.Public, title: '基于公网镜像', + value: CommonTabKeys.Public, icon: , }, { - key: CommonTabKeys.Private, title: '本地上传', + value: CommonTabKeys.Private, icon: , }, ]; diff --git a/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx b/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx index 9cd4ebfb..b4cfb84a 100644 --- a/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx +++ b/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx @@ -401,7 +401,12 @@ function ServiceInfo() { image={require('@/assets/img/mirror-basic.png')} style={{ marginBottom: '26px', flex: 'none' }} > - + { } return ( - + {ModelDeployStatusCell(status)} ); @@ -51,84 +51,69 @@ function VersionBasicInfo({ info }: BasicInfoProps) { { label: '服务名称', value: info?.service_name, - ellipsis: true, }, { label: '版本名称', value: info?.version, - ellipsis: true, }, { label: '代码配置', value: info?.code_config, format: formatCodeConfig, - ellipsis: true, }, { label: '镜像', value: info?.image, - ellipsis: true, }, { label: '状态', value: info?.run_state, format: formatStatus, - ellipsis: true, }, { label: '模型', value: info?.model, format: formatModel, - ellipsis: true, }, { label: '资源规格', value: info?.resource, format: formatResource, - ellipsis: true, }, { label: '挂载路径', value: info?.mount_path, - ellipsis: true, }, { label: 'API URL', value: info?.url, - ellipsis: true, }, - { label: '副本数量', value: info?.replicas, - ellipsis: true, }, { label: '创建时间', value: info?.create_time, format: formatDate, - ellipsis: true, }, { label: '更新时间', value: info?.update_time, format: formatDate, - ellipsis: true, }, { label: '环境变量', value: info?.env_variables, format: formatEnvText, - ellipsis: true, }, { label: '描述', value: info?.description, - ellipsis: true, }, ]; - return ; + return ; } export default VersionBasicInfo; diff --git a/react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.less b/react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.less deleted file mode 100644 index cb77da6d..00000000 --- a/react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.less +++ /dev/null @@ -1,50 +0,0 @@ -.code-selector { - width: 100%; - height: 100%; - - &__search { - width: 100%; - } - - :global { - .ant-input-affix-wrapper { - border-radius: 23px !important; - .ant-input-prefix { - margin-inline-end: 12px; - } - .ant-input-suffix { - margin-inline-end: 12px; - } - .ant-input-clear-icon { - font-size: 16px; - } - } - - .ant-input-group-addon { - display: none; - } - - .ant-pagination { - .ant-select-single { - height: 32px !important; - } - } - } - - &__content { - display: flex; - flex-direction: row; - flex-wrap: wrap; - gap: 10px; - width: 100%; - max-height: 50vh; - margin-top: 24px; - margin-bottom: 30px; - overflow-x: hidden; - overflow-y: auto; - } - - &__empty { - padding-top: 40px; - } -} diff --git a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx index f2624a58..de041c72 100644 --- a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx +++ b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx @@ -1,3 +1,4 @@ +import CodeSelectorModal from '@/components/CodeSelectorModal'; import KFIcon from '@/components/KFIcon'; import ParameterInput, { requiredValidator } from '@/components/ParameterInput'; import ParameterSelect from '@/components/ParameterSelect'; @@ -21,7 +22,6 @@ import { INode } from '@antv/g6'; import { Button, Drawer, Form, Input, MenuProps, Select } from 'antd'; import { NamePath } from 'antd/es/form/interface'; import { forwardRef, useImperativeHandle, useState } from 'react'; -import CodeSelectorModal from '../CodeSelectorModal'; import PropsLabel from '../PropsLabel'; import styles from './index.less'; const { TextArea } = Input; diff --git a/react-ui/src/components/RobotFrame/index.less b/react-ui/src/pages/Workspace/components/RobotFrame/index.less similarity index 100% rename from react-ui/src/components/RobotFrame/index.less rename to react-ui/src/pages/Workspace/components/RobotFrame/index.less diff --git a/react-ui/src/components/RobotFrame/index.tsx b/react-ui/src/pages/Workspace/components/RobotFrame/index.tsx similarity index 100% rename from react-ui/src/components/RobotFrame/index.tsx rename to react-ui/src/pages/Workspace/components/RobotFrame/index.tsx diff --git a/react-ui/src/pages/Workspace/index.tsx b/react-ui/src/pages/Workspace/index.tsx index bdef4f83..691ca550 100644 --- a/react-ui/src/pages/Workspace/index.tsx +++ b/react-ui/src/pages/Workspace/index.tsx @@ -1,4 +1,3 @@ -import RobotFrame from '@/components/RobotFrame'; import { useDraggable } from '@/hooks/draggable'; import { getWorkspaceOverviewReq } from '@/services/workspace'; import { ExperimentInstance } from '@/types'; @@ -9,6 +8,7 @@ import AssetsManagement from './components/AssetsManagement'; import ExperimentChart, { type ExperimentStatistics } from './components/ExperimentChart'; import ExperitableTable from './components/ExperimentTable'; import QuickStart from './components/QuickStart'; +import RobotFrame from './components/RobotFrame'; import TotalStatistics from './components/TotalStatistics'; import UserSpace from './components/UserSpace'; import WorkspaceIntro from './components/WorkspaceIntro'; diff --git a/react-ui/src/pages/missingPage.jsx b/react-ui/src/pages/missingPage.jsx index e8e034a4..9b3e0323 100644 --- a/react-ui/src/pages/missingPage.jsx +++ b/react-ui/src/pages/missingPage.jsx @@ -12,7 +12,7 @@ const MissingPage = () => { content={'很抱歉,您访问的正在开发中,\n请耐心等待。'} hasFooter={true} buttonTitle="返回首页" - onRefresh={() => navigate('/')} + onButtonClick={() => navigate('/')} > ); }; diff --git a/react-ui/src/stories/BasicInfo.stories.tsx b/react-ui/src/stories/BasicInfo.stories.tsx new file mode 100644 index 00000000..80a0337c --- /dev/null +++ b/react-ui/src/stories/BasicInfo.stories.tsx @@ -0,0 +1,102 @@ +import BasicInfo from '@/components/BasicInfo'; +import { formatDate } from '@/utils/date'; +import { formatList } from '@/utils/format'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from 'antd'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/BasicInfo', + component: BasicInfo, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +/** 一行两列 */ +export const Primary: Story = { + args: { + datas: [ + { label: '服务名称', value: '手写体识别' }, + { + label: '无数据', + value: '', + }, + { + label: '外部链接', + value: 'https://www.baidu.com/', + format: (value: string) => { + return { + value: '百度', + url: value, + }; + }, + }, + { + label: '内部链接', + value: 'https://www.baidu.com/', + format: () => { + return { + value: '实验', + link: '/pipeline/experiment/instance/1/1', + }; + }, + }, + { label: '日期', value: new Date(), format: formatDate }, + { label: '数组', value: ['a', 'b', 'c'], format: formatList }, + { + label: '带省略号', + value: '这是一个很长的字符串这是一个很长的字符串这是一个很长的字符串这是一个很长的字符串', + }, + { + label: '多行', + value: [ + { + label: '服务名称', + value: '手写体识别', + }, + { + label: '服务名称', + value: '人脸识别', + }, + ], + format: (value: any) => + value.map((item: any) => ({ + value: item.label + ':' + item.value, + })), + }, + { + label: '自定义组件', + value: ( + + ), + }, + ], + labelWidth: 80, + labelAlign: 'justify', + }, +}; + +/** 一行三列 */ +export const ThreeColumn: Story = { + args: { + ...Primary.args, + labelAlign: 'start', + threeColumns: true, + }, +}; diff --git a/react-ui/src/stories/BasicTableInfo.stories.tsx b/react-ui/src/stories/BasicTableInfo.stories.tsx new file mode 100644 index 00000000..82581f6a --- /dev/null +++ b/react-ui/src/stories/BasicTableInfo.stories.tsx @@ -0,0 +1,32 @@ +import BasicTableInfo from '@/components/BasicTableInfo'; +import type { Meta, StoryObj } from '@storybook/react'; +import * as BasicInfoStories from './BasicInfo.stories'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/BasicTableInfo', + component: BasicTableInfo, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + ...BasicInfoStories.Primary.args, + labelWidth: 100, + }, +}; diff --git a/react-ui/src/stories/CodeSelect.stories.tsx b/react-ui/src/stories/CodeSelect.stories.tsx new file mode 100644 index 00000000..8be5b620 --- /dev/null +++ b/react-ui/src/stories/CodeSelect.stories.tsx @@ -0,0 +1,56 @@ +import CodeSelect from '@/components/CodeSelect'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { Col, Form, Row } from 'antd'; +import { http, HttpResponse } from 'msw'; +import { codeListData } from './mockData'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/CodeSelect', + component: CodeSelect, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + msw: { + handlers: [ + http.get('/api/mmp/codeConfig', () => { + return HttpResponse.json(codeListData); + }), + ], + }, + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onChange: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + render: ({ onChange }) => { + return ( +
+ + + + + + + +
+ ); + }, +}; diff --git a/react-ui/src/stories/CodeSelectorModal.stories.tsx b/react-ui/src/stories/CodeSelectorModal.stories.tsx new file mode 100644 index 00000000..59026ec6 --- /dev/null +++ b/react-ui/src/stories/CodeSelectorModal.stories.tsx @@ -0,0 +1,90 @@ +import CodeSelectorModal from '@/components/CodeSelectorModal'; +import { openAntdModal } from '@/utils/modal'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { Button } from 'antd'; +import { http, HttpResponse } from 'msw'; +import { codeListData } from './mockData'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/CodeSelectorModal', + component: CodeSelectorModal, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + msw: { + handlers: [ + http.get('/api/mmp/codeConfig', () => { + return HttpResponse.json(codeListData); + }), + ], + }, + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + open: { + description: '对话框是否可见', + }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onCancel: fn(), onOk: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + open: false, + }, + render: function Render({ onOk, onCancel, ...args }) { + const [{ open }, updateArgs] = useArgs(); + function onClick() { + updateArgs({ open: true }); + } + function handleOk(res: any) { + updateArgs({ open: false }); + onOk?.(res); + } + + function handleCancel() { + updateArgs({ open: false }); + onCancel?.(); + } + + return ( + <> + + + + ); + }, +}; + +/** 通过 `openAntdModal` 函数打开 */ +export const OpenByFunction: Story = { + render: function Render(args) { + const handleClick = () => { + const { close } = openAntdModal(CodeSelectorModal, { + onOk: (res) => { + const { onOk } = args; + onOk?.(res); + close(); + }, + }); + }; + return ( + + ); + }, +}; diff --git a/react-ui/src/stories/Config.stories.tsx b/react-ui/src/stories/Config.stories.tsx new file mode 100644 index 00000000..0287d718 --- /dev/null +++ b/react-ui/src/stories/Config.stories.tsx @@ -0,0 +1,37 @@ +import ConfigInfo from '@/components/ConfigInfo'; +import type { Meta, StoryObj } from '@storybook/react'; +import * as BasicInfoStories from './BasicInfo.stories'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/ConfigInfo', + component: ConfigInfo, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + title: '基本信息', + datas: BasicInfoStories.Primary.args.datas, + labelAlign: 'start', + labelEllipsis: true, + threeColumns: true, + labelWidth: 80, + children:
I am a child element
, + }, +}; diff --git a/react-ui/src/stories/FullScreenFrame.stories.tsx b/react-ui/src/stories/FullScreenFrame.stories.tsx new file mode 100644 index 00000000..fa334ed1 --- /dev/null +++ b/react-ui/src/stories/FullScreenFrame.stories.tsx @@ -0,0 +1,32 @@ +import FullScreenFrame from '@/components/FullScreenFrame'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/FullScreenFrame', + component: FullScreenFrame, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onLoad: fn(), onError: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + url: 'https://www.hao123.com/', + style: { height: '500px' }, + }, +}; diff --git a/react-ui/src/stories/IFramePage.stories.tsx b/react-ui/src/stories/IFramePage.stories.tsx new file mode 100644 index 00000000..3d1714e8 --- /dev/null +++ b/react-ui/src/stories/IFramePage.stories.tsx @@ -0,0 +1,31 @@ +import IFramePage, { IframePageType } from '@/components/IFramePage'; +import type { Meta, StoryObj } from '@storybook/react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/IFramePage', + component: IFramePage, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + type: { control: 'select', options: Object.values(IframePageType) }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + type: IframePageType.GitLink, + style: { height: '500px' }, + }, +}; diff --git a/react-ui/src/stories/InfoGroup.stories.tsx b/react-ui/src/stories/InfoGroup.stories.tsx new file mode 100644 index 00000000..f3a6def8 --- /dev/null +++ b/react-ui/src/stories/InfoGroup.stories.tsx @@ -0,0 +1,31 @@ +import InfoGroup from '@/components/InfoGroup'; +import type { Meta, StoryObj } from '@storybook/react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/InfoGroup', + component: InfoGroup, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + title: '基本信息', + children:
I am a child element
, + }, +}; diff --git a/react-ui/src/stories/KFEmpty.stories.tsx b/react-ui/src/stories/KFEmpty.stories.tsx new file mode 100644 index 00000000..f26de4ee --- /dev/null +++ b/react-ui/src/stories/KFEmpty.stories.tsx @@ -0,0 +1,53 @@ +import KFEmpty, { EmptyType } from '@/components/KFEmpty'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/KFEmpty', + component: KFEmpty, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + type: { control: 'select', options: Object.values(EmptyType) }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onButtonClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Developing: Story = { + args: { + type: EmptyType.Developing, + title: '敬请期待~', + content: '很抱歉,您访问的正在开发中,\n请耐心等待。', + hasFooter: true, + buttonTitle: '返回首页', + }, +}; + +export const NoData: Story = { + args: { + type: EmptyType.NoData, + title: '暂无数据', + content: '很抱歉,没有搜索到您想要的内容\n建议刷新试试', + hasFooter: true, + }, +}; + +export const NotFound: Story = { + args: { + type: EmptyType.NotFound, + title: '404', + content: '很抱歉,您访问的页面地址有误,\n或者该页面不存在。', + hasFooter: true, + buttonTitle: '返回首页', + }, +}; diff --git a/react-ui/src/stories/KFIcon.stories.tsx b/react-ui/src/stories/KFIcon.stories.tsx new file mode 100644 index 00000000..44cb274e --- /dev/null +++ b/react-ui/src/stories/KFIcon.stories.tsx @@ -0,0 +1,41 @@ +import KFIcon from '@/components/KFIcon'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from 'antd'; +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/KFIcon', + component: KFIcon, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: {}, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + type: 'icon-xiazai', + }, +}; + +export const InButton: Story = { + args: { + type: 'icon-xiazai', + }, + render: function Render(args) { + return ( + + ); + }, +}; diff --git a/react-ui/src/stories/KFModal.stories.tsx b/react-ui/src/stories/KFModal.stories.tsx new file mode 100644 index 00000000..763aaf31 --- /dev/null +++ b/react-ui/src/stories/KFModal.stories.tsx @@ -0,0 +1,93 @@ +import CreateExperiment from '@/assets/img/create-experiment.png'; +import KFModal from '@/components/KFModal'; +import { openAntdModal } from '@/utils/modal'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { Button } from 'antd'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/KFModal', + component: KFModal, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + title: { + description: '标题', + }, + open: { + description: '对话框是否可见', + }, + children: { + description: '子元素', + type: 'string', + }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onCancel: fn(), onOk: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + title: '创建实验', + image: CreateExperiment, + open: false, + children: '这是一个模态框', + }, + render: function Render({ onOk, onCancel, ...args }) { + const [{ open }, updateArgs] = useArgs(); + function onClick() { + updateArgs({ open: true }); + } + function handleOk() { + updateArgs({ open: false }); + onOk?.(); + } + + function handleCancel() { + updateArgs({ open: false }); + onCancel?.(); + } + + return ( + <> + + + + ); + }, +}; + +/** 通过 `openAntdModal` 函数打开 */ +export const OpenByFunction: Story = { + render: function Render() { + const handleClick = () => { + const { close } = openAntdModal(KFModal, { + title: '创建实验', + image: CreateExperiment, + children: '这是一个模态框', + onOk: () => { + close(); + }, + }); + }; + return ( + + ); + }, +}; diff --git a/react-ui/src/stories/KFRadio.stories.tsx b/react-ui/src/stories/KFRadio.stories.tsx new file mode 100644 index 00000000..c48c16e9 --- /dev/null +++ b/react-ui/src/stories/KFRadio.stories.tsx @@ -0,0 +1,52 @@ +import KFIcon from '@/components/KFIcon'; +import KFRadio from '@/components/KFRadio'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/KFRadio', + component: KFRadio, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + items: [ + { + title: '英伟达GPU', + value: 'GPU', + icon: , + }, + { + title: '昇腾NPU', + value: 'NPU', + icon: , + }, + ], + value: 'GPU', + }, + render: function Render(args) { + const [{ value }, updateArgs] = useArgs(); + function onChange(value: string) { + updateArgs({ value: value }); + } + + return ; + }, +}; diff --git a/react-ui/src/stories/KFSpin.stories.tsx b/react-ui/src/stories/KFSpin.stories.tsx new file mode 100644 index 00000000..7f3185d2 --- /dev/null +++ b/react-ui/src/stories/KFSpin.stories.tsx @@ -0,0 +1,35 @@ +import KFSpin from '@/components/KFSpin'; +import type { Meta, StoryObj } from '@storybook/react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/KFSpin', + component: KFSpin, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: {}, +}; diff --git a/react-ui/src/stories/MenuIconSelector.stories.tsx b/react-ui/src/stories/MenuIconSelector.stories.tsx new file mode 100644 index 00000000..f02fb77c --- /dev/null +++ b/react-ui/src/stories/MenuIconSelector.stories.tsx @@ -0,0 +1,67 @@ +import MenuIconSelector from '@/components/MenuIconSelector'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { Button } from 'antd'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/MenuIconSelector', + component: MenuIconSelector, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + open: { + control: 'boolean', + description: '对话框是否可见', + }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onCancel: fn(), onOk: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + selectedIcon: 'manual-icon', + open: false, + }, + render: function Render({ onOk, onCancel, ...args }) { + const [{ open, selectedIcon }, updateArgs] = useArgs(); + function onClick() { + updateArgs({ open: true }); + } + function handleOk(value: string) { + updateArgs({ selectedIcon: value, open: false }); + onOk?.(value); + } + + function handleCancel() { + updateArgs({ open: false }); + onCancel?.(); + } + + return ( + <> + + + + ); + }, +}; diff --git a/react-ui/src/stories/PageTitle.stories.tsx b/react-ui/src/stories/PageTitle.stories.tsx new file mode 100644 index 00000000..216591b1 --- /dev/null +++ b/react-ui/src/stories/PageTitle.stories.tsx @@ -0,0 +1,30 @@ +import PageTitle from '@/components/PageTitle'; +import type { Meta, StoryObj } from '@storybook/react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/PageTitle', + component: PageTitle, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + title: '数据集列表', + }, +}; diff --git a/react-ui/src/stories/ParameterInput.stories.tsx b/react-ui/src/stories/ParameterInput.stories.tsx new file mode 100644 index 00000000..9d9525e2 --- /dev/null +++ b/react-ui/src/stories/ParameterInput.stories.tsx @@ -0,0 +1,108 @@ +import ParameterInput, { ParameterInputValue } from '@/components/ParameterInput'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from 'antd'; +import { useState } from 'react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/ParameterInput', + component: ParameterInput, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Input: Story = { + args: { + placeholder: '请输入工作目录', + style: { width: 300 }, + canInput: true, + textArea: false, + allowClear: true, + size: 'large', + }, +}; + +export const Select: Story = { + args: { + placeholder: '请输入工作目录', + style: { width: 300 }, + value: 'storybook', + canInput: false, + size: 'large', + }, +}; + +export const SelectWithObjctValue: Story = { + args: { + placeholder: '请输入工作目录', + style: { width: 300 }, + value: { + value: 'storybook', + showValue: 'storybook', + fromSelect: true, + }, + canInput: true, + size: 'large', + }, +}; + +export const Disabled: Story = { + args: { + placeholder: '请输入工作目录', + style: { width: 300 }, + value: { + value: 'storybook', + showValue: 'storybook', + fromSelect: true, + }, + canInput: true, + size: 'large', + disabled: true, + }, +}; + +export const Application: Story = { + args: { + placeholder: '请输入工作目录', + style: { width: 300 }, + canInput: true, + size: 'large', + }, + render: function Render(args) { + const [value, setValue] = useState(''); + + const onClick = () => { + setValue({ + value: 'storybook', + showValue: 'storybook', + fromSelect: true, + }); + }; + return ( + <> + setValue(value)} + > + + + ); + }, +}; diff --git a/react-ui/src/stories/ResourceSelect.stories.tsx b/react-ui/src/stories/ResourceSelect.stories.tsx new file mode 100644 index 00000000..b9db1633 --- /dev/null +++ b/react-ui/src/stories/ResourceSelect.stories.tsx @@ -0,0 +1,135 @@ +import ResourceSelect, { + requiredValidator, + ResourceSelectorType, +} from '@/components/ResourceSelect'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { Col, Form, Row } from 'antd'; +import { http, HttpResponse } from 'msw'; +import { + datasetDetailData, + datasetListData, + datasetVersionData, + mirrorListData, + mirrorVerionData, + modelDetailData, + modelListData, + modelVersionData, +} from './mockData'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/ResourceSelect', + component: ResourceSelect, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + msw: { + handlers: [ + http.get('/api/mmp/newdataset/queryDatasets', () => { + return HttpResponse.json(datasetListData); + }), + http.get('/api/mmp/newdataset/getVersionList', () => { + return HttpResponse.json(datasetVersionData); + }), + http.get('/api/mmp/newdataset/getDatasetDetail', () => { + return HttpResponse.json(datasetDetailData); + }), + http.get('/api/mmp/newmodel/queryModels', () => { + return HttpResponse.json(modelListData); + }), + http.get('/api/mmp/newmodel/getVersionList', () => { + return HttpResponse.json(modelVersionData); + }), + http.get('/api/mmp/newmodel/getModelDetail', () => { + return HttpResponse.json(modelDetailData); + }), + http.get('/api/mmp/image', () => { + return HttpResponse.json(mirrorListData); + }), + http.get('/api/mmp/imageVersion', () => { + return HttpResponse.json(mirrorVerionData); + }), + ], + }, + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onChange: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + type: ResourceSelectorType.Dataset, + }, + render: ({ onChange }) => { + return ( +
+ + + + + + + + + + + + + + + + + + + + + +
+ ); + }, +}; diff --git a/react-ui/src/stories/ResourceSelectorModal.mdx b/react-ui/src/stories/ResourceSelectorModal.mdx new file mode 100644 index 00000000..28702123 --- /dev/null +++ b/react-ui/src/stories/ResourceSelectorModal.mdx @@ -0,0 +1,23 @@ +import { Meta, Canvas } from '@storybook/blocks'; +import * as ResourceSelectorModalStories from "./ResourceSelectorModal.stories" + + + +# Usage + +推荐通过 `openAntdModal` 函数打开 `ResourceSelectorModal`,打开 -> 处理 -> 关闭,整套代码在同一个地方 + +```ts +const handleClick = () => { + const { close } = openAntdModal(ResourceSelectorModal, { + type: ResourceSelectorType.Dataset, + onOk: (res) => { + // 处理逻辑 + close(); + }, +}); +``` + + + + diff --git a/react-ui/src/stories/ResourceSelectorModal.stories.tsx b/react-ui/src/stories/ResourceSelectorModal.stories.tsx new file mode 100644 index 00000000..9e72efd2 --- /dev/null +++ b/react-ui/src/stories/ResourceSelectorModal.stories.tsx @@ -0,0 +1,217 @@ +import ResourceSelectorModal, { ResourceSelectorType } from '@/components/ResourceSelectorModal'; +import { openAntdModal } from '@/utils/modal'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { Button } from 'antd'; +import { http, HttpResponse } from 'msw'; +import { + datasetDetailData, + datasetListData, + datasetVersionData, + mirrorListData, + mirrorVerionData, + modelDetailData, + modelListData, + modelVersionData, +} from './mockData'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/ResourceSelectorModal', + component: ResourceSelectorModal, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + open: { + description: '对话框是否可见', + }, + type: { + control: 'select', + options: Object.values(ResourceSelectorType), + }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onCancel: fn(), onOk: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Dataset: Story = { + args: { + type: ResourceSelectorType.Dataset, + open: false, + }, + parameters: { + msw: { + handlers: [ + http.get('/api/mmp/newdataset/queryDatasets', () => { + return HttpResponse.json(datasetListData); + }), + http.get('/api/mmp/newdataset/getVersionList', () => { + return HttpResponse.json(datasetVersionData); + }), + http.get('/api/mmp/newdataset/getDatasetDetail', () => { + return HttpResponse.json(datasetDetailData); + }), + ], + }, + }, + render: function Render({ onOk, onCancel, ...args }) { + const [{ open }, updateArgs] = useArgs(); + function onClick() { + updateArgs({ open: true }); + } + function handleOk(res: any) { + updateArgs({ open: false }); + onOk?.(res); + } + + function handleCancel() { + updateArgs({ open: false }); + onCancel?.(); + } + + return ( + <> + + + + ); + }, +}; + +export const Model: Story = { + args: { + type: ResourceSelectorType.Model, + open: false, + }, + parameters: { + msw: { + handlers: [ + http.get('/api/mmp/newmodel/queryModels', () => { + return HttpResponse.json(modelListData); + }), + http.get('/api/mmp/newmodel/getVersionList', () => { + return HttpResponse.json(modelVersionData); + }), + http.get('/api/mmp/newmodel/getModelDetail', () => { + return HttpResponse.json(modelDetailData); + }), + ], + }, + }, + render: function Render({ onOk, onCancel, ...args }) { + const [{ open }, updateArgs] = useArgs(); + function onClick() { + updateArgs({ open: true }); + } + function handleOk(res: any) { + updateArgs({ open: false }); + onOk?.(res); + } + + function handleCancel() { + updateArgs({ open: false }); + onCancel?.(); + } + + return ( + <> + + + + ); + }, +}; + +export const Mirror: Story = { + args: { + type: ResourceSelectorType.Mirror, + open: false, + }, + parameters: { + msw: { + handlers: [ + http.get('/api/mmp/image', () => { + return HttpResponse.json(mirrorListData); + }), + http.get('/api/mmp/imageVersion', () => { + return HttpResponse.json(mirrorVerionData); + }), + ], + }, + }, + render: function Render({ onOk, onCancel, ...args }) { + const [{ open }, updateArgs] = useArgs(); + function onClick() { + updateArgs({ open: true }); + } + function handleOk(res: any) { + updateArgs({ open: false }); + onOk?.(res); + } + + function handleCancel() { + updateArgs({ open: false }); + onCancel?.(); + } + + return ( + <> + + + + ); + }, +}; + +/** 通过 `openAntdModal` 函数打开 */ +export const OpenByFunction: Story = { + args: { + type: ResourceSelectorType.Mirror, + }, + parameters: { + msw: { + handlers: [ + http.get('/api/mmp/image', () => { + return HttpResponse.json(mirrorListData); + }), + http.get('/api/mmp/imageVersion', () => { + return HttpResponse.json(mirrorVerionData); + }), + ], + }, + }, + render: function Render(args) { + const handleClick = () => { + const { close } = openAntdModal(ResourceSelectorModal, { + type: args.type, + onOk: (res) => { + const { onOk } = args; + onOk?.(res); + close(); + }, + }); + }; + return ( + + ); + }, +}; diff --git a/react-ui/src/stories/SubAreaTitle.stories.tsx b/react-ui/src/stories/SubAreaTitle.stories.tsx new file mode 100644 index 00000000..9fad7d71 --- /dev/null +++ b/react-ui/src/stories/SubAreaTitle.stories.tsx @@ -0,0 +1,32 @@ +import MirrorBasic from '@/assets/img/mirror-basic.png'; +import SubAreaTitle from '@/components/SubAreaTitle'; +import type { Meta, StoryObj } from '@storybook/react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/SubAreaTitle', + component: SubAreaTitle, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + title: '基本信息', + image: MirrorBasic, + }, +}; diff --git a/react-ui/src/stories/docs/Less.mdx b/react-ui/src/stories/docs/Less.mdx new file mode 100644 index 00000000..1b3a6632 --- /dev/null +++ b/react-ui/src/stories/docs/Less.mdx @@ -0,0 +1,199 @@ +import { Meta, Controls } from '@storybook/blocks'; + + + +# Less 规范 + +## Theme + +### 自定义主题 + +`src/styles/theme.less` 定义了 UI 主题颜色变量、Less 函数、Less 混合。在开发过程中使用这个文件的定义的变量、函数以及混合,通过 UmiJS 的配置,我们在 Less 文件不需要收到导入这个文件。 + +颜色变量还可以在 `js/ts/jsx/tsx` 里使用 + +```js +import themes from "@/styles/theme.less" + +const primaryColor = themes['primaryColor']; // #1664ff +``` + +### Ant Design 主题覆盖 + +Ant Design 可以[定制主题](https://ant-design.antgroup.com/docs/react/customize-theme-cn),Ant Design 是通过 [ConfigProvider](https://ant-design.antgroup.com/components/config-provider-cn) 组件进行主题定制,而 UmiJS 可以在[配置文件](https://umijs.org/docs/max/antd#%E6%9E%84%E5%BB%BA%E6%97%B6%E9%85%8D%E7%BD%AE)或者 [`app.ts`](https://umijs.org/docs/max/antd#%E8%BF%90%E8%A1%8C%E6%97%B6%E9%85%8D%E7%BD%AE) 里进行配置。我选择在 [`app.ts`](https://umijs.org/docs/max/antd#%E8%BF%90%E8%A1%8C%E6%97%B6%E9%85%8D%E7%BD%AE) 里进行配置,因为这里可以使用主题颜色变量。 + +```tsx +// 主题修改 +export const antd: RuntimeAntdConfig = (memo) => { + memo.theme ??= {}; + memo.theme.token = { + colorPrimary: themes['primaryColor'], + colorSuccess: themes['successColor'], + colorError: themes['errorColor'], + colorWarning: themes['warningColor'], + colorLink: themes['primaryColor'], + colorText: themes['textColor'], + controlHeightLG: 46, + }; + memo.theme.components ??= {}; + memo.theme.components.Tabs = {}; + memo.theme.components.Button = { + defaultBg: 'rgba(22, 100, 255, 0.06)', + defaultBorderColor: 'rgba(22, 100, 255, 0.11)', + defaultColor: themes['textColor'], + defaultHoverBg: 'rgba(22, 100, 255, 0.06)', + defaultHoverBorderColor: 'rgba(22, 100, 255, 0.5)', + defaultHoverColor: '#3F7FFF', + defaultActiveBg: 'rgba(22, 100, 255, 0.12)', + defaultActiveBorderColor: 'rgba(22, 100, 255, 0.75)', + defaultActiveColor: themes['primaryColor'], + contentFontSize: parseInt(themes['fontSize']), + }; + memo.theme.components.Input = { + inputFontSize: parseInt(themes['fontSizeInput']), + inputFontSizeLG: parseInt(themes['fontSizeInputLg']), + paddingBlockLG: 10, + }; + memo.theme.components.Select = { + singleItemHeightLG: 46, + optionSelectedColor: themes['primaryColor'], + }; + memo.theme.components.Table = { + headerBg: 'rgba(242, 244, 247, 0.36)', + headerBorderRadius: 4, + rowSelectedBg: 'rgba(22, 100, 255, 0.05)', + }; + memo.theme.components.Tabs = { + titleFontSize: 16, + }; + memo.theme.components.Form = { + labelColor: 'rgba(29, 29, 32, 0.8);', + }; + memo.theme.components.Breadcrumb = { + iconFontSize: parseInt(themes['fontSize']), + linkColor: 'rgba(29, 29, 32, 0.7)', + separatorColor: 'rgba(29, 29, 32, 0.7)', + }; + + memo.theme.cssVar = true; + + memo.appConfig = { + message: { + // 配置 message 最大显示数,超过限制时,最早的消息会被自动关闭 + maxCount: 3, + }, + }; + + return memo; +}; +``` + +覆盖 Ant Design 的默认样式,优先选择这种方式,如果没有相应的变量,才覆盖 Ant Design 的样式,在 `src/overrides.less` 文件里覆盖,请查看 UmiJS 关于[`global.less`](https://umijs.org/docs/guides/directory-structure#globalcsslesssassscss) 与 [`overrides.less`](https://umijs.org/docs/guides/directory-structure#overridescsslesssassscss) 的说明。 + +## BEM + +类名遵循 [BEM - Block, Element, Modifier](https://getbem.com/) 规范 + +### Block + +有意义的独立实体,Block 的类名由小写字母、数字和横线组成,比如 `model`、`form`、`paramneter-input` + +### Element + +块的一部分,Element 的类名由 `Block 的类名` + `双下划线(__)` + `Element 的名称` 组成,比如 `model__title`、`form__input`、`paramneter-input__content` + +### Modifier + +块或元素的变种,Modifier 的类名由 `Block 的类名` 或者 `Element 的类名` + `双横线(--)` + `Modifier 的名称` 组成,比如 `button--active`、`form--large` + +举个 🌰 + +```tsx +// @/components/CodeConfigItem/index.tsx + +import classNames from 'classnames'; +import styles from './index.less'; + +function CodeConfigItem({ item, onClick }: CodeConfigItemProps) { + return ( +
+ + + {item.code_repo_name} + +
+ {item.code_repo_vis === AvailableRange.Public ? '公开' : '私有'} +
+
+ + {item.git_url} + +
{item.git_branch}
+
+ ); +} + +``` + +### 一些建议 + +如果你陷入嵌套地狱,比如 + +```tsx +function Component() { + return ( +
+
+
+
+
+ // 等等 +
+
+
+
+
+ ) +} +``` + +说明你需要拆分组件了 + +```tsx +function Component1() { + return ( +
+
+
+
+ ) +} + +function Component() { + return ( +
+
+ +
+
+ ) +} +``` + +既减少了类名的嵌套,又减少了HTML的嵌套,使代码逻辑更加清晰,易于理解与维护 + + + diff --git a/react-ui/src/stories/example/Button.stories.ts b/react-ui/src/stories/example/Button.stories.ts new file mode 100644 index 00000000..2a05e01b --- /dev/null +++ b/react-ui/src/stories/example/Button.stories.ts @@ -0,0 +1,53 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; + +import { Button } from './Button'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Example/Button', + component: Button, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + primary: true, + label: 'Button', + }, +}; + +export const Secondary: Story = { + args: { + label: 'Button', + }, +}; + +export const Large: Story = { + args: { + size: 'large', + label: 'Button', + }, +}; + +export const Small: Story = { + args: { + size: 'small', + label: 'Button', + }, +}; diff --git a/react-ui/src/stories/example/Button.tsx b/react-ui/src/stories/example/Button.tsx new file mode 100644 index 00000000..f35dafdc --- /dev/null +++ b/react-ui/src/stories/example/Button.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import './button.css'; + +export interface ButtonProps { + /** Is this the principal call to action on the page? */ + primary?: boolean; + /** What background color to use */ + backgroundColor?: string; + /** How large should the button be? */ + size?: 'small' | 'medium' | 'large'; + /** Button contents */ + label: string; + /** Optional click handler */ + onClick?: () => void; +} + +/** Primary UI component for user interaction */ +export const Button = ({ + primary = false, + size = 'medium', + backgroundColor, + label, + ...props +}: ButtonProps) => { + const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; + return ( + + ); +}; diff --git a/react-ui/src/stories/example/Configure.mdx b/react-ui/src/stories/example/Configure.mdx new file mode 100644 index 00000000..6a537304 --- /dev/null +++ b/react-ui/src/stories/example/Configure.mdx @@ -0,0 +1,364 @@ +import { Meta } from "@storybook/blocks"; + +import Github from "./assets/github.svg"; +import Discord from "./assets/discord.svg"; +import Youtube from "./assets/youtube.svg"; +import Tutorials from "./assets/tutorials.svg"; +import Styling from "./assets/styling.png"; +import Context from "./assets/context.png"; +import Assets from "./assets/assets.png"; +import Docs from "./assets/docs.png"; +import Share from "./assets/share.png"; +import FigmaPlugin from "./assets/figma-plugin.png"; +import Testing from "./assets/testing.png"; +import Accessibility from "./assets/accessibility.png"; +import Theming from "./assets/theming.png"; +import AddonLibrary from "./assets/addon-library.png"; + +export const RightArrow = () => + + + + + +
+
+ # Configure your project + + Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community. +
+
+
+ A wall of logos representing different styling technologies +

Add styling and CSS

+

Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.

+ Learn more +
+
+ An abstraction representing the composition of data for a component +

Provide context and mocking

+

Often when a story doesn't render, it's because your component is expecting a specific environment or context (like a theme provider) to be available.

+ Learn more +
+
+ A representation of typography and image assets +
+

Load assets and resources

+

To link static files (like fonts) to your projects and stories, use the + `staticDirs` configuration option to specify folders to load when + starting Storybook.

+ Learn more +
+
+
+
+
+
+ # Do more with Storybook + + Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs. +
+ +
+
+
+ A screenshot showing the autodocs tag being set, pointing a docs page being generated +

Autodocs

+

Auto-generate living, + interactive reference documentation from your components and stories.

+ Learn more +
+
+ A browser window showing a Storybook being published to a chromatic.com URL +

Publish to Chromatic

+

Publish your Storybook to review and collaborate with your entire team.

+ Learn more +
+
+ Windows showing the Storybook plugin in Figma +

Figma Plugin

+

Embed your stories into Figma to cross-reference the design and live + implementation in one place.

+ Learn more +
+
+ Screenshot of tests passing and failing +

Testing

+

Use stories to test a component in all its variations, no matter how + complex.

+ Learn more +
+
+ Screenshot of accessibility tests passing and failing +

Accessibility

+

Automatically test your components for a11y issues as you develop.

+ Learn more +
+
+ Screenshot of Storybook in light and dark mode +

Theming

+

Theme Storybook's UI to personalize it to your project.

+ Learn more +
+
+
+
+
+
+

Addons

+

Integrate your tools with Storybook to connect workflows.

+ Discover all addons +
+
+ Integrate your tools with Storybook to connect workflows. +
+
+ +
+
+ Github logo + Join our contributors building the future of UI development. + + Star on GitHub +
+
+ Discord logo +
+ Get support and chat with frontend developers. + + Join Discord server +
+
+
+ Youtube logo +
+ Watch tutorials, feature previews and interviews. + + Watch on YouTube +
+
+
+ A book +

Follow guided walkthroughs on for key workflows.

+ + Discover tutorials +
+
+ + diff --git a/react-ui/src/stories/example/Header.stories.ts b/react-ui/src/stories/example/Header.stories.ts new file mode 100644 index 00000000..80c71d0f --- /dev/null +++ b/react-ui/src/stories/example/Header.stories.ts @@ -0,0 +1,33 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; + +import { Header } from './Header'; + +const meta = { + title: 'Example/Header', + component: Header, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout + layout: 'fullscreen', + }, + args: { + onLogin: fn(), + onLogout: fn(), + onCreateAccount: fn(), + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const LoggedIn: Story = { + args: { + user: { + name: 'Jane Doe', + }, + }, +}; + +export const LoggedOut: Story = {}; diff --git a/react-ui/src/stories/example/Header.tsx b/react-ui/src/stories/example/Header.tsx new file mode 100644 index 00000000..1bf981a4 --- /dev/null +++ b/react-ui/src/stories/example/Header.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import { Button } from './Button'; +import './header.css'; + +type User = { + name: string; +}; + +export interface HeaderProps { + user?: User; + onLogin?: () => void; + onLogout?: () => void; + onCreateAccount?: () => void; +} + +export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => ( +
+
+
+ + + + + + + +

Acme

+
+
+ {user ? ( + <> + + Welcome, {user.name}! + +
+
+
+); diff --git a/react-ui/src/stories/example/Page.stories.ts b/react-ui/src/stories/example/Page.stories.ts new file mode 100644 index 00000000..5d2c688a --- /dev/null +++ b/react-ui/src/stories/example/Page.stories.ts @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, userEvent, within } from '@storybook/test'; + +import { Page } from './Page'; + +const meta = { + title: 'Example/Page', + component: Page, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout + layout: 'fullscreen', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const LoggedOut: Story = {}; + +// More on component testing: https://storybook.js.org/docs/writing-tests/component-testing +export const LoggedIn: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const loginButton = canvas.getByRole('button', { name: /Log in/i }); + await expect(loginButton).toBeInTheDocument(); + await userEvent.click(loginButton); + await expect(loginButton).not.toBeInTheDocument(); + + const logoutButton = canvas.getByRole('button', { name: /Log out/i }); + await expect(logoutButton).toBeInTheDocument(); + }, +}; diff --git a/react-ui/src/stories/example/Page.tsx b/react-ui/src/stories/example/Page.tsx new file mode 100644 index 00000000..e1174830 --- /dev/null +++ b/react-ui/src/stories/example/Page.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +import { Header } from './Header'; +import './page.css'; + +type User = { + name: string; +}; + +export const Page: React.FC = () => { + const [user, setUser] = React.useState(); + + return ( +
+
setUser({ name: 'Jane Doe' })} + onLogout={() => setUser(undefined)} + onCreateAccount={() => setUser({ name: 'Jane Doe' })} + /> + +
+

Pages in Storybook

+

+ We recommend building UIs with a{' '} + + component-driven + {' '} + process starting with atomic components and ending with pages. +

+

+ Render pages with mock data. This makes it easy to build and review page states without + needing to navigate to them in your app. Here are some handy patterns for managing page + data in Storybook: +

+
    +
  • + Use a higher-level connected component. Storybook helps you compose such data from the + "args" of child component stories +
  • +
  • + Assemble data in the page component from your services. You can mock these services out + using Storybook. +
  • +
+

+ Get a guided tutorial on component-driven development at{' '} + + Storybook tutorials + + . Read more in the{' '} + + docs + + . +

+
+ Tip Adjust the width of the canvas with the{' '} + + + + + + Viewports addon in the toolbar +
+
+
+ ); +}; diff --git a/react-ui/src/stories/example/assets/accessibility.png b/react-ui/src/stories/example/assets/accessibility.png new file mode 100644 index 00000000..6ffe6fea Binary files /dev/null and b/react-ui/src/stories/example/assets/accessibility.png differ diff --git a/react-ui/src/stories/example/assets/accessibility.svg b/react-ui/src/stories/example/assets/accessibility.svg new file mode 100644 index 00000000..107e93f8 --- /dev/null +++ b/react-ui/src/stories/example/assets/accessibility.svg @@ -0,0 +1 @@ +Accessibility \ No newline at end of file diff --git a/react-ui/src/stories/example/assets/addon-library.png b/react-ui/src/stories/example/assets/addon-library.png new file mode 100644 index 00000000..95deb38a Binary files /dev/null and b/react-ui/src/stories/example/assets/addon-library.png differ diff --git a/react-ui/src/stories/example/assets/assets.png b/react-ui/src/stories/example/assets/assets.png new file mode 100644 index 00000000..cfba6817 Binary files /dev/null and b/react-ui/src/stories/example/assets/assets.png differ diff --git a/react-ui/src/stories/example/assets/avif-test-image.avif b/react-ui/src/stories/example/assets/avif-test-image.avif new file mode 100644 index 00000000..530709bc Binary files /dev/null and b/react-ui/src/stories/example/assets/avif-test-image.avif differ diff --git a/react-ui/src/stories/example/assets/context.png b/react-ui/src/stories/example/assets/context.png new file mode 100644 index 00000000..e5cd249a Binary files /dev/null and b/react-ui/src/stories/example/assets/context.png differ diff --git a/react-ui/src/stories/example/assets/discord.svg b/react-ui/src/stories/example/assets/discord.svg new file mode 100644 index 00000000..d638958b --- /dev/null +++ b/react-ui/src/stories/example/assets/discord.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/react-ui/src/stories/example/assets/docs.png b/react-ui/src/stories/example/assets/docs.png new file mode 100644 index 00000000..a749629d Binary files /dev/null and b/react-ui/src/stories/example/assets/docs.png differ diff --git a/react-ui/src/stories/example/assets/figma-plugin.png b/react-ui/src/stories/example/assets/figma-plugin.png new file mode 100644 index 00000000..8f79b08c Binary files /dev/null and b/react-ui/src/stories/example/assets/figma-plugin.png differ diff --git a/react-ui/src/stories/example/assets/github.svg b/react-ui/src/stories/example/assets/github.svg new file mode 100644 index 00000000..dc513528 --- /dev/null +++ b/react-ui/src/stories/example/assets/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/react-ui/src/stories/example/assets/share.png b/react-ui/src/stories/example/assets/share.png new file mode 100644 index 00000000..8097a370 Binary files /dev/null and b/react-ui/src/stories/example/assets/share.png differ diff --git a/react-ui/src/stories/example/assets/styling.png b/react-ui/src/stories/example/assets/styling.png new file mode 100644 index 00000000..d341e826 Binary files /dev/null and b/react-ui/src/stories/example/assets/styling.png differ diff --git a/react-ui/src/stories/example/assets/testing.png b/react-ui/src/stories/example/assets/testing.png new file mode 100644 index 00000000..d4ac39a0 Binary files /dev/null and b/react-ui/src/stories/example/assets/testing.png differ diff --git a/react-ui/src/stories/example/assets/theming.png b/react-ui/src/stories/example/assets/theming.png new file mode 100644 index 00000000..1535eb9b Binary files /dev/null and b/react-ui/src/stories/example/assets/theming.png differ diff --git a/react-ui/src/stories/example/assets/tutorials.svg b/react-ui/src/stories/example/assets/tutorials.svg new file mode 100644 index 00000000..b492a9c6 --- /dev/null +++ b/react-ui/src/stories/example/assets/tutorials.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/react-ui/src/stories/example/assets/youtube.svg b/react-ui/src/stories/example/assets/youtube.svg new file mode 100644 index 00000000..a7515d7e --- /dev/null +++ b/react-ui/src/stories/example/assets/youtube.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/react-ui/src/stories/example/button.css b/react-ui/src/stories/example/button.css new file mode 100644 index 00000000..4e3620b0 --- /dev/null +++ b/react-ui/src/stories/example/button.css @@ -0,0 +1,30 @@ +.storybook-button { + display: inline-block; + cursor: pointer; + border: 0; + border-radius: 3em; + font-weight: 700; + line-height: 1; + font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; +} +.storybook-button--primary { + background-color: #555ab9; + color: white; +} +.storybook-button--secondary { + box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; + background-color: transparent; + color: #333; +} +.storybook-button--small { + padding: 10px 16px; + font-size: 12px; +} +.storybook-button--medium { + padding: 11px 20px; + font-size: 14px; +} +.storybook-button--large { + padding: 12px 24px; + font-size: 16px; +} diff --git a/react-ui/src/stories/example/header.css b/react-ui/src/stories/example/header.css new file mode 100644 index 00000000..5efd46c2 --- /dev/null +++ b/react-ui/src/stories/example/header.css @@ -0,0 +1,32 @@ +.storybook-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + padding: 15px 20px; + font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.storybook-header svg { + display: inline-block; + vertical-align: top; +} + +.storybook-header h1 { + display: inline-block; + vertical-align: top; + margin: 6px 0 6px 10px; + font-weight: 700; + font-size: 20px; + line-height: 1; +} + +.storybook-header button + button { + margin-left: 10px; +} + +.storybook-header .welcome { + margin-right: 10px; + color: #333; + font-size: 14px; +} diff --git a/react-ui/src/stories/example/page.css b/react-ui/src/stories/example/page.css new file mode 100644 index 00000000..77c81d2d --- /dev/null +++ b/react-ui/src/stories/example/page.css @@ -0,0 +1,68 @@ +.storybook-page { + margin: 0 auto; + padding: 48px 20px; + max-width: 600px; + color: #333; + font-size: 14px; + line-height: 24px; + font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.storybook-page h2 { + display: inline-block; + vertical-align: top; + margin: 0 0 4px; + font-weight: 700; + font-size: 32px; + line-height: 1; +} + +.storybook-page p { + margin: 1em 0; +} + +.storybook-page a { + color: inherit; +} + +.storybook-page ul { + margin: 1em 0; + padding-left: 30px; +} + +.storybook-page li { + margin-bottom: 8px; +} + +.storybook-page .tip { + display: inline-block; + vertical-align: top; + margin-right: 10px; + border-radius: 1em; + background: #e7fdd8; + padding: 4px 12px; + color: #357a14; + font-weight: 700; + font-size: 11px; + line-height: 12px; +} + +.storybook-page .tip-wrapper { + margin-top: 40px; + margin-bottom: 40px; + font-size: 13px; + line-height: 20px; +} + +.storybook-page .tip-wrapper svg { + display: inline-block; + vertical-align: top; + margin-top: 3px; + margin-right: 4px; + width: 12px; + height: 12px; +} + +.storybook-page .tip-wrapper svg path { + fill: #1ea7fd; +} diff --git a/react-ui/src/stories/mockData.ts b/react-ui/src/stories/mockData.ts new file mode 100644 index 00000000..3d910b06 --- /dev/null +++ b/react-ui/src/stories/mockData.ts @@ -0,0 +1,548 @@ +export const datasetListData = { + msg: '操作成功', + code: 200, + data: { + content: [ + { + name: '手写体识别训练测试数据集', + identifier: 'admin_dataset_20241213140429', + description: '手写体识别数据集', + is_public: false, + time_ago: '2个月前', + id: 1454047, + visits: 0, + create_by: '陈志航', + owner: 'chenzhihang', + }, + { + name: '手写体识别', + identifier: 'admin_dataset_20241213140020', + description: '手写体识别数据集', + is_public: false, + time_ago: '2个月前', + id: 1454046, + visits: 0, + create_by: '陈志航', + owner: 'chenzhihang', + }, + { + name: '生物活性分子数据集', + identifier: 'admin_dataset_20241211151411', + description: '生物活性分子数据集', + is_public: false, + time_ago: '2个月前', + id: 1454004, + visits: 0, + create_by: '陈志航', + owner: 'chenzhihang', + }, + { + name: '介电材料数据集', + identifier: 'admin_dataset_20241211151330', + description: '介电材料数据集', + is_public: false, + time_ago: '2个月前', + id: 1454003, + visits: 0, + create_by: '陈志航', + owner: 'chenzhihang', + }, + ], + pageable: { + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + pageSize: 2000, + pageNumber: 0, + offset: 0, + unpaged: false, + paged: true, + }, + last: true, + totalElements: 4, + totalPages: 1, + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + first: true, + number: 0, + numberOfElements: 4, + size: 2000, + empty: false, + }, +}; + +export const datasetVersionData = { + msg: '操作成功', + code: 200, + data: [ + { + name: 'v2', + http_url: 'https://cdn09022024.gitlink.org.cn/chenzhihang/admin_dataset_20241213140429.git', + zip_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_dataset_20241213140429/archive/v2.zip', + tar_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_dataset_20241213140429/archive/v2.tar.gz', + }, + { + name: 'v3', + http_url: 'https://cdn09022024.gitlink.org.cn/chenzhihang/admin_dataset_20241213140429.git', + zip_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_dataset_20241213140429/archive/v3.zip', + tar_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_dataset_20241213140429/archive/v3.tar.gz', + }, + { + name: 'v1', + http_url: 'https://cdn09022024.gitlink.org.cn/chenzhihang/admin_dataset_20241213140429.git', + zip_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_dataset_20241213140429/archive/v1.zip', + tar_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_dataset_20241213140429/archive/v1.tar.gz', + }, + ], +}; + +export const datasetDetailData = { + msg: '操作成功', + code: 200, + data: { + name: '生物活性分子材料数据集', + identifier: 'admin_dataset_20250217105241', + description: '生物活性分子材料数据集', + is_public: false, + data_type: '自然语言处理', + data_tag: '机器翻译', + version: 'v1', + dataset_version_vos: [ + { + url: '/home/resource/admin/datasets/1454953/admin_dataset_20250217105241/v1/dataset/BBBP.zip', + file_name: 'BBBP.zip', + file_size: '42.14 KB', + }, + ], + id: 1454953, + create_by: '樊帅', + version_desc: '生物活性分子材料数据集', + usage: + '
# 克隆数据集配置文件与存储参数到本地\ngit clone -b v1 https://gitlink.org.cn/fanshuai/admin_dataset_20250217105241.git\n# 远程拉取配置文件\ndvc pull\n
', + update_time: '2025-02-17 10:52:43', + owner: 'fanshuai', + dataset_source: 'add', + relative_paths: 'admin/datasets/1454953/admin_dataset_20250217105241/v1/dataset', + }, +}; + +export const modelListData = { + msg: '操作成功', + code: 200, + data: { + content: [ + { + id: 1454208, + name: '介电材料模型1', + create_by: '陈志航', + description: '介电材料模型1', + time_ago: '2个月前', + owner: 'chenzhihang', + identifier: 'admin_model_20241224095928', + is_public: false, + }, + { + id: 1454007, + name: '手写体识别部署模型', + create_by: '陈志航', + description: '手写体识别部署模型', + time_ago: '2个月前', + owner: 'chenzhihang', + identifier: 'admin_model_20241211151713', + is_public: false, + }, + { + id: 1454006, + name: '生物活性分子材料', + create_by: '陈志航', + description: '生物活性分子材料', + time_ago: '2个月前', + owner: 'chenzhihang', + identifier: 'admin_model_20241211151645', + is_public: false, + }, + { + id: 1454005, + name: '介电材料模型', + create_by: '陈志航', + description: '介电材料模型', + time_ago: '2个月前', + owner: 'chenzhihang', + identifier: 'admin_model_20241211151601', + is_public: false, + }, + ], + pageable: { + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + pageSize: 2000, + pageNumber: 0, + offset: 0, + unpaged: false, + paged: true, + }, + last: true, + totalElements: 4, + totalPages: 1, + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + first: true, + number: 0, + numberOfElements: 4, + size: 2000, + empty: false, + }, +}; + +export const modelVersionData = { + msg: '操作成功', + code: 200, + data: [ + { + name: 'v1', + http_url: 'https://cdn09022024.gitlink.org.cn/chenzhihang/admin_model_20241224095928.git', + zip_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_model_20241224095928/archive/v1.zip', + tar_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_model_20241224095928/archive/v1.tar.gz', + }, + { + name: 'v2', + http_url: 'https://cdn09022024.gitlink.org.cn/chenzhihang/admin_model_20241224095928.git', + zip_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_model_20241224095928/archive/v1.zip', + tar_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_model_20241224095928/archive/v1.tar.gz', + }, + ], +}; + +export const modelDetailData = { + msg: '操作成功', + code: 200, + data: { + id: 1454208, + name: '介电材料模型1', + version: 'v1', + version_desc: '介电材料模型1', + create_by: '陈志航', + create_time: '2024-12-24 09:59:31', + update_time: '2024-12-24 09:59:31', + model_size: '101.90 KB', + model_source: 'add', + model_tag: '图像分类', + model_type: 'PyTorch', + description: '介电材料模型1', + usage: + '
# 克隆模型配置文件与存储参数到本地\ngit clone -b v1 https://gitlink.org.cn/chenzhihang/admin_model_20241224095928.git\n# 远程拉取配置文件\ndvc pull\n
', + owner: 'chenzhihang', + identifier: 'admin_model_20241224095928', + is_public: false, + relative_paths: 'admin/model/1454208/admin_model_20241224095928/v1/model', + model_version_vos: [ + { + url: '/home/resource/admin/model/1454208/admin_model_20241224095928/v1/model/sklearn_svr_good.pkl', + file_name: 'sklearn_svr_good.pkl', + file_size: '101.90 KB', + }, + ], + }, +}; + +export const mirrorListData = { + code: 200, + msg: '操作成功', + data: { + content: [ + { + id: 42, + name: 'ccr.ccs.tencentyun.com/somunslotus/httpserver', + description: 'htttp服务器镜像', + image_type: 0, + create_by: 'admin', + create_time: '2024-04-30T14:44:37.000+08:00', + update_by: 'admin', + update_time: '2024-04-30T14:44:37.000+08:00', + state: 1, + version_count: 1, + }, + { + id: 44, + name: 'minio-test', + description: 'minio镜像', + image_type: 0, + create_by: 'admin', + create_time: '2024-05-08T15:19:00.000+08:00', + update_by: 'admin', + update_time: '2024-05-08T15:19:00.000+08:00', + state: 1, + version_count: 1, + }, + { + id: 46, + name: 'machine-learning/mnist-model-deploy', + description: 'mnist手写体识别部署镜像', + image_type: 0, + create_by: 'admin', + create_time: '2024-05-28T10:18:30.000+08:00', + update_by: 'admin', + update_time: '2024-05-28T10:18:30.000+08:00', + state: 1, + version_count: 2, + }, + { + id: 49, + name: 'go_httpsever', + description: 'golang httpserver镜像', + image_type: 0, + create_by: 'admin', + create_time: '2024-06-25T11:11:41.000+08:00', + update_by: 'admin', + update_time: '2024-06-25T11:11:41.000+08:00', + state: 1, + version_count: 1, + }, + { + id: 51, + name: 'pytorch2_python3_cuda_12', + description: 'pytorch2.1.2_python3.11_cuda_12', + image_type: 0, + create_by: 'admin', + create_time: '2024-10-14T16:16:35.000+08:00', + update_by: 'admin', + update_time: '2024-10-14T16:16:35.000+08:00', + state: 1, + version_count: 2, + }, + { + id: 53, + name: 'jupyterlab', + description: 'v1', + image_type: 0, + create_by: 'admin', + create_time: '2024-10-15T16:19:29.000+08:00', + update_by: 'admin', + update_time: '2024-10-15T16:19:29.000+08:00', + state: 1, + version_count: 1, + }, + { + id: 55, + name: 'machine-learning/ax-pytorch', + description: '自动机器学习Ax镜像,带pytorch', + image_type: 0, + create_by: 'admin', + create_time: '2024-12-13T11:25:37.000+08:00', + update_by: 'admin', + update_time: '2024-12-13T11:25:37.000+08:00', + state: 1, + version_count: 1, + }, + ], + pageable: { + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + pageSize: 2000, + pageNumber: 0, + offset: 0, + unpaged: false, + paged: true, + }, + last: true, + totalElements: 7, + totalPages: 1, + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + first: true, + number: 0, + numberOfElements: 7, + size: 2000, + empty: false, + }, +}; + +export const mirrorVerionData = { + code: 200, + msg: '操作成功', + data: { + content: [ + { + id: 54, + image_id: 42, + version: null, + url: '172.20.32.187/testlib/admin/ccr.ccs.tencentyun.com/somunslotus/httpserver:v1', + tag_name: 'v1', + file_size: '6.98 MB', + status: 'available', + create_by: 'admin', + create_time: '2024-04-30T14:44:37.000+08:00', + update_by: 'admin', + update_time: '2024-04-30T14:44:51.000+08:00', + state: 1, + host_ip: null, + }, + ], + pageable: { + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + pageSize: 2000, + pageNumber: 0, + offset: 0, + unpaged: false, + paged: true, + }, + last: true, + totalElements: 1, + totalPages: 1, + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + first: true, + number: 0, + numberOfElements: 1, + size: 2000, + empty: false, + }, +}; + +export const codeListData = { + code: 200, + msg: '操作成功', + data: { + content: [ + { + id: 2, + code_repo_name: '介电材料代码', + code_repo_vis: 1, + git_url: 'https://gitlink.org.cn/fuli/ML_for_Materials.git', + git_branch: 'master', + verify_mode: null, + git_user_name: null, + git_password: null, + ssh_key: null, + create_by: 'admin', + create_time: '2024-10-14T16:10:45.000+08:00', + update_by: 'admin', + update_time: '2024-10-14T16:10:45.000+08:00', + state: 1, + }, + { + id: 3, + code_repo_name: '生物活性材料代码', + code_repo_vis: 1, + git_url: 'https://gitlink.org.cn/zhaoyihan/test_mole_pre.git', + git_branch: 'parse_dataset', + verify_mode: null, + git_user_name: null, + git_password: null, + ssh_key: null, + create_by: 'admin', + create_time: '2024-10-16T08:41:39.000+08:00', + update_by: 'admin', + update_time: '2024-10-16T08:41:39.000+08:00', + state: 1, + }, + { + id: 4, + code_repo_name: '数据处理', + code_repo_vis: 1, + git_url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git', + git_branch: 'train_ci_test', + verify_mode: null, + git_user_name: null, + git_password: null, + ssh_key: null, + create_by: 'admin', + create_time: '2024-10-16T14:51:18.000+08:00', + update_by: 'admin', + update_time: '2024-10-16T14:51:18.000+08:00', + state: 1, + }, + { + id: 5, + code_repo_name: '手写体识别部署', + code_repo_vis: 1, + git_url: 'https://gitlink.org.cn/somunslotus/mnist-inference.git', + git_branch: 'master', + verify_mode: null, + git_user_name: null, + git_password: null, + ssh_key: null, + create_by: 'admin', + create_time: '2024-10-16T16:36:43.000+08:00', + update_by: 'admin', + update_time: '2024-10-16T16:36:43.000+08:00', + state: 1, + }, + { + id: 7, + code_repo_name: '手写体识别训练', + code_repo_vis: 1, + git_url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git', + git_branch: 'train_ci_test', + verify_mode: null, + git_user_name: null, + git_password: null, + ssh_key: null, + create_by: 'admin', + create_time: '2024-12-13T13:58:50.000+08:00', + update_by: 'admin', + update_time: '2024-12-13T13:58:50.000+08:00', + state: 1, + }, + ], + pageable: { + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + pageSize: 20, + pageNumber: 0, + offset: 0, + unpaged: false, + paged: true, + }, + last: true, + totalElements: 5, + totalPages: 1, + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + first: true, + number: 0, + numberOfElements: 5, + size: 20, + empty: false, + }, +}; diff --git a/react-ui/src/styles/theme.less b/react-ui/src/styles/theme.less index cf1daced..ff7813f9 100644 --- a/react-ui/src/styles/theme.less +++ b/react-ui/src/styles/theme.less @@ -49,7 +49,7 @@ // padding @content-padding: 25px; -// 函数 +// 函数,hex 添加 alpha 值 .addAlpha(@color, @alpha) { @red: red(@color); @green: green(@color); @@ -58,6 +58,7 @@ } // 混合 +// 单行 .singleLine() { overflow: hidden; white-space: nowrap; @@ -65,6 +66,7 @@ word-break: break-all; } +// 多行 .multiLine(@line) { display: -webkit-box; overflow: hidden; diff --git a/react-ui/tsconfig.json b/react-ui/tsconfig.json index 0afa8788..55ce7f74 100644 --- a/react-ui/tsconfig.json +++ b/react-ui/tsconfig.json @@ -9,7 +9,7 @@ "strict": true, // 启用所有严格类型检查选项 "forceConsistentCasingInFileNames": false, // 允许对同一文件的引用使用不一致的大小写 "module": "esnext", // 指定模块代码生成 - "moduleResolution": "node", // 使用Node.js样式解析模块 + "moduleResolution": "bundler", // 使用bundlers样式解析模块 "isolatedModules": true, // 无条件地为未解析的文件发出导入 "resolveJsonModule": true, // 包含.json扩展名的模块 "noEmit": true, // 不发出输出(即不编译代码,只进行类型检查)