| @@ -58,3 +58,5 @@ mvnw | |||
| # web | |||
| **/node_modules | |||
| *storybook.log | |||
| @@ -13,7 +13,7 @@ show_help() { | |||
| echo | |||
| echo "Options:" | |||
| echo " -b Branch to deploy, default is master" | |||
| echo " -s Service to deploy (manage-front, manage, front, all, default is manage-front)" | |||
| echo " -s Service to deploy (manage-front, manage, front, all, system, default is manage-front)" | |||
| echo " -h Show this help message" | |||
| } | |||
| @@ -54,7 +54,7 @@ compile_front() { | |||
| # 编译前端 | |||
| docker run -v ${baseDir}:${baseDir} \ | |||
| -e http_proxy=http://172.20.32.253:3128 -e https_proxy=http://172.20.32.253:3128 \ | |||
| 172.20.32.187/ci4s/node:16.16.0 ${baseDir}/k8s/build-node.sh | |||
| 172.20.32.187/tempimagefile/node:18.16.0 ${baseDir}/k8s/build-node.sh | |||
| if [ $? -ne 0 ]; then | |||
| echo "编译失败,请检查代码!" | |||
| exit 1 | |||
| @@ -19,7 +19,7 @@ show_help() { | |||
| echo | |||
| echo "Options:" | |||
| echo " -b Branch to deploy, default: master" | |||
| echo " -s Service to deploy (manage-front, manage, front, all, default: manage-front)" | |||
| echo " -s Service to deploy (manage-front, manage, front, all, system default: manage-front)" | |||
| echo " -e Environment (e.g., dev, test, default: dev)" | |||
| echo " -h Show this help message" | |||
| } | |||
| @@ -10,7 +10,7 @@ show_help() { | |||
| echo "Usage: $0 [-s service] [-e environment]" | |||
| echo | |||
| echo "Options:" | |||
| echo " -s Service to deploy (manage-front, manage, front, all default: manage-front)" | |||
| echo " -s Service to deploy (manage-front, manage, front, all, system default: manage-front)" | |||
| echo " -e Environment (e.g., dev, test, default: dev)" | |||
| echo " -h Show this help message" | |||
| } | |||
| @@ -43,7 +43,7 @@ fi | |||
| # 根据环境设置 IP 地址 | |||
| if [ "$env" == "dev" ]; then | |||
| remote_ip="172.20.32.181" | |||
| remote_ip="172.20.32.197" | |||
| elif [ "$env" == "test" ]; then | |||
| remote_ip="172.20.32.185" | |||
| else | |||
| @@ -29,11 +29,25 @@ http { | |||
| location /label-studio/ { | |||
| # rewrite ^/label-studio/(.*)$ /$1 break; | |||
| proxy_pass http://label-studio-service.argo.svc:8080/projects/; | |||
| proxy_pass http://label-studio-service.argo.svc:9000/projects/; | |||
| proxy_hide_header X-Frame-Options; | |||
| add_header X-Frame-Options ALLOWALL; | |||
| } | |||
| location /minio/ { | |||
| # rewrite ^/label-studio/(.*)$ /$1 break; | |||
| proxy_pass http://juicefs-s3-gateway.juicefs.svc:9000/; | |||
| proxy_hide_header X-Frame-Options; | |||
| add_header X-Frame-Options ALLOWALL; | |||
| } | |||
| location /neo4j/ { | |||
| # rewrite ^/label-studio/(.*)$ /$1 break; | |||
| proxy_pass http://172.20.20.88:7474/; | |||
| proxy_hide_header X-Frame-Options; | |||
| add_header X-Frame-Options ALLOWALL; | |||
| } | |||
| location / { | |||
| rewrite ^/prod-api/(.*)$ /$1 break; | |||
| root /home/ruoyi/projects/ruoyi-ui; | |||
| @@ -1,36 +0,0 @@ | |||
| apiVersion: apps/v1 | |||
| kind: Deployment | |||
| metadata: | |||
| name: ci4s-gen-deployment | |||
| namespace: ci4s-test | |||
| spec: | |||
| replicas: 1 | |||
| selector: | |||
| matchLabels: | |||
| app: ci4s-gen | |||
| template: | |||
| metadata: | |||
| labels: | |||
| app: ci4s-gen | |||
| spec: | |||
| containers: | |||
| - name: ci4s-gen | |||
| image: ci4s-gen:v1.0 | |||
| ports: | |||
| - containerPort: 9202 | |||
| --- | |||
| apiVersion: v1 | |||
| kind: Service | |||
| metadata: | |||
| name: ci4s-gen-service | |||
| namespace: ci4s-test | |||
| spec: | |||
| type: NodePort | |||
| ports: | |||
| - port: 9202 | |||
| nodePort: 31211 | |||
| protocol: TCP | |||
| selector: | |||
| app: ci4s-gen | |||
| @@ -1,36 +0,0 @@ | |||
| apiVersion: apps/v1 | |||
| kind: Deployment | |||
| metadata: | |||
| name: ci4s-visual-deployment | |||
| namespace: ci4s-test | |||
| spec: | |||
| replicas: 1 | |||
| selector: | |||
| matchLabels: | |||
| app: ci4s-visual | |||
| template: | |||
| metadata: | |||
| labels: | |||
| app: ci4s-visual | |||
| spec: | |||
| containers: | |||
| - name: ci4s-visual | |||
| image: ci4s-visual:v1.0 | |||
| ports: | |||
| - containerPort: 9100 | |||
| --- | |||
| apiVersion: v1 | |||
| kind: Service | |||
| metadata: | |||
| name: ci4s-visual-service | |||
| namespace: ci4s-test | |||
| spec: | |||
| type: NodePort | |||
| ports: | |||
| - port: 9100 | |||
| nodePort: 31212 | |||
| protocol: TCP | |||
| selector: | |||
| app: ci4s-visual | |||
| @@ -1,36 +0,0 @@ | |||
| apiVersion: apps/v1 | |||
| kind: Deployment | |||
| metadata: | |||
| name: ci4s-front-deployment | |||
| namespace: argo | |||
| spec: | |||
| replicas: 1 | |||
| selector: | |||
| matchLabels: | |||
| app: ci4s-front | |||
| template: | |||
| metadata: | |||
| labels: | |||
| app: ci4s-front | |||
| spec: | |||
| containers: | |||
| - name: ci4s-front | |||
| image: 172.20.32.187/ci4s/ci4s-front:20240401 | |||
| ports: | |||
| - containerPort: 8000 | |||
| --- | |||
| apiVersion: v1 | |||
| kind: Service | |||
| metadata: | |||
| name: ci4s-front-service | |||
| namespace: argo | |||
| spec: | |||
| type: NodePort | |||
| ports: | |||
| - port: 8000 | |||
| nodePort: 31213 | |||
| protocol: TCP | |||
| selector: | |||
| app: ci4s-front | |||
| @@ -1,62 +0,0 @@ | |||
| apiVersion: apps/v1 | |||
| kind: Deployment | |||
| metadata: | |||
| namespace: argo | |||
| name: nacos-ci4s | |||
| labels: | |||
| app: nacos-ci4s | |||
| spec: | |||
| replicas: 1 | |||
| selector: | |||
| matchLabels: | |||
| app: nacos-ci4s | |||
| template: | |||
| metadata: | |||
| labels: | |||
| app: nacos-ci4s | |||
| spec: | |||
| containers: | |||
| - name: nacos-ci4s | |||
| image: nacos/nacos-server:v2.2.0 | |||
| env: | |||
| - name: SPRING_DATASOURCE_PLATFORM | |||
| value: mysql | |||
| - name: MODE | |||
| value: standalone | |||
| - name: MYSQL_SERVICE_HOST | |||
| value: mysql.argo.svc | |||
| - name: MYSQL_SERVICE_PORT | |||
| value: "3306" | |||
| - name: MYSQL_SERVICE_DB_NAME | |||
| value: nacos-ci4s-config | |||
| - name: MYSQL_SERVICE_USER | |||
| value: root | |||
| - name: MYSQL_SERVICE_PASSWORD | |||
| value: qazxc123456. | |||
| ports: | |||
| - containerPort: 8848 | |||
| - containerPort: 9848 | |||
| restartPolicy: Always | |||
| --- | |||
| apiVersion: v1 | |||
| kind: Service | |||
| metadata: | |||
| namespace: argo | |||
| name: nacos-ci4s | |||
| labels: | |||
| app: nacos-ci4s | |||
| spec: | |||
| type: NodePort | |||
| selector: | |||
| app: nacos-ci4s | |||
| ports: | |||
| - port: 8848 | |||
| targetPort: 8848 | |||
| nodePort: 31203 | |||
| name: web | |||
| - port: 9848 | |||
| targetPort: 9848 | |||
| nodePort: 31204 | |||
| name: podsa | |||
| @@ -1,36 +0,0 @@ | |||
| apiVersion: apps/v1 | |||
| kind: Deployment | |||
| metadata: | |||
| name: ci4s-gateway-deployment | |||
| namespace: argo | |||
| spec: | |||
| replicas: 1 | |||
| selector: | |||
| matchLabels: | |||
| app: ci4s-gateway | |||
| template: | |||
| metadata: | |||
| labels: | |||
| app: ci4s-gateway | |||
| spec: | |||
| containers: | |||
| - name: ci4s-gateway | |||
| image: 172.20.32.187/ci4s/ci4s-gateway:20240401 | |||
| ports: | |||
| - containerPort: 8082 | |||
| --- | |||
| apiVersion: v1 | |||
| kind: Service | |||
| metadata: | |||
| name: ci4s-gateway-service | |||
| namespace: argo | |||
| spec: | |||
| type: NodePort | |||
| ports: | |||
| - port: 8082 | |||
| nodePort: 31205 | |||
| protocol: TCP | |||
| selector: | |||
| app: ci4s-gateway | |||
| @@ -1,36 +0,0 @@ | |||
| apiVersion: apps/v1 | |||
| kind: Deployment | |||
| metadata: | |||
| name: ci4s-auth-deployment | |||
| namespace: argo | |||
| spec: | |||
| replicas: 1 | |||
| selector: | |||
| matchLabels: | |||
| app: ci4s-auth | |||
| template: | |||
| metadata: | |||
| labels: | |||
| app: ci4s-auth | |||
| spec: | |||
| containers: | |||
| - name: ci4s-auth | |||
| image: 172.20.32.187/ci4s/ci4s-auth:20240401 | |||
| ports: | |||
| - containerPort: 9200 | |||
| --- | |||
| apiVersion: v1 | |||
| kind: Service | |||
| metadata: | |||
| name: ci4s-auth-service | |||
| namespace: argo | |||
| spec: | |||
| type: NodePort | |||
| ports: | |||
| - port: 9200 | |||
| nodePort: 31206 | |||
| protocol: TCP | |||
| selector: | |||
| app: ci4s-auth | |||
| @@ -1,36 +0,0 @@ | |||
| apiVersion: apps/v1 | |||
| kind: Deployment | |||
| metadata: | |||
| name: ci4s-system-deployment | |||
| namespace: argo | |||
| spec: | |||
| replicas: 1 | |||
| selector: | |||
| matchLabels: | |||
| app: ci4s-system | |||
| template: | |||
| metadata: | |||
| labels: | |||
| app: ci4s-system | |||
| spec: | |||
| containers: | |||
| - name: ci4s-system | |||
| image: 172.20.32.187/ci4s/ci4s-system:20240401 | |||
| ports: | |||
| - containerPort: 9201 | |||
| --- | |||
| apiVersion: v1 | |||
| kind: Service | |||
| metadata: | |||
| name: ci4s-system-service | |||
| namespace: argo | |||
| spec: | |||
| type: NodePort | |||
| ports: | |||
| - port: 9201 | |||
| nodePort: 31207 | |||
| protocol: TCP | |||
| selector: | |||
| app: ci4s-system | |||
| @@ -1,44 +0,0 @@ | |||
| apiVersion: apps/v1 | |||
| kind: Deployment | |||
| metadata: | |||
| name: ci4s-management-platform-deployment | |||
| namespace: argo | |||
| spec: | |||
| replicas: 1 | |||
| selector: | |||
| matchLabels: | |||
| app: ci4s-management-platform | |||
| template: | |||
| metadata: | |||
| labels: | |||
| app: ci4s-management-platform | |||
| spec: | |||
| containers: | |||
| - name: ci4s-management-platform | |||
| image: 172.20.32.187/ci4s/managent:20240401 | |||
| ports: | |||
| - containerPort: 9213 | |||
| volumeMounts: | |||
| - name: resource | |||
| mountPath: /home/resource/ | |||
| volumes: | |||
| - name: resource | |||
| hostPath: | |||
| path: /home/resource/ | |||
| type: DirectoryOrCreate | |||
| --- | |||
| apiVersion: v1 | |||
| kind: Service | |||
| metadata: | |||
| name: ci4s-management-platform-service | |||
| namespace: argo | |||
| spec: | |||
| type: NodePort | |||
| ports: | |||
| - port: 9213 | |||
| nodePort: 31208 | |||
| protocol: TCP | |||
| selector: | |||
| app: ci4s-management-platform | |||
| @@ -1,36 +0,0 @@ | |||
| apiVersion: apps/v1 | |||
| kind: Deployment | |||
| metadata: | |||
| name: ci4s-file-deployment | |||
| namespace: ci4s-test | |||
| spec: | |||
| replicas: 1 | |||
| selector: | |||
| matchLabels: | |||
| app: ci4s-file | |||
| template: | |||
| metadata: | |||
| labels: | |||
| app: ci4s-file | |||
| spec: | |||
| containers: | |||
| - name: ci4s-file | |||
| image: ci4s-file:v1.0 | |||
| ports: | |||
| - containerPort: 9300 | |||
| --- | |||
| apiVersion: v1 | |||
| kind: Service | |||
| metadata: | |||
| name: ci4s-file-service | |||
| namespace: ci4s-test | |||
| spec: | |||
| type: NodePort | |||
| ports: | |||
| - port: 9300 | |||
| nodePort: 31209 | |||
| protocol: TCP | |||
| selector: | |||
| app: ci4s-file | |||
| @@ -1,36 +0,0 @@ | |||
| apiVersion: apps/v1 | |||
| kind: Deployment | |||
| metadata: | |||
| name: ci4s-job-deployment | |||
| namespace: ci4s-test | |||
| spec: | |||
| replicas: 1 | |||
| selector: | |||
| matchLabels: | |||
| app: ci4s-job | |||
| template: | |||
| metadata: | |||
| labels: | |||
| app: ci4s-job | |||
| spec: | |||
| containers: | |||
| - name: ci4s-job | |||
| image: ci4s-job:v1.0 | |||
| ports: | |||
| - containerPort: 9203 | |||
| --- | |||
| apiVersion: v1 | |||
| kind: Service | |||
| metadata: | |||
| name: ci4s-job-service | |||
| namespace: ci4s-test | |||
| spec: | |||
| type: NodePort | |||
| ports: | |||
| - port: 9203 | |||
| nodePort: 31210 | |||
| protocol: TCP | |||
| selector: | |||
| app: ci4s-job | |||
| @@ -1,36 +0,0 @@ | |||
| apiVersion: apps/v1 | |||
| kind: Deployment | |||
| metadata: | |||
| name: ci4s-front-deployment | |||
| namespace: argo | |||
| spec: | |||
| replicas: 1 | |||
| selector: | |||
| matchLabels: | |||
| app: ci4s-front | |||
| template: | |||
| metadata: | |||
| labels: | |||
| app: ci4s-front | |||
| spec: | |||
| containers: | |||
| - name: ci4s-front | |||
| image: 172.20.32.187/ci4s/ci4s-front:202406120836 | |||
| ports: | |||
| - containerPort: 8000 | |||
| --- | |||
| apiVersion: v1 | |||
| kind: Service | |||
| metadata: | |||
| name: ci4s-front-service | |||
| namespace: argo | |||
| spec: | |||
| type: NodePort | |||
| ports: | |||
| - port: 8000 | |||
| nodePort: 31213 | |||
| protocol: TCP | |||
| selector: | |||
| app: ci4s-front | |||
| @@ -1,53 +0,0 @@ | |||
| apiVersion: apps/v1 | |||
| kind: Deployment | |||
| metadata: | |||
| name: ci4s-management-platform-deployment | |||
| namespace: argo | |||
| spec: | |||
| replicas: 1 | |||
| selector: | |||
| matchLabels: | |||
| app: ci4s-management-platform | |||
| template: | |||
| metadata: | |||
| labels: | |||
| app: ci4s-management-platform | |||
| spec: | |||
| containers: | |||
| - name: ci4s-management-platform | |||
| image: 172.20.32.187/ci4s/ci4s-managent:202409201355 | |||
| env: | |||
| - name: TZ | |||
| value: Asia/Shanghai | |||
| - name: JAVA_TOOL_OPTIONS | |||
| value: "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005" | |||
| ports: | |||
| - containerPort: 9213 | |||
| volumeMounts: | |||
| - name: resource-volume | |||
| mountPath: /home/resource/ | |||
| volumes: | |||
| - name: resource-volume | |||
| persistentVolumeClaim: | |||
| claimName: platform-data-pvc-nfs | |||
| --- | |||
| apiVersion: v1 | |||
| kind: Service | |||
| metadata: | |||
| name: ci4s-management-platform-service | |||
| namespace: argo | |||
| spec: | |||
| type: NodePort | |||
| ports: | |||
| - name: http | |||
| port: 9213 | |||
| nodePort: 31208 | |||
| protocol: TCP | |||
| - name: debug | |||
| nodePort: 34567 | |||
| port: 5005 | |||
| protocol: TCP | |||
| targetPort: 5005 | |||
| selector: | |||
| app: ci4s-management-platform | |||
| @@ -0,0 +1,43 @@ | |||
| apiVersion: apps/v1 | |||
| kind: Deployment | |||
| metadata: | |||
| name: ci4s-oauth2-authenticator-deployment | |||
| namespace: argo | |||
| spec: | |||
| replicas: 1 | |||
| selector: | |||
| matchLabels: | |||
| app: ci4s-oauth2-authenticator | |||
| template: | |||
| metadata: | |||
| labels: | |||
| app: ci4s-oauth2-authenticator | |||
| spec: | |||
| containers: | |||
| - name: ci4s-oauth2-authenticator | |||
| image: 172.20.32.187/ci4s/spring-oauth2-authenticator:latest | |||
| env: | |||
| - name: DB_URL | |||
| value: mysql.argo.svc:3306 | |||
| - name: DB_USERNAME | |||
| value: root | |||
| - name: DB_PASSWORD | |||
| value: qazxc123456. | |||
| ports: | |||
| - containerPort: 8080 | |||
| --- | |||
| apiVersion: v1 | |||
| kind: Service | |||
| metadata: | |||
| name: ci4s-oauth2-authenticator-service | |||
| namespace: argo | |||
| spec: | |||
| type: NodePort | |||
| ports: | |||
| - name: http | |||
| port: 8080 | |||
| nodePort: 31080 | |||
| protocol: TCP | |||
| selector: | |||
| app: ci4s-oauth2-authenticator | |||
| @@ -17,7 +17,7 @@ spec: | |||
| spec: | |||
| containers: | |||
| - name: nacos-ci4s | |||
| image: nacos/nacos-server:v2.2.0 | |||
| image: 172.20.32.187/ci4s/nacos-server:v2.2.0 | |||
| env: | |||
| - name: SPRING_DATASOURCE_PLATFORM | |||
| value: mysql | |||
| @@ -38,8 +38,8 @@ spec: | |||
| - containerPort: 9848 | |||
| - containerPort: 9849 | |||
| initContainers: | |||
| - name: init-mydb | |||
| image: busybox:1.31 | |||
| - name: init-mydb-check | |||
| image: 172.20.32.187/ci4s/busybox:1.31 | |||
| command: [ 'sh', '-c', 'nc -zv mysql.argo.svc 3306' ] | |||
| restartPolicy: Always | |||
| @@ -16,6 +16,8 @@ spec: | |||
| containers: | |||
| - name: ci4s-management-platform | |||
| image: ${k8s-7management-image} | |||
| securityContext: | |||
| privileged: true | |||
| env: | |||
| - name: TZ | |||
| value: Asia/Shanghai | |||
| @@ -27,10 +29,30 @@ spec: | |||
| - name: resource-volume | |||
| mountPath: /home/resource/ | |||
| subPath: mini-model-platform-data | |||
| mountPropagation: Bidirectional | |||
| volumes: | |||
| - name: resource-volume | |||
| hostPath: | |||
| path: /platform-data | |||
| initContainers: | |||
| - name: init-fs-check | |||
| image: 172.20.32.187/ci4s/ci4s-managent:202502141722 | |||
| securityContext: | |||
| privileged: true | |||
| volumeMounts: | |||
| - name: resource-volume | |||
| mountPath: /home/resource/ | |||
| subPath: mini-model-platform-data | |||
| mountPropagation: Bidirectional | |||
| command: [ "/bin/sh", "-c" ] | |||
| args: | |||
| - | | |||
| mounted=$(findmnt /home/resource/ | grep 'fuse.juicefs') | |||
| if [ -z "$mounted" ]; then | |||
| echo "/platform-data not mounted"; | |||
| exit 1 | |||
| fi | |||
| restartPolicy: Always | |||
| --- | |||
| apiVersion: v1 | |||
| kind: Service | |||
| @@ -0,0 +1,68 @@ | |||
| apiVersion: rbac.authorization.k8s.io/v1 | |||
| kind: Role | |||
| metadata: | |||
| name: custom-workflow | |||
| namespace: argo | |||
| rules: | |||
| - apiGroups: | |||
| - argoproj.io | |||
| resources: | |||
| - workflows | |||
| verbs: | |||
| - create | |||
| - get | |||
| - list | |||
| - watch | |||
| - update | |||
| - patch | |||
| - delete | |||
| - apiGroups: | |||
| - "" | |||
| resources: | |||
| - pods | |||
| - services | |||
| verbs: | |||
| - get | |||
| - list | |||
| - watch | |||
| - create | |||
| - update | |||
| - patch | |||
| - delete | |||
| - apiGroups: | |||
| - "" | |||
| resources: | |||
| - pods/exec | |||
| verbs: | |||
| - create | |||
| - get | |||
| - list | |||
| - watch | |||
| - update | |||
| - patch | |||
| - delete | |||
| - apiGroups: | |||
| - "apps" | |||
| resources: | |||
| - deployments | |||
| verbs: | |||
| - get | |||
| - list | |||
| - watch | |||
| - create | |||
| - update | |||
| - patch | |||
| - delete | |||
| --- | |||
| apiVersion: rbac.authorization.k8s.io/v1 | |||
| kind: RoleBinding | |||
| metadata: | |||
| name: custom-workflow-default | |||
| roleRef: | |||
| apiGroup: rbac.authorization.k8s.io | |||
| kind: Role | |||
| name: custom-workflow | |||
| subjects: | |||
| - kind: ServiceAccount | |||
| name: default | |||
| @@ -5,4 +5,5 @@ | |||
| public | |||
| dist | |||
| .umi | |||
| mock | |||
| mock | |||
| /src/iconfont/ | |||
| @@ -1,10 +1,16 @@ | |||
| module.exports = { | |||
| extends: [require.resolve('@umijs/lint/dist/config/eslint')], | |||
| extends: [ | |||
| require.resolve('@umijs/lint/dist/config/eslint'), | |||
| 'plugin:react/recommended', | |||
| "plugin:react-hooks/recommended" | |||
| ], | |||
| globals: { | |||
| page: true, | |||
| REACT_APP_ENV: true, | |||
| }, | |||
| rules: { | |||
| '@typescript-eslint/no-use-before-define': 'off', | |||
| 'react/react-in-jsx-scope': 'off', | |||
| 'react/display-name': 'off' | |||
| }, | |||
| }; | |||
| @@ -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 | |||
| @@ -0,0 +1 @@ | |||
| v18.20.7 | |||
| @@ -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"; | |||
| } | |||
| } | |||
| }, | |||
| }, | |||
| }; | |||
| }; | |||
| @@ -0,0 +1,19 @@ | |||
| import { Of, useOf } from '@storybook/blocks'; | |||
| /** | |||
| * A block that displays the story name or title from the of prop | |||
| * - if a story reference is passed, it renders the story name | |||
| * - if a meta reference is passed, it renders the stories' title | |||
| * - if nothing is passed, it defaults to the primary story | |||
| */ | |||
| export const StoryName = ({ of }: { of?: Of }) => { | |||
| const resolvedOf = useOf(of || 'story', ['story', 'meta']); | |||
| switch (resolvedOf.type) { | |||
| case 'story': { | |||
| return <h3 className="css-wzniqs">{resolvedOf.story.name}</h3>; | |||
| } | |||
| case 'meta': { | |||
| return <h3 className="css-wzniqs">{resolvedOf.preparedMeta.title}</h3>; | |||
| } | |||
| } | |||
| }; | |||
| @@ -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; | |||
| @@ -0,0 +1,6 @@ | |||
| import { addons } from '@storybook/manager-api'; | |||
| import theme from './theme'; | |||
| addons.setConfig({ | |||
| theme: theme, | |||
| }); | |||
| @@ -0,0 +1,19 @@ | |||
| export const Link = ({ to, children, ...props }: any) => ( | |||
| <a href={typeof to === 'string' ? to : '#'} {...props}> | |||
| {children} | |||
| </a> | |||
| ); | |||
| 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; | |||
| @@ -0,0 +1,92 @@ | |||
| export const createWebSocketMock = () => { | |||
| class WebSocketMock { | |||
| constructor(url) { | |||
| this.url = url; | |||
| this.readyState = WebSocket.OPEN; | |||
| this.listeners = {}; | |||
| this.count = 0; | |||
| console.log("Mock WebSocket connected to:", url); | |||
| // 模拟服务器推送消息 | |||
| this.intervalId = setInterval(() => { | |||
| this.count += 1; | |||
| if (this.count > 5) { | |||
| this.count = 0; | |||
| clearInterval(this.intervalId); | |||
| return; | |||
| } | |||
| this.sendMessage(JSON.stringify(logStreamData)); | |||
| }, 3000); | |||
| } | |||
| sendMessage(data) { | |||
| if (this.listeners["message"]) { | |||
| this.listeners["message"].forEach((callback) => callback({ data })); | |||
| } | |||
| } | |||
| addEventListener(event, callback) { | |||
| if (!this.listeners[event]) { | |||
| this.listeners[event] = []; | |||
| } | |||
| this.listeners[event].push(callback); | |||
| } | |||
| removeEventListener(event, callback) { | |||
| if (this.listeners[event]) { | |||
| this.listeners[event] = this.listeners[event].filter((cb) => cb !== callback); | |||
| } | |||
| } | |||
| close() { | |||
| this.readyState = WebSocket.CLOSED; | |||
| console.log("Mock WebSocket closed"); | |||
| } | |||
| } | |||
| return WebSocketMock; | |||
| }; | |||
| export const logStreamData = { | |||
| streams: [ | |||
| { | |||
| stream: { | |||
| workflows_argoproj_io_completed: 'false', | |||
| workflows_argoproj_io_workflow: 'workflow-p2ddj', | |||
| container: 'init', | |||
| filename: | |||
| '/var/log/pods/argo_workflow-p2ddj-git-clone-f33abcda-3988047653_e31cf6be-e013-4885-9eb6-ec84f83b9ba9/init/0.log', | |||
| job: 'argo/workflow-p2ddj-git-clone-f33abcda-3988047653', | |||
| namespace: 'argo', | |||
| pod: 'workflow-p2ddj-git-clone-f33abcda-3988047653', | |||
| stream: 'stderr', | |||
| }, | |||
| values: [ | |||
| [ | |||
| '1742179591969785990', | |||
| 'time="2025-03-17T02:46:31.969Z" level=info msg="Starting Workflow Executor" version=v3.5.10\n', | |||
| ], | |||
| ], | |||
| }, | |||
| { | |||
| stream: { | |||
| filename: | |||
| '/var/log/pods/argo_workflow-p2ddj-git-clone-f33abcda-3988047653_e31cf6be-e013-4885-9eb6-ec84f83b9ba9/init/0.log', | |||
| job: 'argo/workflow-p2ddj-git-clone-f33abcda-3988047653', | |||
| namespace: 'argo', | |||
| pod: 'workflow-p2ddj-git-clone-f33abcda-3988047653', | |||
| stream: 'stderr', | |||
| workflows_argoproj_io_completed: 'false', | |||
| workflows_argoproj_io_workflow: 'workflow-p2ddj', | |||
| container: 'init', | |||
| }, | |||
| values: [ | |||
| [ | |||
| '1742179591973414064', | |||
| 'time="2025-03-17T02:46:31.973Z" level=info msg="Using executor retry strategy" Duration=1s Factor=1.6 Jitter=0.5 Steps=5\n', | |||
| ], | |||
| ], | |||
| }, | |||
| ], | |||
| }; | |||
| @@ -0,0 +1,112 @@ | |||
| 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 { createWebSocketMock } from './mock/websocket.mock'; | |||
| import './storybook.css'; | |||
| /* | |||
| * Initializes MSW | |||
| * See https://github.com/mswjs/msw-storybook-addon#configuring-msw | |||
| * to learn how to customize it | |||
| */ | |||
| initialize(); | |||
| // 替换全局 WebSocket 为 Mock 版本 | |||
| // @ts-ignore | |||
| global.WebSocket = createWebSocketMock(); | |||
| const preview: Preview = { | |||
| parameters: { | |||
| controls: { | |||
| expanded: true, | |||
| sort: 'requiredFirst', | |||
| matchers: { | |||
| color: /(background|color)$/i, | |||
| date: /Date$/i, | |||
| }, | |||
| }, | |||
| backgrounds: { | |||
| values: [ | |||
| { name: 'Dark', value: '#000' }, | |||
| { name: 'Gray', value: '#f9fafb' }, | |||
| { name: 'Light', value: '#FFF' }, | |||
| ], | |||
| default: 'Light', | |||
| }, | |||
| options: { | |||
| storySort: { | |||
| method: 'alphabetical', | |||
| order: ['Documentation', 'Components'], | |||
| }, | |||
| }, | |||
| }, | |||
| decorators: [ | |||
| (Story) => ( | |||
| <ConfigProvider | |||
| locale={zhCN} | |||
| theme={{ | |||
| cssVar: true, | |||
| token: { | |||
| colorPrimary: themes['primaryColor'], | |||
| colorSuccess: themes['successColor'], | |||
| colorError: themes['errorColor'], | |||
| colorWarning: themes['warningColor'], | |||
| colorLink: themes['primaryColor'], | |||
| colorText: themes['textColor'], | |||
| controlHeightLG: 46, | |||
| }, | |||
| 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']), | |||
| }, | |||
| Input: { | |||
| inputFontSize: parseInt(themes['fontSizeInput']), | |||
| inputFontSizeLG: parseInt(themes['fontSizeInputLg']), | |||
| paddingBlockLG: 10, | |||
| }, | |||
| Select: { | |||
| singleItemHeightLG: 46, | |||
| optionSelectedColor: themes['primaryColor'], | |||
| }, | |||
| Table: { | |||
| headerBg: 'rgba(242, 244, 247, 0.36)', | |||
| headerBorderRadius: 4, | |||
| rowSelectedBg: 'rgba(22, 100, 255, 0.05)', | |||
| }, | |||
| Tabs: { | |||
| titleFontSize: 16, | |||
| }, | |||
| Form: { | |||
| labelColor: 'rgba(29, 29, 32, 0.8);', | |||
| }, | |||
| Breadcrumb: { | |||
| iconFontSize: parseInt(themes['fontSize']), | |||
| linkColor: 'rgba(29, 29, 32, 0.7)', | |||
| separatorColor: 'rgba(29, 29, 32, 0.7)', | |||
| }, | |||
| }, | |||
| }} | |||
| > | |||
| <App message={{ maxCount: 3 }}> | |||
| <Story /> | |||
| </App> | |||
| </ConfigProvider> | |||
| ), | |||
| ], | |||
| loaders: [mswLoader], // 👈 Add the MSW loader to all stories | |||
| }; | |||
| export default preview; | |||
| @@ -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; | |||
| } | |||
| @@ -0,0 +1,7 @@ | |||
| import { create } from '@storybook/theming'; | |||
| export default create({ | |||
| base: 'light', | |||
| brandTitle: '组件库文档', | |||
| brandUrl: 'https://storybook.js.org/docs', | |||
| brandTarget: '_blank', | |||
| }); | |||
| @@ -0,0 +1,27 @@ | |||
| { | |||
| "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检查 | |||
| "importHelpers": true, | |||
| "baseUrl": "./" | |||
| } | |||
| } | |||
| @@ -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; | |||
| @@ -75,7 +75,7 @@ export default defineConfig({ | |||
| * @name layout 插件 | |||
| * @doc https://umijs.org/docs/max/layout-menu | |||
| */ | |||
| title: '复杂智能软件', | |||
| title: '智能材料科研平台', | |||
| layout: { | |||
| ...defaultSettings, | |||
| }, | |||
| @@ -17,7 +17,7 @@ const Settings: ProLayoutProps & { | |||
| fixSiderbar: false, | |||
| splitMenus: false, | |||
| colorWeak: false, | |||
| title: '复杂智能软件', | |||
| title: '智能材料科研平台', | |||
| pwa: true, | |||
| token: { | |||
| // 参见ts声明,demo 见文档,通过token 修改样式 | |||
| @@ -44,7 +44,7 @@ export default [ | |||
| { | |||
| name: 'login', | |||
| path: '/user/login', | |||
| component: './User/Login/login', | |||
| component: process.env.NO_SSO ? './User/Login/login' : './User/Login', | |||
| }, | |||
| ], | |||
| }, | |||
| @@ -181,6 +181,42 @@ export default [ | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| name: '超参数自动寻优', | |||
| path: 'hyperparameter', | |||
| routes: [ | |||
| { | |||
| name: '超参数寻优', | |||
| path: '', | |||
| component: './HyperParameter/List/index', | |||
| }, | |||
| { | |||
| name: '实验详情', | |||
| path: 'info/:id', | |||
| component: './HyperParameter/Info/index', | |||
| }, | |||
| { | |||
| name: '创建实验', | |||
| path: 'create', | |||
| component: './HyperParameter/Create/index', | |||
| }, | |||
| { | |||
| name: '编辑实验', | |||
| path: 'edit/:id', | |||
| component: './HyperParameter/Create/index', | |||
| }, | |||
| { | |||
| name: '复制实验', | |||
| path: 'copy/:id', | |||
| component: './HyperParameter/Create/index', | |||
| }, | |||
| { | |||
| name: '实验实例详情', | |||
| path: 'instance/:autoMLId/:id', | |||
| component: './HyperParameter/Instance/index', | |||
| }, | |||
| ], | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| @@ -255,60 +291,61 @@ export default [ | |||
| }, | |||
| ], | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| name: '模型部署', | |||
| path: '/modelDeployment', | |||
| routes: [ | |||
| { | |||
| name: '模型部署', | |||
| path: '', | |||
| component: './ModelDeployment/List', | |||
| }, | |||
| { | |||
| name: '创建推理服务', | |||
| path: 'createService', | |||
| component: './ModelDeployment/CreateService', | |||
| }, | |||
| { | |||
| name: '编辑推理服务', | |||
| path: 'editService/:serviceId', | |||
| component: './ModelDeployment/CreateService', | |||
| }, | |||
| { | |||
| name: '服务详情', | |||
| path: 'serviceInfo/:serviceId', | |||
| path: 'modelDeployment', | |||
| routes: [ | |||
| { | |||
| name: '服务详情', | |||
| name: '模型部署', | |||
| path: '', | |||
| component: './ModelDeployment/ServiceInfo', | |||
| component: './ModelDeployment/List', | |||
| }, | |||
| { | |||
| name: '新增服务版本', | |||
| path: 'createVersion', | |||
| component: './ModelDeployment/CreateVersion', | |||
| name: '创建推理服务', | |||
| path: 'createService', | |||
| component: './ModelDeployment/CreateService', | |||
| }, | |||
| { | |||
| name: '更新服务版本', | |||
| path: 'updateVersion', | |||
| component: './ModelDeployment/CreateVersion', | |||
| name: '编辑推理服务', | |||
| path: 'editService/:serviceId', | |||
| component: './ModelDeployment/CreateService', | |||
| }, | |||
| { | |||
| name: '重启服务版本', | |||
| path: 'restartVersion', | |||
| component: './ModelDeployment/CreateVersion', | |||
| }, | |||
| { | |||
| name: '服务版本详情', | |||
| path: 'versionInfo/:id', | |||
| component: './ModelDeployment/VersionInfo', | |||
| name: '服务详情', | |||
| path: 'serviceInfo/:serviceId', | |||
| routes: [ | |||
| { | |||
| name: '服务详情', | |||
| path: '', | |||
| component: './ModelDeployment/ServiceInfo', | |||
| }, | |||
| { | |||
| name: '新增服务版本', | |||
| path: 'createVersion', | |||
| component: './ModelDeployment/CreateVersion', | |||
| }, | |||
| { | |||
| name: '更新服务版本', | |||
| path: 'updateVersion', | |||
| component: './ModelDeployment/CreateVersion', | |||
| }, | |||
| { | |||
| name: '重启服务版本', | |||
| path: 'restartVersion', | |||
| component: './ModelDeployment/CreateVersion', | |||
| }, | |||
| { | |||
| name: '服务版本详情', | |||
| path: 'versionInfo/:id', | |||
| component: './ModelDeployment/VersionInfo', | |||
| }, | |||
| ], | |||
| }, | |||
| ], | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| name: '应用开发', | |||
| path: '/appsDeployment', | |||
| @@ -8,6 +8,7 @@ | |||
| "build": "max build", | |||
| "deploy": "npm run build && npm run gh-pages", | |||
| "dev": "npm run start:dev", | |||
| "dev-no-sso": "cross-env NO_SSO=true npm run start:dev", | |||
| "docker-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./", | |||
| "docker-prod:build": "docker-compose -f ./docker/docker-compose.yml build", | |||
| "docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up", | |||
| @@ -36,6 +37,10 @@ | |||
| "start:mock": "cross-env REACT_APP_ENV=dev UMI_ENV=dev max dev", | |||
| "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev", | |||
| "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev", | |||
| "storybook": "storybook dev -p 6006", | |||
| "storybook-build": "storybook build", | |||
| "storybook-docs": "storybook dev --docs", | |||
| "storybook-docs-build": "storybook build --docs", | |||
| "test": "jest", | |||
| "test:coverage": "npm run jest -- --coverage", | |||
| "test:update": "npm run jest -- -u", | |||
| @@ -83,6 +88,19 @@ | |||
| }, | |||
| "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/manager-api": "~8.6.0", | |||
| "@storybook/react": "~8.5.3", | |||
| "@storybook/react-webpack5": "~8.5.3", | |||
| "@storybook/test": "~8.5.3", | |||
| "@storybook/theming": "~8.6.0", | |||
| "@testing-library/react": "^14.0.0", | |||
| "@types/antd": "^1.0.0", | |||
| "@types/express": "^4.17.14", | |||
| @@ -96,15 +114,23 @@ | |||
| "@umijs/max": "^4.0.66", | |||
| "cross-env": "^7.0.3", | |||
| "eslint": "^8.39.0", | |||
| "eslint-plugin-react-hooks": "~5.2.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 +166,10 @@ | |||
| "CNAME", | |||
| "create-umi" | |||
| ] | |||
| }, | |||
| "msw": { | |||
| "workerDirectory": [ | |||
| "public" | |||
| ] | |||
| } | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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<GlobalInitialState> { | |||
| roleNames: response.user.roles, | |||
| } as API.CurrentUser; | |||
| } catch (error) { | |||
| console.error('1111', error); | |||
| console.error('getInitialState', error); | |||
| gotoLoginPage(); | |||
| } | |||
| return undefined; | |||
| @@ -169,7 +168,7 @@ export const onRouteChange: RuntimeConfig['onRouteChange'] = async (e) => { | |||
| } | |||
| }; | |||
| export const patchRoutes: RuntimeConfig['patchRoutes'] = (e) => { | |||
| export const patchRoutes: RuntimeConfig['patchRoutes'] = () => { | |||
| //console.log('patchRoutes', e); | |||
| }; | |||
| @@ -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'], | |||
| @@ -228,11 +227,12 @@ export const antd: RuntimeAntdConfig = (memo) => { | |||
| }; | |||
| 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)', | |||
| // rowSelectedBg: 'rgba(22, 100, 255, 0.05)', 固定列时,横向滑动导致重叠 | |||
| }; | |||
| memo.theme.components.Tabs = { | |||
| titleFontSize: 16, | |||
| @@ -245,9 +245,12 @@ export const antd: RuntimeAntdConfig = (memo) => { | |||
| linkColor: 'rgba(29, 29, 32, 0.7)', | |||
| separatorColor: 'rgba(29, 29, 32, 0.7)', | |||
| }; | |||
| memo.theme.components.Tree = { | |||
| directoryNodeSelectedBg: 'rgba(22, 100, 255, 0.7)', | |||
| }; | |||
| memo.theme.cssVar = true; | |||
| // memo.theme.hashed = false; | |||
| memo.theme.hashed = false; | |||
| memo.appConfig = { | |||
| message: { | |||
| @@ -0,0 +1,86 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-11-29 09:27:19 | |||
| * @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, | |||
| 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 = <div className={`${myClassName}__node`}>{formatValue}</div>; | |||
| } else if (Array.isArray(formatValue)) { | |||
| valueComponent = ( | |||
| <div className={`${myClassName}__value-container`}> | |||
| {formatValue.map((item: BasicInfoLink) => ( | |||
| <BasicInfoItemValue | |||
| key={item.value} | |||
| value={item.value} | |||
| link={item.link} | |||
| url={item.url} | |||
| ellipsis={ellipsis} | |||
| classPrefix={classPrefix} | |||
| /> | |||
| ))} | |||
| </div> | |||
| ); | |||
| } else if (typeof formatValue === 'object' && formatValue) { | |||
| valueComponent = ( | |||
| <BasicInfoItemValue | |||
| value={formatValue.value} | |||
| link={formatValue.link} | |||
| url={formatValue.url} | |||
| ellipsis={ellipsis} | |||
| classPrefix={classPrefix} | |||
| /> | |||
| ); | |||
| } else { | |||
| valueComponent = ( | |||
| <BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} /> | |||
| ); | |||
| } | |||
| return ( | |||
| <div className={myClassName} key={label}> | |||
| <div | |||
| className={`${myClassName}__label`} | |||
| style={{ width: labelWidth, textAlign: labelAlign, textAlignLast: labelAlign }} | |||
| > | |||
| <Typography.Text | |||
| ellipsis={labelEllipsis !== false ? { tooltip: label } : false} | |||
| style={{ width: labelAlign === 'justify' ? '100%' : 'auto' }} | |||
| > | |||
| {label} | |||
| </Typography.Text> | |||
| </div> | |||
| {valueComponent} | |||
| </div> | |||
| ); | |||
| } | |||
| export default BasicInfoItem; | |||
| @@ -0,0 +1,58 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-11-29 09:27:19 | |||
| * @Description: 用于 BasicInfoItem 的组件 | |||
| */ | |||
| import { isEmpty } from '@/utils'; | |||
| import { Link } from '@umijs/max'; | |||
| import { Typography } from 'antd'; | |||
| type BasicInfoItemValueProps = { | |||
| /** 值是否显示省略号 */ | |||
| ellipsis?: boolean; | |||
| /** 自定义类名前缀 */ | |||
| classPrefix: string; | |||
| /** 值 */ | |||
| value?: string; | |||
| /** 内部链接 */ | |||
| link?: string; | |||
| /** 外部链接 */ | |||
| url?: string; | |||
| }; | |||
| function BasicInfoItemValue({ | |||
| value, | |||
| link, | |||
| url, | |||
| classPrefix, | |||
| ellipsis = true, | |||
| }: BasicInfoItemValueProps) { | |||
| const myClassName = `${classPrefix}__item__value`; | |||
| let component = undefined; | |||
| if (url && value) { | |||
| component = ( | |||
| <a className={`${myClassName}__link`} href={url} target="_blank" rel="noopener noreferrer"> | |||
| {value} | |||
| </a> | |||
| ); | |||
| } else if (link && value) { | |||
| component = ( | |||
| <Link to={link} className={`${myClassName}__link`}> | |||
| {value} | |||
| </Link> | |||
| ); | |||
| } else { | |||
| component = <span className={`${myClassName}__text`}>{!isEmpty(value) ? value : '--'}</span>; | |||
| } | |||
| return ( | |||
| <div className={myClassName}> | |||
| <Typography.Text ellipsis={ellipsis !== false ? { tooltip: value } : false}> | |||
| {component} | |||
| </Typography.Text> | |||
| </div> | |||
| ); | |||
| } | |||
| export default BasicInfoItemValue; | |||
| @@ -1,113 +0,0 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-11-29 09:27:19 | |||
| * @Description: 用于 BasicInfo 和 BasicTableInfo 组件的子组件 | |||
| */ | |||
| import { Link } from '@umijs/max'; | |||
| import { Typography } from 'antd'; | |||
| import React from 'react'; | |||
| import { type BasicInfoData, type BasicInfoLink } from './types'; | |||
| type BasicInfoItemProps = { | |||
| data: BasicInfoData; | |||
| labelWidth: number; | |||
| classPrefix: string; | |||
| }; | |||
| export function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemProps) { | |||
| const { label, value, format, ellipsis } = data; | |||
| const formatValue = format ? format(value) : value; | |||
| const myClassName = `${classPrefix}__item`; | |||
| let valueComponent = undefined; | |||
| if (Array.isArray(formatValue)) { | |||
| valueComponent = ( | |||
| <div className={`${myClassName}__value-container`}> | |||
| {formatValue.map((item: BasicInfoLink) => ( | |||
| <BasicInfoItemValue | |||
| key={item.value} | |||
| value={item.value} | |||
| link={item.link} | |||
| url={item.url} | |||
| ellipsis={ellipsis} | |||
| classPrefix={classPrefix} | |||
| /> | |||
| ))} | |||
| </div> | |||
| ); | |||
| } else if (React.isValidElement(formatValue)) { | |||
| // 这个判断必须在下面的判断之前 | |||
| valueComponent = ( | |||
| <BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} /> | |||
| ); | |||
| } else if (typeof formatValue === 'object' && formatValue) { | |||
| valueComponent = ( | |||
| <BasicInfoItemValue | |||
| value={formatValue.value} | |||
| link={formatValue.link} | |||
| url={formatValue.url} | |||
| ellipsis={ellipsis} | |||
| classPrefix={classPrefix} | |||
| /> | |||
| ); | |||
| } else { | |||
| valueComponent = ( | |||
| <BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} /> | |||
| ); | |||
| } | |||
| return ( | |||
| <div className={myClassName} key={label}> | |||
| <div className={`${myClassName}__label`} style={{ width: labelWidth }}> | |||
| {label} | |||
| </div> | |||
| {valueComponent} | |||
| </div> | |||
| ); | |||
| } | |||
| type BasicInfoItemValueProps = { | |||
| ellipsis?: boolean; | |||
| classPrefix: string; | |||
| value: string | React.ReactNode; | |||
| link?: string; | |||
| url?: string; | |||
| }; | |||
| export function BasicInfoItemValue({ | |||
| value, | |||
| link, | |||
| url, | |||
| ellipsis, | |||
| classPrefix, | |||
| }: BasicInfoItemValueProps) { | |||
| const myClassName = `${classPrefix}__item__value`; | |||
| let component = undefined; | |||
| if (url && value) { | |||
| component = ( | |||
| <a className={`${myClassName}__link`} href={url} target="_blank" rel="noopener noreferrer"> | |||
| {value} | |||
| </a> | |||
| ); | |||
| } else if (link && value) { | |||
| component = ( | |||
| <Link to={link} className={`${myClassName}__link`}> | |||
| {value} | |||
| </Link> | |||
| ); | |||
| } else if (React.isValidElement(value)) { | |||
| return value; | |||
| } else { | |||
| component = <span className={`${myClassName}__text`}>{value ?? '--'}</span>; | |||
| } | |||
| return ( | |||
| <div className={myClassName}> | |||
| <Typography.Text | |||
| ellipsis={ellipsis ? { tooltip: value } : false} | |||
| style={{ fontSize: 'inherit' }} | |||
| > | |||
| {component} | |||
| </Typography.Text> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -1,48 +0,0 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-11-29 09:27:19 | |||
| * @Description: 用于 BasicInfo 和 BasicTableInfo 组件的常用转化格式 | |||
| */ | |||
| // 格式化日期 | |||
| export { formatDate } from '@/utils/date'; | |||
| /** | |||
| * 格式化字符串数组 | |||
| * @param value - 字符串数组 | |||
| * @returns 逗号分隔的字符串 | |||
| */ | |||
| export const formatList = (value: string[] | null | undefined): string => { | |||
| if ( | |||
| value === undefined || | |||
| value === null || | |||
| Array.isArray(value) === false || | |||
| value.length === 0 | |||
| ) { | |||
| return '--'; | |||
| } | |||
| return value.join(','); | |||
| }; | |||
| /** | |||
| * 格式化布尔值 | |||
| * @param value - 布尔值 | |||
| * @returns "是" 或 "否" | |||
| */ | |||
| export const formatBoolean = (value: boolean): string => { | |||
| return value ? '是' : '否'; | |||
| }; | |||
| type FormatEnum = (value: string | number) => string; | |||
| /** | |||
| * 格式化枚举 | |||
| * @param options - 枚举选项 | |||
| * @returns 格式化枚举函数 | |||
| */ | |||
| export const formatEnum = (options: { value: string | number; label: string }[]): FormatEnum => { | |||
| return (value: string | number) => { | |||
| const option = options.find((item) => item.value === value); | |||
| return option ? option.label : '--'; | |||
| }; | |||
| }; | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,27 +1,56 @@ | |||
| import classNames from 'classnames'; | |||
| import React from 'react'; | |||
| import { BasicInfoItem } from './components'; | |||
| import BasicInfoItem from './BasicInfoItem'; | |||
| import './index.less'; | |||
| import type { BasicInfoData, BasicInfoLink } from './types'; | |||
| export * from './format'; | |||
| 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, | |||
| labelWidth, | |||
| labelEllipsis = true, | |||
| labelAlign = 'start', | |||
| threeColumns = false, | |||
| className, | |||
| style, | |||
| }: BasicInfoProps) { | |||
| return ( | |||
| <div className={classNames('kf-basic-info', className)} style={style}> | |||
| <div | |||
| className={classNames( | |||
| 'kf-basic-info', | |||
| { 'kf-basic-info--three-columns': threeColumns }, | |||
| className, | |||
| )} | |||
| style={style} | |||
| > | |||
| {datas.map((item) => ( | |||
| <BasicInfoItem | |||
| key={item.label} | |||
| data={item} | |||
| labelWidth={labelWidth} | |||
| classPrefix="kf-basic-info" | |||
| labelEllipsis={labelEllipsis} | |||
| labelAlign={labelAlign} | |||
| /> | |||
| ))} | |||
| </div> | |||
| @@ -3,12 +3,12 @@ export type BasicInfoData = { | |||
| label: string; | |||
| value?: any; | |||
| ellipsis?: boolean; | |||
| format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined; | |||
| format?: (_value?: any) => string | React.ReactNode | BasicInfoLink | BasicInfoLink[] | undefined; | |||
| }; | |||
| // 值为链接的类型 | |||
| export type BasicInfoLink = { | |||
| value: string; | |||
| value?: string; | |||
| link?: string; | |||
| url?: string; | |||
| }; | |||
| @@ -4,7 +4,7 @@ | |||
| flex-wrap: wrap; | |||
| align-items: stretch; | |||
| width: 100%; | |||
| border: 1px solid @border-color-base; | |||
| border: 1px solid @border-color; | |||
| border-bottom: none; | |||
| border-radius: 4px; | |||
| @@ -12,7 +12,7 @@ | |||
| display: flex; | |||
| align-items: stretch; | |||
| width: 25%; | |||
| border-bottom: 1px solid @border-color-base; | |||
| border-bottom: 1px solid @border-color; | |||
| &__label { | |||
| flex: none; | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,22 +1,21 @@ | |||
| import classNames from 'classnames'; | |||
| import { BasicInfoItem } from '../BasicInfo/components'; | |||
| import { BasicInfoProps } from '../BasicInfo'; | |||
| import BasicInfoItem from '../BasicInfo/BasicInfoItem'; | |||
| import { type BasicInfoData, type BasicInfoLink } from '../BasicInfo/types'; | |||
| import './index.less'; | |||
| export * from '../BasicInfo/format'; | |||
| export type { BasicInfoData, BasicInfoLink }; | |||
| type BasicTableInfoProps = { | |||
| datas: BasicInfoData[]; | |||
| className?: string; | |||
| style?: React.CSSProperties; | |||
| labelWidth: number; | |||
| }; | |||
| export type BasicTableInfoProps = Omit<BasicInfoProps, 'labelAlign' | 'threeColumns'>; | |||
| /** | |||
| * 表格基础信息展示组件,用于展示基础信息,一行四列,支持数据格式化 | |||
| */ | |||
| export default function BasicTableInfo({ | |||
| datas, | |||
| labelWidth, | |||
| labelEllipsis, | |||
| className, | |||
| style, | |||
| labelWidth, | |||
| }: BasicTableInfoProps) { | |||
| const remainder = datas.length % 4; | |||
| const array = []; | |||
| @@ -24,7 +23,7 @@ export default function BasicTableInfo({ | |||
| for (let i = 0; i < 4 - remainder; i++) { | |||
| array.push({ | |||
| label: '', | |||
| value: '', | |||
| value: false, // 用于区分是否是空数据,不能是空字符串、null、undefined | |||
| }); | |||
| } | |||
| } | |||
| @@ -37,6 +36,7 @@ export default function BasicTableInfo({ | |||
| key={`${item.label}-${index}`} | |||
| data={item} | |||
| labelWidth={labelWidth} | |||
| labelEllipsis={labelEllipsis} | |||
| classPrefix="kf-basic-table-info" | |||
| /> | |||
| ))} | |||
| @@ -4,59 +4,82 @@ | |||
| * @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 classNames from 'classnames'; | |||
| import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; | |||
| import './index.less'; | |||
| export { requiredValidator, type ParameterInputObject } from '../ParameterInput'; | |||
| export { | |||
| requiredValidator, | |||
| type ParameterInputObject, | |||
| type ParameterInputValue, | |||
| } from '../ParameterInput'; | |||
| type CodeSelectProps = ParameterInputProps; | |||
| function CodeSelect({ value, onChange, disabled, ...rest }: CodeSelectProps) { | |||
| /** 代码配置选择表单组件 */ | |||
| function CodeSelect({ | |||
| value, | |||
| size, | |||
| disabled, | |||
| className, | |||
| style, | |||
| onChange, | |||
| ...rest | |||
| }: CodeSelectProps) { | |||
| // 选择代码配置 | |||
| const selectResource = () => { | |||
| const { close } = openAntdModal(CodeSelectorModal, { | |||
| onOk: (res) => { | |||
| if (res) { | |||
| const { git_url, git_branch, code_repo_name } = res; | |||
| const { id, code_repo_name, git_url, git_branch, git_user_name, git_password, ssh_key } = | |||
| res; | |||
| const jsonObj = { | |||
| id, | |||
| name: code_repo_name, | |||
| code_path: git_url, | |||
| branch: git_branch, | |||
| username: git_user_name, | |||
| password: git_password, | |||
| ssh_private_key: ssh_key, | |||
| }; | |||
| const jsonObjStr = JSON.stringify(jsonObj); | |||
| const showValue = code_repo_name; | |||
| onChange?.({ | |||
| value: jsonObjStr, | |||
| showValue, | |||
| showValue: code_repo_name, | |||
| fromSelect: true, | |||
| ...jsonObj, | |||
| }); | |||
| } else { | |||
| onChange?.({ | |||
| value: undefined, | |||
| showValue: undefined, | |||
| fromSelect: false, | |||
| }); | |||
| onChange?.(undefined); | |||
| } | |||
| close(); | |||
| }, | |||
| }); | |||
| }; | |||
| // 删除 | |||
| const handleRemove = () => { | |||
| onChange?.(undefined); | |||
| }; | |||
| return ( | |||
| <div className="kf-code-select"> | |||
| <div className={classNames('kf-code-select', className)} style={style}> | |||
| <ParameterInput | |||
| {...rest} | |||
| size={size} | |||
| disabled={disabled} | |||
| value={value} | |||
| onChange={onChange} | |||
| onClick={selectResource} | |||
| onRemove={handleRemove} | |||
| ></ParameterInput> | |||
| <Button | |||
| className="kf-code-select__button" | |||
| size="large" | |||
| size={size} | |||
| type="link" | |||
| icon={<KFIcon type="icon-xuanzedaimapeizhi" font={16} />} | |||
| disabled={disabled} | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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<ModalProps, 'onOk'> { | |||
| onOk?: (params: CodeConfigData | undefined) => void; | |||
| } | |||
| /** 选择代码配置的弹窗,推荐使用函数的方式打开 */ | |||
| function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||
| const [dataList, setDataList] = useState<CodeConfigData[]>([]); | |||
| const [total, setTotal] = useState(0); | |||
| @@ -32,23 +33,23 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||
| const [inputText, setInputText] = useState<string | undefined>(undefined); | |||
| useEffect(() => { | |||
| // 获取数据请求 | |||
| const getDataList = async () => { | |||
| const params = { | |||
| page: pagination.current! - 1, | |||
| size: pagination.pageSize, | |||
| code_repo_name: searchText || undefined, | |||
| }; | |||
| const [res] = await to(getCodeConfigListReq(params)); | |||
| if (res && res.data && res.data.content) { | |||
| setDataList(res.data.content); | |||
| setTotal(res.data.totalElements); | |||
| } | |||
| }; | |||
| getDataList(); | |||
| }, [pagination, searchText]); | |||
| // 获取数据请求 | |||
| const getDataList = async () => { | |||
| const params = { | |||
| page: pagination.current! - 1, | |||
| size: pagination.pageSize, | |||
| code_repo_name: searchText || undefined, | |||
| }; | |||
| const [res] = await to(getCodeConfigListReq(params)); | |||
| if (res && res.data && res.data.content) { | |||
| setDataList(res.data.content); | |||
| setTotal(res.data.totalElements); | |||
| } | |||
| }; | |||
| // 搜索 | |||
| const handleSearch = (value: string) => { | |||
| setSearchText(value); | |||
| @@ -79,9 +80,9 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||
| footer={null} | |||
| destroyOnClose | |||
| > | |||
| <div className={styles['code-selector']}> | |||
| <div className="kf-code-selector-modal"> | |||
| <Input.Search | |||
| className={styles['code-selector__search']} | |||
| className="kf-code-selector-modal__search" | |||
| placeholder="按代码仓库名称筛选" | |||
| allowClear | |||
| onSearch={handleSearch} | |||
| @@ -90,12 +91,15 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||
| suffix={null} | |||
| value={inputText} | |||
| prefix={ | |||
| <Icon icon="local:magnifying-glass" style={{ marginLeft: '10px', marginTop: '2px' }} /> | |||
| <KFIcon type="icon-sousuo" color="rgba(22,100,255,0.4" style={{ marginLeft: '10px' }} /> | |||
| } | |||
| // prefix={ | |||
| // <Icon icon="local:magnifying-glass" style={{ marginLeft: '10px', marginTop: '2px' }} /> | |||
| // } | |||
| /> | |||
| {dataList?.length !== 0 ? ( | |||
| <> | |||
| <div className={styles['code-selector__content']}> | |||
| <div className="kf-code-selector-modal__content"> | |||
| {dataList?.map((item) => ( | |||
| <CodeConfigItem item={item} key={item.id} onClick={handleClick} /> | |||
| ))} | |||
| @@ -112,7 +116,7 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||
| /> | |||
| </> | |||
| ) : ( | |||
| <div className={styles['code-selector__empty']}> | |||
| <div className="kf-code-selector-modal__empty"> | |||
| <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}></Empty> | |||
| </div> | |||
| )} | |||
| @@ -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 ( | |||
| <InfoGroup title={title} className={classNames('kf-config-info', className)} style={style}> | |||
| <div className={'kf-config-info__content'}> | |||
| <BasicInfo | |||
| datas={datas} | |||
| labelWidth={labelWidth} | |||
| labelAlign={labelAlign} | |||
| labelEllipsis={labelEllipsis} | |||
| threeColumns={threeColumns} | |||
| /> | |||
| {children} | |||
| </div> | |||
| </InfoGroup> | |||
| ); | |||
| } | |||
| export default ConfigInfo; | |||
| @@ -1,9 +0,0 @@ | |||
| .disabled-input { | |||
| padding: 4px 11px; | |||
| color: rgba(0, 0, 0, 0.25); | |||
| font-size: @font-size-input; | |||
| background-color: rgba(0, 0, 0, 0.04); | |||
| border: 1px solid #d9d9d9; | |||
| border-radius: 6px; | |||
| cursor: not-allowed; | |||
| } | |||
| @@ -1,20 +0,0 @@ | |||
| import { Typography } from 'antd'; | |||
| import styles from './index.less'; | |||
| type DisabledInputProps = { | |||
| value?: any; | |||
| valuePropName?: string; | |||
| }; | |||
| function DisabledInput({ value, valuePropName }: DisabledInputProps) { | |||
| const data = valuePropName ? value[valuePropName] : value; | |||
| return ( | |||
| <div className={styles['disabled-input']}> | |||
| <Typography.Text ellipsis={{ tooltip: data }} style={{ color: 'inherit' }}> | |||
| {data} | |||
| </Typography.Text> | |||
| </div> | |||
| ); | |||
| } | |||
| export default DisabledInput; | |||
| @@ -0,0 +1,19 @@ | |||
| .form-info { | |||
| min-height: 32px; | |||
| padding: 4px 11px; | |||
| color: @text-disabled-color; | |||
| font-size: @font-size-input; | |||
| background-color: rgba(0, 0, 0, 0.04); | |||
| border: 1px solid #d9d9d9; | |||
| border-radius: 6px; | |||
| .ant-typography { | |||
| margin: 0 !important; | |||
| } | |||
| } | |||
| .form-info--multiline { | |||
| .ant-typography { | |||
| white-space: pre-wrap; | |||
| } | |||
| } | |||
| @@ -0,0 +1,74 @@ | |||
| import { formatEnum } from '@/utils/format'; | |||
| import { Typography, type SelectProps } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import './index.less'; | |||
| type FormInfoProps = { | |||
| /** 值 */ | |||
| value?: any; | |||
| /** 如果 `value` 是对象,取对象的哪个属性作为值 */ | |||
| valuePropName?: string; | |||
| /** 是否是多行文本 */ | |||
| textArea?: boolean; | |||
| /** 是否是下拉框 */ | |||
| select?: boolean; | |||
| /** 下拉框数据 */ | |||
| options?: SelectProps['options']; | |||
| /** 自定义节点 label、value 的字段 */ | |||
| fieldNames?: SelectProps['fieldNames']; | |||
| /** 自定义类名 */ | |||
| className?: string; | |||
| /** 自定义样式 */ | |||
| style?: React.CSSProperties; | |||
| }; | |||
| /** | |||
| * 模拟禁用的输入框,但是内容超长时,hover 时显示所有内容 | |||
| */ | |||
| function FormInfo({ | |||
| value, | |||
| valuePropName, | |||
| textArea = false, | |||
| select = false, | |||
| options, | |||
| fieldNames, | |||
| className, | |||
| style, | |||
| }: FormInfoProps) { | |||
| let showValue = value; | |||
| if (value && typeof value === 'object' && valuePropName) { | |||
| showValue = value[valuePropName]; | |||
| } else if (select === true && options) { | |||
| let _options: SelectProps['options'] = options; | |||
| if (fieldNames) { | |||
| _options = options.map((v) => { | |||
| return { | |||
| ...v, | |||
| label: fieldNames.label && v[fieldNames.label], | |||
| value: fieldNames.value && v[fieldNames.value], | |||
| options: fieldNames.options && v[fieldNames.options], | |||
| }; | |||
| }); | |||
| } | |||
| showValue = formatEnum(_options)(value); | |||
| } | |||
| return ( | |||
| <div | |||
| className={classNames( | |||
| 'form-info', | |||
| { | |||
| 'form-info--multiline': textArea, | |||
| }, | |||
| className, | |||
| )} | |||
| style={style} | |||
| > | |||
| <Typography.Paragraph ellipsis={textArea ? false : { tooltip: showValue }}> | |||
| {showValue} | |||
| </Typography.Paragraph> | |||
| </div> | |||
| ); | |||
| } | |||
| export default FormInfo; | |||
| @@ -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<HTMLIFrameElement, Event>) => void; | |||
| onerror?: (e?: React.SyntheticEvent<HTMLIFrameElement, Event>) => void; | |||
| /** 加载完成回调 */ | |||
| onLoad?: (e?: React.SyntheticEvent<HTMLIFrameElement, Event>) => void; | |||
| /** 加载失败回调 */ | |||
| onError?: (e?: React.SyntheticEvent<HTMLIFrameElement, Event>) => void; | |||
| }; | |||
| function FullScreenFrame({ url, className, style, onload, onerror }: FullScreenFrameProps) { | |||
| /** | |||
| * 全屏 iframe,IFramePage 组件的子组件,开发中应该使用 IFramePage | |||
| */ | |||
| function FullScreenFrame({ url, className, style, onLoad, onError }: FullScreenFrameProps) { | |||
| return ( | |||
| <div className={classNames('kf-full-screen-frame', className ?? '')} style={style}> | |||
| {url && ( | |||
| <iframe | |||
| src={url} | |||
| className="kf-full-screen-frame__iframe" | |||
| onLoad={onload} | |||
| onError={onerror} | |||
| onLoad={onLoad} | |||
| onError={onError} | |||
| ></iframe> | |||
| )} | |||
| </div> | |||
| @@ -1,6 +1,6 @@ | |||
| import FullScreenFrame from '@/components/FullScreenFrame'; | |||
| import KFSpin from '@/components/KFSpin'; | |||
| // import { getLabelStudioUrl } from '@/services/developmentEnvironment'; | |||
| import { getLabelStudioUrl } from '@/services/developmentEnvironment'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import classNames from 'classnames'; | |||
| @@ -12,52 +12,53 @@ export enum IframePageType { | |||
| DatasetAnnotation = 'DatasetAnnotation', // 数据标注 | |||
| AppDevelopment = 'AppDevelopment', // 应用开发 | |||
| DevEnv = 'DevEnv', // 开发环境 | |||
| GitLink = 'GitLink', | |||
| GitLink = 'GitLink', // git link | |||
| } | |||
| const getRequestAPI = (type: IframePageType): (() => Promise<any>) => { | |||
| switch (type) { | |||
| case IframePageType.DatasetAnnotation: | |||
| return () => Promise.resolve({ code: 200, data: 'http://172.20.32.181:18888/oauth/login' }); //getLabelStudioUrl; | |||
| case IframePageType.AppDevelopment: | |||
| case IframePageType.DatasetAnnotation: // 数据标注 | |||
| return getLabelStudioUrl; | |||
| 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); | |||
| useEffect(() => { | |||
| requestIframeUrl(); | |||
| return () => { | |||
| if (type === IframePageType.DevEnv) { | |||
| SessionStorage.removeItem(SessionStorage.editorUrlKey); | |||
| const requestIframeUrl = async () => { | |||
| setLoading(true); | |||
| const [res] = await to(getRequestAPI(type)()); | |||
| if (res && res.data) { | |||
| setIframeUrl(res.data); | |||
| } else { | |||
| setLoading(false); | |||
| } | |||
| }; | |||
| }, []); | |||
| const requestIframeUrl = async () => { | |||
| setLoading(true); | |||
| const [res] = await to(getRequestAPI(type)()); | |||
| if (res && res.data) { | |||
| setIframeUrl(res.data); | |||
| } else { | |||
| setLoading(false); | |||
| } | |||
| }; | |||
| requestIframeUrl(); | |||
| }, [type]); | |||
| const hideLoading = () => { | |||
| setLoading(false); | |||
| @@ -66,7 +67,7 @@ function IframePage({ type, className, style }: IframePageProps) { | |||
| return ( | |||
| <div className={classNames('kf-iframe-page', className)} style={style}> | |||
| {loading && createPortal(<KFSpin size="large" />, document.body)} | |||
| <FullScreenFrame url={iframeUrl} onload={hideLoading} onerror={hideLoading} /> | |||
| <FullScreenFrame url={iframeUrl} onLoad={hideLoading} onError={hideLoading} /> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -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; | |||
| @@ -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 ( | |||
| <Flex align="center" className={classNames('kf-info-group-title', className)} style={style}> | |||
| @@ -4,7 +4,7 @@ | |||
| &__content { | |||
| padding: 20px @content-padding; | |||
| background-color: white; | |||
| border: 1px solid @border-color-base; | |||
| border: 1px solid @border-color; | |||
| border-top: none; | |||
| border-radius: 0 0 4px 4px; | |||
| } | |||
| @@ -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) { | |||
| @@ -33,7 +33,7 @@ | |||
| margin-top: 20px; | |||
| margin-bottom: 30px; | |||
| &__back-btn { | |||
| &__button { | |||
| height: 32px; | |||
| } | |||
| } | |||
| @@ -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() | |||
| ) : ( | |||
| <Button className="kf-empty__footer__back-btn" type="primary" onClick={onRefresh}> | |||
| <Button className="kf-empty__footer__button" type="primary" onClick={onButtonClick}> | |||
| {buttonTitle} | |||
| </Button> | |||
| )} | |||
| @@ -14,14 +14,20 @@ const Icon = createFromIconfontCN({ | |||
| type IconFontProps = Parameters<typeof Icon>[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, | |||
| @@ -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; | |||
| }; | |||
| @@ -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={<ModalTitle title={title} image={image}></ModalTitle>} | |||
| title={<KFModalTitle title={title} image={image} />} | |||
| > | |||
| {children} | |||
| </Modal> | |||
| @@ -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 ( | |||
| <span className={classNames('kf-radio', className)} style={style}> | |||
| {items.map((item) => { | |||
| return ( | |||
| <span | |||
| key={item.key} | |||
| key={item.value} | |||
| className={ | |||
| value === item.key | |||
| value === item.value | |||
| ? classNames('kf-radio__item', 'kf-radio__item--active') | |||
| : 'kf-radio__item' | |||
| } | |||
| onClick={() => onChange?.(item.key)} | |||
| onClick={() => onChange?.(item.value)} | |||
| > | |||
| {item.icon} | |||
| <span style={{ marginLeft: '5px' }}>{item.title}</span> | |||
| @@ -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 ( | |||
| <div className={styles['kf-spin']}> | |||
| <Spin {...props} /> | |||
| <div className={styles['kf-spin__label']}>加载中</div> | |||
| <div className={'kf-spin'}> | |||
| <Spin {...rest} /> | |||
| <div className={'kf-spin__label'}>{label}</div> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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 ( | |||
| <div className={classNames('kf-label-value', className)} style={style}> | |||
| <div className="kf-label-value__label">{label}</div> | |||
| <div className="kf-label-value__value">{value ?? '--'}</div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default LabelValue; | |||
| @@ -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; | |||
| @@ -12,7 +12,9 @@ import { useEffect, useState } from 'react'; | |||
| import styles from './index.less'; | |||
| interface MenuIconSelectorProps extends Omit<ModalProps, 'onOk'> { | |||
| /** 选中的图标 */ | |||
| 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<IconObject[]>([]); | |||
| useEffect(() => { | |||
| @@ -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 ( | |||
| <div className={classNames('kf-page-title', className)} style={style}> | |||
| @@ -39,7 +39,7 @@ | |||
| &__placeholder { | |||
| min-height: 22px; | |||
| color: rgba(0, 0, 0, 0.25); | |||
| color: @text-placeholder-color; | |||
| font-size: @font-size-input; | |||
| line-height: 1.5714285714285714; | |||
| } | |||
| @@ -49,18 +49,31 @@ | |||
| padding: 10px 11px; | |||
| font-size: @font-size-input-lg; | |||
| .parameter-input__placeholder { | |||
| .parameter-input__placeholder, | |||
| .parameter-input__content__value { | |||
| min-height: 24px; | |||
| font-size: @font-size-input-lg; | |||
| line-height: 1.5; | |||
| } | |||
| .parameter-input__content__close-icon { | |||
| font-size: 12px; | |||
| } | |||
| } | |||
| .parameter-input.parameter-input--small { | |||
| padding: 0 7px; | |||
| font-size: @font-size-input; | |||
| .parameter-input__placeholder, | |||
| .parameter-input__content__value { | |||
| font-size: @font-size-input-lg; | |||
| line-height: 1.5; | |||
| min-height: 22px; | |||
| font-size: @font-size-input; | |||
| line-height: 1.5714285714285714; | |||
| } | |||
| .parameter-input__content__close-icon { | |||
| font-size: 12px; | |||
| font-size: 10px; | |||
| } | |||
| } | |||
| @@ -1,21 +1,22 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-04-16 08:42:57 | |||
| * @Description: 参数输入组件 | |||
| * @Description: 参数输入表单组件,支持手动输入,也支持选择全局参数 | |||
| */ | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { CloseOutlined } from '@ant-design/icons'; | |||
| import { Form, Input } from 'antd'; | |||
| import { ConfigProvider, Form, Input } from 'antd'; | |||
| import { RuleObject } from 'antd/es/form'; | |||
| import classNames from 'classnames'; | |||
| import './index.less'; | |||
| // 对象 | |||
| // 如果值是对象时的类型 | |||
| export type ParameterInputObject = { | |||
| value?: any; // 值 | |||
| showValue?: any; // 显示值 | |||
| fromSelect?: boolean; // 是否来自选择 | |||
| activeTab?: string; // 选择镜像、数据集、模型时,保存当前激活的tab | |||
| activeTab?: CommonTabKeys; // 选择镜像、数据集、模型时,保存当前激活的tab | |||
| expandedKeys?: string[]; // 选择镜像、数据集、模型时,保存展开的keys | |||
| checkedKeys?: string[]; // 选择镜像、数据集、模型时,保存选中的keys | |||
| [key: string]: any; | |||
| @@ -25,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; | |||
| } | |||
| @@ -50,7 +67,7 @@ function ParameterInput({ | |||
| allowClear, | |||
| className, | |||
| style, | |||
| size = 'middle', | |||
| size, | |||
| disabled = false, | |||
| id, | |||
| ...rest | |||
| @@ -64,10 +81,17 @@ function ParameterInput({ | |||
| const placeholder = valueObj?.placeholder || rest?.placeholder; | |||
| const InputComponent = textArea ? Input.TextArea : Input; | |||
| const { status } = Form.Item.useStatus(); | |||
| const { componentSize } = ConfigProvider.useConfig(); | |||
| const mySize = size || componentSize; | |||
| // 删除 | |||
| const handleRemove = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => { | |||
| e.stopPropagation(); | |||
| if (onRemove) { | |||
| onRemove(); | |||
| return; | |||
| } | |||
| onChange?.({ | |||
| ...valueObj, | |||
| value: undefined, | |||
| @@ -77,7 +101,6 @@ function ParameterInput({ | |||
| expandedKeys: [], | |||
| checkedKeys: [], | |||
| }); | |||
| onRemove?.(); | |||
| }; | |||
| return ( | |||
| @@ -87,7 +110,8 @@ function ParameterInput({ | |||
| id={id} | |||
| className={classNames( | |||
| 'parameter-input', | |||
| { 'parameter-input--large': size === 'large' }, | |||
| { 'parameter-input--large': mySize === 'large' }, | |||
| { 'parameter-input--small': mySize === 'small' }, | |||
| { [`parameter-input--${status}`]: status }, | |||
| className, | |||
| )} | |||
| @@ -110,7 +134,7 @@ function ParameterInput({ | |||
| <InputComponent | |||
| {...rest} | |||
| id={id} | |||
| size={size} | |||
| size={mySize} | |||
| className={className} | |||
| style={style} | |||
| placeholder={placeholder} | |||
| @@ -1,21 +1,10 @@ | |||
| import { filterResourceStandard, resourceFieldNames } from '@/hooks/resource'; | |||
| import { ServiceData } from '@/pages/ModelDeployment/types'; | |||
| import { getDatasetList, getModelList } from '@/services/dataset/index.js'; | |||
| import { getServiceListReq } from '@/services/modelDeployment'; | |||
| import { getComputingResourceReq } from '@/services/pipeline'; | |||
| import { ComputingResource } from '@/types'; | |||
| import { type SelectProps } from 'antd'; | |||
| import { pick } from 'lodash'; | |||
| // 过滤资源规格 | |||
| const filterResourceStandard: SelectProps<string, ComputingResource>['filterOption'] = ( | |||
| input: string, | |||
| option?: ComputingResource, | |||
| ) => { | |||
| return ( | |||
| option?.computing_resource?.toLocaleLowerCase()?.includes(input.toLocaleLowerCase()) ?? false | |||
| ); | |||
| }; | |||
| // id 从 number 转换为 string | |||
| const convertId = (item: any) => ({ | |||
| ...item, | |||
| @@ -86,17 +75,10 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = { | |||
| }, | |||
| resource: { | |||
| getOptions: async () => { | |||
| const res = await getComputingResourceReq({ | |||
| page: 0, | |||
| size: 1000, | |||
| resource_type: '', | |||
| }); | |||
| return res?.data?.content ?? []; | |||
| }, | |||
| fieldNames: { | |||
| label: 'description', | |||
| value: 'standard', | |||
| // 不需要这个函数 | |||
| return []; | |||
| }, | |||
| fieldNames: resourceFieldNames, | |||
| filterOption: filterResourceStandard as SelectProps['filterOption'], | |||
| }, | |||
| }; | |||
| @@ -1,60 +1,95 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-04-16 08:42:57 | |||
| * @Description: 参数选择组件 | |||
| * @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务 | |||
| */ | |||
| import { PipelineNodeModelParameter } from '@/types'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Select } from 'antd'; | |||
| import { Select, type SelectProps } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import FormInfo from '../FormInfo'; | |||
| import { paramSelectConfig } from './config'; | |||
| type ParameterSelectProps = { | |||
| value?: PipelineNodeModelParameter; | |||
| onChange?: (value: PipelineNodeModelParameter) => void; | |||
| disabled?: boolean; | |||
| export type ParameterSelectObject = { | |||
| value: any; | |||
| [key: string]: any; | |||
| }; | |||
| function ParameterSelect({ value, onChange, disabled = false }: ParameterSelectProps) { | |||
| const [options, setOptions] = useState([]); | |||
| const valueNonNullable = value ?? ({} as PipelineNodeModelParameter); | |||
| const { item_type } = valueNonNullable; | |||
| const propsConfig = paramSelectConfig[item_type]; | |||
| export interface ParameterSelectProps extends SelectProps { | |||
| /** 类型 */ | |||
| dataType: 'dataset' | 'model' | 'service' | 'resource'; | |||
| /** 是否只是展示信息 */ | |||
| display?: boolean; | |||
| /** 值,支持对象,对象必须包含 value */ | |||
| value?: string | ParameterSelectObject; | |||
| /** 修改后回调 */ | |||
| onChange?: (value: string | ParameterSelectObject) => void; | |||
| } | |||
| /** 参数选择器,支持资源规格、数据集、模型、服务 */ | |||
| function ParameterSelect({ | |||
| dataType, | |||
| display = false, | |||
| value, | |||
| onChange, | |||
| ...rest | |||
| }: ParameterSelectProps) { | |||
| const [options, setOptions] = useState<SelectProps['options']>([]); | |||
| const propsConfig = paramSelectConfig[dataType]; | |||
| const valueText = typeof value === 'object' && value !== null ? value.value : value; | |||
| const [resourceStandardList] = useComputingResource(); | |||
| useEffect(() => { | |||
| // 获取下拉数据 | |||
| const getSelectOptions = async () => { | |||
| if (!propsConfig) { | |||
| return; | |||
| } | |||
| const getOptions = propsConfig.getOptions; | |||
| const [res] = await to(getOptions()); | |||
| if (res) { | |||
| setOptions(res); | |||
| } | |||
| }; | |||
| getSelectOptions(); | |||
| }, []); | |||
| }, [propsConfig]); | |||
| const hangleChange = (e: string) => { | |||
| onChange?.({ | |||
| ...valueNonNullable, | |||
| value: e, | |||
| }); | |||
| }; | |||
| const selectOptions = dataType === 'resource' ? resourceStandardList : options; | |||
| // 获取下拉数据 | |||
| const getSelectOptions = async () => { | |||
| if (!propsConfig) { | |||
| return; | |||
| } | |||
| const getOptions = propsConfig.getOptions; | |||
| const [res] = await to(getOptions()); | |||
| if (res) { | |||
| setOptions(res); | |||
| const handleChange = (text: string) => { | |||
| if (typeof value === 'object' && value !== null) { | |||
| onChange?.({ | |||
| ...value, | |||
| value: text, | |||
| }); | |||
| } else { | |||
| onChange?.(text); | |||
| } | |||
| }; | |||
| // 只用于展示,FormInfo 组件带有 Tooltip | |||
| if (display) { | |||
| return ( | |||
| <FormInfo | |||
| select | |||
| value={valueText} | |||
| options={selectOptions} | |||
| fieldNames={propsConfig?.fieldNames} | |||
| ></FormInfo> | |||
| ); | |||
| } | |||
| return ( | |||
| <Select | |||
| placeholder={valueNonNullable.placeholder} | |||
| {...rest} | |||
| filterOption={propsConfig?.filterOption} | |||
| options={options} | |||
| options={selectOptions} | |||
| fieldNames={propsConfig?.fieldNames} | |||
| value={valueNonNullable.value} | |||
| optionFilterProp={propsConfig.optionFilterProp} | |||
| onChange={hangleChange} | |||
| disabled={disabled} | |||
| optionFilterProp={propsConfig?.optionFilterProp} | |||
| value={valueText} | |||
| onChange={handleChange} | |||
| showSearch | |||
| allowClear | |||
| /> | |||
| @@ -9,39 +9,76 @@ import ResourceSelectorModal, { | |||
| ResourceSelectorResponse, | |||
| ResourceSelectorType, | |||
| selectorTypeConfig, | |||
| } from '@/pages/Pipeline/components/ResourceSelectorModal'; | |||
| } from '@/components/ResourceSelectorModal'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { Button } from 'antd'; | |||
| import { useState } from 'react'; | |||
| import { Button, ConfigProvider } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { pick } from 'lodash'; | |||
| import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; | |||
| import './index.less'; | |||
| export { requiredValidator, type ParameterInputObject } from '../ParameterInput'; | |||
| export { | |||
| requiredValidator, | |||
| type ParameterInputObject, | |||
| type ParameterInputValue, | |||
| } from '../ParameterInput'; | |||
| export { ResourceSelectorType, selectorTypeConfig, type ResourceSelectorResponse }; | |||
| type ResourceSelectProps = { | |||
| interface ResourceSelectProps extends ParameterInputProps { | |||
| /** 类型,数据集、模型、镜像 */ | |||
| type: ResourceSelectorType; | |||
| } & ParameterInputProps; | |||
| } | |||
| // 获取选择数据集、模型后面按钮 icon | |||
| // 获取选择数据集、模型、镜像后面按钮 icon | |||
| const getSelectBtnIcon = (type: ResourceSelectorType) => { | |||
| return <KFIcon type={selectorTypeConfig[type].buttonIcon} font={16} />; | |||
| }; | |||
| function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSelectProps) { | |||
| const [selectedResource, setSelectedResource] = useState<ResourceSelectorResponse | undefined>( | |||
| undefined, | |||
| ); | |||
| /** 数据集、模型、镜像选择表单组件 */ | |||
| function ResourceSelect({ | |||
| type, | |||
| value, | |||
| size, | |||
| disabled, | |||
| className, | |||
| style, | |||
| onChange, | |||
| ...rest | |||
| }: ResourceSelectProps) { | |||
| const { componentSize } = ConfigProvider.useConfig(); | |||
| const mySize = size || componentSize; | |||
| let selectedResource: ResourceSelectorResponse | undefined = undefined; | |||
| if ( | |||
| value && | |||
| typeof value === 'object' && | |||
| value.activeTab && | |||
| value.id && | |||
| value.name && | |||
| value.version && | |||
| value.path && | |||
| (type === ResourceSelectorType.Mirror || (value.identifier && value.owner)) | |||
| ) { | |||
| selectedResource = pick(value, [ | |||
| 'activeTab', | |||
| 'id', | |||
| 'identifier', | |||
| 'name', | |||
| 'owner', | |||
| 'version', | |||
| 'path', | |||
| ]) as ResourceSelectorResponse; | |||
| } | |||
| // 选择数据集、模型、镜像 | |||
| const selectResource = () => { | |||
| const resource = selectedResource; | |||
| const { close } = openAntdModal(ResourceSelectorModal, { | |||
| type, | |||
| defaultExpandedKeys: resource ? [resource.id] : [], | |||
| defaultCheckedKeys: resource ? [`${resource.id}-${resource.version}`] : [], | |||
| defaultActiveTab: resource?.activeTab, | |||
| defaultExpandedKeys: selectedResource ? [selectedResource.id] : [], | |||
| defaultCheckedKeys: selectedResource | |||
| ? [`${selectedResource.id}-${selectedResource.version}`] | |||
| : [], | |||
| defaultActiveTab: selectedResource?.activeTab, | |||
| onOk: (res) => { | |||
| setSelectedResource(res); | |||
| if (res) { | |||
| const { activeTab, id, name, version, path, identifier, owner } = res; | |||
| if (type === ResourceSelectorType.Mirror) { | |||
| @@ -50,8 +87,10 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe | |||
| showValue: path, | |||
| fromSelect: true, | |||
| activeTab, | |||
| expandedKeys: [`${id}`], | |||
| checkedKeys: [`${id}-${version}`], | |||
| id, | |||
| name, | |||
| version, | |||
| path, | |||
| }); | |||
| } else { | |||
| const jsonObj = { | |||
| @@ -69,39 +108,36 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe | |||
| showValue, | |||
| fromSelect: true, | |||
| activeTab, | |||
| expandedKeys: [`${id}`], | |||
| checkedKeys: [`${id}-${version}`], | |||
| ...jsonObj, | |||
| }); | |||
| } | |||
| } else { | |||
| onChange?.({ | |||
| value: undefined, | |||
| showValue: undefined, | |||
| fromSelect: false, | |||
| activeTab: undefined, | |||
| expandedKeys: [], | |||
| checkedKeys: [], | |||
| }); | |||
| onChange?.(undefined); | |||
| } | |||
| close(); | |||
| }, | |||
| }); | |||
| }; | |||
| // 删除 | |||
| const handleRemove = () => { | |||
| onChange?.(undefined); | |||
| }; | |||
| return ( | |||
| <div className="kf-resource-select"> | |||
| <div className={classNames('kf-resource-select', className)} style={style}> | |||
| <ParameterInput | |||
| {...rest} | |||
| disabled={disabled} | |||
| value={value} | |||
| size={mySize} | |||
| onChange={onChange} | |||
| onRemove={() => setSelectedResource(undefined)} | |||
| onRemove={handleRemove} | |||
| onClick={selectResource} | |||
| ></ParameterInput> | |||
| <Button | |||
| className="kf-resource-select__button" | |||
| size="large" | |||
| size={mySize} | |||
| type="link" | |||
| icon={getSelectBtnIcon(type)} | |||
| disabled={disabled} | |||
| @@ -22,8 +22,8 @@ | |||
| height: 398px; | |||
| margin-right: 15px; | |||
| padding: 15px; | |||
| background-color: @background-color-primary; | |||
| border: 1px solid @border-color; | |||
| background-color: rgba(22, 100, 255, 0.03); | |||
| border: 1px solid rgba(22, 100, 255, 0.3); | |||
| border-radius: 8px; | |||
| &__search { | |||
| @@ -31,7 +31,7 @@ | |||
| padding-left: 0; | |||
| background-color: transparent; | |||
| border-width: 0; | |||
| border-bottom: 1px solid @border-color-secondary; | |||
| border-bottom: 1px solid rgba(22, 100, 255, 0.1); | |||
| border-radius: 0; | |||
| } | |||
| @@ -45,8 +45,8 @@ | |||
| width: calc(100% - 488px - 15px); | |||
| height: 398px; | |||
| padding: 15px; | |||
| background-color: @background-color-primary; | |||
| border: 1px solid @border-color; | |||
| background-color: rgba(22, 100, 255, 0.03); | |||
| border: 1px solid rgba(22, 100, 255, 0.3); | |||
| border-radius: 8px; | |||
| &__title { | |||
| @@ -56,7 +56,7 @@ | |||
| color: @text-color; | |||
| font-size: @font-size; | |||
| line-height: 46px; | |||
| border-bottom: 1px solid @border-color-secondary; | |||
| border-bottom: 1px solid rgba(22, 100, 255, 0.1); | |||
| } | |||
| &__files { | |||
| height: calc(100% - 75px); | |||
| @@ -68,7 +68,7 @@ | |||
| color: @text-color-secondary; | |||
| font-size: 13px; | |||
| word-break: break-all; | |||
| background: @background-color-gray; | |||
| background: rgba(4, 3, 3, 0.06); | |||
| border-radius: 4px; | |||
| } | |||
| } | |||
| @@ -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,22 +16,30 @@ import { ResourceSelectorType, selectorTypeConfig } from './config'; | |||
| import styles from './index.less'; | |||
| export { ResourceSelectorType, selectorTypeConfig }; | |||
| // 选择数据集\模型\镜像的返回类型 | |||
| // 选择数据集、模型、镜像的返回类型 | |||
| export type ResourceSelectorResponse = { | |||
| activeTab: CommonTabKeys; // 是我的还是公开的 | |||
| id: string; // 数据集\模型\镜像 id | |||
| name: string; // 数据集\模型\镜像 name | |||
| version: string; // 数据集\模型\镜像版本 | |||
| path: string; // 数据集\模型\镜像版本路径 | |||
| identifier: string; // 数据集\模型 identifier | |||
| owner: string; // 数据集\模型 owner | |||
| activeTab: CommonTabKeys; // 是我的还是公开的 | |||
| identifier: string; // 数据集\模型 identifier,镜像这个字段为空 | |||
| owner: string; // 数据集\模型 owner,镜像这个字段为空 | |||
| }; | |||
| export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> { | |||
| 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 = [], | |||
| @@ -69,7 +78,7 @@ function ResourceSelectorModal({ | |||
| onOk, | |||
| ...rest | |||
| }: ResourceSelectorModalProps) { | |||
| const [activeTab, setActiveTab] = useState<string>(defaultActiveTab); | |||
| const [activeTab, setActiveTab] = useState<CommonTabKeys>(defaultActiveTab); | |||
| const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]); | |||
| const [checkedKeys, setCheckedKeys] = useState<React.Key[]>([]); | |||
| const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]); | |||
| @@ -82,16 +91,7 @@ function ResourceSelectorModal({ | |||
| const treeRef = useRef<TreeRef>(null); | |||
| const config = selectorTypeConfig[type]; | |||
| useEffect(() => { | |||
| setExpandedKeys([]); | |||
| setCheckedKeys([]); | |||
| setLoadedKeys([]); | |||
| setFiles([]); | |||
| setVersionPath(''); | |||
| setSearchText(''); | |||
| getTreeData(); | |||
| }, [activeTab, type]); | |||
| // 搜索 | |||
| const treeData = useMemo( | |||
| () => | |||
| originTreeData.filter((v) => | |||
| @@ -100,19 +100,45 @@ function ResourceSelectorModal({ | |||
| [originTreeData, searchText], | |||
| ); | |||
| // 获取数据集\模型\镜像列表 | |||
| const getTreeData = async () => { | |||
| const isPublic = activeTab === CommonTabKeys.Private ? false : true; | |||
| const [res] = await to(config.getList(isPublic)); | |||
| if (res) { | |||
| setOriginTreeData(res); | |||
| useEffect(() => { | |||
| // 获取数据集\模型\镜像列表 | |||
| const getTreeData = async () => { | |||
| const isPublic = activeTab === CommonTabKeys.Private ? false : true; | |||
| const [res] = await to(config.getList(isPublic)); | |||
| if (res) { | |||
| setOriginTreeData(res); | |||
| // 恢复上一次的 Expand 操作 | |||
| restoreLastExpand(); | |||
| } else { | |||
| setOriginTreeData([]); | |||
| } | |||
| }; | |||
| // 恢复上一次的 Expand 操作 | |||
| setFirstLoadList(true); | |||
| } else { | |||
| setOriginTreeData([]); | |||
| } | |||
| }; | |||
| setExpandedKeys([]); | |||
| setCheckedKeys([]); | |||
| setLoadedKeys([]); | |||
| setFiles([]); | |||
| setVersionPath(''); | |||
| setSearchText(''); | |||
| getTreeData(); | |||
| }, [activeTab, config]); | |||
| useEffect(() => { | |||
| // 恢复上一次的 Expand 操作 | |||
| // 判断是否有 defaultExpandedKeys,如果有,设置 expandedKeys | |||
| // fisrtLoadList 标志位 | |||
| const restoreLastExpand = () => { | |||
| if (firstLoadList && Array.isArray(defaultExpandedKeys) && defaultExpandedKeys.length > 0) { | |||
| setExpandedKeys(defaultExpandedKeys); | |||
| // 延时滑动到 defaultExpandedKeys,不然不会加载 defaultExpandedKeys,不然不会加载版本 | |||
| setTimeout(() => { | |||
| treeRef.current?.scrollTo({ key: defaultExpandedKeys[0], align: 'bottom' }); | |||
| }, 100); | |||
| } | |||
| }; | |||
| restoreLastExpand(); | |||
| }, [firstLoadList, defaultExpandedKeys]); | |||
| // 获取数据集\模型\镜像版本列表 | |||
| const getVersions = async (parentId: string, parentNode: any) => { | |||
| @@ -127,10 +153,10 @@ function ResourceSelectorModal({ | |||
| setLoadedKeys((prev) => prev.concat(parentId)); | |||
| } | |||
| // 恢复上一次的 Check 操作 | |||
| // 恢复上一次的 Check 操作,需要延时以便 TreeData 更新完 | |||
| setTimeout(() => { | |||
| restoreLastCheck(parentId, res); | |||
| }, 300); | |||
| }, 100); | |||
| } else { | |||
| setExpandedKeys([]); | |||
| return Promise.reject(error); | |||
| @@ -149,7 +175,7 @@ function ResourceSelectorModal({ | |||
| } | |||
| }; | |||
| // 动态加载 tree children | |||
| // 展开时,动态加载 tree children | |||
| const onLoadData = ({ key, children, ...rest }: TreeDataNode) => { | |||
| if (children) { | |||
| return Promise.resolve(); | |||
| @@ -178,42 +204,25 @@ function ResourceSelectorModal({ | |||
| } | |||
| }; | |||
| // 恢复上一次的 Expand 操作 | |||
| // 判断是否有 defaultExpandedKeys,如果有,设置 expandedKeys | |||
| // fisrtLoadList 标志位 | |||
| const restoreLastExpand = () => { | |||
| if (!firstLoadList && defaultExpandedKeys.length > 0) { | |||
| setTimeout(() => { | |||
| setExpandedKeys(defaultExpandedKeys); | |||
| setFirstLoadList(true); | |||
| setTimeout(() => { | |||
| treeRef.current?.scrollTo({ key: defaultExpandedKeys[0], align: 'bottom' }); | |||
| }, 100); | |||
| }, 0); | |||
| } | |||
| }; | |||
| // 恢复上一次的 Check 操作 | |||
| // 判断是否有 defaultCheckedKeys,如果有,设置 checkedKeys,并且调用获取文件列表接口 | |||
| // fisrtLoadVersions 标志位 | |||
| const restoreLastCheck = (parentId: string, versions: TreeDataNode[]) => { | |||
| if (!firstLoadVersions && defaultCheckedKeys.length > 0) { | |||
| if (!firstLoadVersions && Array.isArray(defaultCheckedKeys) && defaultCheckedKeys.length > 0) { | |||
| const last = defaultCheckedKeys[0] as string; | |||
| const { id } = getIdAndVersion(last); | |||
| // 判断正在打开的 id 和 defaultCheckedKeys 的 id 是否一致 | |||
| if (id === parentId) { | |||
| setCheckedKeys(defaultCheckedKeys); | |||
| const parentNode = versions.find((v) => v.key === last); | |||
| getFiles(last, parentNode); | |||
| setFirstLoadVersions(true); | |||
| setTimeout(() => { | |||
| setCheckedKeys(defaultCheckedKeys); | |||
| const parentNode = versions.find((v) => v.key === last); | |||
| getFiles(last, parentNode); | |||
| setFirstLoadVersions(true); | |||
| setTimeout(() => { | |||
| treeRef?.current?.scrollTo({ | |||
| key: defaultCheckedKeys[0], | |||
| align: 'bottom', | |||
| }); | |||
| }, 100); | |||
| }, 0); | |||
| treeRef?.current?.scrollTo({ | |||
| key: defaultCheckedKeys[0], | |||
| align: 'bottom', | |||
| }); | |||
| }, 100); | |||
| } | |||
| } | |||
| }; | |||
| @@ -234,7 +243,7 @@ function ResourceSelectorModal({ | |||
| version, | |||
| identifier, | |||
| owner, | |||
| activeTab: activeTab as CommonTabKeys, | |||
| activeTab: activeTab, | |||
| }; | |||
| onOk?.(res); | |||
| } else { | |||
| @@ -255,7 +264,7 @@ function ResourceSelectorModal({ | |||
| <Tabs | |||
| activeKey={activeTab} | |||
| items={tabItems} | |||
| onChange={setActiveTab} | |||
| onChange={(e) => setActiveTab(e as CommonTabKeys)} | |||
| className={styles['model-tabs']} | |||
| /> | |||
| <div className={styles['model-selector']}> | |||
| @@ -267,7 +276,14 @@ function ResourceSelectorModal({ | |||
| variant="borderless" | |||
| value={searchText} | |||
| onChange={(e) => setSearchText(e.target.value)} | |||
| prefix={<Icon icon="local:magnifying-glass" style={{ height: '15px' }} />} | |||
| prefix={ | |||
| <KFIcon | |||
| type="icon-sousuo" | |||
| color="rgba(22,100,255,0.4)" | |||
| style={{ height: '15px' }} | |||
| /> | |||
| } | |||
| // prefix={<Icon icon="local:magnifying-glass" style={{ height: '15px' }} />} | |||
| /> | |||
| <Tree | |||
| ref={treeRef} | |||
| @@ -1,6 +1,8 @@ | |||
| import { clearSessionToken } from '@/access'; | |||
| import { setRemoteMenu } from '@/services/session'; | |||
| import { logout } from '@/services/system/auth'; | |||
| import { ClientInfo } from '@/types'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import { gotoLoginPage } from '@/utils/ui'; | |||
| import { LogoutOutlined, UserOutlined } from '@ant-design/icons'; | |||
| import { setAlpha } from '@ant-design/pro-components'; | |||
| @@ -64,11 +66,11 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => { | |||
| clearSessionToken(); | |||
| setRemoteMenu(null); | |||
| gotoLoginPage(); | |||
| // const clientInfo: ClientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true); | |||
| // if (clientInfo) { | |||
| // const { logoutUri } = clientInfo; | |||
| // location.replace(logoutUri); | |||
| // } | |||
| const clientInfo: ClientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true); | |||
| if (clientInfo) { | |||
| const { logoutUri } = clientInfo; | |||
| location.replace(logoutUri); | |||
| } | |||
| }; | |||
| const actionClassName = useEmotionCss(({ token }) => { | |||
| return { | |||
| @@ -1,9 +1,8 @@ | |||
| import { useModel } from '@umijs/max'; | |||
| import React from 'react'; | |||
| // import KFBreadcrumb from '../KFBreadcrumb'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { ProBreadcrumb } from '@ant-design/pro-components'; | |||
| import { useModel } from '@umijs/max'; | |||
| import { Button } from 'antd'; | |||
| import React from 'react'; | |||
| import Avatar from './AvatarDropdown'; | |||
| import styles from './index.less'; | |||
| // import { SelectLang } from '@umijs/max'; | |||
| @@ -44,8 +43,6 @@ const GlobalHeaderRight: React.FC = () => { | |||
| <ProBreadcrumb></ProBreadcrumb> | |||
| {/* <KFBreadcrumb /> */} | |||
| <Avatar menu={true} /> | |||
| {/* <SelectLang className={actionClassName} /> */} | |||
| </div> | |||
| @@ -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 ( | |||
| <div className={classNames('kf-subarea-title', className)} style={style}> | |||
| {image && ( | |||
| @@ -0,0 +1,3 @@ | |||
| .ant-table .ant-table-cell .kf-table-col-title { | |||
| margin-bottom: 0; | |||
| } | |||
| @@ -0,0 +1,32 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2025-03-11 10:52:23 | |||
| * @Description: 用于内容可变的表格类标题 | |||
| */ | |||
| import { Typography } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import './index.less'; | |||
| type TableColTitleProps = { | |||
| /** 标题 */ | |||
| title: string; | |||
| /** 自定义类名 */ | |||
| className?: string; | |||
| /** 自定义样式 */ | |||
| style?: React.CSSProperties; | |||
| }; | |||
| function TableColTitle({ title, className, style }: TableColTitleProps) { | |||
| return ( | |||
| <Typography.Paragraph | |||
| ellipsis={{ tooltip: title }} | |||
| className={classNames('kf-table-col-title', className)} | |||
| style={style} | |||
| > | |||
| {title} | |||
| </Typography.Paragraph> | |||
| ); | |||
| } | |||
| export default TableColTitle; | |||