Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4ea23dd14 | ||
|
|
5bd0020e49 | ||
|
|
782e81568d | ||
|
|
2ecf83ddae | ||
|
|
de0982d590 | ||
|
|
e5755a2eec | ||
|
|
e755c34ab5 | ||
|
|
85e4f1a5ed | ||
|
|
3eb403a2c4 | ||
|
|
79442a6a6c | ||
|
|
f49001b742 | ||
|
|
e80ecfac8d | ||
|
|
ff67fba2c9 | ||
|
|
3f63831488 | ||
|
|
19babbc73d | ||
|
|
26a4d5474d | ||
|
|
af3e03230a | ||
|
|
551f04011a | ||
|
|
ea39a45f31 | ||
|
|
ebff5695ff | ||
|
|
9b63c1a515 | ||
|
|
de882dc62e | ||
|
|
8f9562d6a4 | ||
|
|
5d383ec39b | ||
|
|
2687fb187d | ||
|
|
ca43c0b4b2 | ||
|
|
71bbf59467 | ||
|
|
18f861f771 | ||
|
|
e85d59dfb0 | ||
|
|
78c8b60121 | ||
|
|
e22b5ed516 | ||
|
|
906f59a566 | ||
|
|
d1c3edfd9b | ||
|
|
e8a0f33bde | ||
|
|
825687f2a8 | ||
|
|
2852648a65 | ||
|
|
0f3f31ef45 | ||
|
|
864cf76c07 | ||
|
|
98e6c71bf8 | ||
|
|
aee9566bbb | ||
|
|
b61c0d6dee | ||
|
|
f963b3a000 | ||
|
|
03963b736a | ||
|
|
a34c375e2d | ||
|
|
e8eea9573e | ||
|
|
b7656d65e7 | ||
|
|
d1fa6dd03c | ||
|
|
2045bfaf78 | ||
|
|
653ada5f0f | ||
|
|
cbc000d35a | ||
|
|
11a676be83 | ||
|
|
7eedea8035 | ||
|
|
2d4ddea915 | ||
|
|
76d7a50fac | ||
|
|
84187eedc4 | ||
|
|
ee5c30934b | ||
|
|
b4b25d4300 | ||
|
|
9eb1dba4a1 | ||
|
|
285e274818 | ||
|
|
0077d109e7 | ||
|
|
9cd508a92a | ||
|
|
9ffd4def64 | ||
|
|
fdb6ac7575 | ||
|
|
64e2cfa35a | ||
|
|
39efca0d4c | ||
|
|
cd2942405a | ||
|
|
fadff34f5f | ||
|
|
7334bcdf83 | ||
|
|
e1e38f0404 | ||
|
|
38cc179068 | ||
|
|
5147d0d004 | ||
|
|
c237c9dbde | ||
|
|
34bd2c3031 | ||
|
|
6173f3b459 | ||
|
|
333b430b97 | ||
|
|
4c22d08da5 | ||
|
|
219f9871a7 | ||
|
|
80527d5985 | ||
|
|
7e5edd6387 | ||
|
|
4ca6a221e4 | ||
|
|
e5733a7f6e | ||
|
|
23b7936924 | ||
|
|
9be465a8cf | ||
|
|
4bbae673dc | ||
|
|
e66ee5d3f1 | ||
|
|
20872100d1 | ||
|
|
06f33ded10 | ||
|
|
adb5cd72b9 | ||
|
|
303429c52c | ||
|
|
1a92cc8543 | ||
|
|
d5cc617848 | ||
|
|
4af71b5b52 | ||
|
|
2dab89363d | ||
|
|
ab9f9a9679 | ||
|
|
c5166f07f1 | ||
|
|
86c2f8eda1 | ||
|
|
57d41e79c7 | ||
|
|
5cd9b8c22f | ||
|
|
56eba0d4fc | ||
|
|
7936d0063d | ||
|
|
3613fb3f69 | ||
|
|
608b2e31c7 | ||
|
|
2006827968 | ||
|
|
f6a3497b0c | ||
|
|
d7ef64746c | ||
|
|
b733b3615d | ||
|
|
e9fa3e3aa3 | ||
|
|
b77f796661 | ||
|
|
c077db1129 | ||
|
|
801c88a3e2 | ||
|
|
646d235c65 | ||
|
|
a86bed294e | ||
|
|
697f1f9c25 | ||
|
|
61065ee0b7 | ||
|
|
df65d3e2f6 | ||
|
|
6969cb0b9c | ||
|
|
7e81871118 | ||
|
|
ba80ade643 | ||
|
|
0401cf5fb9 | ||
|
|
53b431f100 | ||
|
|
1a082529c6 | ||
|
|
bf57290197 | ||
|
|
a815e71bfe | ||
|
|
cc0580301e | ||
|
|
5853bd1eeb | ||
|
|
ab93c53092 | ||
|
|
818c4fc9a2 | ||
|
|
fc7350b7e2 | ||
|
|
1e4018350f | ||
|
|
f017d609fb | ||
|
|
ccb6e72057 | ||
|
|
32ad9525a6 | ||
|
|
afedd4337b | ||
|
|
1f7acc7cf6 | ||
|
|
e6a5c19f0b | ||
|
|
8e07302ac3 | ||
|
|
0238431b96 | ||
|
|
3a7a4ccf30 | ||
|
|
9b843151fe | ||
|
|
19e275a17a | ||
|
|
49c6bb71a1 | ||
|
|
005295ac97 | ||
|
|
37a3aa75cc | ||
|
|
07243b206e | ||
|
|
f732f5c390 | ||
|
|
1da16a04fe | ||
|
|
e2da7c5f96 | ||
|
|
ca9ceda2f7 | ||
|
|
059416bb47 | ||
|
|
4196351e48 | ||
|
|
f02b972dc0 | ||
|
|
8398ac59a1 | ||
|
|
83403a1a1a | ||
|
|
620c113fae | ||
|
|
903500dcfa | ||
|
|
68d6993acd | ||
|
|
2c9f5062a9 | ||
|
|
d3f39ae06b | ||
|
|
f42f159785 | ||
|
|
bb7c8e9c85 | ||
|
|
663b6c7b4e | ||
|
|
1fa2124efd | ||
|
|
bcd1a339f9 | ||
|
|
c41e5d9c55 | ||
|
|
b5fa40047a | ||
|
|
caabeb065a | ||
|
|
d393f9ee9e | ||
|
|
eb509166ee | ||
|
|
0dfe77df37 | ||
|
|
46ecc2467b | ||
|
|
a5c1b8e15f | ||
|
|
54703c67c1 | ||
|
|
e863b84016 | ||
|
|
1a693f70ac | ||
|
|
a9de153c00 | ||
|
|
42217cf079 | ||
|
|
bca1e8140e | ||
|
|
ee95fabc47 | ||
|
|
67552c14af | ||
|
|
521bf211a2 | ||
|
|
8e77e19631 | ||
|
|
8b91a0546c | ||
|
|
715c71f9a5 | ||
|
|
1ec4a949b8 | ||
|
|
fc2388093d | ||
|
|
3a9bf17d7e | ||
|
|
a3002ce450 | ||
|
|
2515e8f598 | ||
|
|
dcc0db35b5 | ||
|
|
749adb56f8 | ||
|
|
f542ebecb4 | ||
|
|
b640963f65 | ||
|
|
8f5990c537 | ||
|
|
bf400547e1 | ||
|
|
4d9dec8511 | ||
|
|
1ac63e36df | ||
|
|
f36613227e | ||
|
|
d063dfd652 | ||
|
|
c9c9401b0b | ||
|
|
913f2f5077 | ||
|
|
336bafd6ce | ||
|
|
5e1a26b743 | ||
|
|
d9d5ca95df | ||
|
|
f1bb305cbc | ||
|
|
c467280170 | ||
|
|
deeee70807 | ||
|
|
07ade24ea1 | ||
|
|
8de272239e | ||
|
|
6e5a7035c8 | ||
|
|
632f206228 | ||
|
|
f04f1f828f | ||
|
|
4f91443957 | ||
|
|
0c1057d554 | ||
|
|
43936e21d9 | ||
|
|
949eded4ee | ||
|
|
84859d1b7c | ||
|
|
553ec724c2 | ||
|
|
207c90e26c | ||
|
|
64bbba7576 | ||
|
|
62dbd18ef8 | ||
|
|
55b0a20a4a | ||
|
|
29b74f85c5 | ||
|
|
ed49e03802 | ||
|
|
4711c848f8 | ||
|
|
33df825b73 | ||
|
|
89001dbb92 | ||
|
|
43e1dd0b0e |
@@ -1,4 +1,2 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
npx lint-staged
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineConfig } from '@umijs/max';
|
||||
import keepAlive from './keep-alive';
|
||||
import { compressionPluginConfig, monacoPluginConfig } from './plugins';
|
||||
import proxy from './proxy';
|
||||
import routes from './routes';
|
||||
import { getBranchInfo } from './utils';
|
||||
@@ -29,7 +30,7 @@ export default defineConfig({
|
||||
defaultSizes: 'parsed' // stat // gzip
|
||||
},
|
||||
mfsu: {
|
||||
exclude: ['lodash', 'ml-pca']
|
||||
exclude: ['lodash', 'ml-pca', 'monaco-editor']
|
||||
},
|
||||
base: process.env.npm_config_base || '/',
|
||||
...(isProduction
|
||||
@@ -56,21 +57,15 @@ export default defineConfig({
|
||||
config.output
|
||||
.filename(`js/[name].${t}.js`)
|
||||
.chunkFilename(`js/[name].${t}.chunk.js`);
|
||||
config
|
||||
.plugin('compression-webpack-plugin')
|
||||
.use(CompressionWebpackPlugin, [
|
||||
{
|
||||
filename: '[path][base].gz',
|
||||
algorithm: 'gzip',
|
||||
test: /\.(js|css|html|svg)$/,
|
||||
threshold: 10240,
|
||||
minRatio: 0.8
|
||||
}
|
||||
]);
|
||||
compressionPluginConfig(config);
|
||||
monacoPluginConfig(config);
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
|
||||
: {
|
||||
chainWebpack(config) {
|
||||
monacoPluginConfig(config);
|
||||
}
|
||||
}),
|
||||
favicons: ['/static/favicon.png'],
|
||||
jsMinifier: 'terser',
|
||||
cssMinifier: 'cssnano',
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin';
|
||||
const CompressionWebpackPlugin = require('compression-webpack-plugin');
|
||||
|
||||
export function monacoPluginConfig(config: any) {
|
||||
return config
|
||||
.plugin('monaco-editor-webpack-plugin')
|
||||
.use(MonacoWebpackPlugin, [
|
||||
{
|
||||
languages: ['yaml'],
|
||||
customLanguages: [
|
||||
{
|
||||
label: 'yaml',
|
||||
entry: 'monaco-yaml',
|
||||
worker: {
|
||||
id: 'monaco-yaml/yamlWorker',
|
||||
entry: 'monaco-yaml/yaml.worker'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
export function compressionPluginConfig(config: any) {
|
||||
return config
|
||||
.plugin('compression-webpack-plugin')
|
||||
.use(CompressionWebpackPlugin, [
|
||||
{
|
||||
filename: '[path][base].gz',
|
||||
algorithm: 'gzip',
|
||||
test: /\.(js|css|html|svg)$/,
|
||||
threshold: 10240,
|
||||
minRatio: 0.8
|
||||
}
|
||||
]);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
const proxyTableList = [
|
||||
'cli',
|
||||
'v1',
|
||||
'v2',
|
||||
'auth',
|
||||
'v1-openai',
|
||||
'version',
|
||||
|
||||
@@ -80,7 +80,6 @@ export default [
|
||||
name: 'models',
|
||||
path: '/models',
|
||||
key: 'models',
|
||||
access: 'canSeeAdmin',
|
||||
routes: [
|
||||
{
|
||||
path: '/models',
|
||||
@@ -105,6 +104,16 @@ export default [
|
||||
defaultIcon: 'icon-rocket-launch1',
|
||||
access: 'canSeeAdmin',
|
||||
component: './llmodels/index'
|
||||
},
|
||||
{
|
||||
name: 'userModels',
|
||||
path: '/models/user-models',
|
||||
key: 'userModels',
|
||||
icon: 'icon-models',
|
||||
selectedIcon: 'icon-models-filled',
|
||||
defaultIcon: 'icon-models',
|
||||
access: 'canSeeUser',
|
||||
component: './llmodels/user-models'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -136,6 +145,16 @@ export default [
|
||||
defaultIcon: 'icon-gpu1',
|
||||
component: './resources/components/gpus'
|
||||
},
|
||||
{
|
||||
name: 'backendsList',
|
||||
path: '/resources/backends',
|
||||
key: 'backendsList',
|
||||
icon: 'icon-backend',
|
||||
selectedIcon: 'icon-backend-filled',
|
||||
defaultIcon: 'icon-backend',
|
||||
access: 'canSeeAdmin',
|
||||
component: './backends/index'
|
||||
},
|
||||
{
|
||||
name: 'modelfiles',
|
||||
path: '/resources/modelfiles',
|
||||
@@ -147,6 +166,60 @@ export default [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'clusterManagement',
|
||||
path: '/cluster-management',
|
||||
key: 'clusterManagement',
|
||||
access: 'canSeeAdmin',
|
||||
routes: [
|
||||
{
|
||||
path: '/cluster-management',
|
||||
redirect: '/cluster-management/clusters/list'
|
||||
},
|
||||
{
|
||||
name: 'clusters',
|
||||
path: '/cluster-management/clusters/list',
|
||||
key: 'clusters',
|
||||
icon: 'icon-cluster2-outline',
|
||||
selectedIcon: 'icon-cluster2-filled',
|
||||
defaultIcon: 'icon-cluster2-outline',
|
||||
component: './cluster-management/clusters',
|
||||
subMenu: [
|
||||
'/cluster-management/clusters/detail',
|
||||
'/cluster-management/clusters/create'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'clusterDetail',
|
||||
path: '/cluster-management/clusters/detail',
|
||||
key: 'clusterDetail',
|
||||
icon: 'icon-cluster2-outline',
|
||||
selectedIcon: 'icon-cluster2-filled',
|
||||
defaultIcon: 'icon-cluster2-outline',
|
||||
hideInMenu: true,
|
||||
component: './cluster-management/cluster-detail'
|
||||
},
|
||||
{
|
||||
name: 'clusterCreate',
|
||||
path: '/cluster-management/clusters/create',
|
||||
key: 'clusterCreate',
|
||||
icon: 'icon-cluster2-outline',
|
||||
selectedIcon: 'icon-cluster2-filled',
|
||||
defaultIcon: 'icon-cluster2-outline',
|
||||
hideInMenu: true,
|
||||
component: './cluster-management/cluster-create'
|
||||
},
|
||||
{
|
||||
name: 'credentials',
|
||||
path: '/cluster-management/credentials',
|
||||
key: 'credentials',
|
||||
icon: 'icon-credential-outline',
|
||||
selectedIcon: 'icon-credential-filled',
|
||||
defaultIcon: 'icon-credential-outline',
|
||||
component: './cluster-management/credentials'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'accessControl',
|
||||
path: '/access-control',
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"ahooks": "^3.8.5",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"antd": "^5.21.6",
|
||||
"antd": "^5.25.4",
|
||||
"antd-style": "^3.6.2",
|
||||
"axios": "^1.8.2",
|
||||
"classnames": "^2.5.1",
|
||||
@@ -45,7 +45,9 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"has-ansi": "^5.0.1",
|
||||
"highlight.js": "^11.10.0",
|
||||
"jdenticon": "^3.3.0",
|
||||
"jotai": "^2.8.4",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jszip": "^3.10.1",
|
||||
"katex": "^0.16.21",
|
||||
"lamejs": "github:zhuker/lamejs",
|
||||
@@ -53,19 +55,24 @@
|
||||
"lodash": "^4.17.21",
|
||||
"mammoth": "^1.8.0",
|
||||
"marked": "^14.1.0",
|
||||
"minimatch": "^10.0.3",
|
||||
"ml-dataset-iris": "^1.2.1",
|
||||
"ml-pca": "^4.1.1",
|
||||
"monaco-editor": "^0.53.0",
|
||||
"monaco-editor-webpack-plugin": "^7.1.0",
|
||||
"monaco-yaml": "^5.4.0",
|
||||
"numeral": "^2.0.6",
|
||||
"overlayscrollbars": "^2.10.0",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"pdfjs-dist": "^4.7.76",
|
||||
"query-string": "^9.0.0",
|
||||
"rc-resize-observer": "^1.4.0",
|
||||
"rc-resize-observer": "^1.4.3",
|
||||
"rc-virtual-list": "^3.14.8",
|
||||
"re-resizable": "^6.10.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hotkeys-hook": "^4.5.0",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"react-markdown": "^9.0.3",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-breaks": "^4.0.0",
|
||||
@@ -73,12 +80,12 @@
|
||||
"remark-math": "^6.0.0",
|
||||
"simplebar-react": "^3.2.6",
|
||||
"styled-components": "^6.1.15",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"umi-presets-pro": "^2.0.3",
|
||||
"wavesurfer.js": "^7.8.8",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/marked": "^6.0.0",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@umijs/case-sensitive-paths-webpack-plugin": "^1.0.1",
|
||||
|
||||
@@ -4,8 +4,15 @@ export default (initialState: { currentUser?: Global.UserInfo }) => {
|
||||
initialState.currentUser &&
|
||||
initialState.currentUser.is_admin
|
||||
);
|
||||
const canSeeUser = !!(
|
||||
initialState &&
|
||||
initialState.currentUser &&
|
||||
!initialState.currentUser.is_admin
|
||||
);
|
||||
|
||||
return {
|
||||
canSeeAdmin,
|
||||
canSeeUser,
|
||||
canDelete: true,
|
||||
canLogin: true
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user';
|
||||
import { setAtomStorage } from '@/atoms/utils';
|
||||
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
|
||||
import { requestConfig } from '@/request-config';
|
||||
import {
|
||||
queryCurrentUserState,
|
||||
@@ -13,8 +14,7 @@ import {
|
||||
writeState
|
||||
} from '@/utils/localstore/index';
|
||||
import { RequestConfig, history } from '@umijs/max';
|
||||
|
||||
const loginPath = '/login';
|
||||
import { message } from 'antd';
|
||||
|
||||
// only for the first login and access from http://localhost
|
||||
|
||||
@@ -23,7 +23,7 @@ const checkDefaultPage = async (userInfo: any) => {
|
||||
if (isFirstLogin === null && isOnline()) {
|
||||
writeState(IS_FIRST_LOGIN, true);
|
||||
if (userInfo && userInfo?.is_admin) {
|
||||
history.push('/models/deployments');
|
||||
history.push(DEFAULT_ENTER_PAGE.adminForFirst);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -48,7 +48,9 @@ export async function getInitialState(): Promise<{
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUserInfo = async (): Promise<Global.UserInfo> => {
|
||||
const fetchUserInfo = async (config?: {
|
||||
skipErrorHandler?: boolean;
|
||||
}): Promise<Global.UserInfo> => {
|
||||
try {
|
||||
const data = await queryCurrentUserState({
|
||||
skipErrorHandler: true
|
||||
@@ -57,8 +59,19 @@ export async function getInitialState(): Promise<{
|
||||
getUpdateCheck();
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
history.push(loginPath);
|
||||
} catch (error: any) {
|
||||
const data = error?.response?.data;
|
||||
if (data?.code === 401 && data?.message.includes('deactivate')) {
|
||||
message.error({
|
||||
content: (
|
||||
<div>
|
||||
<span>{data?.message}</span>
|
||||
</div>
|
||||
),
|
||||
duration: 5
|
||||
});
|
||||
}
|
||||
history.push(DEFAULT_ENTER_PAGE.login);
|
||||
}
|
||||
return {} as Global.UserInfo;
|
||||
};
|
||||
@@ -78,7 +91,7 @@ export async function getInitialState(): Promise<{
|
||||
|
||||
getAppVersionInfo();
|
||||
|
||||
if (![loginPath].includes(location.pathname)) {
|
||||
if (![DEFAULT_ENTER_PAGE.login].includes(location.pathname)) {
|
||||
const userInfo = await fetchUserInfo();
|
||||
checkDefaultPage(userInfo);
|
||||
return {
|
||||
@@ -92,6 +105,6 @@ export async function getInitialState(): Promise<{
|
||||
}
|
||||
|
||||
export const request: RequestConfig = {
|
||||
baseURL: ' /v1',
|
||||
baseURL: `/${GPUSTACK_API_BASE_URL}`,
|
||||
...requestConfig
|
||||
};
|
||||
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 393 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -104,6 +104,11 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -164,6 +169,10 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.gap-20 {
|
||||
gap: 20px;
|
||||
}
|
||||
@@ -220,6 +229,10 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.m-t-0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.m-b-16 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@@ -244,6 +257,10 @@
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--ant-color-text-secondary);
|
||||
}
|
||||
@@ -252,6 +269,10 @@
|
||||
color: var(--ant-color-text-tertiary);
|
||||
}
|
||||
|
||||
.text-quaternary {
|
||||
color: var(--ant-color-text-quaternary);
|
||||
}
|
||||
|
||||
.tips-desc-list {
|
||||
list-style-type: decimal;
|
||||
padding-left: 16px;
|
||||
@@ -319,3 +340,7 @@ textarea:hover {
|
||||
.ant-result-extra {
|
||||
color: var(--ant-color-text-secondary);
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: var(--ant-color-success);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
// models expand keys: create, update , delete,
|
||||
export const expandKeysAtom = atom<string[]>([]);
|
||||
|
||||
export const regionListAtom = atom<
|
||||
{
|
||||
datacenter: string;
|
||||
label: string;
|
||||
value: string;
|
||||
icon: string;
|
||||
sizes: string[];
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
export const regionInstanceTypeListAtom = atom<
|
||||
{
|
||||
label: string;
|
||||
value: string;
|
||||
description: string;
|
||||
specInfo: Record<string, any>;
|
||||
vendor: string;
|
||||
available: boolean;
|
||||
regions: string[];
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
export const regionOSImageListAtom = atom<
|
||||
{
|
||||
label: string;
|
||||
value: string;
|
||||
name: string;
|
||||
description: string;
|
||||
vendor: string;
|
||||
specInfo: Record<string, any>;
|
||||
regions: string[];
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
export const allRegionOSImageListAtom = atom<
|
||||
{
|
||||
label: string;
|
||||
value: string;
|
||||
regions: string[];
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
export const allRegionInstanceTypeListAtom = atom<
|
||||
{
|
||||
label: string;
|
||||
value: string;
|
||||
regions: string[];
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
export const fromClusterCreationAtom = atom(false);
|
||||
|
||||
/**
|
||||
* for temporary cluster session storage during creation/editing
|
||||
*/
|
||||
export const clusterSessionAtom = atom<{
|
||||
firstAddWorker: boolean;
|
||||
} | null>(null);
|
||||
@@ -1,8 +1,11 @@
|
||||
import { BackendOption } from '@/pages/llmodels/config/types';
|
||||
import { atom, getDefaultStore } from 'jotai';
|
||||
|
||||
// models expand keys: create, update , delete,
|
||||
export const modelsExpandKeysAtom = atom<string[]>([]);
|
||||
|
||||
export const modelsSessionAtom = atom<Record<string, any>>({});
|
||||
|
||||
export const requestIdAtom = atom<number>(0);
|
||||
|
||||
export const setRquestId = () => {
|
||||
@@ -16,3 +19,12 @@ export const getRequestId = () => {
|
||||
const store = getDefaultStore();
|
||||
return store.get(requestIdAtom);
|
||||
};
|
||||
|
||||
export const clusterListAtom = atom<
|
||||
{
|
||||
label: string;
|
||||
value: number;
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
export const backendOptionsAtom = atom<BackendOption[]>([]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { colorPrimary } from '@/config/theme';
|
||||
import { COLOR_PRIMARY } from '@/config/theme';
|
||||
import { atom } from 'jotai';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
|
||||
@@ -14,7 +14,7 @@ const defaultSettings: UserSettings = {
|
||||
theme: 'light',
|
||||
mode: 'auto',
|
||||
isDarkTheme: false,
|
||||
colorPrimary: colorPrimary,
|
||||
colorPrimary: COLOR_PRIMARY,
|
||||
collapsed: false
|
||||
};
|
||||
|
||||
@@ -47,7 +47,7 @@ export const userSettingsHelperAtom = atom(
|
||||
};
|
||||
set(userSettingsAtom, {
|
||||
...newSettings,
|
||||
colorPrimary: newSettings.colorPrimary || colorPrimary,
|
||||
colorPrimary: newSettings.colorPrimary || COLOR_PRIMARY,
|
||||
isDarkTheme: newSettings.theme === 'realDark'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import { getDefaultStore } from 'jotai';
|
||||
|
||||
export const clearStorageUserSettings = () => {
|
||||
const savedSettings = JSON.parse(
|
||||
localStorage.getItem('userSettings') || '{}'
|
||||
);
|
||||
localStorage.setItem(
|
||||
'userSettings',
|
||||
JSON.stringify({
|
||||
...savedSettings,
|
||||
colorPrimary: undefined
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const clearAtomStorage = (atom: any) => {
|
||||
if (!atom) {
|
||||
return;
|
||||
|
||||
@@ -28,6 +28,8 @@ const AlertInfo: React.FC<AlertInfoProps> = (props) => {
|
||||
}
|
||||
}
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
whiteSpace: 'pre-line',
|
||||
textAlign: 'center',
|
||||
padding: '2px 5px',
|
||||
borderRadius: 'var(--border-radius-base)',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CloseCircleOutlined } from '@ant-design/icons';
|
||||
import { Progress, ProgressProps } from 'antd';
|
||||
import { CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
import { Progress, ProgressProps, Spin } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { round } from 'lodash';
|
||||
import ResizeObserver from 'rc-resize-observer';
|
||||
@@ -94,6 +94,17 @@ const SingleImage: React.FC<SingleImageProps> = (props) => {
|
||||
|
||||
const handleOnLoad = React.useCallback(async () => {}, []);
|
||||
|
||||
const renderProgress = () => {
|
||||
<Progress
|
||||
percent={round(progress, 0)}
|
||||
type="dashboard"
|
||||
size={loadingSize}
|
||||
steps={{ count: 50, gap: 2 }}
|
||||
format={() => <span className="font-size-20">{round(progress, 0)}%</span>}
|
||||
trailColor="var(--ant-color-fill-secondary)"
|
||||
/>;
|
||||
};
|
||||
|
||||
return (
|
||||
<ResizeObserver onResize={handleResize}>
|
||||
<div
|
||||
@@ -135,15 +146,8 @@ const SingleImage: React.FC<SingleImageProps> = (props) => {
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<Progress
|
||||
percent={round(progress, 0)}
|
||||
type="dashboard"
|
||||
size={loadingSize}
|
||||
steps={{ count: 50, gap: 2 }}
|
||||
format={() => (
|
||||
<span className="font-size-20">{round(progress, 0)}%</span>
|
||||
)}
|
||||
trailColor="var(--ant-color-fill-secondary)"
|
||||
<Spin
|
||||
indicator={<LoadingOutlined style={{ fontSize: 32 }} spin />}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
|
||||
@@ -29,6 +29,7 @@ interface AutoTooltipProps extends Omit<TagProps, 'title'> {
|
||||
}
|
||||
|
||||
const StyledTag = styled(Tag)`
|
||||
margin: 0;
|
||||
&.tag-filled {
|
||||
border: none;
|
||||
background-color: var(--ant-color-fill-secondary);
|
||||
@@ -106,7 +107,7 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
|
||||
<TooltipOverlayScroller
|
||||
toolTipProps={{
|
||||
...tooltipProps,
|
||||
destroyTooltipOnHide: false
|
||||
destroyOnHidden: false
|
||||
}}
|
||||
title={isOverflowing || showTitle ? title || children : false}
|
||||
>
|
||||
@@ -120,9 +121,9 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
|
||||
className={`${tagProps.className || ''} ${filled ? 'tag-filled' : ''}`}
|
||||
ref={contentRef}
|
||||
style={{
|
||||
...tagStyle,
|
||||
paddingInline: tagProps.closable ? '8px 22px' : 8,
|
||||
borderRadius: radius
|
||||
borderRadius: radius,
|
||||
...tagStyle
|
||||
}}
|
||||
closeIcon={
|
||||
tagProps.closable ? (
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
border-radius: var(--border-radius-small);
|
||||
background-color: var(--color-white-1);
|
||||
box-shadow: none;
|
||||
padding: 10px;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid var(--ant-color-border);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import IconFont from '@/components/icon-font';
|
||||
import { Card } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import classNames from 'classnames';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const CardStyled = styled(Card)`
|
||||
box-shadow: none !important;
|
||||
&.isOpen {
|
||||
.ant-card-head {
|
||||
border-bottom: 1px solid var(--ant-color-border-secondary);
|
||||
border-radius: var(--ant-border-radius) var(--ant-border-radius) 0 0;
|
||||
}
|
||||
}
|
||||
.ant-card-head {
|
||||
cursor: pointer;
|
||||
background-color: var(--ant-color-fill-quaternary);
|
||||
border-bottom: none;
|
||||
border-radius: var(--ant-border-radius);
|
||||
padding: 0 16px;
|
||||
&:hover {
|
||||
background-color: var(--ant-color-fill-secondary);
|
||||
.del-btn {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => {
|
||||
return {
|
||||
title: css`
|
||||
font-weight: 400;
|
||||
height: 56px;
|
||||
font-size: var(--font-size-base);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
`,
|
||||
expandIcon: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`,
|
||||
subtitle: css`
|
||||
font-size: 14px;
|
||||
color: ${token.colorTextSecondary};
|
||||
`,
|
||||
content: css`
|
||||
padding-top: 8px;
|
||||
`,
|
||||
left: css`
|
||||
flex: 1;
|
||||
`,
|
||||
right: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.del-btn {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
};
|
||||
});
|
||||
|
||||
export interface CollapsibleContainerProps {
|
||||
title?: React.ReactNode;
|
||||
subtitle?: React.ReactNode;
|
||||
right?: React.ReactNode;
|
||||
deleteBtn?: React.ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
open?: boolean;
|
||||
collapsible?: boolean;
|
||||
showExpandIcon?: boolean;
|
||||
onToggle?: (open: boolean) => void;
|
||||
disabled?: boolean;
|
||||
variant?: 'outlined' | 'borderless' | undefined;
|
||||
iconPosition?: 'left' | 'right';
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
styles?: {
|
||||
body?: React.CSSProperties;
|
||||
header?: React.CSSProperties;
|
||||
content?: React.CSSProperties;
|
||||
};
|
||||
}
|
||||
|
||||
export default function CollapsibleContainer({
|
||||
title,
|
||||
subtitle,
|
||||
right,
|
||||
deleteBtn,
|
||||
defaultOpen = true,
|
||||
open,
|
||||
onToggle,
|
||||
disabled = false,
|
||||
showExpandIcon = true,
|
||||
variant = 'borderless',
|
||||
className = '',
|
||||
collapsible,
|
||||
iconPosition = 'left',
|
||||
styles: cardStyles,
|
||||
children
|
||||
}: CollapsibleContainerProps) {
|
||||
const { styles } = useStyles();
|
||||
const isControlled = typeof open === 'boolean';
|
||||
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
||||
const isOpen = collapsible
|
||||
? isControlled
|
||||
? (open as boolean)
|
||||
: internalOpen
|
||||
: true;
|
||||
|
||||
const toggle = () => {
|
||||
if (disabled || !collapsible) return;
|
||||
const next = !isOpen;
|
||||
if (!isControlled) setInternalOpen(next);
|
||||
onToggle?.(next);
|
||||
};
|
||||
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [height, setHeight] = useState(isOpen ? 'auto' : '0px');
|
||||
|
||||
const renderIcon = () => {
|
||||
if (showExpandIcon) {
|
||||
return (
|
||||
<IconFont
|
||||
rotate={isOpen ? 180 : 0}
|
||||
type="icon-down"
|
||||
style={{
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
fontSize: 12
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderTitle = () => {
|
||||
if (!collapsible) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={styles.title} onClick={toggle}>
|
||||
<div className={styles.left}>
|
||||
<div className={styles.expandIcon}>
|
||||
{iconPosition === 'left' && renderIcon()}
|
||||
{title && <div>{title}</div>}
|
||||
</div>
|
||||
{subtitle && <div className={styles.subtitle}>{subtitle}</div>}
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
{right && <span>{right}</span>}
|
||||
{deleteBtn && <span className="del-btn">{deleteBtn}</span>}
|
||||
{iconPosition === 'right' && renderIcon()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!collapsible) {
|
||||
setHeight('auto');
|
||||
return;
|
||||
}
|
||||
if (isOpen) {
|
||||
const scrollHeight = contentRef.current?.scrollHeight || 0;
|
||||
setHeight(scrollHeight + 'px');
|
||||
const timer = setTimeout(() => setHeight('auto'), 200);
|
||||
return () => clearTimeout(timer);
|
||||
} else {
|
||||
const scrollHeight = contentRef.current?.scrollHeight || 0;
|
||||
setHeight(scrollHeight + 'px');
|
||||
requestAnimationFrame(() => setHeight('0px'));
|
||||
return () => {};
|
||||
}
|
||||
}, [isOpen, collapsible]);
|
||||
|
||||
return (
|
||||
<CardStyled
|
||||
className={classNames(className, { collapsible, disabled, isOpen })}
|
||||
variant={variant}
|
||||
styles={{
|
||||
body: {
|
||||
padding: 0,
|
||||
...cardStyles?.body
|
||||
},
|
||||
header: {
|
||||
...cardStyles?.header
|
||||
}
|
||||
}}
|
||||
title={renderTitle()}
|
||||
>
|
||||
<div
|
||||
ref={contentRef}
|
||||
style={{
|
||||
height: height,
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<div style={{ paddingTop: 8, ...cardStyles?.content }}>{children}</div>
|
||||
</div>
|
||||
</CardStyled>
|
||||
);
|
||||
}
|
||||
@@ -55,9 +55,7 @@ const CopyButton: React.FC<CopyButtonProps> = ({
|
||||
});
|
||||
|
||||
clipboardRef.current.on('error', (e: any) => {
|
||||
message.success(
|
||||
intl.formatMessage({ id: 'common.copy.fail' }) as string
|
||||
);
|
||||
message.error(intl.formatMessage({ id: 'common.copy.fail' }) as string);
|
||||
e.clearSelection();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ const DeleteModal = forwardRef((props, ref) => {
|
||||
open={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
destroyOnClose={false}
|
||||
destroyOnHidden={false}
|
||||
closeIcon={false}
|
||||
maskClosable={false}
|
||||
keyboard={false}
|
||||
@@ -155,7 +155,7 @@ const DeleteModal = forwardRef((props, ref) => {
|
||||
? intl.formatMessage({ id: config.cancelText })
|
||||
: intl.formatMessage({ id: 'common.button.cancel' })}
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleOk} size="middle">
|
||||
<Button type="primary" onClick={handleOk} size="middle" danger>
|
||||
{config.okText
|
||||
? intl.formatMessage({ id: config.okText })
|
||||
: intl.formatMessage({ id: 'common.button.delete' })}
|
||||
|
||||
@@ -16,6 +16,9 @@ const DropDownActions: React.FC<DropDownProps> = (props) => {
|
||||
const items = useMemo(() => {
|
||||
return menu?.items?.map((item: any) => ({
|
||||
..._.omit(item, 'locale'),
|
||||
icon: item.icon
|
||||
? React.cloneElement(item.icon, { style: { fontSize: 14 } })
|
||||
: null,
|
||||
label: item.locale ? intl.formatMessage({ id: item.label }) : item.label
|
||||
}));
|
||||
}, [menu?.items, intl]);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button, Dropdown, Tooltip, type MenuProps } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import './index.less';
|
||||
|
||||
type Trigger = 'click' | 'hover';
|
||||
@@ -19,6 +20,17 @@ interface DropdownButtonsProps {
|
||||
onSelect: (val: any, item?: any) => void;
|
||||
}
|
||||
|
||||
const DropdownWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--ant-color-bg-elevated);
|
||||
padding: 5px;
|
||||
align-items: flex-start;
|
||||
border-radius: var(--border-radius-base);
|
||||
box-shadow: var(--ant-box-shadow-secondary);
|
||||
min-width: 160px;
|
||||
`;
|
||||
|
||||
const DropdownButtons: React.FC<DropdownButtonsProps> = ({
|
||||
items,
|
||||
size = 'middle',
|
||||
@@ -64,18 +76,9 @@ const DropdownButtons: React.FC<DropdownButtonsProps> = ({
|
||||
disabled={disabled}
|
||||
trigger={trigger}
|
||||
type="primary"
|
||||
dropdownRender={(menus: any) => {
|
||||
popupRender={(menus: any) => {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-column "
|
||||
style={{
|
||||
backgroundColor: 'var(--ant-color-bg-elevated)',
|
||||
padding: 5,
|
||||
alignItems: 'flex-start',
|
||||
borderRadius: 'var(--border-radius-base)',
|
||||
boxShadow: 'var(--ant-box-shadow-secondary)'
|
||||
}}
|
||||
>
|
||||
<DropdownWrapper>
|
||||
{_.map(_.tail(items), (item: any) => {
|
||||
return (
|
||||
<Button
|
||||
@@ -86,13 +89,17 @@ const DropdownButtons: React.FC<DropdownButtonsProps> = ({
|
||||
key={item.key}
|
||||
disabled={item.disabled}
|
||||
onClick={() => handleMenuClick(item)}
|
||||
style={{ width: '100%', justifyContent: 'flex-start' }}
|
||||
style={{
|
||||
width: '100%',
|
||||
justifyContent: 'flex-start',
|
||||
paddingInline: 10
|
||||
}}
|
||||
>
|
||||
{intl.formatMessage({ id: item.label })}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</DropdownWrapper>
|
||||
);
|
||||
}}
|
||||
buttonsRender={([leftButton, rightButton]) => [
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import ComponentsMap from '@/components/seal-form/config/components';
|
||||
import { SealFormItemProps } from '@/components/seal-form/types';
|
||||
import { Form } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
interface FieldItemProps extends SealFormItemProps {
|
||||
widget: keyof typeof ComponentsMap;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const FieldItem: React.FC<FieldItemProps> = (props) => {
|
||||
const { name, widget, required = [], ...rest } = props;
|
||||
|
||||
const Component = ComponentsMap[widget];
|
||||
|
||||
return <Form.Item name={name}></Form.Item>;
|
||||
};
|
||||
|
||||
export default FieldItem;
|
||||
@@ -0,0 +1,59 @@
|
||||
import ComponentsMap from '@/components/seal-form/config/components';
|
||||
import { FormWidgetProps } from '../config/types';
|
||||
|
||||
const FormWidget: React.FC<
|
||||
FormWidgetProps & {
|
||||
onChange?: (data: any) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
> = ({
|
||||
widget,
|
||||
title: label,
|
||||
required,
|
||||
placeholder,
|
||||
options,
|
||||
description,
|
||||
enum: enumValues,
|
||||
style,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
status,
|
||||
checked,
|
||||
isInFormItems,
|
||||
disabled,
|
||||
onChange
|
||||
}) => {
|
||||
const Component = ComponentsMap[widget];
|
||||
|
||||
const optionList = enumValues?.map((item: string | number) => ({
|
||||
label: item,
|
||||
value: item
|
||||
}));
|
||||
|
||||
return Component ? (
|
||||
<Component
|
||||
{...{
|
||||
label,
|
||||
required,
|
||||
placeholder,
|
||||
description,
|
||||
min,
|
||||
max
|
||||
}}
|
||||
status={status}
|
||||
isInFormItems={isInFormItems}
|
||||
disabled={disabled}
|
||||
options={options || optionList}
|
||||
value={value}
|
||||
checked={checked}
|
||||
style={{
|
||||
width: '100%',
|
||||
...style
|
||||
}}
|
||||
onChange={onChange}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default FormWidget;
|
||||
@@ -0,0 +1,175 @@
|
||||
import Wrapper from '@/components/label-selector/wrapper';
|
||||
import { MinusOutlined } from '@ant-design/icons';
|
||||
import { Button } from 'antd';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { statusType } from '../config/types';
|
||||
import FormWidget from './form-widget';
|
||||
|
||||
const RowWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
`;
|
||||
|
||||
const WidgetBox = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
`;
|
||||
interface ListMapProps {
|
||||
minItems?: number;
|
||||
dataList: any[];
|
||||
label?: React.ReactNode;
|
||||
btnText?: string;
|
||||
requiredFields?: string[];
|
||||
validateStatusList?: Record<string, statusType>[];
|
||||
properties: Record<string, any>;
|
||||
disabled?: boolean;
|
||||
onAdd?: (data: any[]) => void;
|
||||
onDelete?: (deletedItem: any, data: any[]) => void;
|
||||
onChange?: (data: any) => void;
|
||||
}
|
||||
|
||||
interface ListItemProps {
|
||||
schemaList: any[];
|
||||
data: Record<string, any>;
|
||||
disabled?: boolean;
|
||||
validateStatus?: Record<string, statusType>;
|
||||
onChange?: (data: any) => void;
|
||||
}
|
||||
|
||||
const ListItem: React.FC<ListItemProps> = ({
|
||||
schemaList,
|
||||
data,
|
||||
onChange,
|
||||
validateStatus,
|
||||
disabled
|
||||
}) => {
|
||||
const handleValueChange = (name: string, target: any) => {
|
||||
if (target?.target?.type === 'checkbox') {
|
||||
const checked = target.target?.checked;
|
||||
onChange?.({ [name]: checked });
|
||||
} else {
|
||||
const value = target?.target ? target.target.value : target;
|
||||
onChange?.({ [name]: value });
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{schemaList.map((schema: any) => (
|
||||
<FormWidget
|
||||
status={validateStatus?.[schema.name]}
|
||||
widget={schema.type}
|
||||
{...schema}
|
||||
disabled={disabled || schema.readOnly}
|
||||
key={schema.name}
|
||||
value={data?.[schema.name]}
|
||||
checked={data?.[schema.name]}
|
||||
isInFormItems={false}
|
||||
onChange={(target) => handleValueChange(schema.name, target)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ListMap: React.FC<ListMapProps> = ({
|
||||
dataList = [],
|
||||
label,
|
||||
btnText,
|
||||
properties = {},
|
||||
requiredFields = [],
|
||||
minItems = 0,
|
||||
validateStatusList = [],
|
||||
disabled,
|
||||
onAdd,
|
||||
onDelete,
|
||||
onChange
|
||||
}) => {
|
||||
const [items, setItems] = React.useState(dataList || []);
|
||||
|
||||
const schemaList = useMemo(() => {
|
||||
const list = Object.entries(properties).map(([key, value]) => ({
|
||||
...value,
|
||||
required: requiredFields.includes(key),
|
||||
name: key
|
||||
}));
|
||||
return list;
|
||||
}, [properties, requiredFields]);
|
||||
|
||||
const handleOnAdd = () => {
|
||||
const keys = Object.keys(properties);
|
||||
const newItems = [
|
||||
...items,
|
||||
{ ...keys.reduce((acc, key) => ({ ...acc, [key]: '' }), {}) }
|
||||
];
|
||||
setItems(newItems);
|
||||
onAdd?.(newItems);
|
||||
};
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
const deleteItem = items[index];
|
||||
const newItems = items.filter((_, i) => i !== index);
|
||||
setItems(newItems);
|
||||
onDelete?.(deleteItem, newItems);
|
||||
};
|
||||
|
||||
const handleItemChange = (index: number, data: { [key: string]: any }) => {
|
||||
const newItems = [...items];
|
||||
newItems[index] = { ...newItems[index], ...data };
|
||||
setItems(newItems);
|
||||
onChange?.(newItems);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!dataList.length && minItems > 0) {
|
||||
handleOnAdd();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setItems(dataList);
|
||||
}, [dataList]);
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
label={label}
|
||||
btnText={btnText}
|
||||
onAdd={handleOnAdd}
|
||||
disabled={disabled}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<RowWrapper key={index}>
|
||||
<WidgetBox>
|
||||
<ListItem
|
||||
schemaList={schemaList}
|
||||
data={item}
|
||||
validateStatus={validateStatusList?.[index]}
|
||||
disabled={disabled}
|
||||
onChange={(value) => handleItemChange(index, value)}
|
||||
/>
|
||||
</WidgetBox>
|
||||
{!disabled && (
|
||||
<Button
|
||||
size="small"
|
||||
type="default"
|
||||
shape="circle"
|
||||
style={{
|
||||
width: 24,
|
||||
marginLeft: 10,
|
||||
flex: 'none'
|
||||
}}
|
||||
icon={<MinusOutlined />}
|
||||
onClick={() => handleDelete(index)}
|
||||
/>
|
||||
)}
|
||||
</RowWrapper>
|
||||
))}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListMap;
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
|
||||
// refer to json schema
|
||||
export interface FieldSchema {
|
||||
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
|
||||
title?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
properties?: Record<string, FieldSchema>;
|
||||
default?: any;
|
||||
enum?: string[];
|
||||
minItems?: number;
|
||||
maxItems?: number;
|
||||
items?: FieldSchema[];
|
||||
widget?: string;
|
||||
min?: number;
|
||||
style?: React.CSSProperties;
|
||||
required?: string[];
|
||||
}
|
||||
|
||||
export type statusType = 'error' | 'warning' | '' | undefined;
|
||||
export interface FormWidgetProps {
|
||||
status?: statusType;
|
||||
isInFormItems?: boolean;
|
||||
widget: 'Input' | 'Select' | 'Checkbox' | 'InputNumber';
|
||||
name: string;
|
||||
title?: string;
|
||||
required?: boolean;
|
||||
placeholder?: string;
|
||||
readOnly?: boolean;
|
||||
options?: { label: string; value: string | number }[];
|
||||
description?: string;
|
||||
enum?: (string | number)[];
|
||||
style?: React.CSSProperties;
|
||||
value?: any;
|
||||
checked?: boolean;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useMemo } from 'react';
|
||||
import { FieldSchema } from '../config/types';
|
||||
|
||||
interface ParsedField {
|
||||
name: (string | number)[];
|
||||
schema: FieldSchema;
|
||||
}
|
||||
|
||||
const parseSchema = (
|
||||
schema: Record<string, FieldSchema>,
|
||||
parentName: (string | number)[] = []
|
||||
): ParsedField[] => {
|
||||
const fields: ParsedField[] = [];
|
||||
|
||||
Object.entries(schema).forEach(([key, fieldSchema]) => {
|
||||
const currentName = [...parentName, key];
|
||||
if (fieldSchema.type === 'object' && fieldSchema.properties) {
|
||||
fields.push(...parseSchema(fieldSchema.properties, currentName));
|
||||
} else if (fieldSchema.type === 'array' && fieldSchema.items) {
|
||||
fields.push({ name: currentName, schema: fieldSchema });
|
||||
} else {
|
||||
fields.push({ name: currentName, schema: fieldSchema });
|
||||
}
|
||||
});
|
||||
|
||||
return fields;
|
||||
};
|
||||
|
||||
const useParsedFields = (schema: Record<string, FieldSchema>) => {
|
||||
return useMemo(() => parseSchema(schema), [schema]);
|
||||
};
|
||||
|
||||
export default useParsedFields;
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useRef } from 'react';
|
||||
import { statusType } from '../config/types';
|
||||
|
||||
export default function useValidateFields(params: {
|
||||
requiredFields?: string[];
|
||||
setValidateStatusList: (statusList: { [key: string]: statusType }[]) => void;
|
||||
}) {
|
||||
const { requiredFields, setValidateStatusList } = params;
|
||||
const validationEnabled = useRef(false);
|
||||
|
||||
const isEmptyValue = (value: any, key: string) => {
|
||||
return !value;
|
||||
};
|
||||
|
||||
const validateRule = (value: any, key: string) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const listMapValidator = async (_: any, valueList: any) => {
|
||||
if (!validationEnabled.current) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const fields = new Set<string>();
|
||||
const statusList: { [key: string]: statusType }[] = [];
|
||||
|
||||
(valueList || []).forEach((item: any, index: number) => {
|
||||
const status: { [key: string]: statusType } = {};
|
||||
Object.entries(item || {}).forEach(([key, value]) => {
|
||||
if (isEmptyValue(value, key)) {
|
||||
fields.add(key);
|
||||
if (requiredFields?.includes(key)) {
|
||||
status[key] = 'error';
|
||||
} else {
|
||||
status[key] = '';
|
||||
}
|
||||
} else if (validateRule(value, key)) {
|
||||
status[key] = '';
|
||||
}
|
||||
});
|
||||
statusList.push(status);
|
||||
});
|
||||
|
||||
setValidateStatusList(statusList);
|
||||
|
||||
if (fields.size > 0) {
|
||||
return Promise.reject(`${Array.from(fields).join(', ')} is required`);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const toggleValidation = (enabled: boolean) => {
|
||||
validationEnabled.current = enabled;
|
||||
};
|
||||
|
||||
return {
|
||||
listMapValidator,
|
||||
toggleValidation
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Form } from 'antd';
|
||||
import React from 'react';
|
||||
import { FieldSchema } from './config/types';
|
||||
|
||||
interface DynamicFormProps {
|
||||
schema: FieldSchema;
|
||||
onSubmit: (values: any) => void;
|
||||
}
|
||||
|
||||
const DynamicForm: React.FC<DynamicFormProps> = ({ schema, onSubmit }) => {
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
const handleFinish = (values: any) => {
|
||||
onSubmit(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form form={form} onFinish={handleFinish}>
|
||||
{/* Render form fields based on schema */}
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DynamicForm;
|
||||
@@ -33,7 +33,7 @@ const Chart: React.FC<{
|
||||
};
|
||||
|
||||
const setOption = (options: ECOption) => {
|
||||
console.log('setOption', options);
|
||||
if (!chart.current) return;
|
||||
chart.current?.clear();
|
||||
chart.current?.setOption(options, {
|
||||
notMerge: true,
|
||||
@@ -107,8 +107,8 @@ const Chart: React.FC<{
|
||||
}
|
||||
|
||||
return () => {
|
||||
chart.current?.dispose();
|
||||
chart.current?.off('finished', handleOnFinished);
|
||||
chart.current?.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -128,13 +128,11 @@ const Chart: React.FC<{
|
||||
|
||||
if (container.current) {
|
||||
resizeObserver.current = new ResizeObserver(handleResize);
|
||||
|
||||
resizeObserver.current.observe(container.current);
|
||||
}
|
||||
return () => {
|
||||
if (container.current) {
|
||||
resizeObserver.current?.unobserve(container.current);
|
||||
}
|
||||
resizeObserver.current?.disconnect();
|
||||
resizeObserver.current = undefined;
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -16,14 +16,14 @@ export default function useChartConfig() {
|
||||
const { useToken } = theme;
|
||||
const { token } = useToken();
|
||||
|
||||
console.log('token:', token);
|
||||
|
||||
const chartColorMap = useMemo(() => {
|
||||
return {
|
||||
titleColor: token.colorText,
|
||||
splitLineColor: token.colorBorder,
|
||||
tickLineColor: token.colorSplit,
|
||||
axislabelColor: token.colorTextTertiary,
|
||||
colorSecondary: token.colorTextSecondary,
|
||||
colorTertiary: token.colorTextTertiary,
|
||||
gaugeBgColor: token.colorFillSecondary,
|
||||
gaugeSplitLineColor: isDarkTheme
|
||||
? 'rgba(255,255,255,.3)'
|
||||
@@ -65,6 +65,7 @@ export default function useChartConfig() {
|
||||
const legend = {
|
||||
itemWidth: 8,
|
||||
itemHeight: 8,
|
||||
itemGap: 12,
|
||||
textStyle: {
|
||||
color: chartColorMap.axislabelColor
|
||||
}
|
||||
@@ -123,7 +124,6 @@ export default function useChartConfig() {
|
||||
type: 'bar',
|
||||
barMaxWidth: 20,
|
||||
barMinWidth: 8,
|
||||
// stack: 'total',
|
||||
barGap: '30%',
|
||||
barCategoryGap: '50%'
|
||||
};
|
||||
@@ -206,13 +206,13 @@ export default function useChartConfig() {
|
||||
rich: {
|
||||
value: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
fontWeight: 500,
|
||||
color: chartColorMap.titleColor
|
||||
},
|
||||
unit: {
|
||||
fontSize: 14,
|
||||
color: chartColorMap.titleColor,
|
||||
fontWeight: '500',
|
||||
fontWeight: 500,
|
||||
padding: [0, 0, 0, 2]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import Chart from '@/components/echarts/chart';
|
||||
import useChartConfig from '@/components/echarts/config';
|
||||
import EmptyData from '@/components/empty-data';
|
||||
import React, { memo } from 'react';
|
||||
import React from 'react';
|
||||
import { ChartProps } from './types';
|
||||
|
||||
const strokeColorFunc = (percent: number) => {
|
||||
if (percent <= 50 || percent === undefined) {
|
||||
return 'rgb(84, 204, 152, 80%)';
|
||||
}
|
||||
if (percent <= 80) {
|
||||
return 'rgba(250, 173, 20, 80%)';
|
||||
}
|
||||
return 'rgba(255, 77, 79, 80%)';
|
||||
};
|
||||
|
||||
const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = (
|
||||
props
|
||||
) => {
|
||||
@@ -12,28 +22,46 @@ const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = (
|
||||
title: titleConfig,
|
||||
chartColorMap
|
||||
} = useChartConfig();
|
||||
const { value, height, width, labelFormatter, title, color } = props;
|
||||
const { value, height, width, labelFormatter, title, color, gaugeConfig } =
|
||||
props;
|
||||
const titleText = typeof title === 'string' ? title : title?.text;
|
||||
|
||||
if (!value && value !== 0) {
|
||||
return <EmptyData height={height} title={title}></EmptyData>;
|
||||
return <EmptyData height={height} title={titleText}></EmptyData>;
|
||||
}
|
||||
|
||||
const setDataOptions = () => {
|
||||
const colorValue = color || strokeColorFunc(value);
|
||||
const combineGaugeConfig = {
|
||||
...gaugeItemConfig,
|
||||
...gaugeConfig
|
||||
};
|
||||
|
||||
combineGaugeConfig.detail.rich.value.color = colorValue;
|
||||
combineGaugeConfig.detail.rich.unit.color = colorValue;
|
||||
|
||||
return {
|
||||
title: {
|
||||
...titleConfig,
|
||||
text: title,
|
||||
top: '0',
|
||||
left: 'center'
|
||||
text: titleText,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: chartColorMap.colorSecondary,
|
||||
fontWeight: 400
|
||||
},
|
||||
top: 10,
|
||||
left: 'center',
|
||||
...(typeof title === 'object' ? title : {})
|
||||
},
|
||||
series: [
|
||||
{
|
||||
...gaugeItemConfig,
|
||||
...combineGaugeConfig,
|
||||
axisLine: {
|
||||
...gaugeItemConfig.axisLine,
|
||||
...combineGaugeConfig.axisLine,
|
||||
lineStyle: {
|
||||
...gaugeItemConfig.axisLine.lineStyle,
|
||||
...combineGaugeConfig.axisLine.lineStyle,
|
||||
color: [
|
||||
[value / 100, color],
|
||||
[value / 100, colorValue],
|
||||
[1, chartColorMap.gaugeBgColor]
|
||||
]
|
||||
}
|
||||
@@ -42,7 +70,11 @@ const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = (
|
||||
color: 'transparent'
|
||||
},
|
||||
detail: {
|
||||
...gaugeItemConfig.detail,
|
||||
...combineGaugeConfig.detail,
|
||||
borderColor: colorValue,
|
||||
lineHeight: 20,
|
||||
height: 18,
|
||||
width: 50,
|
||||
formatter: labelFormatter || gaugeItemConfig.detail.formatter
|
||||
},
|
||||
data: [{ value }]
|
||||
@@ -62,4 +94,4 @@ const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = (
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(GaugeChart);
|
||||
export default GaugeChart;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import Chart from '@/components/echarts/chart';
|
||||
import useChartConfig from '@/components/echarts/config';
|
||||
import EmptyData from '@/components/empty-data';
|
||||
import { genColors } from '@/utils';
|
||||
import _ from 'lodash';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import echarts from '.';
|
||||
import { ChartProps } from './types';
|
||||
|
||||
const LinearGradient = echarts.graphic.LinearGradient;
|
||||
const LineChart: React.FC<ChartProps> = (props) => {
|
||||
const {
|
||||
seriesData,
|
||||
@@ -16,7 +19,11 @@ const LineChart: React.FC<ChartProps> = (props) => {
|
||||
tooltipValueFormatter = null,
|
||||
legendData = [],
|
||||
smooth,
|
||||
title
|
||||
title,
|
||||
legendOptions,
|
||||
gridOptions,
|
||||
titleOptions,
|
||||
showArea
|
||||
} = props;
|
||||
const {
|
||||
grid,
|
||||
@@ -42,7 +49,10 @@ const LineChart: React.FC<ChartProps> = (props) => {
|
||||
title: {
|
||||
text: ''
|
||||
},
|
||||
grid,
|
||||
grid: {
|
||||
...grid,
|
||||
...gridOptions
|
||||
},
|
||||
tooltip: {
|
||||
...tooltip,
|
||||
formatter(params: any) {
|
||||
@@ -61,6 +71,7 @@ const LineChart: React.FC<ChartProps> = (props) => {
|
||||
yAxis,
|
||||
legend: {
|
||||
...legend,
|
||||
...legendOptions,
|
||||
data: legendData.map((item: any) => {
|
||||
return {
|
||||
name: item,
|
||||
@@ -74,6 +85,11 @@ const LineChart: React.FC<ChartProps> = (props) => {
|
||||
|
||||
const dataOptions = useMemo((): any => {
|
||||
const data = _.map(seriesData, (item: any) => {
|
||||
const colors = genColors({
|
||||
color: item.color,
|
||||
alpha1: 0.25,
|
||||
alpha2: 0.1
|
||||
});
|
||||
return {
|
||||
...item,
|
||||
...lineItemConfig,
|
||||
@@ -85,7 +101,21 @@ const LineChart: React.FC<ChartProps> = (props) => {
|
||||
lineStyle: {
|
||||
...lineItemConfig.lineStyle,
|
||||
color: item.color
|
||||
}
|
||||
},
|
||||
areaStyle: showArea
|
||||
? {
|
||||
color: new LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: colors[0]
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: colors[1]
|
||||
}
|
||||
])
|
||||
}
|
||||
: null
|
||||
};
|
||||
});
|
||||
return {
|
||||
@@ -93,6 +123,7 @@ const LineChart: React.FC<ChartProps> = (props) => {
|
||||
animation: false,
|
||||
title: {
|
||||
...titleConfig,
|
||||
...titleOptions,
|
||||
text: title
|
||||
},
|
||||
yAxis: {
|
||||
@@ -109,12 +140,24 @@ const LineChart: React.FC<ChartProps> = (props) => {
|
||||
},
|
||||
series: data
|
||||
};
|
||||
}, [seriesData, xAxisData, yAxisName, title, smooth, legendData, options]);
|
||||
}, [
|
||||
seriesData,
|
||||
xAxisData,
|
||||
yAxisName,
|
||||
title,
|
||||
smooth,
|
||||
titleOptions,
|
||||
legendData,
|
||||
options
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!seriesData.length ? (
|
||||
<EmptyData height={height} title={title}></EmptyData>
|
||||
<EmptyData
|
||||
height={height}
|
||||
title={_.get(title, 'text', title || '')}
|
||||
></EmptyData>
|
||||
) : (
|
||||
<Chart
|
||||
height={height}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import Chart from '@/components/echarts/chart';
|
||||
import useChartConfig from '@/components/echarts/config';
|
||||
import EmptyData from '@/components/empty-data';
|
||||
import { genColors } from '@/utils';
|
||||
import _ from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
import echarts from '.';
|
||||
import { ChartProps } from './types';
|
||||
|
||||
const LinearGradient = echarts.graphic.LinearGradient;
|
||||
|
||||
const MixLineBarChart: React.FC<
|
||||
ChartProps & {
|
||||
chartData: {
|
||||
@@ -78,6 +82,11 @@ const MixLineBarChart: React.FC<
|
||||
|
||||
const dataOptions = useMemo((): any => {
|
||||
const linedata = _.map(lineSeriesData, (item: any) => {
|
||||
const colors = genColors({
|
||||
color: item.color,
|
||||
alpha1: 0.5,
|
||||
alpha2: 0.1
|
||||
});
|
||||
return {
|
||||
...item,
|
||||
...lineItemConfig,
|
||||
@@ -91,6 +100,18 @@ const MixLineBarChart: React.FC<
|
||||
...lineItemConfig.lineStyle,
|
||||
color: item.color
|
||||
}
|
||||
// areaStyle: {
|
||||
// color: new LinearGradient(0, 0, 0, 1, [
|
||||
// {
|
||||
// offset: 0,
|
||||
// color: colors[0]
|
||||
// },
|
||||
// {
|
||||
// offset: 1,
|
||||
// color: colors[1]
|
||||
// }
|
||||
// ])
|
||||
// }
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,18 +1,40 @@
|
||||
import type { LegendComponentOption } from 'echarts/components';
|
||||
import type {
|
||||
LegendComponentOption,
|
||||
TitleComponentOption
|
||||
} from 'echarts/components';
|
||||
export interface ChartProps {
|
||||
seriesData: any[];
|
||||
showEmpty?: boolean;
|
||||
showArea?: boolean;
|
||||
xAxisData: string[];
|
||||
legendData?: LegendComponentOption['data'];
|
||||
legendOptions?: {
|
||||
[K in keyof LegendComponentOption]?: LegendComponentOption[K];
|
||||
};
|
||||
gridOptions?: {
|
||||
left?: string | number;
|
||||
right?: string | number;
|
||||
top?: string | number;
|
||||
bottom?: string | number;
|
||||
};
|
||||
labelFormatter?: (val?: any, index?: number) => string;
|
||||
tooltipValueFormatter?: (val: any) => string;
|
||||
height: string | number;
|
||||
width?: string | number;
|
||||
title?: string;
|
||||
title?: string | TitleComponentOption;
|
||||
titleOptions?: {
|
||||
[K in keyof TitleComponentOption]?: TitleComponentOption[K];
|
||||
};
|
||||
value?: number;
|
||||
smooth?: boolean;
|
||||
color?: string;
|
||||
yAxisName?: string;
|
||||
gaugeConfig?: {
|
||||
radius?: string;
|
||||
center?: string[];
|
||||
startAngle?: number;
|
||||
endAngle?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AreaChartItemProps {
|
||||
|
||||
@@ -1,74 +1,66 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import CopyButton from '../copy-button';
|
||||
import SegmentLine from '../segment-line';
|
||||
import './index.less';
|
||||
|
||||
const Wrapper = styled.div<{ $height: number }>`
|
||||
height: ${(props) => props.$height}px;
|
||||
const HeaderWrapper = styled.div<{ $height?: number }>`
|
||||
height: ${(props) => (props.$height ? `${props.$height}px` : 'auto')};
|
||||
display: flex;
|
||||
padding-block: 0;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
border-radius: var(--border-radius-mini);
|
||||
overflow: hidden;
|
||||
|
||||
&.bordered {
|
||||
border: 1px solid var(--ant-color-border);
|
||||
}
|
||||
&.borderless {
|
||||
border: none;
|
||||
}
|
||||
.code-pre {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.scrollbar {
|
||||
.slider {
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface EditorwrapProps {
|
||||
headerHeight?: number;
|
||||
header?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
showHeader?: boolean;
|
||||
copyText: string;
|
||||
defaultValue?: string;
|
||||
langOptions?: Global.BaseOption<string | number>[];
|
||||
variant?: 'bordered' | 'borderless';
|
||||
styles?: {
|
||||
wrapper?: React.CSSProperties;
|
||||
header?: React.CSSProperties;
|
||||
content?: React.CSSProperties;
|
||||
};
|
||||
onChangeLang?: (value: string | number) => void;
|
||||
}
|
||||
const EditorWrap: React.FC<EditorwrapProps> = ({
|
||||
headerHeight = 40,
|
||||
header,
|
||||
children,
|
||||
copyText,
|
||||
langOptions = [],
|
||||
onChangeLang,
|
||||
defaultValue,
|
||||
styles = {},
|
||||
showHeader = true
|
||||
variant = 'borderless',
|
||||
styles = {}
|
||||
}) => {
|
||||
const handleChangeLang = (value: string | number) => {
|
||||
onChangeLang?.(value);
|
||||
};
|
||||
const renderHeader = () => {
|
||||
if (header) {
|
||||
return <div className="editor-header">{header}</div>;
|
||||
}
|
||||
if (showHeader) {
|
||||
return (
|
||||
<Wrapper className="editor-header" $height={headerHeight}>
|
||||
<SegmentLine
|
||||
height={headerHeight}
|
||||
defaultValue={defaultValue}
|
||||
size="small"
|
||||
options={langOptions}
|
||||
onChange={handleChangeLang}
|
||||
></SegmentLine>
|
||||
<CopyButton
|
||||
text={copyText}
|
||||
size="small"
|
||||
style={{
|
||||
color: 'rgba(255,255,255,.7)'
|
||||
}}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return (
|
||||
<div className="editor-wrap" style={{ ...styles.wrapper }}>
|
||||
{renderHeader()}
|
||||
<div className="editor-content">{children}</div>
|
||||
</div>
|
||||
<Wrapper
|
||||
style={{ ...styles.wrapper }}
|
||||
className={classNames({
|
||||
bordered: variant === 'bordered',
|
||||
borderless: variant === 'borderless'
|
||||
})}
|
||||
>
|
||||
{header && <HeaderWrapper $height={headerHeight}>{header}</HeaderWrapper>}
|
||||
<div>{children}</div>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(EditorWrap);
|
||||
export default EditorWrap;
|
||||
|
||||
@@ -1,30 +1,39 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef
|
||||
} from 'react';
|
||||
import EditorWrap from '../editor-wrap';
|
||||
|
||||
interface ViewerProps {
|
||||
ref?: any;
|
||||
lang: string;
|
||||
defaultLang?: string;
|
||||
langOptions?: Global.BaseOption<string>[];
|
||||
config?: any;
|
||||
value: string;
|
||||
height?: string | number;
|
||||
theme?: string;
|
||||
showHeader?: boolean;
|
||||
header?: React.ReactNode;
|
||||
placeholder?: string;
|
||||
variant?: 'bordered' | 'borderless';
|
||||
}
|
||||
|
||||
const ViewerEditor: React.FC<ViewerProps> = (props) => {
|
||||
const editorRef = useRef<any>(null);
|
||||
const ViewerEditor: React.FC<ViewerProps> = forwardRef((props, ref) => {
|
||||
const {
|
||||
lang,
|
||||
value,
|
||||
config,
|
||||
langOptions,
|
||||
defaultLang,
|
||||
height = 380,
|
||||
showHeader
|
||||
theme = 'vs-dark',
|
||||
header,
|
||||
variant = 'borderless',
|
||||
placeholder
|
||||
} = props;
|
||||
const [langType, setLangType] = useState(defaultLang);
|
||||
|
||||
const editorRef = useRef<any>(null);
|
||||
|
||||
const handleBeforeMount = (monaco: any) => {
|
||||
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
|
||||
@@ -36,12 +45,8 @@ const ViewerEditor: React.FC<ViewerProps> = (props) => {
|
||||
|
||||
const handleEditorDidMount = (editor: any, monaco: any) => {
|
||||
editorRef.current = editor;
|
||||
console.log('loaded====', editor, monaco);
|
||||
};
|
||||
|
||||
const handleOnChangeLang = (value: string) => {
|
||||
setLangType(value);
|
||||
};
|
||||
const formatCode = () => {
|
||||
if (editorRef.current) {
|
||||
setTimeout(() => {
|
||||
@@ -55,35 +60,50 @@ const ViewerEditor: React.FC<ViewerProps> = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
format: () => {
|
||||
formatCode();
|
||||
},
|
||||
getValue: () => {
|
||||
return editorRef.current?.getValue?.();
|
||||
},
|
||||
setValue: (val: string) => {
|
||||
editorRef.current?.setValue?.(val);
|
||||
},
|
||||
editor: editorRef.current
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
formatCode();
|
||||
setTimeout(() => {
|
||||
const lineCount = editorRef.current?.getModel().getLineCount(); // 获取总行数
|
||||
editorRef.current?.revealLine(lineCount); // 滚动到最后一行
|
||||
}, 100); // 可以调整延时,确保编辑器完全加载
|
||||
const lineCount = editorRef.current?.getModel().getLineCount();
|
||||
editorRef.current?.revealLine(lineCount);
|
||||
}, 100);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<EditorWrap
|
||||
copyText={value}
|
||||
langOptions={langOptions}
|
||||
defaultValue={lang}
|
||||
showHeader={showHeader}
|
||||
onChangeLang={handleOnChangeLang}
|
||||
>
|
||||
<EditorWrap header={header} variant={variant}>
|
||||
<Editor
|
||||
height={height}
|
||||
theme="vs-dark"
|
||||
theme={theme}
|
||||
className="monaco-editor"
|
||||
defaultLanguage={defaultLang}
|
||||
language={langType}
|
||||
language={lang}
|
||||
value={value}
|
||||
options={config}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
scrollbar: {
|
||||
verticalScrollbarSize: 6,
|
||||
horizontalScrollbarSize: 6
|
||||
},
|
||||
placeholder: placeholder
|
||||
}}
|
||||
loading={<LoadingOutlined style={{ fontSize: 24 }}></LoadingOutlined>}
|
||||
beforeMount={handleBeforeMount}
|
||||
onMount={handleEditorDidMount}
|
||||
/>
|
||||
</EditorWrap>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default React.memo(ViewerEditor);
|
||||
export default ViewerEditor;
|
||||
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: iconfont; /* Project id 4613488 */
|
||||
src: url('iconfont.woff2?t=1755160439217') format('woff2'),
|
||||
url('iconfont.woff?t=1755160439217') format('woff'),
|
||||
url('iconfont.ttf?t=1755160439217') format('truetype');
|
||||
src: url('iconfont.woff2?t=1763436184844') format('woff2'),
|
||||
url('iconfont.woff?t=1763436184844') format('woff'),
|
||||
url('iconfont.ttf?t=1763436184844') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,174 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-basic::before {
|
||||
content: "\e6bb";
|
||||
}
|
||||
|
||||
.icon-settings::before {
|
||||
content: "\e6b8";
|
||||
}
|
||||
|
||||
.icon-speed::before {
|
||||
content: "\e6ba";
|
||||
}
|
||||
|
||||
.icon-permission::before {
|
||||
content: "\e6b6";
|
||||
}
|
||||
|
||||
.icon-captive_portal::before {
|
||||
content: "\e6b7";
|
||||
}
|
||||
|
||||
.icon-lock_open_right::before {
|
||||
content: "\e6b3";
|
||||
}
|
||||
|
||||
.icon-lock_person::before {
|
||||
content: "\e6b4";
|
||||
}
|
||||
|
||||
.icon-lock_open::before {
|
||||
content: "\e6b5";
|
||||
}
|
||||
|
||||
.icon-question::before {
|
||||
content: "\e6b2";
|
||||
}
|
||||
|
||||
.icon-aws::before {
|
||||
content: "\e616";
|
||||
}
|
||||
|
||||
.icon-aws1::before {
|
||||
content: "\e62f";
|
||||
}
|
||||
|
||||
.icon-manage_user::before {
|
||||
content: "\e6b1";
|
||||
}
|
||||
|
||||
.icon-private::before {
|
||||
content: "\e6b0";
|
||||
}
|
||||
|
||||
.icon-stop3::before {
|
||||
content: "\e6af";
|
||||
}
|
||||
|
||||
.icon-version::before {
|
||||
content: "\e6ae";
|
||||
}
|
||||
|
||||
.icon-edit-content::before {
|
||||
content: "\e6ab";
|
||||
}
|
||||
|
||||
.icon-code_block::before {
|
||||
content: "\e6ac";
|
||||
}
|
||||
|
||||
.icon-parameters::before {
|
||||
content: "\e6ad";
|
||||
}
|
||||
|
||||
.icon-backend-filled::before {
|
||||
content: "\e6a9";
|
||||
}
|
||||
|
||||
.icon-backend::before {
|
||||
content: "\e6aa";
|
||||
}
|
||||
|
||||
.icon-down2::before {
|
||||
content: "\e6a7";
|
||||
}
|
||||
|
||||
.icon-nvidia2::before {
|
||||
content: "\e60d";
|
||||
}
|
||||
|
||||
.icon-centos::before {
|
||||
content: "\e6cd";
|
||||
}
|
||||
|
||||
.icon-redhat::before {
|
||||
content: "\ec7b";
|
||||
}
|
||||
|
||||
.icon-ubuntu::before {
|
||||
content: "\edd3";
|
||||
}
|
||||
|
||||
.icon-debian::before {
|
||||
content: "\eb74";
|
||||
}
|
||||
|
||||
.icon-alma-linux::before {
|
||||
content: "\e6a8";
|
||||
}
|
||||
|
||||
.icon-fedora::before {
|
||||
content: "\e61a";
|
||||
}
|
||||
|
||||
.icon-rocky-linux::before {
|
||||
content: "\e620";
|
||||
}
|
||||
|
||||
.icon-nvidia1::before {
|
||||
content: "\e980";
|
||||
}
|
||||
|
||||
.icon-nvidia::before {
|
||||
content: "\e60c";
|
||||
}
|
||||
|
||||
.icon-amd::before {
|
||||
content: "\e6a6";
|
||||
}
|
||||
|
||||
.icon-huawei::before {
|
||||
content: "\e615";
|
||||
}
|
||||
|
||||
.icon-metax::before {
|
||||
content: "\e6a4";
|
||||
}
|
||||
|
||||
.icon-ascend::before {
|
||||
content: "\e6a3";
|
||||
}
|
||||
|
||||
.icon-huaweicloud::before {
|
||||
content: "\e614";
|
||||
}
|
||||
|
||||
.icon-alicloud::before {
|
||||
content: "\e784";
|
||||
}
|
||||
|
||||
.icon-tencentcloud::before {
|
||||
content: "\e609";
|
||||
}
|
||||
|
||||
.icon-cluster2-outline::before {
|
||||
content: "\e6a1";
|
||||
}
|
||||
|
||||
.icon-cluster2-filled::before {
|
||||
content: "\e6a2";
|
||||
}
|
||||
|
||||
.icon-cluster-filled::before {
|
||||
content: "\e69f";
|
||||
}
|
||||
|
||||
.icon-cluster-outline::before {
|
||||
content: "\e6a0";
|
||||
}
|
||||
|
||||
.icon-admin-user::before {
|
||||
content: "\e69d";
|
||||
}
|
||||
@@ -21,7 +189,7 @@
|
||||
content: "\e69e";
|
||||
}
|
||||
|
||||
.icon-quick_reference_24dp_1F1F1F_FILL0_wght300_GRAD0_op::before {
|
||||
.icon-detail-info::before {
|
||||
content: "\e69b";
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,300 @@
|
||||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "46055620",
|
||||
"name": "basic",
|
||||
"font_class": "basic",
|
||||
"unicode": "e6bb",
|
||||
"unicode_decimal": 59067
|
||||
},
|
||||
{
|
||||
"icon_id": "46055265",
|
||||
"name": "settings",
|
||||
"font_class": "settings",
|
||||
"unicode": "e6b8",
|
||||
"unicode_decimal": 59064
|
||||
},
|
||||
{
|
||||
"icon_id": "46055263",
|
||||
"name": "speed",
|
||||
"font_class": "speed",
|
||||
"unicode": "e6ba",
|
||||
"unicode_decimal": 59066
|
||||
},
|
||||
{
|
||||
"icon_id": "46039838",
|
||||
"name": "permission",
|
||||
"font_class": "permission",
|
||||
"unicode": "e6b6",
|
||||
"unicode_decimal": 59062
|
||||
},
|
||||
{
|
||||
"icon_id": "46039837",
|
||||
"name": "captive_portal",
|
||||
"font_class": "captive_portal",
|
||||
"unicode": "e6b7",
|
||||
"unicode_decimal": 59063
|
||||
},
|
||||
{
|
||||
"icon_id": "46001737",
|
||||
"name": "lock_open_right",
|
||||
"font_class": "lock_open_right",
|
||||
"unicode": "e6b3",
|
||||
"unicode_decimal": 59059
|
||||
},
|
||||
{
|
||||
"icon_id": "46001736",
|
||||
"name": "lock_person",
|
||||
"font_class": "lock_person",
|
||||
"unicode": "e6b4",
|
||||
"unicode_decimal": 59060
|
||||
},
|
||||
{
|
||||
"icon_id": "46001735",
|
||||
"name": "lock_open",
|
||||
"font_class": "lock_open",
|
||||
"unicode": "e6b5",
|
||||
"unicode_decimal": 59061
|
||||
},
|
||||
{
|
||||
"icon_id": "45937227",
|
||||
"name": "question_mark_24dp_1F1F1F_FILL0_wght300_GRAD0_opsz",
|
||||
"font_class": "question",
|
||||
"unicode": "e6b2",
|
||||
"unicode_decimal": 59058
|
||||
},
|
||||
{
|
||||
"icon_id": "10786885",
|
||||
"name": "aws",
|
||||
"font_class": "aws",
|
||||
"unicode": "e616",
|
||||
"unicode_decimal": 58902
|
||||
},
|
||||
{
|
||||
"icon_id": "12014352",
|
||||
"name": "aws",
|
||||
"font_class": "aws1",
|
||||
"unicode": "e62f",
|
||||
"unicode_decimal": 58927
|
||||
},
|
||||
{
|
||||
"icon_id": "45704693",
|
||||
"name": "manage_accounts_24dp_1F1F1F_FILL1_wght300_GRAD0_op",
|
||||
"font_class": "manage_user",
|
||||
"unicode": "e6b1",
|
||||
"unicode_decimal": 59057
|
||||
},
|
||||
{
|
||||
"icon_id": "45702717",
|
||||
"name": "private",
|
||||
"font_class": "private",
|
||||
"unicode": "e6b0",
|
||||
"unicode_decimal": 59056
|
||||
},
|
||||
{
|
||||
"icon_id": "45702697",
|
||||
"name": "stop",
|
||||
"font_class": "stop3",
|
||||
"unicode": "e6af",
|
||||
"unicode_decimal": 59055
|
||||
},
|
||||
{
|
||||
"icon_id": "45688107",
|
||||
"name": "version",
|
||||
"font_class": "version",
|
||||
"unicode": "e6ae",
|
||||
"unicode_decimal": 59054
|
||||
},
|
||||
{
|
||||
"icon_id": "45631011",
|
||||
"name": "contract_edit_24dp_1F1F1F_FILL0_wght300_GRAD0_opsz",
|
||||
"font_class": "edit-content",
|
||||
"unicode": "e6ab",
|
||||
"unicode_decimal": 59051
|
||||
},
|
||||
{
|
||||
"icon_id": "45631010",
|
||||
"name": "code_blocks_24dp_1F1F1F_FILL0_wght300_GRAD0_opsz24",
|
||||
"font_class": "code_block",
|
||||
"unicode": "e6ac",
|
||||
"unicode_decimal": 59052
|
||||
},
|
||||
{
|
||||
"icon_id": "45631009",
|
||||
"name": "settings_applications_24dp_1F1F1F_FILL0_wght300_GR",
|
||||
"font_class": "parameters",
|
||||
"unicode": "e6ad",
|
||||
"unicode_decimal": 59053
|
||||
},
|
||||
{
|
||||
"icon_id": "45623375",
|
||||
"name": "bolt_24dp_1F1F1F_FILL1_wght300_GRAD0_opsz24",
|
||||
"font_class": "backend-filled",
|
||||
"unicode": "e6a9",
|
||||
"unicode_decimal": 59049
|
||||
},
|
||||
{
|
||||
"icon_id": "45623374",
|
||||
"name": "bolt_24dp_1F1F1F_FILL0_wght300_GRAD0_opsz24",
|
||||
"font_class": "backend",
|
||||
"unicode": "e6aa",
|
||||
"unicode_decimal": 59050
|
||||
},
|
||||
{
|
||||
"icon_id": "45554217",
|
||||
"name": "arrow_down",
|
||||
"font_class": "down2",
|
||||
"unicode": "e6a7",
|
||||
"unicode_decimal": 59047
|
||||
},
|
||||
{
|
||||
"icon_id": "11155543",
|
||||
"name": "nvidia",
|
||||
"font_class": "nvidia2",
|
||||
"unicode": "e60d",
|
||||
"unicode_decimal": 58893
|
||||
},
|
||||
{
|
||||
"icon_id": "11425971",
|
||||
"name": "centos",
|
||||
"font_class": "centos",
|
||||
"unicode": "e6cd",
|
||||
"unicode_decimal": 59085
|
||||
},
|
||||
{
|
||||
"icon_id": "15378743",
|
||||
"name": "redhat",
|
||||
"font_class": "redhat",
|
||||
"unicode": "ec7b",
|
||||
"unicode_decimal": 60539
|
||||
},
|
||||
{
|
||||
"icon_id": "6808499",
|
||||
"name": "Ubuntu",
|
||||
"font_class": "ubuntu",
|
||||
"unicode": "edd3",
|
||||
"unicode_decimal": 60883
|
||||
},
|
||||
{
|
||||
"icon_id": "15378275",
|
||||
"name": "debian",
|
||||
"font_class": "debian",
|
||||
"unicode": "eb74",
|
||||
"unicode_decimal": 60276
|
||||
},
|
||||
{
|
||||
"icon_id": "38395859",
|
||||
"name": "almalinux",
|
||||
"font_class": "alma-linux",
|
||||
"unicode": "e6a8",
|
||||
"unicode_decimal": 59048
|
||||
},
|
||||
{
|
||||
"icon_id": "43280042",
|
||||
"name": "linux-fedora",
|
||||
"font_class": "fedora",
|
||||
"unicode": "e61a",
|
||||
"unicode_decimal": 58906
|
||||
},
|
||||
{
|
||||
"icon_id": "43280143",
|
||||
"name": "linux-rocky_linux",
|
||||
"font_class": "rocky-linux",
|
||||
"unicode": "e620",
|
||||
"unicode_decimal": 58912
|
||||
},
|
||||
{
|
||||
"icon_id": "36227448",
|
||||
"name": "nvidia",
|
||||
"font_class": "nvidia1",
|
||||
"unicode": "e980",
|
||||
"unicode_decimal": 59776
|
||||
},
|
||||
{
|
||||
"icon_id": "40723469",
|
||||
"name": "nvidia",
|
||||
"font_class": "nvidia",
|
||||
"unicode": "e60c",
|
||||
"unicode_decimal": 58892
|
||||
},
|
||||
{
|
||||
"icon_id": "45463937",
|
||||
"name": "amd",
|
||||
"font_class": "amd",
|
||||
"unicode": "e6a6",
|
||||
"unicode_decimal": 59046
|
||||
},
|
||||
{
|
||||
"icon_id": "24164616",
|
||||
"name": "华为",
|
||||
"font_class": "huawei",
|
||||
"unicode": "e615",
|
||||
"unicode_decimal": 58901
|
||||
},
|
||||
{
|
||||
"icon_id": "45461362",
|
||||
"name": "metax",
|
||||
"font_class": "metax",
|
||||
"unicode": "e6a4",
|
||||
"unicode_decimal": 59044
|
||||
},
|
||||
{
|
||||
"icon_id": "45459906",
|
||||
"name": "ascend",
|
||||
"font_class": "ascend",
|
||||
"unicode": "e6a3",
|
||||
"unicode_decimal": 59043
|
||||
},
|
||||
{
|
||||
"icon_id": "8152996",
|
||||
"name": "HUAWEI",
|
||||
"font_class": "huaweicloud",
|
||||
"unicode": "e614",
|
||||
"unicode_decimal": 58900
|
||||
},
|
||||
{
|
||||
"icon_id": "34290284",
|
||||
"name": "alicloud",
|
||||
"font_class": "alicloud",
|
||||
"unicode": "e784",
|
||||
"unicode_decimal": 59268
|
||||
},
|
||||
{
|
||||
"icon_id": "36654764",
|
||||
"name": "tencent",
|
||||
"font_class": "tencentcloud",
|
||||
"unicode": "e609",
|
||||
"unicode_decimal": 58889
|
||||
},
|
||||
{
|
||||
"icon_id": "45310241",
|
||||
"name": "cluster2-outline",
|
||||
"font_class": "cluster2-outline",
|
||||
"unicode": "e6a1",
|
||||
"unicode_decimal": 59041
|
||||
},
|
||||
{
|
||||
"icon_id": "45310242",
|
||||
"name": "cluster2-filled",
|
||||
"font_class": "cluster2-filled",
|
||||
"unicode": "e6a2",
|
||||
"unicode_decimal": 59042
|
||||
},
|
||||
{
|
||||
"icon_id": "45310070",
|
||||
"name": "cluster-filled",
|
||||
"font_class": "cluster-filled",
|
||||
"unicode": "e69f",
|
||||
"unicode_decimal": 59039
|
||||
},
|
||||
{
|
||||
"icon_id": "45310069",
|
||||
"name": "cluster-outline",
|
||||
"font_class": "cluster-outline",
|
||||
"unicode": "e6a0",
|
||||
"unicode_decimal": 59040
|
||||
},
|
||||
{
|
||||
"icon_id": "45240470",
|
||||
"name": "admin-user",
|
||||
@@ -21,8 +315,8 @@
|
||||
},
|
||||
{
|
||||
"icon_id": "45221182",
|
||||
"name": "quick_reference_24dp_1F1F1F_FILL0_wght300_GRAD0_op",
|
||||
"font_class": "quick_reference_24dp_1F1F1F_FILL0_wght300_GRAD0_op",
|
||||
"name": "detail-info",
|
||||
"font_class": "detail-info",
|
||||
"unicode": "e69b",
|
||||
"unicode_decimal": 59035
|
||||
},
|
||||
|
||||
@@ -2,9 +2,13 @@ import IconFont from '@/components/icon-font';
|
||||
import {
|
||||
ApiOutlined,
|
||||
DeleteOutlined,
|
||||
DockerOutlined,
|
||||
DownloadOutlined,
|
||||
EditOutlined,
|
||||
ExperimentOutlined,
|
||||
FileTextOutlined,
|
||||
KubernetesOutlined,
|
||||
ProfileOutlined,
|
||||
RetweetOutlined,
|
||||
ThunderboltOutlined
|
||||
} from '@ant-design/icons';
|
||||
@@ -17,7 +21,11 @@ const icons = {
|
||||
ThunderboltOutlined: React.createElement(ThunderboltOutlined),
|
||||
RetweetOutlined: React.createElement(RetweetOutlined),
|
||||
DownloadOutlined: React.createElement(DownloadOutlined),
|
||||
FileTextOutlined: React.createElement(FileTextOutlined),
|
||||
ApiOutlined: React.createElement(ApiOutlined),
|
||||
KubernetesOutlined: React.createElement(KubernetesOutlined),
|
||||
ProfileOutlined: React.createElement(ProfileOutlined),
|
||||
DockerOutlined: React.createElement(DockerOutlined),
|
||||
Stop: React.createElement(IconFont, { type: 'icon-stop1' }),
|
||||
Play: React.createElement(IconFont, { type: 'icon-outline-play' }),
|
||||
Catalog: React.createElement(IconFont, { type: 'icon-catalog' }),
|
||||
@@ -26,7 +34,31 @@ const icons = {
|
||||
ModelScope: React.createElement(IconFont, { type: 'icon-tu2' }),
|
||||
LocalPath: React.createElement(IconFont, { type: 'icon-hard-disk' }),
|
||||
Launch: React.createElement(IconFont, { type: 'icon-rocket-launch' }),
|
||||
Deployment: React.createElement(IconFont, { type: 'icon-rocket-launch1' })
|
||||
Deployment: React.createElement(IconFont, { type: 'icon-rocket-launch1' }),
|
||||
Docker: React.createElement(IconFont, { type: 'icon-docker' }),
|
||||
DigitalOcean: React.createElement(IconFont, { type: 'icon-digitalocean' }),
|
||||
DetailInfo: React.createElement(IconFont, { type: 'icon-detail-info' }),
|
||||
HuaweiCloud: React.createElement(IconFont, { type: 'icon-huaweicloud' }),
|
||||
AliCloud: React.createElement(IconFont, { type: 'icon-alicloud' }),
|
||||
TencentCloud: React.createElement(IconFont, { type: 'icon-tencentcloud' }),
|
||||
Nvidia: React.createElement(IconFont, { type: 'icon-nvidia' }),
|
||||
Ascend: React.createElement(IconFont, { type: 'icon-ascend' }),
|
||||
Catalog1: React.createElement(IconFont, { type: 'icon-catalog1' }),
|
||||
AMD: React.createElement(IconFont, { type: 'icon-amd' }),
|
||||
KubernetesFilled: React.createElement(IconFont, { type: 'icon-k8s-filled' }),
|
||||
EditContent: React.createElement(IconFont, { type: 'icon-edit-content' }),
|
||||
Yaml: React.createElement(IconFont, { type: 'icon-code_block' }),
|
||||
Version: React.createElement(IconFont, { type: 'icon-version' }),
|
||||
Parameter: React.createElement(IconFont, { type: 'icon-parameters' }),
|
||||
Private: React.createElement(IconFont, { type: 'icon-private' }),
|
||||
AWS: React.createElement(IconFont, { type: 'icon-aws' }),
|
||||
LockOpenRight: React.createElement(IconFont, {
|
||||
type: 'icon-lock_open_right'
|
||||
}),
|
||||
LockPerson: React.createElement(IconFont, { type: 'icon-lock_person' }),
|
||||
LockOpen: React.createElement(IconFont, { type: 'icon-lock_open' }),
|
||||
Permission: React.createElement(IconFont, { type: 'icon-permission' }),
|
||||
CaptivePortal: React.createElement(IconFont, { type: 'icon-captive_portal' })
|
||||
};
|
||||
|
||||
export default icons;
|
||||
|
||||
@@ -48,12 +48,14 @@ const ToolsBar: React.FC<ToolsBarProps> = (props) => {
|
||||
<Tooltip
|
||||
placement="bottomLeft"
|
||||
arrow={false}
|
||||
overlayInnerStyle={{
|
||||
background: 'var(--color-white-1)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
width: 160
|
||||
styles={{
|
||||
body: {
|
||||
background: 'var(--color-white-1)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
width: 160
|
||||
}
|
||||
}}
|
||||
title={
|
||||
<div className="flex-column" style={{ width: '100%' }}>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import AutoTooltip from '@/components/auto-tooltip';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const LabelsWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
`;
|
||||
|
||||
interface LabelCellProps {
|
||||
labels: Record<string, any>;
|
||||
}
|
||||
|
||||
const LabelsCell: React.FC<LabelCellProps> = ({ labels }) => (
|
||||
<LabelsWrapper>
|
||||
{_.map(labels, (value: string, key: string) => (
|
||||
<AutoTooltip
|
||||
key={key}
|
||||
className="m-r-0"
|
||||
maxWidth={155}
|
||||
style={{ paddingInline: 8, borderRadius: 12 }}
|
||||
>
|
||||
<span>{key}</span>
|
||||
<span>:{value}</span>
|
||||
</AutoTooltip>
|
||||
))}
|
||||
</LabelsWrapper>
|
||||
);
|
||||
|
||||
export default LabelsCell;
|
||||
@@ -0,0 +1,138 @@
|
||||
import AutoComplete from '@/components/seal-form/auto-complete';
|
||||
import { MinusOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useLabelSelectorContext } from './context';
|
||||
import './styles/label-item.less';
|
||||
interface LabelItemProps {
|
||||
label: {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
labels?: Record<string, any>;
|
||||
labelKey?: string;
|
||||
labelValue?: string;
|
||||
keyAddon?: React.ReactNode;
|
||||
valueAddon?: React.ReactNode;
|
||||
seperator?: string;
|
||||
labelList: { key: string; value: string }[];
|
||||
disabled?: boolean;
|
||||
onDelete?: () => void;
|
||||
onChange?: (params: { key: string; value: string }) => void;
|
||||
onPaste?: (e: any) => void;
|
||||
onBlur?: (e: any, type: string) => void;
|
||||
}
|
||||
const LabelItem: React.FC<LabelItemProps> = ({
|
||||
labels,
|
||||
label,
|
||||
labelList,
|
||||
seperator,
|
||||
keyAddon,
|
||||
valueAddon,
|
||||
disabled,
|
||||
onChange,
|
||||
onDelete,
|
||||
onBlur
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { options } = useLabelSelectorContext();
|
||||
|
||||
const keyOptions = useMemo(() => {
|
||||
return options?.filter(
|
||||
(item) => !_.has(labels, item.value) || item.value === label.key
|
||||
);
|
||||
}, [labels, options]);
|
||||
|
||||
const valueOptions = useMemo(() => {
|
||||
return options?.find((item) => item.value === label.key)?.children || [];
|
||||
}, [label.key, options]);
|
||||
|
||||
const handleOnValueChange = (value: string) => {
|
||||
onChange?.({
|
||||
key: label.key,
|
||||
value: value
|
||||
});
|
||||
};
|
||||
|
||||
const handleOnKeyChange = (key: any) => {
|
||||
onChange?.({
|
||||
key,
|
||||
value: label.value
|
||||
});
|
||||
};
|
||||
|
||||
const handleKeyOnBlur = (e: any, type: string) => {
|
||||
const val = e.target.value;
|
||||
// has duplicate key
|
||||
const duplicates = _.filter(
|
||||
labelList,
|
||||
(item: Global.BaseListItem<string>) => val && val === item.key
|
||||
);
|
||||
if (duplicates.length > 1) {
|
||||
setOpen(true);
|
||||
onChange?.({
|
||||
key: '',
|
||||
value: label.value
|
||||
});
|
||||
setTimeout(() => {
|
||||
setOpen(false);
|
||||
}, 1000);
|
||||
} else {
|
||||
setOpen(false);
|
||||
}
|
||||
onBlur?.(e, type);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="label-item">
|
||||
<div className="label-key">
|
||||
{keyAddon ?? (
|
||||
<Tooltip
|
||||
open={open}
|
||||
title={intl.formatMessage({ id: 'resources.table.key.tips' })}
|
||||
>
|
||||
<AutoComplete
|
||||
options={keyOptions}
|
||||
disabled={disabled}
|
||||
checkStatus="success"
|
||||
label={intl.formatMessage({ id: 'common.input.key' })}
|
||||
value={label.key}
|
||||
onChange={handleOnKeyChange}
|
||||
onBlur={(e: any) => handleKeyOnBlur(e, 'key')}
|
||||
></AutoComplete>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
{seperator && <span className="seprator">{seperator}</span>}
|
||||
<div className="label-value">
|
||||
{valueAddon ?? (
|
||||
<AutoComplete
|
||||
options={valueOptions}
|
||||
disabled={disabled}
|
||||
checkStatus={label.value ? 'success' : ''}
|
||||
label={intl.formatMessage({ id: 'common.input.value' })}
|
||||
value={label.value}
|
||||
onChange={handleOnValueChange}
|
||||
onBlur={(e: any) => onBlur?.(e, 'value')}
|
||||
></AutoComplete>
|
||||
)}
|
||||
</div>
|
||||
{!disabled && (
|
||||
<Button
|
||||
size="small"
|
||||
className="btn"
|
||||
type="default"
|
||||
shape="circle"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<MinusOutlined />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LabelItem;
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
interface LabelSelectorContextProps {
|
||||
options?: Array<{
|
||||
label: string;
|
||||
value: string | number;
|
||||
children?: { label: string; value: string | number }[];
|
||||
}>;
|
||||
currentData?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const LabelSelectorContext =
|
||||
React.createContext<LabelSelectorContextProps>(
|
||||
{} as LabelSelectorContextProps
|
||||
);
|
||||
|
||||
export const useLabelSelectorContext = () => {
|
||||
const context = React.useContext(LabelSelectorContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useLabelSelectorContext must be used within a LabelSelectorProvider'
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useIntl } from '@umijs/max';
|
||||
import _ from 'lodash';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Inner from './inner';
|
||||
|
||||
interface LabelSelectorProps {
|
||||
@@ -8,6 +8,8 @@ interface LabelSelectorProps {
|
||||
label?: string;
|
||||
btnText?: string;
|
||||
description?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
isAutoComplete?: boolean;
|
||||
onChange?: (labels: Record<string, any>) => void;
|
||||
onBlur?: (e: any, type: string, index: number) => void;
|
||||
onDelete?: (index: number) => void;
|
||||
@@ -18,9 +20,11 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
|
||||
onChange,
|
||||
onBlur,
|
||||
onDelete,
|
||||
disabled,
|
||||
label,
|
||||
btnText,
|
||||
description
|
||||
description,
|
||||
isAutoComplete
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [labelsData, setLabelsData] = useState({});
|
||||
@@ -41,18 +45,30 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
|
||||
}
|
||||
}, [labels]);
|
||||
|
||||
const handleLabelListChange = useCallback(
|
||||
(list: { key: string; value: string }[]) => {
|
||||
setLabelList(list);
|
||||
},
|
||||
[setLabelList]
|
||||
);
|
||||
const handleLabelListChange = (list: { key: string; value: string }[]) => {
|
||||
setLabelList(list);
|
||||
};
|
||||
|
||||
const handleLabelsChange = (data: Record<string, any>) => {
|
||||
console.log('handleLabelsChange', data);
|
||||
setLabelsData(data);
|
||||
onChange?.(data);
|
||||
};
|
||||
|
||||
const updateLabels = (list: { key: string; value: string }[]) => {
|
||||
const newLabels = _.reduce(
|
||||
list,
|
||||
(result: any, item: any) => {
|
||||
if (item.key) {
|
||||
result[item.key] = item.value;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
{}
|
||||
);
|
||||
onChange?.(newLabels);
|
||||
};
|
||||
|
||||
const handleOnPaste = (
|
||||
e: React.ClipboardEvent<HTMLTextAreaElement>,
|
||||
index: number
|
||||
@@ -70,22 +86,23 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
|
||||
const [key, value] = line.split('=').map((part) => part.trim());
|
||||
return { key, value };
|
||||
});
|
||||
console.log(lines, parsedData);
|
||||
|
||||
setLabelList((prevPairs) => {
|
||||
const newPairs = [...prevPairs];
|
||||
newPairs.splice(index, 1, ...parsedData);
|
||||
return newPairs;
|
||||
});
|
||||
const newPairs = [...labelList];
|
||||
newPairs.splice(index, 1, ...parsedData);
|
||||
|
||||
updateLabels(newPairs);
|
||||
setLabelList(newPairs);
|
||||
};
|
||||
|
||||
return (
|
||||
<Inner
|
||||
disabled={disabled}
|
||||
label={label}
|
||||
btnText={btnText}
|
||||
description={
|
||||
description ?? intl.formatMessage({ id: 'models.form.keyvalue.paste' })
|
||||
}
|
||||
isAutoComplete={isAutoComplete}
|
||||
labels={labelsData}
|
||||
labelList={labelList}
|
||||
onChange={handleLabelsChange}
|
||||
@@ -97,4 +114,4 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(LabelSelector);
|
||||
export default LabelSelector;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, { useRef } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import AutoCompleteItem from './autocomplete-item';
|
||||
import LabelItem from './label-item';
|
||||
import Wrapper from './wrapper';
|
||||
interface LabelSelectorProps {
|
||||
labels: Record<string, any>;
|
||||
label?: string;
|
||||
btnText?: string;
|
||||
isAutoComplete?: boolean;
|
||||
labelList: Array<{ key: string; value: string }>;
|
||||
onLabelListChange: (list: { key: string; value: string }[]) => void;
|
||||
onChange?: (labels: Record<string, any>) => void;
|
||||
@@ -16,6 +16,7 @@ interface LabelSelectorProps {
|
||||
onBlur?: (e: any, type: string, index: number) => void;
|
||||
onDelete?: (index: number) => void;
|
||||
description?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Inner: React.FC<LabelSelectorProps> = ({
|
||||
@@ -26,13 +27,17 @@ const Inner: React.FC<LabelSelectorProps> = ({
|
||||
onPaste,
|
||||
onBlur,
|
||||
onDelete,
|
||||
disabled,
|
||||
label,
|
||||
btnText,
|
||||
description
|
||||
description,
|
||||
isAutoComplete
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const boxRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('labels changed in Inner', labels);
|
||||
}, [labels]);
|
||||
|
||||
const updateLabels = (list: { key: string; value: string }[]) => {
|
||||
const newLabels = _.reduce(
|
||||
@@ -75,42 +80,49 @@ const Inner: React.FC<LabelSelectorProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper label={label} description={description}>
|
||||
<Wrapper
|
||||
label={label}
|
||||
description={description}
|
||||
onAdd={handleAddLabel}
|
||||
disabled={disabled}
|
||||
btnText={btnText}
|
||||
>
|
||||
<>
|
||||
{labelList?.map((item: any, index: number) => {
|
||||
return (
|
||||
<LabelItem
|
||||
key={index}
|
||||
label={item}
|
||||
seperator=":"
|
||||
labelList={labelList}
|
||||
onDelete={() => handleOnDelete(index)}
|
||||
onChange={(obj) => handleOnChange(index, obj)}
|
||||
onPaste={(e) => onPaste?.(e, index)}
|
||||
onBlur={(e: any, type: string) => onBlur?.(e, type, index)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
type="text"
|
||||
block
|
||||
style={{
|
||||
marginTop: 16,
|
||||
backgroundColor: 'var(--ant-color-fill-secondary)'
|
||||
}}
|
||||
onClick={handleAddLabel}
|
||||
>
|
||||
<PlusOutlined className="font-size-14" />{' '}
|
||||
{intl.formatMessage({
|
||||
id: btnText || 'common.button.addSelector'
|
||||
{isAutoComplete
|
||||
? labelList?.map((item: any, index: number) => {
|
||||
return (
|
||||
<AutoCompleteItem
|
||||
disabled={disabled}
|
||||
key={index}
|
||||
label={item}
|
||||
seperator=":"
|
||||
labels={labels}
|
||||
labelList={labelList}
|
||||
onDelete={() => handleOnDelete(index)}
|
||||
onChange={(obj) => handleOnChange(index, obj)}
|
||||
onPaste={(e) => onPaste?.(e, index)}
|
||||
onBlur={(e: any, type: string) => onBlur?.(e, type, index)}
|
||||
/>
|
||||
);
|
||||
})
|
||||
: labelList?.map((item: any, index: number) => {
|
||||
return (
|
||||
<LabelItem
|
||||
disabled={disabled}
|
||||
key={index}
|
||||
label={item}
|
||||
seperator=":"
|
||||
labelList={labelList}
|
||||
onDelete={() => handleOnDelete(index)}
|
||||
onChange={(obj) => handleOnChange(index, obj)}
|
||||
onPaste={(e) => onPaste?.(e, index)}
|
||||
onBlur={(e: any, type: string) => onBlur?.(e, type, index)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Inner);
|
||||
export default Inner;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import SealInput from '@/components/seal-form/seal-input';
|
||||
import { Global } from '@/config/global';
|
||||
import { MinusOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
@@ -15,8 +16,9 @@ interface LabelItemProps {
|
||||
keyAddon?: React.ReactNode;
|
||||
valueAddon?: React.ReactNode;
|
||||
seperator?: string;
|
||||
onDelete?: () => void;
|
||||
labelList: { key: string; value: string }[];
|
||||
disabled?: boolean;
|
||||
onDelete?: () => void;
|
||||
onChange?: (params: { key: string; value: string }) => void;
|
||||
onPaste?: (e: any) => void;
|
||||
onBlur?: (e: any, type: string) => void;
|
||||
@@ -27,6 +29,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
|
||||
seperator,
|
||||
keyAddon,
|
||||
valueAddon,
|
||||
disabled,
|
||||
onChange,
|
||||
onDelete,
|
||||
onPaste,
|
||||
@@ -82,6 +85,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
|
||||
title={intl.formatMessage({ id: 'resources.table.key.tips' })}
|
||||
>
|
||||
<SealInput.Input
|
||||
disabled={disabled}
|
||||
checkStatus="success"
|
||||
label={intl.formatMessage({ id: 'common.input.key' })}
|
||||
value={label.key}
|
||||
@@ -96,6 +100,8 @@ const LabelItem: React.FC<LabelItemProps> = ({
|
||||
<div className="label-value">
|
||||
{valueAddon ?? (
|
||||
<SealInput.Input
|
||||
trim={false}
|
||||
disabled={disabled}
|
||||
checkStatus={label.value ? 'success' : ''}
|
||||
label={intl.formatMessage({ id: 'common.input.value' })}
|
||||
value={label.value}
|
||||
@@ -104,17 +110,19 @@ const LabelItem: React.FC<LabelItemProps> = ({
|
||||
></SealInput.Input>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
className="btn"
|
||||
type="default"
|
||||
shape="circle"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<MinusOutlined />
|
||||
</Button>
|
||||
{!disabled && (
|
||||
<Button
|
||||
size="small"
|
||||
className="btn"
|
||||
type="default"
|
||||
shape="circle"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<MinusOutlined />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(LabelItem);
|
||||
export default LabelItem;
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
.wrapper {
|
||||
position: relative;
|
||||
padding: 14px;
|
||||
padding-top: 34px;
|
||||
border: 1px solid var(--ant-color-border);
|
||||
border-radius: var(--border-radius-base);
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
|
||||
:global {
|
||||
.label {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
line-height: 1;
|
||||
top: 12px;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,56 @@
|
||||
import LabelInfo from '@/components/seal-form/components/label-info';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button } from 'antd';
|
||||
import React from 'react';
|
||||
import styles from './styles/wrapper.less';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper: React.FC<{
|
||||
interface WrapperProps {
|
||||
label?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
labelExtra?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}> = ({ children, label, description, labelExtra, ...rest }) => {
|
||||
btnText?: string;
|
||||
disabled?: boolean;
|
||||
onAdd?: () => void;
|
||||
button?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
padding: 14px;
|
||||
padding-top: 34px;
|
||||
border: 1px solid var(--ant-color-border);
|
||||
border-radius: var(--border-radius-base);
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
.label {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
line-height: 1;
|
||||
top: 12px;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
}
|
||||
`;
|
||||
|
||||
const ButtonWrapper = styled.div`
|
||||
margin-top: 16px;
|
||||
`;
|
||||
|
||||
const Wrapper: React.FC<WrapperProps> = ({
|
||||
children,
|
||||
label,
|
||||
description,
|
||||
labelExtra,
|
||||
onAdd,
|
||||
btnText,
|
||||
disabled,
|
||||
button
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<div className={styles['wrapper']}>
|
||||
<Container>
|
||||
{label && (
|
||||
<span className="label">
|
||||
<LabelInfo
|
||||
@@ -19,10 +60,21 @@ const Wrapper: React.FC<{
|
||||
></LabelInfo>
|
||||
</span>
|
||||
)}
|
||||
{React.isValidElement(children)
|
||||
? React.cloneElement(children, { ...rest })
|
||||
: children}
|
||||
</div>
|
||||
{children}
|
||||
{!disabled && (
|
||||
<ButtonWrapper>
|
||||
{button || (
|
||||
<Button variant="filled" color="default" block onClick={onAdd}>
|
||||
<PlusOutlined className="font-size-14" />
|
||||
{btnText ||
|
||||
intl.formatMessage({
|
||||
id: 'common.button.addSelector'
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</ButtonWrapper>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import Wrapper from '../label-selector/wrapper';
|
||||
@@ -8,7 +6,7 @@ import ListItem from './list-item';
|
||||
|
||||
interface ListInputProps {
|
||||
dataList: string[];
|
||||
label: React.ReactNode;
|
||||
label?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
btnText?: string;
|
||||
options?: Global.HintOptions[];
|
||||
@@ -34,7 +32,6 @@ const ListInput: React.FC<ListInputProps> = (props) => {
|
||||
} = props;
|
||||
const [list, setList] = React.useState<{ value: string; uid: number }[]>([]);
|
||||
const countRef = React.useRef(0);
|
||||
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
||||
|
||||
const updateCountRef = () => {
|
||||
countRef.current = countRef.current + 1;
|
||||
@@ -65,9 +62,6 @@ const ListInput: React.FC<ListInputProps> = (props) => {
|
||||
uid: countRef.current
|
||||
});
|
||||
setList(values);
|
||||
// setTimeout(() => {
|
||||
// buttonRef.current?.scrollIntoView?.({ behavior: 'smooth' });
|
||||
// }, 100);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -85,7 +79,13 @@ const ListInput: React.FC<ListInputProps> = (props) => {
|
||||
}, [dataList]);
|
||||
|
||||
return (
|
||||
<Wrapper label={label} description={description} labelExtra={labelExtra}>
|
||||
<Wrapper
|
||||
label={label}
|
||||
description={description}
|
||||
labelExtra={labelExtra}
|
||||
onAdd={handleOnAdd}
|
||||
btnText={btnText}
|
||||
>
|
||||
<>
|
||||
{_.map(list, (item: any, index: number) => {
|
||||
return (
|
||||
@@ -100,26 +100,9 @@ const ListInput: React.FC<ListInputProps> = (props) => {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
variant="filled"
|
||||
color="default"
|
||||
block
|
||||
style={{
|
||||
marginTop: 16
|
||||
}}
|
||||
onClick={handleOnAdd}
|
||||
>
|
||||
<PlusOutlined className="font-size-14" />{' '}
|
||||
{intl.formatMessage({
|
||||
id: btnText
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ListInput);
|
||||
export default ListInput;
|
||||
|
||||
@@ -1,25 +1,32 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const useResizeObserver = (ref: React.RefObject<HTMLElement>) => {
|
||||
export const useResizeObserver = (ref: React.RefObject<HTMLElement>) => {
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element) return;
|
||||
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
if (entries[0]) {
|
||||
const { width, height } = entries[0].contentRect;
|
||||
setSize({ width, height });
|
||||
}
|
||||
const updateSize = () => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
setSize((prev) => {
|
||||
if (prev.width === rect.width && prev.height === rect.height)
|
||||
return prev;
|
||||
return { width: rect.width, height: rect.height };
|
||||
});
|
||||
};
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
updateSize();
|
||||
});
|
||||
|
||||
observer.observe(element);
|
||||
updateSize();
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [ref]);
|
||||
}, [ref.current]);
|
||||
|
||||
return size;
|
||||
};
|
||||
|
||||
@@ -20,8 +20,9 @@ export const OverlayScroller: React.FC<
|
||||
maxHeight?: number;
|
||||
style?: React.CSSProperties;
|
||||
children: React.ReactNode;
|
||||
onScroll?: (e: React.UIEvent<HTMLDivElement, UIEvent>) => void;
|
||||
}
|
||||
> = ({ children, maxHeight, scrollbars, oppositeTheme, style }) => {
|
||||
> = ({ children, maxHeight, scrollbars, oppositeTheme, style, onScroll }) => {
|
||||
const scroller = React.useRef<any>(null);
|
||||
const { initialize } = useOverlayScroller({
|
||||
options: {
|
||||
@@ -43,6 +44,7 @@ export const OverlayScroller: React.FC<
|
||||
className="overlay-scroller-wrapper"
|
||||
$maxHeight={maxHeight || 200}
|
||||
hidden={false}
|
||||
onWheel={onScroll}
|
||||
style={{
|
||||
paddingInlineStart: 8,
|
||||
paddingInlineEnd: 8,
|
||||
@@ -75,12 +77,15 @@ export const TooltipOverlayScroller: React.FC<
|
||||
scrollbars,
|
||||
oppositeTheme
|
||||
}) => {
|
||||
const { overlayInnerStyle, ...rest } = toolTipProps || {};
|
||||
const { styles, ...rest } = toolTipProps || {};
|
||||
return (
|
||||
<Tooltip
|
||||
overlayInnerStyle={{
|
||||
paddingInline: 0,
|
||||
...overlayInnerStyle
|
||||
styles={{
|
||||
...styles,
|
||||
body: {
|
||||
paddingInline: 0,
|
||||
...styles?.body
|
||||
}
|
||||
}}
|
||||
title={
|
||||
title && (
|
||||
|
||||
@@ -3,11 +3,13 @@ import {
|
||||
DeleteOutlined,
|
||||
DownOutlined,
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
SyncOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Input, Select, Space } from 'antd';
|
||||
import { Button, Input, Space } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import BaseSelect from '../seal-form/base/select';
|
||||
import './index.less';
|
||||
|
||||
type PageToolsProps = {
|
||||
@@ -62,7 +64,7 @@ interface FilterBarProps {
|
||||
actionItems?: ActionItem[];
|
||||
selectOptions?: Global.BaseOption<string | number>[];
|
||||
showSelect?: boolean;
|
||||
buttonText: string;
|
||||
buttonText?: string;
|
||||
buttonIcon?: React.ReactNode;
|
||||
marginBottom?: number;
|
||||
marginTop?: number;
|
||||
@@ -71,6 +73,8 @@ interface FilterBarProps {
|
||||
actionType?: 'dropdown' | 'button';
|
||||
showPrimaryButton?: boolean;
|
||||
showDeleteButton?: boolean;
|
||||
right?: React.ReactNode;
|
||||
left?: React.ReactNode;
|
||||
width?: {
|
||||
input?: number;
|
||||
select?: number;
|
||||
@@ -82,10 +86,10 @@ export const FilterBar: React.FC<FilterBarProps> = (props) => {
|
||||
handleInputChange,
|
||||
handleSelectChange,
|
||||
handleSearch,
|
||||
handleDeleteByBatch,
|
||||
handleClickPrimary,
|
||||
handleDeleteByBatch = null,
|
||||
handleClickPrimary = null,
|
||||
rowSelection,
|
||||
actionItems,
|
||||
actionItems = [],
|
||||
selectOptions,
|
||||
showSelect,
|
||||
buttonText,
|
||||
@@ -93,57 +97,61 @@ export const FilterBar: React.FC<FilterBarProps> = (props) => {
|
||||
actionType = 'button',
|
||||
marginBottom = 10,
|
||||
marginTop = 10,
|
||||
inputHolder = 'common.filter.name',
|
||||
inputHolder,
|
||||
selectHolder,
|
||||
showPrimaryButton = true,
|
||||
showDeleteButton = true,
|
||||
right,
|
||||
left,
|
||||
width
|
||||
} = props;
|
||||
const intl = useIntl();
|
||||
|
||||
const renderRight = useMemo(() => {
|
||||
if (!showPrimaryButton && !showDeleteButton) {
|
||||
if (!handleClickPrimary && !handleDeleteByBatch) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Space size={20}>
|
||||
{actionType === 'dropdown' ? (
|
||||
<DropDownActions
|
||||
menu={{
|
||||
items: actionItems,
|
||||
onClick: handleClickPrimary
|
||||
}}
|
||||
>
|
||||
{handleClickPrimary ? (
|
||||
actionType === 'dropdown' ? (
|
||||
<DropDownActions
|
||||
menu={{
|
||||
items: actionItems,
|
||||
onClick: handleClickPrimary
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
icon={<DownOutlined></DownOutlined>}
|
||||
type="primary"
|
||||
iconPosition="end"
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</DropDownActions>
|
||||
) : (
|
||||
<Button
|
||||
icon={<DownOutlined></DownOutlined>}
|
||||
icon={buttonIcon ?? <PlusOutlined></PlusOutlined>}
|
||||
type="primary"
|
||||
iconPosition="end"
|
||||
onClick={handleClickPrimary}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</DropDownActions>
|
||||
) : (
|
||||
)
|
||||
) : null}
|
||||
{handleDeleteByBatch && (
|
||||
<Button
|
||||
icon={buttonIcon ?? <PlusOutlined></PlusOutlined>}
|
||||
type="primary"
|
||||
onClick={handleClickPrimary}
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
onClick={handleDeleteByBatch}
|
||||
disabled={!rowSelection?.selectedRowKeys?.length}
|
||||
>
|
||||
{buttonText}
|
||||
<span>
|
||||
{intl?.formatMessage?.({ id: 'common.button.delete' })}
|
||||
{rowSelection?.selectedRowKeys?.length > 0 && (
|
||||
<span>({rowSelection?.selectedRowKeys?.length})</span>
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
onClick={handleDeleteByBatch}
|
||||
disabled={!rowSelection.selectedRowKeys.length}
|
||||
>
|
||||
<span>
|
||||
{intl?.formatMessage?.({ id: 'common.button.delete' })}
|
||||
{rowSelection.selectedRowKeys.length > 0 && (
|
||||
<span>({rowSelection.selectedRowKeys?.length})</span>
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}, [
|
||||
@@ -157,29 +165,35 @@ export const FilterBar: React.FC<FilterBarProps> = (props) => {
|
||||
return (
|
||||
<PageTools
|
||||
marginBottom={marginBottom}
|
||||
marginTop={marginTop}
|
||||
marginTop={0}
|
||||
left={
|
||||
<Space>
|
||||
<Input
|
||||
placeholder={intl.formatMessage({
|
||||
id: inputHolder
|
||||
})}
|
||||
prefix={
|
||||
<SearchOutlined
|
||||
style={{ color: 'var(--ant-color-text-placeholder)' }}
|
||||
></SearchOutlined>
|
||||
}
|
||||
placeholder={
|
||||
inputHolder ||
|
||||
intl.formatMessage({
|
||||
id: 'common.filter.name'
|
||||
})
|
||||
}
|
||||
style={{ width: width?.input || 230 }}
|
||||
allowClear
|
||||
onChange={handleInputChange}
|
||||
></Input>
|
||||
{showSelect && (
|
||||
<Select
|
||||
<BaseSelect
|
||||
allowClear
|
||||
showSearch={false}
|
||||
placeholder={intl.formatMessage({
|
||||
id: selectHolder
|
||||
})}
|
||||
placeholder={selectHolder}
|
||||
style={{ width: width?.select || 230 }}
|
||||
size="large"
|
||||
onChange={handleSelectChange}
|
||||
options={selectOptions}
|
||||
></Select>
|
||||
></BaseSelect>
|
||||
)}
|
||||
<Button
|
||||
type="text"
|
||||
@@ -189,7 +203,7 @@ export const FilterBar: React.FC<FilterBarProps> = (props) => {
|
||||
></Button>
|
||||
</Space>
|
||||
}
|
||||
right={renderRight}
|
||||
right={right || renderRight}
|
||||
></PageTools>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -65,7 +65,11 @@ const RenderProgress = memo(
|
||||
title={label}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
overlayInnerStyle={{ paddingInline: 12 }}
|
||||
styles={{
|
||||
body: {
|
||||
paddingInline: 12
|
||||
}
|
||||
}}
|
||||
>
|
||||
{renderProgress}
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,15 +1,41 @@
|
||||
import { useEscHint } from '@/hooks/use-esc-hint';
|
||||
import { Drawer, type DrawerProps } from 'antd';
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import { Button, Drawer, type DrawerProps } from 'antd';
|
||||
|
||||
const ScrollerModal = (props: DrawerProps) => {
|
||||
const { title, closable = true, maskClosable = false, ...restProps } = props;
|
||||
const { EscHint } = useEscHint({
|
||||
enabled: !props.keyboard && props.open
|
||||
});
|
||||
|
||||
const handleCancel = (e: React.MouseEvent<HTMLElement>) => {
|
||||
props.onClose?.(e);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Drawer {...props}>
|
||||
{props.children}
|
||||
<Drawer
|
||||
{...restProps}
|
||||
closable={false}
|
||||
maskClosable={maskClosable}
|
||||
title={
|
||||
<div className="flex-between flex-center">
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--ant-color-text)',
|
||||
fontWeight: 'var(--font-weight-medium)',
|
||||
fontSize: 'var(--font-size-middle)'
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
{closable && (
|
||||
<Button type="text" size="small" onClick={handleCancel}>
|
||||
<CloseOutlined></CloseOutlined>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{restProps.children}
|
||||
{EscHint}
|
||||
</Drawer>
|
||||
</>
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import useBodyScroll from '@/hooks/use-body-scroll';
|
||||
import useOverlayScroller from '@/hooks/use-overlay-scroller';
|
||||
import { Modal, type ModalProps } from 'antd';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const ScrollerModal = (props: ModalProps) => {
|
||||
const Wrapper = styled.div<{ $maxHeight?: number | string }>`
|
||||
max-height: ${({ $maxHeight }) =>
|
||||
typeof $maxHeight === 'number' ? `${$maxHeight}px` : $maxHeight};
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const ScrollerModal = (
|
||||
props: ModalProps & { maxContentHeight?: number | string }
|
||||
) => {
|
||||
const scroller = React.useRef<any>(null);
|
||||
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
|
||||
const { initialize, destroyInstance } = useOverlayScroller();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (props.open) {
|
||||
@@ -13,7 +26,60 @@ const ScrollerModal = (props: ModalProps) => {
|
||||
}
|
||||
}, [props.open]);
|
||||
|
||||
return <Modal {...props} />;
|
||||
// init scroller, delay to ensure modal is fully open
|
||||
React.useEffect(() => {
|
||||
let timeout = null;
|
||||
if (props.open) {
|
||||
timeout = setTimeout(() => {
|
||||
if (scroller.current) {
|
||||
initialize(scroller.current);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
return () => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
destroyInstance();
|
||||
};
|
||||
}, [props.open, initialize]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
{...props}
|
||||
destroyOnHidden={true}
|
||||
styles={{
|
||||
content: {
|
||||
padding: 0
|
||||
},
|
||||
header: {
|
||||
padding: 'var(--ant-modal-content-padding)',
|
||||
paddingBottom: '0'
|
||||
},
|
||||
body: {
|
||||
padding: '0',
|
||||
paddingBlockEnd: props.footer ? '0' : '24px'
|
||||
},
|
||||
footer: props.footer
|
||||
? {
|
||||
padding: '12px 24px 24px',
|
||||
margin: '0'
|
||||
}
|
||||
: {}
|
||||
}}
|
||||
>
|
||||
<Wrapper
|
||||
ref={scroller}
|
||||
data-overlayscrollbars-initialize
|
||||
className="overlay-scroller-wrapper"
|
||||
$maxHeight={props.maxContentHeight || 500}
|
||||
hidden={false}
|
||||
style={{ paddingInline: 24, paddingBlockEnd: 0 }}
|
||||
>
|
||||
{props.children}
|
||||
</Wrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScrollerModal;
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { AutoComplete, Form, Spin } from 'antd';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { AutoComplete, Form, Typography } from 'antd';
|
||||
import type { AutoCompleteProps } from 'antd/lib';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { LoadingContent } from './components/not-found-content';
|
||||
import { SealFormItemProps } from './types';
|
||||
import Wrapper from './wrapper';
|
||||
import SelectWrapper from './wrapper/select';
|
||||
|
||||
const Link = Typography.Link;
|
||||
|
||||
const SealAutoComplete: React.FC<
|
||||
AutoCompleteProps & SealFormItemProps & { onInput?: (e: Event) => void }
|
||||
AutoCompleteProps &
|
||||
SealFormItemProps & {
|
||||
onInput?: (e: Event) => void;
|
||||
clearSpaceOnBlur?: boolean;
|
||||
}
|
||||
> = (props) => {
|
||||
const {
|
||||
label,
|
||||
@@ -17,10 +25,13 @@ const SealAutoComplete: React.FC<
|
||||
trim = true,
|
||||
onSelect,
|
||||
onBlur,
|
||||
checkStatus,
|
||||
extra,
|
||||
style,
|
||||
addAfter,
|
||||
loading,
|
||||
allowClear,
|
||||
clearSpaceOnBlur,
|
||||
...rest
|
||||
} = props;
|
||||
const [isFocus, setIsFocus] = useState(false);
|
||||
@@ -45,6 +56,7 @@ const SealAutoComplete: React.FC<
|
||||
};
|
||||
|
||||
const handleChange = (val: string, option: any) => {
|
||||
console.log('handleChange val:', val);
|
||||
let value = val;
|
||||
if (trim) {
|
||||
value = value?.trim?.();
|
||||
@@ -61,7 +73,13 @@ const SealAutoComplete: React.FC<
|
||||
if (!props.value) {
|
||||
setIsFocus(false);
|
||||
}
|
||||
e.target.value = e.target.value?.trim?.();
|
||||
|
||||
if (clearSpaceOnBlur) {
|
||||
e.target.value = e.target.value?.replace(/\s+/g, '');
|
||||
props.onChange?.(e.target.value);
|
||||
} else {
|
||||
e.target.value = e.target.value?.trim();
|
||||
}
|
||||
props.onBlur?.(e);
|
||||
};
|
||||
|
||||
@@ -74,23 +92,33 @@ const SealAutoComplete: React.FC<
|
||||
};
|
||||
const renderAfter = () => {
|
||||
if (loading) {
|
||||
return <Spin size="small"></Spin>;
|
||||
return (
|
||||
<Link>
|
||||
<LoadingOutlined />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return addAfter;
|
||||
return null;
|
||||
};
|
||||
|
||||
const popupRender = (originNode: React.ReactElement): React.ReactElement => {
|
||||
if (loading) {
|
||||
return <LoadingContent />;
|
||||
}
|
||||
return originNode || null;
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectWrapper style={style}>
|
||||
<Wrapper
|
||||
className="seal-select-wrapper"
|
||||
status={status}
|
||||
status={checkStatus || status}
|
||||
extra={extra}
|
||||
label={label}
|
||||
isFocus={isFocus}
|
||||
required={required}
|
||||
description={description}
|
||||
disabled={props.disabled}
|
||||
addAfter={renderAfter()}
|
||||
onClick={handleClickWrapper}
|
||||
>
|
||||
<AutoComplete
|
||||
@@ -103,11 +131,16 @@ const SealAutoComplete: React.FC<
|
||||
''
|
||||
)
|
||||
}
|
||||
allowClear={!loading && allowClear}
|
||||
suffixIcon={renderAfter()}
|
||||
// @ts-ignore
|
||||
status={checkStatus || status}
|
||||
onSelect={handleOnSelect}
|
||||
onFocus={handleOnFocus}
|
||||
onBlur={handleOnBlur}
|
||||
onSearch={handleSearch}
|
||||
onChange={handleChange}
|
||||
popupRender={popupRender}
|
||||
></AutoComplete>
|
||||
</Wrapper>
|
||||
</SelectWrapper>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import IconFont from '@/components/icon-font';
|
||||
import type { SelectProps } from 'antd';
|
||||
import { Select } from 'antd';
|
||||
import React, { forwardRef, useImperativeHandle } from 'react';
|
||||
import NotFoundContent from '../components/not-found-content';
|
||||
|
||||
const BaseSelect: React.FC<SelectProps & { ref?: any }> = forwardRef(
|
||||
(props, ref) => {
|
||||
const { notFoundContent, loading, ...restProps } = props;
|
||||
const [isFocus, setIsFocus] = React.useState(false);
|
||||
const inputRef = React.useRef<any>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
...(inputRef.current || ({} as any))
|
||||
}));
|
||||
|
||||
const handleFocus = (e: React.FocusEvent<HTMLDivElement>) => {
|
||||
setIsFocus(true);
|
||||
props.onFocus?.(e);
|
||||
};
|
||||
const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
|
||||
setIsFocus(false);
|
||||
props.onBlur?.(e);
|
||||
};
|
||||
const renderSuffixIcon = () => {
|
||||
if (props.suffixIcon) {
|
||||
return props.suffixIcon;
|
||||
}
|
||||
if (!props.showSearch) {
|
||||
return <IconFont type="icon-down"></IconFont>;
|
||||
}
|
||||
return !isFocus ? <IconFont type="icon-down"></IconFont> : undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
{...restProps}
|
||||
notFoundContent={
|
||||
<NotFoundContent
|
||||
loading={loading}
|
||||
notFoundContent={notFoundContent}
|
||||
/>
|
||||
}
|
||||
ref={inputRef}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
suffixIcon={renderSuffixIcon()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default BaseSelect;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { Checkbox, CheckboxProps, Tooltip } from 'antd';
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import React from 'react';
|
||||
|
||||
const CheckboxField: React.FC<
|
||||
{
|
||||
description?: React.ReactNode;
|
||||
label: React.ReactNode;
|
||||
checked?: boolean;
|
||||
onChange?: (e: CheckboxChangeEvent) => void;
|
||||
} & CheckboxProps
|
||||
> = ({ description, label, checked, onChange, ...rest }) => {
|
||||
return (
|
||||
<Checkbox className="p-l-6" checked={checked} onChange={onChange} {...rest}>
|
||||
<Tooltip title={description || false}>
|
||||
<span style={{ color: 'var(--ant-color-text-tertiary)' }}>{label}</span>
|
||||
{!!description && (
|
||||
<QuestionCircleOutlined
|
||||
className="m-l-4"
|
||||
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
||||
/>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Checkbox>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckboxField;
|
||||
@@ -5,6 +5,11 @@
|
||||
color: var(--ant-color-text-tertiary);
|
||||
font-size: var(--font-size-base);
|
||||
|
||||
> span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.note-info {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
@@ -41,11 +41,7 @@ const NoteInfo: React.FC<NoteInfoProps> = (props) => {
|
||||
|
||||
return (
|
||||
<span className="label-text">
|
||||
{description ? (
|
||||
<Tooltip title={description}>{labelContent}</Tooltip>
|
||||
) : (
|
||||
labelContent
|
||||
)}
|
||||
<Tooltip title={description || false}>{labelContent}</Tooltip>
|
||||
{labelExtra}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Empty, Typography } from 'antd';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const { Link } = Typography;
|
||||
|
||||
const NoContent = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-block: 12px;
|
||||
`;
|
||||
|
||||
export const LoadingContent: React.FC = () => {
|
||||
return (
|
||||
<NoContent>
|
||||
<Link>
|
||||
<LoadingOutlined />
|
||||
</Link>
|
||||
</NoContent>
|
||||
);
|
||||
};
|
||||
|
||||
const NotFoundContent: React.FC<{
|
||||
loading?: boolean;
|
||||
notFoundContent: React.ReactNode;
|
||||
}> = ({ loading, notFoundContent }) => {
|
||||
const intl = useIntl();
|
||||
if (loading) {
|
||||
return <LoadingContent />;
|
||||
}
|
||||
return (
|
||||
<NoContent>
|
||||
{notFoundContent || (
|
||||
<Empty
|
||||
style={{ marginBlock: 0 }}
|
||||
description={intl.formatMessage({ id: 'common.data.empty' })}
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
/>
|
||||
)}
|
||||
</NoContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFoundContent;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Checkbox } from 'antd';
|
||||
import AutoComplete from '../auto-complete';
|
||||
import SealInput from '../seal-input';
|
||||
import SealSelect from '../seal-select';
|
||||
import Slider from '../seal-slider';
|
||||
@@ -12,6 +13,7 @@ const components: {
|
||||
Input: typeof SealInput.Input;
|
||||
Checkbox: typeof Checkbox;
|
||||
Switch: typeof Switch;
|
||||
AutoComplete: typeof AutoComplete;
|
||||
} = {
|
||||
InputNumber: SealInput.Number,
|
||||
Select: SealSelect,
|
||||
@@ -19,7 +21,8 @@ const components: {
|
||||
TextArea: SealInput.TextArea,
|
||||
Input: SealInput.Input,
|
||||
Checkbox: Checkbox,
|
||||
Switch: Switch
|
||||
Switch: Switch,
|
||||
AutoComplete: AutoComplete
|
||||
};
|
||||
|
||||
export default components;
|
||||
|
||||
@@ -23,6 +23,8 @@ const SealInputNumber: React.FC<InputNumberProps & SealFormItemProps> = (
|
||||
if (isInFormItems) {
|
||||
const statusData = Form?.Item?.useStatus?.();
|
||||
status = statusData?.status || '';
|
||||
} else {
|
||||
status = props.status || '';
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { isNotEmptyValue } from '@/utils/index';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import type { SelectProps } from 'antd';
|
||||
import { Form } from 'antd';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import SimpleSelect from './simple-select';
|
||||
import { SealFormItemProps } from './types';
|
||||
import Wrapper from './wrapper';
|
||||
import SelectWrapper from './wrapper/select';
|
||||
|
||||
const SealSelect: React.FC<
|
||||
SelectProps & SealFormItemProps & { showTags?: boolean }
|
||||
> = (props) => {
|
||||
const {
|
||||
label,
|
||||
placeholder,
|
||||
children,
|
||||
required,
|
||||
description,
|
||||
options,
|
||||
allowNull,
|
||||
isInFormItems = true,
|
||||
notFoundContent = null,
|
||||
...rest
|
||||
} = props;
|
||||
const intl = useIntl();
|
||||
const [isFocus, setIsFocus] = useState(false);
|
||||
const inputRef = useRef<any>(null);
|
||||
|
||||
let status = '';
|
||||
|
||||
// the status can be controlled by Form.Item
|
||||
if (isInFormItems) {
|
||||
const statusData = Form?.Item?.useStatus?.();
|
||||
status = statusData?.status || '';
|
||||
} else {
|
||||
status = props.status || '';
|
||||
}
|
||||
|
||||
const _options = useMemo(() => {
|
||||
if (!options?.length) {
|
||||
return [];
|
||||
}
|
||||
const list = cloneDeep(options);
|
||||
return list.map((item: any) => {
|
||||
if (item.locale) {
|
||||
item.label = intl.formatMessage({ id: item.label as string });
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}, [options, intl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isNotEmptyValue(props.value) ||
|
||||
(allowNull && (props.value === null || props.value === undefined))
|
||||
) {
|
||||
setIsFocus(true);
|
||||
}
|
||||
}, [props.value, allowNull]);
|
||||
|
||||
const handleClickWrapper = () => {
|
||||
if (!props.disabled && !isFocus) {
|
||||
inputRef.current?.focus?.();
|
||||
setIsFocus(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (val: any, options: any) => {
|
||||
if (isNotEmptyValue(val) || (allowNull && val === null)) {
|
||||
setIsFocus(true);
|
||||
} else {
|
||||
setIsFocus(false);
|
||||
}
|
||||
props.onChange?.(val, options);
|
||||
};
|
||||
|
||||
const handleOnFocus = (e: any) => {
|
||||
setIsFocus(true);
|
||||
props.onFocus?.(e);
|
||||
};
|
||||
|
||||
const handleOnBlur = (e: any) => {
|
||||
if (allowNull && props.value === null) {
|
||||
setIsFocus(true);
|
||||
} else if (!props.value) {
|
||||
setIsFocus(false);
|
||||
}
|
||||
props.onBlur?.(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectWrapper>
|
||||
<Wrapper
|
||||
className="seal-select-wrapper"
|
||||
status={status}
|
||||
label={label}
|
||||
isFocus={isFocus}
|
||||
required={required}
|
||||
description={description}
|
||||
disabled={props.disabled}
|
||||
onClick={handleClickWrapper}
|
||||
>
|
||||
<SimpleSelect
|
||||
{...rest}
|
||||
ref={inputRef}
|
||||
options={children ? null : _options}
|
||||
onFocus={handleOnFocus}
|
||||
onBlur={handleOnBlur}
|
||||
onChange={handleChange}
|
||||
notFoundContent={notFoundContent}
|
||||
>
|
||||
{children}
|
||||
</SimpleSelect>
|
||||
</Wrapper>
|
||||
</SelectWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SealSelect;
|
||||
@@ -1,3 +1,4 @@
|
||||
import IconFont from '@/components/icon-font';
|
||||
import { isNotEmptyValue } from '@/utils/index';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import type { CascaderAutoProps } from 'antd';
|
||||
@@ -20,10 +21,10 @@ const tag = (props: any) => {
|
||||
const renderTag = (props: any) => {
|
||||
return (
|
||||
<AutoTooltip
|
||||
className="m-r-4"
|
||||
closable={props.closable}
|
||||
onClose={props.onClose}
|
||||
maxWidth={240}
|
||||
style={{ marginRight: 4 }}
|
||||
filled
|
||||
>
|
||||
{tag(props)}
|
||||
@@ -33,10 +34,11 @@ const renderTag = (props: any) => {
|
||||
|
||||
const OptionNodes = (props: {
|
||||
data: any;
|
||||
notFoundContent?: React.ReactNode;
|
||||
optionNode: React.FC<{ data: any }>;
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { data, optionNode: OptionNode } = props;
|
||||
const { data, optionNode: OptionNode, notFoundContent } = props;
|
||||
if (data.value === '__EMPTY__') {
|
||||
return (
|
||||
<Empty
|
||||
@@ -48,15 +50,17 @@ const OptionNodes = (props: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
description={intl.formatMessage({
|
||||
id: 'common.search.empty'
|
||||
})}
|
||||
description={
|
||||
notFoundContent ||
|
||||
intl.formatMessage({
|
||||
id: 'common.search.empty'
|
||||
})
|
||||
}
|
||||
></Empty>
|
||||
);
|
||||
}
|
||||
let width: any = {
|
||||
maxWidth: 140,
|
||||
minWidth: 140
|
||||
maxWidth: 140
|
||||
};
|
||||
if (!data.parent) {
|
||||
width = undefined;
|
||||
@@ -85,6 +89,7 @@ const SealCascader: React.FC<
|
||||
allowNull,
|
||||
isInFormItems = true,
|
||||
optionNode,
|
||||
notFoundContent,
|
||||
tagRender,
|
||||
...rest
|
||||
} = props;
|
||||
@@ -168,11 +173,13 @@ const SealCascader: React.FC<
|
||||
>
|
||||
<Cascader
|
||||
{...rest}
|
||||
suffixIcon={<IconFont type="icon-down"></IconFont>}
|
||||
optionRender={
|
||||
optionNode
|
||||
? (data) => (
|
||||
<OptionNodes
|
||||
data={data}
|
||||
notFoundContent={notFoundContent}
|
||||
optionNode={optionNode}
|
||||
></OptionNodes>
|
||||
)
|
||||
@@ -185,7 +192,7 @@ const SealCascader: React.FC<
|
||||
onBlur={handleOnBlur}
|
||||
onChange={handleChange}
|
||||
notFoundContent={null}
|
||||
onDropdownVisibleChange={handleDropdownVisibleChange}
|
||||
onOpenChange={handleDropdownVisibleChange}
|
||||
></Cascader>
|
||||
</Wrapper>
|
||||
</SelectWrapper>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Checkbox, Form, type CheckboxProps } from 'antd';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import LabelInfo from './components/label-info';
|
||||
import { SealFormItemProps } from './types';
|
||||
import Wrapper from './wrapper';
|
||||
|
||||
const Inner = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding-inline: 14px;
|
||||
cursor: pointer;
|
||||
.ant-checkbox-wrapper {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
`;
|
||||
|
||||
interface SealCheckboxProps extends CheckboxProps, SealFormItemProps {}
|
||||
|
||||
const SealCheckbox: React.FC<SealCheckboxProps> = (props) => {
|
||||
const {
|
||||
label,
|
||||
checked,
|
||||
required,
|
||||
description,
|
||||
isInFormItems = true,
|
||||
defaultChecked,
|
||||
checkStatus,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
let status = '';
|
||||
if (isInFormItems) {
|
||||
const statusData = Form?.Item?.useStatus?.();
|
||||
status = statusData?.status || '';
|
||||
}
|
||||
|
||||
const handleChange = (e: any) => {
|
||||
props.onChange?.(e);
|
||||
};
|
||||
|
||||
const handleContainerClick = () => {
|
||||
// Create a synthetic event to toggle the checkbox
|
||||
const syntheticEvent = {
|
||||
target: {
|
||||
checked: !checked
|
||||
}
|
||||
};
|
||||
handleChange(syntheticEvent);
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
required={required}
|
||||
status={checkStatus || status}
|
||||
className="no-focus"
|
||||
>
|
||||
<Inner onClick={handleContainerClick}>
|
||||
<LabelInfo label={label} description={description}></LabelInfo>
|
||||
<Checkbox
|
||||
{...rest}
|
||||
defaultChecked={defaultChecked}
|
||||
style={{ marginBottom: 0, marginTop: 16, marginInline: 0 }}
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Inner>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SealCheckbox;
|
||||
@@ -22,6 +22,7 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
|
||||
trim = true,
|
||||
loading,
|
||||
labelExtra,
|
||||
style,
|
||||
...rest
|
||||
} = props;
|
||||
const [isFocus, setIsFocus] = useState(false);
|
||||
@@ -30,6 +31,8 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
|
||||
if (isInFormItems) {
|
||||
const statusData = Form?.Item?.useStatus?.();
|
||||
status = statusData?.status || '';
|
||||
} else {
|
||||
status = props.status || '';
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -60,11 +63,13 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
|
||||
};
|
||||
|
||||
const handleOnBlur = (e: any) => {
|
||||
e.target.value = e.target.value?.trim?.();
|
||||
if (!inputRef.current?.input?.value) {
|
||||
setIsFocus(false);
|
||||
}
|
||||
props.onChange?.(e);
|
||||
if (!trim) {
|
||||
e.target.value = e.target.value?.trim?.();
|
||||
props.onChange?.(e);
|
||||
}
|
||||
props.onBlur?.(e);
|
||||
};
|
||||
|
||||
@@ -73,7 +78,7 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<InputWrapper>
|
||||
<InputWrapper style={style}>
|
||||
<Wrapper
|
||||
status={checkStatus || status}
|
||||
label={label}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { isNotEmptyValue } from '@/utils/index';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import type { SelectProps } from 'antd';
|
||||
import { Form, Select } from 'antd';
|
||||
import { Form } from 'antd';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import BaseSelect from './base/select';
|
||||
import NotFoundContent from './components/not-found-content';
|
||||
import { SealFormItemProps } from './types';
|
||||
import Wrapper from './wrapper';
|
||||
import SelectWrapper from './wrapper/select';
|
||||
@@ -18,18 +20,22 @@ const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
|
||||
options,
|
||||
allowNull,
|
||||
isInFormItems = true,
|
||||
notFoundContent = null,
|
||||
loading,
|
||||
...rest
|
||||
} = props;
|
||||
const intl = useIntl();
|
||||
const [isFocus, setIsFocus] = useState(false);
|
||||
const inputRef = useRef<any>(null);
|
||||
const boxRef = useRef<any>(null);
|
||||
|
||||
let status = '';
|
||||
|
||||
// the status can be controlled by Form.Item
|
||||
if (isInFormItems) {
|
||||
const statusData = Form?.Item?.useStatus?.();
|
||||
status = statusData?.status || '';
|
||||
} else {
|
||||
status = props.status || '';
|
||||
}
|
||||
|
||||
const _options = useMemo(() => {
|
||||
@@ -46,7 +52,10 @@ const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
|
||||
}, [options, intl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isNotEmptyValue(props.value) || (allowNull && props.value === null)) {
|
||||
if (
|
||||
isNotEmptyValue(props.value) ||
|
||||
(allowNull && (props.value === null || props.value === undefined))
|
||||
) {
|
||||
setIsFocus(true);
|
||||
}
|
||||
}, [props.value, allowNull]);
|
||||
@@ -64,7 +73,7 @@ const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
|
||||
} else {
|
||||
setIsFocus(false);
|
||||
}
|
||||
props.onChange?.(val, options);
|
||||
props.onChange?.(val || null, options);
|
||||
};
|
||||
|
||||
const handleOnFocus = (e: any) => {
|
||||
@@ -93,17 +102,22 @@ const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
|
||||
disabled={props.disabled}
|
||||
onClick={handleClickWrapper}
|
||||
>
|
||||
<Select
|
||||
<BaseSelect
|
||||
{...rest}
|
||||
ref={inputRef}
|
||||
options={children ? null : _options}
|
||||
onFocus={handleOnFocus}
|
||||
onBlur={handleOnBlur}
|
||||
onChange={handleChange}
|
||||
notFoundContent={null}
|
||||
notFoundContent={
|
||||
<NotFoundContent
|
||||
loading={loading}
|
||||
notFoundContent={notFoundContent}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Select>
|
||||
</BaseSelect>
|
||||
</Wrapper>
|
||||
</SelectWrapper>
|
||||
);
|
||||
|
||||
@@ -34,6 +34,7 @@ const SealSlider: React.FC<SealSwitchProps> = (props) => {
|
||||
defaultValue,
|
||||
checkStatus,
|
||||
labelWidth,
|
||||
size = 'small',
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
@@ -57,7 +58,7 @@ const SealSlider: React.FC<SealSwitchProps> = (props) => {
|
||||
<LabelInfo label={label} description={description}></LabelInfo>
|
||||
<Switch
|
||||
{...rest}
|
||||
size="small"
|
||||
size={size}
|
||||
defaultValue={defaultValue}
|
||||
style={{ marginBottom: 0, marginTop: 16, marginInline: 0 }}
|
||||
value={value}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { Form, Input } from 'antd';
|
||||
import type { TextAreaProps } from 'antd/es/input/TextArea';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { SealFormItemProps } from './types';
|
||||
import Wrapper from './wrapper';
|
||||
import InputWrapper from './wrapper/input';
|
||||
|
||||
const LabelWrapper = styled.div`
|
||||
const LabelWrapper = styled.div.attrs({
|
||||
className: 'seal-textarea-label'
|
||||
})`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding-bottom: 2px;
|
||||
background-color: var(--ant-color-bg-container);
|
||||
`;
|
||||
|
||||
interface InputTextareaProps extends TextAreaProps {
|
||||
scaleSize?: boolean;
|
||||
alwaysFocus?: boolean; // it's order to display the placeholder
|
||||
}
|
||||
|
||||
const SealTextArea: React.FC<InputTextareaProps & SealFormItemProps> = (
|
||||
@@ -39,6 +40,7 @@ const SealTextArea: React.FC<InputTextareaProps & SealFormItemProps> = (
|
||||
addAfter,
|
||||
trim,
|
||||
scaleSize,
|
||||
alwaysFocus,
|
||||
...rest
|
||||
} = props;
|
||||
const [isFocus, setIsFocus] = useState(false);
|
||||
@@ -65,53 +67,43 @@ const SealTextArea: React.FC<InputTextareaProps & SealFormItemProps> = (
|
||||
return focusRows;
|
||||
}, [props.autoSize, isFocus, scaleSize]);
|
||||
|
||||
const handleClickWrapper = useCallback(() => {
|
||||
const handleClickWrapper = () => {
|
||||
if (!props.disabled && !isFocus) {
|
||||
inputRef.current?.focus?.({
|
||||
cursor: 'all'
|
||||
});
|
||||
setIsFocus(true);
|
||||
}
|
||||
}, [props.disabled, isFocus]);
|
||||
};
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: any) => {
|
||||
onChange?.(e);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
const handleChange = (e: any) => {
|
||||
onChange?.(e);
|
||||
};
|
||||
|
||||
const handleOnFocus = useCallback(
|
||||
(e: any) => {
|
||||
setIsFocus(true);
|
||||
onFocus?.(e);
|
||||
},
|
||||
[onFocus]
|
||||
);
|
||||
const handleOnFocus = (e: any) => {
|
||||
setIsFocus(true);
|
||||
onFocus?.(e);
|
||||
};
|
||||
|
||||
const handleOnBlur = useCallback(
|
||||
(e: any) => {
|
||||
if (!inputRef.current?.resizableTextArea?.textArea?.value) {
|
||||
setIsFocus(false);
|
||||
onBlur?.(e);
|
||||
}
|
||||
},
|
||||
[onBlur, scaleSize]
|
||||
);
|
||||
const handleOnBlur = (e: any) => {
|
||||
if (!inputRef.current?.resizableTextArea?.textArea?.value) {
|
||||
setIsFocus(false);
|
||||
}
|
||||
onBlur?.(e);
|
||||
};
|
||||
|
||||
const handleInput = useCallback(
|
||||
(e: any) => {
|
||||
onInput?.(e);
|
||||
},
|
||||
[onInput]
|
||||
);
|
||||
const handleInput = (e: any) => {
|
||||
onInput?.(e);
|
||||
};
|
||||
|
||||
console.log('style=========', style, rest);
|
||||
|
||||
return (
|
||||
<InputWrapper>
|
||||
<Wrapper
|
||||
status={status}
|
||||
label={<LabelWrapper>{label}</LabelWrapper>}
|
||||
isFocus={isFocus}
|
||||
isFocus={alwaysFocus || isFocus}
|
||||
required={required}
|
||||
description={description}
|
||||
className="seal-textarea-wrapper"
|
||||
@@ -122,9 +114,11 @@ const SealTextArea: React.FC<InputTextareaProps & SealFormItemProps> = (
|
||||
>
|
||||
<Input.TextArea
|
||||
{...rest}
|
||||
placeholder={placeholder}
|
||||
spellCheck={rest.spellCheck ?? false}
|
||||
autoSize={autoSize}
|
||||
ref={inputRef}
|
||||
style={{ minHeight: '80px', ...style }}
|
||||
style={{ ...style }}
|
||||
className="seal-textarea"
|
||||
onFocus={handleOnFocus}
|
||||
onBlur={handleOnBlur}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useIntl } from '@umijs/max';
|
||||
import type { SelectProps } from 'antd';
|
||||
import { Checkbox, Select, Tag } from 'antd';
|
||||
import { Checkbox, Tag } from 'antd';
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { forwardRef, useEffect, useImperativeHandle } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import AutoTooltip from '../auto-tooltip';
|
||||
import BaseSelect from './base/select';
|
||||
|
||||
const OptionWrapper = styled.div`
|
||||
display: flex;
|
||||
@@ -42,218 +43,239 @@ const TagWrapper = styled(Tag)`
|
||||
border-radius: 12px;
|
||||
`;
|
||||
|
||||
const SimpleSelect: React.FC<SelectProps> = (props) => {
|
||||
const intl = useIntl();
|
||||
const { options = [], ...restProps } = props;
|
||||
const SimpleSelect: React.FC<SelectProps & { ref?: any; showTags?: boolean }> =
|
||||
forwardRef((props, ref) => {
|
||||
const intl = useIntl();
|
||||
const { options = [], showTags, ...restProps } = props;
|
||||
|
||||
const [allSelection, setAllSelection] = React.useState<{
|
||||
checked: boolean;
|
||||
indeterminate: boolean;
|
||||
}>({
|
||||
checked: false,
|
||||
indeterminate: false
|
||||
});
|
||||
const [optionsList, setOptionsList] = React.useState<any[]>(options || []);
|
||||
const selectRef = React.useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setOptionsList(options || []);
|
||||
}, [options]);
|
||||
|
||||
const optionRender = (option: any, info: any) => {
|
||||
const { value, label } = option;
|
||||
return (
|
||||
<OptionWrapper>
|
||||
{restProps.value?.includes(value) ? (
|
||||
<Checkbox checked></Checkbox>
|
||||
) : (
|
||||
<Checkbox></Checkbox>
|
||||
)}
|
||||
<AutoTooltip ghost>{label}</AutoTooltip>
|
||||
</OptionWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const handleOnCheckboxChange = (e: CheckboxChangeEvent) => {
|
||||
const isChecked = e.target.checked;
|
||||
const allValues = optionsList?.map((opt: any) => opt.value) || [];
|
||||
|
||||
setAllSelection({
|
||||
checked: isChecked,
|
||||
const [allSelection, setAllSelection] = React.useState<{
|
||||
checked: boolean;
|
||||
indeterminate: boolean;
|
||||
}>({
|
||||
checked: false,
|
||||
indeterminate: false
|
||||
});
|
||||
const [optionsList, setOptionsList] = React.useState<any[]>(options || []);
|
||||
const selectRef = React.useRef<any>(null);
|
||||
const selectorRef = React.useRef<any>(null);
|
||||
|
||||
let allSelectedValues = [...(restProps.value || [])];
|
||||
useEffect(() => {
|
||||
setOptionsList(options || []);
|
||||
}, [options]);
|
||||
|
||||
if (isChecked) {
|
||||
// Select all options
|
||||
allSelectedValues = Array.from(
|
||||
new Set([...allSelectedValues, ...allValues])
|
||||
const optionRender = (option: any, info: any) => {
|
||||
const { value, label } = option;
|
||||
return (
|
||||
<OptionWrapper>
|
||||
{restProps.value?.includes?.(value) ? (
|
||||
<Checkbox checked></Checkbox>
|
||||
) : (
|
||||
<Checkbox></Checkbox>
|
||||
)}
|
||||
<AutoTooltip ghost>{label}</AutoTooltip>
|
||||
</OptionWrapper>
|
||||
);
|
||||
} else {
|
||||
// Deselect all options
|
||||
allSelectedValues = allSelectedValues.filter(
|
||||
(value) => !allValues.includes(value)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
restProps.onChange?.(allSelectedValues, optionsList || []);
|
||||
};
|
||||
const handleOnCheckboxChange = (e: CheckboxChangeEvent) => {
|
||||
const isChecked = e.target.checked;
|
||||
const allValues = optionsList?.map((opt: any) => opt.value) || [];
|
||||
|
||||
const dropdownRender = (originPanel: React.ReactNode) => {
|
||||
return (
|
||||
<DropdownWrapper>
|
||||
{restProps.mode === 'multiple' && (
|
||||
<SelectAllWrapper>
|
||||
<Checkbox
|
||||
checked={allSelection.checked}
|
||||
indeterminate={allSelection.indeterminate}
|
||||
onChange={handleOnCheckboxChange}
|
||||
>
|
||||
{intl.formatMessage({ id: 'common.checbox.all' })}
|
||||
</Checkbox>
|
||||
</SelectAllWrapper>
|
||||
)}
|
||||
{originPanel}
|
||||
</DropdownWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const handleOnChange = (value: any, option: any) => {
|
||||
const selectedValues = Array.isArray(value) ? value : [value];
|
||||
const allSelected = optionsList?.map((opt: any) => opt.value) || [];
|
||||
const isAllSelected = selectedValues.length === allSelected?.length;
|
||||
|
||||
setAllSelection({
|
||||
checked: isAllSelected,
|
||||
indeterminate: !isAllSelected && selectedValues.length > 0
|
||||
});
|
||||
|
||||
restProps.onChange?.(selectedValues, option);
|
||||
};
|
||||
|
||||
const filterOption = (inputValue: string, option: any) => {
|
||||
if (!option || !option.label) return false;
|
||||
return option.label.toLowerCase().includes(inputValue.toLowerCase());
|
||||
};
|
||||
|
||||
const checkAllSelection = (list: Global.BaseOption<string | number>[]) => {
|
||||
if (
|
||||
!restProps.value ||
|
||||
!Array.isArray(restProps.value) ||
|
||||
list.length === 0
|
||||
) {
|
||||
setAllSelection({
|
||||
checked: false,
|
||||
checked: isChecked,
|
||||
indeterminate: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
const selectedValues = new Set(restProps.value);
|
||||
const allValues = list?.map((opt: any) => opt.value) || [];
|
||||
|
||||
const isAllSelected = allValues.every((val: any) =>
|
||||
selectedValues.has(val)
|
||||
);
|
||||
let allSelectedValues = [...(restProps.value || [])];
|
||||
|
||||
const isSomeSelected = allValues.some((val: any) =>
|
||||
selectedValues.has(val)
|
||||
);
|
||||
if (isChecked) {
|
||||
// Select all options
|
||||
allSelectedValues = Array.from(
|
||||
new Set([...allSelectedValues, ...allValues])
|
||||
);
|
||||
} else {
|
||||
// Deselect all options
|
||||
allSelectedValues = allSelectedValues.filter(
|
||||
(value) => !allValues.includes(value)
|
||||
);
|
||||
}
|
||||
|
||||
setAllSelection({
|
||||
checked: isAllSelected,
|
||||
indeterminate: isSomeSelected && !isAllSelected
|
||||
});
|
||||
};
|
||||
restProps.onChange?.(allSelectedValues, optionsList || []);
|
||||
};
|
||||
|
||||
const TagRender = (props: any) => {
|
||||
const { label } = props;
|
||||
const count = props.isMaxTag ? label.slice(0, -3).slice(1) : label;
|
||||
const dropdownRender = (originPanel: React.ReactNode) => {
|
||||
return (
|
||||
<DropdownWrapper>
|
||||
{restProps.mode === 'multiple' && (
|
||||
<SelectAllWrapper>
|
||||
<Checkbox
|
||||
checked={allSelection.checked}
|
||||
indeterminate={allSelection.indeterminate}
|
||||
onChange={handleOnCheckboxChange}
|
||||
>
|
||||
{intl.formatMessage({ id: 'common.checbox.all' })}
|
||||
</Checkbox>
|
||||
</SelectAllWrapper>
|
||||
)}
|
||||
{originPanel}
|
||||
</DropdownWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<TagWrapper
|
||||
bordered={false}
|
||||
style={{
|
||||
height: 24,
|
||||
backgroundColor: 'var(--ant-color-fill-tertiary)',
|
||||
fontSize: 'var(--ant-font-size)'
|
||||
}}
|
||||
className="flex-center"
|
||||
>
|
||||
{intl.formatMessage({ id: 'common.select.count' }, { count: count })}
|
||||
</TagWrapper>
|
||||
);
|
||||
};
|
||||
const handleOnChange = (value: any, option: any) => {
|
||||
const selectedValues = Array.isArray(value) ? value : [value];
|
||||
const allSelected = optionsList?.map((opt: any) => opt.value) || [];
|
||||
const isAllSelected = selectedValues.length === allSelected?.length;
|
||||
|
||||
const handleOnSearch = (value: string) => {
|
||||
if (restProps.onSearch) {
|
||||
restProps.onSearch(value);
|
||||
} else {
|
||||
const filteredOptions = options?.filter((option: any) =>
|
||||
option.label.toLowerCase().includes(value.toLowerCase())
|
||||
) as Global.BaseOption<string | number>[];
|
||||
setOptionsList(filteredOptions || []);
|
||||
checkAllSelection(filteredOptions || []);
|
||||
}
|
||||
};
|
||||
setAllSelection({
|
||||
checked: isAllSelected,
|
||||
indeterminate: !isAllSelected && selectedValues.length > 0
|
||||
});
|
||||
|
||||
const handleOnBlur = (e: any) => {
|
||||
restProps.onBlur?.(e);
|
||||
};
|
||||
restProps.onChange?.(selectedValues, option);
|
||||
};
|
||||
|
||||
const handleOnFocus = (e: any) => {
|
||||
restProps.onFocus?.(e);
|
||||
};
|
||||
const filterOption = (inputValue: string, option: any) => {
|
||||
if (!option || !option.label) return false;
|
||||
return option.label.toLowerCase().includes(inputValue.toLowerCase());
|
||||
};
|
||||
|
||||
const handleOnOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
checkAllSelection(options as Global.BaseOption<string | number>[]);
|
||||
setOptionsList(options || []);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const input = selectRef.current?.querySelector?.('input');
|
||||
|
||||
if (!input) return;
|
||||
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
const checkAllSelection = (list: Global.BaseOption<string | number>[]) => {
|
||||
if (
|
||||
event.key === 'Backspace' &&
|
||||
(input as HTMLInputElement).value === ''
|
||||
!restProps.value ||
|
||||
!Array.isArray(restProps.value) ||
|
||||
list.length === 0
|
||||
) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
setAllSelection({
|
||||
checked: false,
|
||||
indeterminate: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
const selectedValues = new Set(restProps.value);
|
||||
const allValues = list?.map((opt: any) => opt.value) || [];
|
||||
|
||||
const isAllSelected = allValues.every((val: any) =>
|
||||
selectedValues.has(val)
|
||||
);
|
||||
|
||||
const isSomeSelected = allValues.some((val: any) =>
|
||||
selectedValues.has(val)
|
||||
);
|
||||
|
||||
setAllSelection({
|
||||
checked: isAllSelected,
|
||||
indeterminate: isSomeSelected && !isAllSelected
|
||||
});
|
||||
};
|
||||
|
||||
const TagRender = (props: any) => {
|
||||
const { label } = props;
|
||||
const count = props.isMaxTag ? label.slice(0, -3).slice(1) : label;
|
||||
|
||||
return (
|
||||
<TagWrapper
|
||||
bordered={false}
|
||||
closable={props.closable}
|
||||
onClose={props.onClose}
|
||||
style={{
|
||||
height: 24,
|
||||
backgroundColor: 'var(--ant-color-fill-tertiary)',
|
||||
fontSize: 'var(--ant-font-size)'
|
||||
}}
|
||||
className="flex-center"
|
||||
>
|
||||
{showTags
|
||||
? label
|
||||
: intl.formatMessage(
|
||||
{ id: 'common.select.count' },
|
||||
{ count: count }
|
||||
)}
|
||||
</TagWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const handleOnSearch = (value: string) => {
|
||||
if (restProps.onSearch) {
|
||||
restProps.onSearch(value);
|
||||
} else {
|
||||
const filteredOptions = options?.filter((option: any) =>
|
||||
option.label.toLowerCase().includes(value.toLowerCase())
|
||||
) as Global.BaseOption<string | number>[];
|
||||
setOptionsList(filteredOptions || []);
|
||||
checkAllSelection(filteredOptions || []);
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener('keydown', handler);
|
||||
|
||||
return () => {
|
||||
input.removeEventListener('keydown', handler);
|
||||
const handleOnBlur = (e: any) => {
|
||||
restProps.onBlur?.(e);
|
||||
};
|
||||
}, [selectRef.current]);
|
||||
|
||||
return (
|
||||
<div ref={selectRef}>
|
||||
<Select
|
||||
{...restProps}
|
||||
options={optionsList}
|
||||
maxTagCount={0}
|
||||
defaultActiveFirstOption={false}
|
||||
dropdownRender={dropdownRender}
|
||||
optionRender={optionRender}
|
||||
menuItemSelectedIcon={false}
|
||||
onChange={handleOnChange}
|
||||
tagRender={TagRender}
|
||||
onBlur={handleOnBlur}
|
||||
onFocus={handleOnFocus}
|
||||
onSearch={handleOnSearch}
|
||||
filterOption={filterOption}
|
||||
onDropdownVisibleChange={handleOnOpenChange}
|
||||
></Select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const handleOnFocus = (e: any) => {
|
||||
restProps.onFocus?.(e);
|
||||
};
|
||||
|
||||
const handleOnOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
checkAllSelection(options as Global.BaseOption<string | number>[]);
|
||||
setOptionsList(options || []);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const input = selectRef.current?.querySelector?.('input');
|
||||
|
||||
if (!input) return;
|
||||
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === 'Backspace' &&
|
||||
(input as HTMLInputElement).value === ''
|
||||
) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener('keydown', handler);
|
||||
|
||||
return () => {
|
||||
input.removeEventListener('keydown', handler);
|
||||
};
|
||||
}, [selectRef.current]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
selectorRef.current?.focus();
|
||||
},
|
||||
blur: () => {
|
||||
selectorRef.current?.blur();
|
||||
}
|
||||
}));
|
||||
|
||||
return (
|
||||
<div ref={selectRef} style={{ width: 'inherit' }}>
|
||||
<BaseSelect
|
||||
{...restProps}
|
||||
ref={selectorRef}
|
||||
options={optionsList}
|
||||
maxTagCount={restProps.maxTagCount || 0}
|
||||
defaultActiveFirstOption={false}
|
||||
popupRender={dropdownRender}
|
||||
optionRender={optionRender}
|
||||
menuItemSelectedIcon={false}
|
||||
onChange={handleOnChange}
|
||||
tagRender={TagRender}
|
||||
onBlur={handleOnBlur}
|
||||
onFocus={handleOnFocus}
|
||||
onSearch={handleOnSearch}
|
||||
filterOption={filterOption}
|
||||
onOpenChange={handleOnOpenChange}
|
||||
>
|
||||
{props.children}
|
||||
</BaseSelect>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default SimpleSelect;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import styled from 'styled-components';
|
||||
import { BGCOLOR, INPUT_INNER_PADDING } from '../config';
|
||||
|
||||
const AutoCompleteLabel = styled.span`
|
||||
position: absolute;
|
||||
left: ${INPUT_INNER_PADDING}px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
top: 26px;
|
||||
pointer-events: none;
|
||||
transition: all 0.2s;
|
||||
background-color: var(--ant-color-bg-container);
|
||||
z-index: 10;
|
||||
&.disabled {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
&.isfoucs-has-value {
|
||||
// display: none;
|
||||
z-index: -1;
|
||||
// top: 9px;
|
||||
// font-size: 12px;
|
||||
// color: var(--ant-color-text);
|
||||
// background-color: ${BGCOLOR};
|
||||
// padding: 0 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default AutoCompleteLabel;
|
||||
@@ -154,8 +154,11 @@ export const Label = styled.div.attrs<{
|
||||
`;
|
||||
|
||||
// inner
|
||||
const Inner = styled.div`
|
||||
const Inner = styled.div.attrs({
|
||||
className: '__inner__'
|
||||
})`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const Extra = styled.div`
|
||||
@@ -167,9 +170,13 @@ const Extra = styled.div`
|
||||
|
||||
const AddAfter = styled.div`
|
||||
position: relative;
|
||||
color: var(--ant-color-text-quaternary);
|
||||
color: var(--ant-color-text-tertiary);
|
||||
font-size: var(--font-size-base);
|
||||
padding-right: calc(var(--ant-padding-sm) - 1px);
|
||||
padding-inline: calc(var(--ant-padding-sm) - 1px);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--ant-color-fill-secondary);
|
||||
`;
|
||||
|
||||
// for wrapper
|
||||
|
||||
@@ -76,9 +76,11 @@ const InputWrapper = styled.div`
|
||||
}
|
||||
.ant-input.seal-textarea {
|
||||
flex: 1;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.ant-input-number {
|
||||
flex: 1;
|
||||
position: static;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -87,6 +89,7 @@ const InputWrapper = styled.div`
|
||||
padding: 0;
|
||||
height: ${INPUTHEIGHT}px !important;
|
||||
background-color: ${BGCOLOR};
|
||||
flex: 1;
|
||||
|
||||
&:hover .ant-input-number-handler-wrap,
|
||||
&-focused .ant-input-number-handler-wrap {
|
||||
@@ -160,6 +163,15 @@ const InputWrapper = styled.div`
|
||||
.seal-textarea-wrapper {
|
||||
height: auto;
|
||||
padding-right: 10px;
|
||||
.ant-input-outlined {
|
||||
height: auto !important;
|
||||
}
|
||||
textarea {
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
}
|
||||
.ant-input-textarea-allow-clear.ant-input-affix-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -97,9 +97,14 @@ const SelectWrapper = styled.div`
|
||||
align-items: center;
|
||||
height: 54px;
|
||||
|
||||
.ant-select-selection-search {
|
||||
top: 20px !important;
|
||||
inset-inline-start: ${INPUT_INNER_PADDING}px;
|
||||
.ant-select-selection-wrap {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.ant-select-auto-complete {
|
||||
.ant-select-selection-search {
|
||||
padding-inline-start: ${INPUT_INNER_PADDING}px;
|
||||
}
|
||||
}
|
||||
&.ant-select-multiple.ant-cascader .ant-select-selection-search {
|
||||
top: 0 !important;
|
||||
|
||||
@@ -6,9 +6,13 @@ const SliderWrapper = styled.div`
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.borderless {
|
||||
background-color: transparent;
|
||||
}
|
||||
.ant-slider {
|
||||
flex: 1;
|
||||
}
|
||||
padding-block: 0;
|
||||
padding-inline: 2px;
|
||||
input.ant-input-number-input {
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { CheckOutlined, FormOutlined, UndoOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Input, InputNumber, Tooltip } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import RowContext from '../row-context';
|
||||
import '../styles/cell.less';
|
||||
import { SealColumnProps } from '../types';
|
||||
import { CellContentProps } from '../types';
|
||||
|
||||
const CellContentWrapper = styled.div`
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
interface EditButtonsProps {
|
||||
isEditing: boolean;
|
||||
@@ -16,7 +21,7 @@ interface EditButtonsProps {
|
||||
handleEdit: () => void;
|
||||
}
|
||||
|
||||
interface CellContentProps {
|
||||
interface ContentProps {
|
||||
isEditing: boolean;
|
||||
current: any;
|
||||
editable: any;
|
||||
@@ -89,7 +94,7 @@ const EditButtons: React.FC<EditButtonsProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const CellContent: React.FC<CellContentProps> = (props) => {
|
||||
const Content: React.FC<ContentProps> = (props) => {
|
||||
const { editable, current, isEditing, row, render, onChange } = props;
|
||||
if (isEditing && editable) {
|
||||
const isNumType =
|
||||
@@ -112,9 +117,9 @@ const CellContent: React.FC<CellContentProps> = (props) => {
|
||||
return current;
|
||||
};
|
||||
|
||||
const SealColumn: React.FC<SealColumnProps> = (props) => {
|
||||
const CellContent: React.FC<CellContentProps> = (props) => {
|
||||
const { row, onCell } = useContext(RowContext);
|
||||
const { dataIndex, render, align, editable } = props;
|
||||
const { dataIndex, render, editable } = props;
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
const [current, setCurrent] = React.useState(row[dataIndex]);
|
||||
const cachedValue = React.useRef(null);
|
||||
@@ -130,7 +135,11 @@ const SealColumn: React.FC<SealColumnProps> = (props) => {
|
||||
...row,
|
||||
[dataIndex]: current
|
||||
},
|
||||
dataIndex
|
||||
{
|
||||
dataIndex,
|
||||
newValue: current,
|
||||
oldValue: row[dataIndex]
|
||||
}
|
||||
);
|
||||
setIsEditing(false);
|
||||
};
|
||||
@@ -150,32 +159,24 @@ const SealColumn: React.FC<SealColumnProps> = (props) => {
|
||||
}, [row[dataIndex]]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('cell', {
|
||||
'cell-left': align === 'left',
|
||||
'cell-center': align === 'center',
|
||||
'cell-right': align === 'right'
|
||||
})}
|
||||
>
|
||||
<span className="cell-content flex-center">
|
||||
<CellContent
|
||||
onChange={handleValueChange}
|
||||
isEditing={isEditing}
|
||||
editable={editable}
|
||||
current={current}
|
||||
row={row}
|
||||
render={render}
|
||||
></CellContent>
|
||||
<EditButtons
|
||||
editable={editable}
|
||||
isEditing={isEditing}
|
||||
handleEdit={handleEdit}
|
||||
handleSubmit={handleSubmit}
|
||||
handleUndo={handleUndo}
|
||||
></EditButtons>
|
||||
</span>
|
||||
</div>
|
||||
<CellContentWrapper>
|
||||
<Content
|
||||
onChange={handleValueChange}
|
||||
isEditing={isEditing}
|
||||
editable={editable}
|
||||
current={current}
|
||||
row={row}
|
||||
render={render}
|
||||
></Content>
|
||||
<EditButtons
|
||||
editable={editable}
|
||||
isEditing={isEditing}
|
||||
handleEdit={handleEdit}
|
||||
handleSubmit={handleSubmit}
|
||||
handleUndo={handleUndo}
|
||||
></EditButtons>
|
||||
</CellContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SealColumn;
|
||||
export default CellContent;
|
||||
@@ -18,6 +18,7 @@ const Header: React.FC<HeaderProps> = (props) => {
|
||||
title,
|
||||
dataIndex,
|
||||
align,
|
||||
width,
|
||||
span,
|
||||
headerStyle,
|
||||
sortOrder,
|
||||
@@ -31,6 +32,7 @@ const Header: React.FC<HeaderProps> = (props) => {
|
||||
sorter={sorter}
|
||||
dataIndex={dataIndex}
|
||||
sortOrder={sortOrder}
|
||||
width={width}
|
||||
defaultSortOrder={defaultSortOrder}
|
||||
title={title}
|
||||
style={headerStyle}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { SealColumnProps } from '../types';
|
||||
import CellContent from './cell-content';
|
||||
|
||||
const CellWrapper = styled.div`
|
||||
padding: var(--ant-table-cell-padding-block)
|
||||
var(--ant-table-cell-padding-inline);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
min-height: 68px;
|
||||
word-break: break-word;
|
||||
min-width: 20px;
|
||||
overflow: hidden;
|
||||
|
||||
&.left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&.right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&.center {
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const TableCell: React.FC<SealColumnProps> = (props) => {
|
||||
const { dataIndex, render, align, editable } = props;
|
||||
|
||||
return (
|
||||
<CellWrapper
|
||||
className={classNames('cell', {
|
||||
left: align === 'left',
|
||||
center: align === 'center',
|
||||
right: align === 'right'
|
||||
})}
|
||||
>
|
||||
<CellContent
|
||||
dataIndex={dataIndex}
|
||||
render={render}
|
||||
editable={editable}
|
||||
></CellContent>
|
||||
</CellWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableCell;
|
||||
@@ -1,3 +1,4 @@
|
||||
import AutoTooltip from '@/components/auto-tooltip';
|
||||
import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
@@ -15,6 +16,7 @@ const TableHeader: React.FC<TableHeaderProps> = (props) => {
|
||||
sortOrder,
|
||||
onSort,
|
||||
sorter,
|
||||
width,
|
||||
dataIndex
|
||||
} = props;
|
||||
|
||||
@@ -27,7 +29,7 @@ const TableHeader: React.FC<TableHeaderProps> = (props) => {
|
||||
};
|
||||
return (
|
||||
<div
|
||||
style={{ ...style }}
|
||||
style={{ width, ...style }}
|
||||
className={classNames('table-header', {
|
||||
'table-header-left': align === 'left',
|
||||
'table-header-center': align === 'center',
|
||||
@@ -39,7 +41,9 @@ const TableHeader: React.FC<TableHeaderProps> = (props) => {
|
||||
>
|
||||
{sorter ? (
|
||||
<span className="sorter-header" onClick={handleOnSort}>
|
||||
<span className="table-header-cell">{title}</span>
|
||||
<AutoTooltip ghost>
|
||||
<span className="table-header-cell">{title}</span>
|
||||
</AutoTooltip>
|
||||
<span className="sorter">
|
||||
<CaretUpOutlined
|
||||
className={classNames('sorter-up', {
|
||||
@@ -54,7 +58,9 @@ const TableHeader: React.FC<TableHeaderProps> = (props) => {
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<div className="table-header-cell">{title}</div>
|
||||
<AutoTooltip ghost>
|
||||
<span className="table-header-cell">{title}</span>
|
||||
</AutoTooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||