feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { IconCozEdit, IconCozTrashCan } from '@coze-arch/coze-design/icons';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
|
||||
import { type TableViewRecord } from '../types';
|
||||
|
||||
import styles from './index.module.less';
|
||||
export interface ActionsRenderProps {
|
||||
record: TableViewRecord;
|
||||
index: number;
|
||||
editProps?: {
|
||||
disabled: boolean;
|
||||
// 编辑回调
|
||||
onEdit?: (record: TableViewRecord, index: number) => void;
|
||||
};
|
||||
deleteProps?: {
|
||||
disabled: boolean;
|
||||
// 删除回调
|
||||
onDelete?: (index: number) => void;
|
||||
};
|
||||
className?: string;
|
||||
}
|
||||
export const ActionsRender = ({
|
||||
record,
|
||||
index,
|
||||
editProps = { disabled: false },
|
||||
deleteProps = { disabled: false },
|
||||
}: ActionsRenderProps) => {
|
||||
const { disabled: editDisabled, onEdit } = editProps;
|
||||
const { disabled: deleteDisabled, onDelete } = deleteProps;
|
||||
|
||||
return (
|
||||
<div className={classNames(styles['actions-render'], 'table-view-actions')}>
|
||||
{!editDisabled && (
|
||||
<Button
|
||||
size="mini"
|
||||
color="secondary"
|
||||
icon={<IconCozEdit className="text-[14px]" />}
|
||||
className={styles['action-edit']}
|
||||
onClick={() => onEdit && onEdit(record, index)}
|
||||
></Button>
|
||||
)}
|
||||
{!deleteDisabled && (
|
||||
<Button
|
||||
size="mini"
|
||||
color="secondary"
|
||||
icon={<IconCozTrashCan className="text-[14px]" />}
|
||||
className={styles['action-delete']}
|
||||
onClick={() => onDelete && onDelete(index)}
|
||||
></Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { Tooltip, UIButton, UIInput } from '@coze-arch/bot-semi';
|
||||
import { IconDeleteOutline, IconToastError } from '@coze-arch/bot-icons';
|
||||
|
||||
import { type ValidatorProps } from '../types';
|
||||
|
||||
import styles from './index.module.less';
|
||||
export interface EditHeaderRenderProps {
|
||||
value: string;
|
||||
deleteProps?: {
|
||||
// 禁用删除
|
||||
disabled: boolean;
|
||||
// 删除回调
|
||||
onDelete?: (v: string) => void;
|
||||
};
|
||||
editProps?: {
|
||||
// 编辑回调
|
||||
onChange?: (v: string) => void;
|
||||
// 失焦回调
|
||||
onBlur?: (v: string) => void;
|
||||
};
|
||||
// 失焦回调
|
||||
onBlur: (v: string) => void;
|
||||
// 表头校验逻辑
|
||||
validator: ValidatorProps;
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
export const EditHeaderRender = ({
|
||||
value,
|
||||
validator = {},
|
||||
deleteProps = { disabled: false },
|
||||
editProps = {},
|
||||
editable = true,
|
||||
}: EditHeaderRenderProps) => {
|
||||
const { validate, errorMsg } = validator;
|
||||
|
||||
const { onChange, onBlur } = editProps;
|
||||
const { disabled: deleteDisabled, onDelete } = deleteProps;
|
||||
|
||||
const [isEditCom, setIsEditCom] = useState(false);
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
const [readonly, setReadonly] = useState(true);
|
||||
|
||||
const onBlurFn = () => {
|
||||
if (onBlur) {
|
||||
onBlur(inputValue);
|
||||
}
|
||||
setReadonly(true);
|
||||
setIsEditCom(false);
|
||||
};
|
||||
const onChangeFn = (v: string) => {
|
||||
if (onChange) {
|
||||
onChange(v);
|
||||
}
|
||||
setInputValue(v);
|
||||
};
|
||||
const isError = useMemo(() => validate && validate(value), [inputValue]);
|
||||
return (
|
||||
<div className={styles['edit-header-render']}>
|
||||
{/* 编辑态组件 */}
|
||||
{isEditCom && (
|
||||
<UIInput
|
||||
autoFocus
|
||||
readonly={readonly}
|
||||
validateStatus={isError ? 'error' : 'default'}
|
||||
suffix={
|
||||
isError ? (
|
||||
<Tooltip content={errorMsg}>
|
||||
<IconToastError />
|
||||
</Tooltip>
|
||||
) : null
|
||||
}
|
||||
className={styles['header-input']}
|
||||
value={inputValue}
|
||||
onClick={() => {
|
||||
if (editable) {
|
||||
setReadonly(false);
|
||||
}
|
||||
}}
|
||||
onBlur={onBlurFn}
|
||||
onChange={onChangeFn}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 预览态组件 */}
|
||||
{!isEditCom && (
|
||||
<div
|
||||
className={styles['header-preview']}
|
||||
onClick={() => setIsEditCom(true)}
|
||||
>
|
||||
{inputValue}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 列删除按钮 */}
|
||||
{editable && (
|
||||
<UIButton
|
||||
disabled={deleteDisabled}
|
||||
icon={<IconDeleteOutline />}
|
||||
className={styles['header-delete']}
|
||||
onClick={() => onDelete && onDelete(inputValue)}
|
||||
></UIButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Image } from '@coze-arch/bot-semi';
|
||||
import { IconImageFailOutlined } from '@coze-arch/bot-icons';
|
||||
|
||||
import styles from '../index.module.less';
|
||||
import { useImagePreview } from './use-image-preview';
|
||||
export interface ImageRenderProps {
|
||||
srcList: string[];
|
||||
// 图片是否可编辑,默认为false
|
||||
editable?: boolean;
|
||||
onChange?: (tosKey: string, src: string) => void;
|
||||
dataIndex?: string;
|
||||
className?: string;
|
||||
customEmpty?: (props: { onClick?: () => void }) => React.ReactNode;
|
||||
}
|
||||
|
||||
export interface ImageContainerProps {
|
||||
srcList: string[];
|
||||
onClick?: () => void;
|
||||
setCurSrc?: (src: string) => void;
|
||||
}
|
||||
|
||||
const ImageContainer = ({
|
||||
srcList,
|
||||
onClick,
|
||||
setCurSrc,
|
||||
...imageProps
|
||||
}: ImageContainerProps) => (
|
||||
<div
|
||||
className={styles['image-container']}
|
||||
onClick={() => {
|
||||
if (!srcList.length || !srcList[0]) {
|
||||
onClick?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{srcList.map(src => (
|
||||
<Image
|
||||
{...imageProps}
|
||||
onClick={() => {
|
||||
setCurSrc?.(src);
|
||||
onClick?.();
|
||||
}}
|
||||
preview={false}
|
||||
src={src}
|
||||
// 失败时兜底图
|
||||
fallback={
|
||||
<IconImageFailOutlined
|
||||
className={styles['image-failed']}
|
||||
onClick={() => {
|
||||
setCurSrc?.(src);
|
||||
onClick?.();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
// 图片加载时的占位图,主要用于大图加载
|
||||
placeholder={<div className="image-skeleton" onClick={onClick} />}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
export const ImageRender: React.FC<ImageRenderProps> = ({
|
||||
srcList = [],
|
||||
editable = true,
|
||||
onChange,
|
||||
className = '',
|
||||
customEmpty,
|
||||
}) => {
|
||||
const [curSrc, setCurSrc] = useState(srcList?.[0] || '');
|
||||
const { open, node: imagePreviewModal } = useImagePreview({
|
||||
editable,
|
||||
src: curSrc,
|
||||
setSrc: setCurSrc,
|
||||
onChange,
|
||||
});
|
||||
useEffect(() => {
|
||||
setCurSrc(srcList?.[0] || '');
|
||||
}, [srcList]);
|
||||
return (
|
||||
<div
|
||||
className={`${className} ${styles['image-render-wrapper']} ${
|
||||
!curSrc ? styles['image-render-empty'] : ''
|
||||
}`}
|
||||
>
|
||||
{(!srcList || !srcList.length) && customEmpty ? (
|
||||
customEmpty({ onClick: open })
|
||||
) : (
|
||||
<ImageContainer
|
||||
srcList={srcList}
|
||||
onClick={open}
|
||||
setCurSrc={setCurSrc}
|
||||
/>
|
||||
)}
|
||||
|
||||
{imagePreviewModal}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { REPORT_EVENTS } from '@coze-arch/report-events';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozUpload } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
Upload,
|
||||
Input,
|
||||
Image,
|
||||
Typography,
|
||||
Spin,
|
||||
Toast,
|
||||
} from '@coze-arch/coze-design';
|
||||
import { type UploadProps } from '@coze-arch/bot-semi/Upload';
|
||||
import { IconImageFailOutlined } from '@coze-arch/bot-icons';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
import { FileBizType } from '@coze-arch/bot-api/developer_api';
|
||||
import { DeveloperApi } from '@coze-arch/bot-api';
|
||||
import { useDataModalWithCoze } from '@coze-data/utils';
|
||||
|
||||
import styles from '../index.module.less';
|
||||
import { getBase64, getFileExtension, isValidSize } from './utils';
|
||||
|
||||
export interface UseImagePreviewProps {
|
||||
src: string;
|
||||
setSrc: (src: string) => void;
|
||||
onChange?: (src: string, tosKey: string) => void;
|
||||
editable?: boolean;
|
||||
}
|
||||
export const useImagePreview = ({
|
||||
src,
|
||||
setSrc,
|
||||
onChange,
|
||||
editable = true,
|
||||
}: UseImagePreviewProps) => {
|
||||
const [tosKey, setTosKey] = useState('');
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const { open, close, modal } = useDataModalWithCoze({
|
||||
width: 640,
|
||||
title: I18n.t('knowledge_insert_img_004'),
|
||||
okText: I18n.t('Confirm'),
|
||||
okButtonProps: {
|
||||
disabled: uploading,
|
||||
},
|
||||
cancelText: I18n.t('Cancel'),
|
||||
onCancel: () => {
|
||||
close();
|
||||
},
|
||||
onOk: () => {
|
||||
onChange?.(src, tosKey);
|
||||
close();
|
||||
},
|
||||
});
|
||||
const customRequest: UploadProps['customRequest'] = async options => {
|
||||
const { onSuccess, onProgress, file } = options;
|
||||
|
||||
if (typeof file === 'string') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 业务
|
||||
const { name, fileInstance, url } = file;
|
||||
setUploading(true);
|
||||
if (fileInstance) {
|
||||
setSrc(url || '');
|
||||
const extension = getFileExtension(name);
|
||||
const base64 = await getBase64(fileInstance);
|
||||
const result = await DeveloperApi.UploadFile(
|
||||
{
|
||||
file_head: {
|
||||
file_type: extension,
|
||||
biz_type: FileBizType.BIZ_BOT_DATASET,
|
||||
},
|
||||
data: base64,
|
||||
},
|
||||
{
|
||||
onUploadProgress: e => {
|
||||
onProgress({
|
||||
total: e.total ?? fileInstance.size,
|
||||
loaded: e.loaded,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
onSuccess(result.data);
|
||||
setTosKey(result?.data?.upload_uri || '');
|
||||
setSrc(result?.data?.upload_url || '');
|
||||
} else {
|
||||
throw new CustomError(
|
||||
REPORT_EVENTS.KnowledgeUploadFile,
|
||||
'Upload image fail',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new CustomError(
|
||||
REPORT_EVENTS.KnowledgeUploadFile,
|
||||
`Upload image fail: ${error}`,
|
||||
);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
const Empty = ({ showTips = false }) => (
|
||||
<div className={styles['image-upload-empty']}>
|
||||
<IconCozUpload className={'text-[32px] coz-fg-hglt'} />
|
||||
<div className={styles['image-upload-text']}>
|
||||
{I18n.t('knowledge_insert_img_006')}
|
||||
</div>
|
||||
{showTips ? (
|
||||
<div className={styles['image-upload-tips']}>
|
||||
{I18n.t('knowledge_insert_img_007')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
return {
|
||||
node: modal(
|
||||
<div className={styles['image-preview-modal']}>
|
||||
<Upload
|
||||
className={styles['image-upload']}
|
||||
maxSize={20480}
|
||||
fileList={[]}
|
||||
limit={1}
|
||||
accept="image/*"
|
||||
disabled={!editable || uploading}
|
||||
customRequest={customRequest}
|
||||
draggable
|
||||
onChange={fileItem => {
|
||||
const { currentFile } = fileItem;
|
||||
if (currentFile) {
|
||||
const isValid = isValidSize(currentFile?.fileInstance?.size || 0);
|
||||
if (!isValid) {
|
||||
Toast.error(I18n.t('knowledge_insert_img_013'));
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Spin
|
||||
spinning={uploading}
|
||||
tip={I18n.t('knowledge_insert_img_009')}
|
||||
wrapperClassName={uploading ? 'spin-uploading' : ''}
|
||||
>
|
||||
<div className={styles['image-wrapper']}>
|
||||
{editable ? (
|
||||
<div className={styles['image-hover']}>
|
||||
<Empty showTips />
|
||||
</div>
|
||||
) : null}
|
||||
<Image
|
||||
src={src}
|
||||
preview={false}
|
||||
fallback={<IconImageFailOutlined />}
|
||||
></Image>
|
||||
</div>
|
||||
</Spin>
|
||||
</Upload>
|
||||
|
||||
<div className="mb-[16px]">
|
||||
<Typography className="coz-fg-secondary text-[12px] fw-[500] px-[8px]">
|
||||
{I18n.t('knowledge_insert_img_005')}
|
||||
</Typography>
|
||||
<Input
|
||||
value={src}
|
||||
onChange={v => {
|
||||
setSrc(v);
|
||||
setTosKey('');
|
||||
}}
|
||||
disabled={!editable || uploading}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
),
|
||||
open,
|
||||
close,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
|
||||
export const getBase64 = (file: Blob): Promise<string> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = event => {
|
||||
const result = event.target?.result;
|
||||
|
||||
if (!result || typeof result !== 'string') {
|
||||
reject(new CustomError('getBase64', 'file read invalid'));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(result.replace(/^.*?,/, ''));
|
||||
};
|
||||
fileReader.onerror = () => {
|
||||
reject(new CustomError('getBase64', 'file read fail'));
|
||||
};
|
||||
fileReader.onabort = () => {
|
||||
reject(new CustomError('getBase64', 'file read abort'));
|
||||
};
|
||||
fileReader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
export const getUint8Array = (file: Blob): Promise<Uint8Array> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
|
||||
fileReader.onload = event => {
|
||||
if (event.target?.result) {
|
||||
const arrayBuffer = event.target.result as ArrayBuffer;
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
resolve(uint8Array);
|
||||
} else {
|
||||
reject(new CustomError('getUint8Array', 'file read invalid'));
|
||||
}
|
||||
};
|
||||
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
});
|
||||
|
||||
export const getFileExtension = (name: string) => {
|
||||
const index = name.lastIndexOf('.');
|
||||
return name.slice(index + 1);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
const LIMIT_SIZE = 20 * 1024 * 1024;
|
||||
export const isValidSize = (size: number) => LIMIT_SIZE > size;
|
||||
@@ -0,0 +1,390 @@
|
||||
.cell-text-render {
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 32px;
|
||||
margin-left: -16px;
|
||||
padding: 0 16px;
|
||||
|
||||
&:hover {
|
||||
background-color:var(--coz-mg-secondary-hovered);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.cell-text-preview {
|
||||
overflow: hidden;
|
||||
|
||||
height: 100%;
|
||||
min-height: 32px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 32px;
|
||||
color: var(--coz-fg-primary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.cell-text-edit,
|
||||
.cell-text-readonly {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 12px;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
width: calc(100% - 16px);
|
||||
|
||||
background: var(--coz-bg-max);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 4px 0 rgb(0 0 0 / 10%), 0 0 1px 0 rgb(0 0 0 / 8%);
|
||||
|
||||
.cell-text-area {
|
||||
border: 0;
|
||||
|
||||
textarea {
|
||||
min-height: 22px;
|
||||
max-height: 120px;
|
||||
padding: 2px 13px;
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-icon-default {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cell-text-edit:not(.cell-text-error) {
|
||||
top: 11px;
|
||||
// margin: -1px 0 0;
|
||||
border: 1px solid var(--coz-fg-hglt);
|
||||
|
||||
.cell-text-area textarea {
|
||||
padding-left: 12px;
|
||||
}
|
||||
}
|
||||
.cell-text-edit-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.cell-text-error {
|
||||
border: 1px solid var(--coz-stroke-hglt-red);
|
||||
|
||||
:global {
|
||||
.semi-icon {
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cell-text-readonly {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
|
||||
:global {
|
||||
.semi-input-textarea-wrapper {
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
background-color: var(--coz-bg-max);;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-input-textarea-wrapper:active .semi-input-textarea-wrapper-disabled,
|
||||
.semi-input-textarea-wrapper-readonly,
|
||||
.semi-input-textarea-wrapper-focus {
|
||||
background-color: var(--coz-bg-max);
|
||||
}
|
||||
|
||||
.semi-input-textarea-readonly {
|
||||
// color: rgb(29 28 35);
|
||||
}
|
||||
|
||||
.semi-input-textarea-autosize {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.tag-render {
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.actions-render {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
.action-edit,
|
||||
.action-delete {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--coz-mg-secondary-hovered)
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-button-light {
|
||||
border: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-header-render {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&:hover {
|
||||
background: var(--coz-mg-secondary-hovered);
|
||||
}
|
||||
|
||||
.header-preview {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header-input {
|
||||
border: 1px solid #4D53E8;
|
||||
|
||||
}
|
||||
|
||||
.header-delete {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: #e0e0e4;
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-input-wrapper-error {
|
||||
background-color: var(--coz-bg-max);
|
||||
border-color: var(--coz-stroke-hglt-red);
|
||||
}
|
||||
|
||||
.semi-input-textarea-wrapper:active .semi-input-textarea-wrapper-disabled,
|
||||
.semi-input-textarea-wrapper-readonly,
|
||||
.semi-input-textarea-wrapper-focus {
|
||||
background-color: var(--coz-bg-max);;
|
||||
}
|
||||
|
||||
.semi-input-textarea-autosize {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-render-empty {
|
||||
&:hover {
|
||||
background: #e0e0e4;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.image-render-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.image-failed,
|
||||
.image-failed svg {
|
||||
width: 32px;
|
||||
height: 32px
|
||||
}
|
||||
|
||||
.image-container {
|
||||
height: 100%;
|
||||
line-height: 0
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-image-status {
|
||||
.semi-icon-default {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-image-img-error {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.semi-image,
|
||||
img {
|
||||
cursor: pointer;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-preview-modal {
|
||||
|
||||
.image-upload-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 430px;
|
||||
|
||||
background-color: var(--semi-color-primary-light-default);
|
||||
border: 1px dashed var(--semi-color-primary);
|
||||
border-radius: 4px;
|
||||
|
||||
.image-upload-text {
|
||||
margin-bottom: 4px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
color: var(--coz-fg-primary);
|
||||
}
|
||||
|
||||
.image-upload-tips {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
color: var(--coz-fg-dim);
|
||||
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-icon {
|
||||
margin-bottom: 4px;
|
||||
font-size: 24px;
|
||||
color: #4D53E8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-upload {
|
||||
cursor: pointer;
|
||||
width: 592px;
|
||||
height: 430px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.image-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.image-hover {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.image-hover {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 430px;
|
||||
|
||||
background: var(--coz-mg-hglt);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
:global {
|
||||
|
||||
.semi-upload-add {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
.semi-upload {
|
||||
.semi-image {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 430px;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-input-wrapper {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.semi-spin-block.semi-spin {
|
||||
width: 100%;
|
||||
|
||||
.semi-spin-children {
|
||||
width: 100%;
|
||||
height: 430px;
|
||||
}
|
||||
}
|
||||
|
||||
.spin-uploading {
|
||||
background-color: #1D1C2359;
|
||||
}
|
||||
|
||||
.semi-spin {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
|
||||
.semi-spin-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 6px;
|
||||
|
||||
width: auto;
|
||||
padding: 4px 8px;
|
||||
|
||||
color: var(--coz-bg-max);;
|
||||
|
||||
background: #1D1C2399;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { TextRender } from './text-render';
|
||||
export { EditHeaderRender } from './edit-header-render';
|
||||
export { TagRender } from './tag-render';
|
||||
export { ActionsRender } from './actions-render';
|
||||
export { ImageRender } from './image-render';
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { type TagColor } from '@coze-arch/coze-design/types';
|
||||
import { Tag } from '@coze-arch/coze-design';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface TagRenderProps {
|
||||
value: string | ReactNode;
|
||||
className?: string;
|
||||
size?: 'small' | 'mini';
|
||||
color?: TagColor;
|
||||
}
|
||||
export const TagRender = ({
|
||||
value,
|
||||
className,
|
||||
size,
|
||||
color,
|
||||
}: TagRenderProps) => (
|
||||
<Tag
|
||||
className={classNames(className, styles['tag-render'])}
|
||||
size={size}
|
||||
color={color ?? 'primary'}
|
||||
>
|
||||
{value}
|
||||
</Tag>
|
||||
);
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useMemo, useState, useEffect, useRef } from 'react';
|
||||
|
||||
import { TextArea } from '@coze-arch/coze-design';
|
||||
import { Tooltip } from '@coze-arch/bot-semi';
|
||||
import { IconToastError } from '@coze-arch/bot-icons';
|
||||
import { CommonE2e } from '@coze-data/e2e';
|
||||
|
||||
import {
|
||||
type TableViewRecord,
|
||||
type ValidatorProps,
|
||||
type TableViewValue,
|
||||
} from '../types';
|
||||
|
||||
import styles from './index.module.less';
|
||||
export interface TextRenderProps {
|
||||
value: TableViewValue;
|
||||
record: TableViewRecord;
|
||||
index: number;
|
||||
onBlur?: (v: TableViewValue, record: TableViewRecord, index: number) => void;
|
||||
onChange?: (
|
||||
v: TableViewValue,
|
||||
record: TableViewRecord,
|
||||
index: number,
|
||||
) => void;
|
||||
validator?: ValidatorProps;
|
||||
editable?: boolean;
|
||||
isEditing?: boolean;
|
||||
dataIndex?: string;
|
||||
}
|
||||
|
||||
export const TextRender = ({
|
||||
value,
|
||||
record,
|
||||
index,
|
||||
onBlur,
|
||||
onChange,
|
||||
dataIndex = '',
|
||||
validator = {},
|
||||
editable = false,
|
||||
isEditing,
|
||||
}: TextRenderProps) => {
|
||||
const { validate, errorMsg } = validator;
|
||||
const [isEditCom, setIsEditCom] = useState(isEditing);
|
||||
const [inputValue, setInputValue] = useState<TableViewValue>(String(value));
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
useEffect(() => {
|
||||
setIsEditCom(isEditing);
|
||||
}, [isEditing]);
|
||||
const onBlurFn = async () => {
|
||||
if (onBlur && value !== inputValue) {
|
||||
const updateRecord = { ...record, [dataIndex]: inputValue };
|
||||
delete updateRecord.tableViewKey;
|
||||
if (!isError) {
|
||||
try {
|
||||
await onBlur(inputValue, updateRecord, index);
|
||||
} catch (e) {
|
||||
// 更新失败,恢复原值
|
||||
console.log('update table content error', e);
|
||||
setInputValue(String(value));
|
||||
}
|
||||
} else {
|
||||
setInputValue(String(value));
|
||||
}
|
||||
}
|
||||
setIsEditCom(false);
|
||||
};
|
||||
const onChangeFn = (v: string) => {
|
||||
if (onChange) {
|
||||
onChange(v, record, index);
|
||||
}
|
||||
setInputValue(v);
|
||||
};
|
||||
// 校验状态
|
||||
const isError = useMemo(
|
||||
() => !!validate?.(String(inputValue), record, index),
|
||||
[inputValue, validate],
|
||||
);
|
||||
useEffect(() => {
|
||||
setInputValue(value);
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
const target = textAreaRef.current;
|
||||
if (!isEditCom || !target) {
|
||||
return;
|
||||
}
|
||||
const valueLength = String(inputValue).length;
|
||||
target.focus();
|
||||
if (!valueLength) {
|
||||
return;
|
||||
}
|
||||
target.setSelectionRange(valueLength, valueLength);
|
||||
}, [isEditCom]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles['cell-text-render']} text-render-wrapper`}
|
||||
data-testid={CommonE2e.CommonTableViewTextRender}
|
||||
>
|
||||
{/* 编辑态组件 */}
|
||||
{isEditCom ? (
|
||||
<span
|
||||
className={`${styles['cell-text-edit']} ${
|
||||
isError ? styles['cell-text-error'] : ''
|
||||
} cell-text-area-wrapper`}
|
||||
>
|
||||
<TextArea
|
||||
ref={textAreaRef}
|
||||
autoFocus
|
||||
autosize
|
||||
validateStatus={isError ? 'error' : 'default'}
|
||||
rows={1}
|
||||
className={styles['cell-text-area']}
|
||||
value={String(inputValue)}
|
||||
onBlur={onBlurFn}
|
||||
onChange={onChangeFn}
|
||||
/>
|
||||
{isError ? (
|
||||
<div className={styles['cell-text-edit-error']}>
|
||||
<Tooltip content={errorMsg}>
|
||||
<IconToastError />
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : null}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
{/* 预览态组件 */}
|
||||
{!isEditCom && (
|
||||
<div
|
||||
className={`${styles['cell-text-preview']} text-content`}
|
||||
onClick={() => setIsEditCom(true)}
|
||||
>
|
||||
{inputValue}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type CSSProperties, useEffect } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
|
||||
import {
|
||||
Menu,
|
||||
Divider,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Space,
|
||||
} from '@coze-arch/coze-design';
|
||||
import { IconClose } from '@douyinfe/semi-icons';
|
||||
|
||||
import { type EditMenuItem, type TableViewRecord } from '../types';
|
||||
import { getRowOpConfig } from './utils';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface EditMenuProps {
|
||||
configs: EditMenuItem[];
|
||||
visible: boolean;
|
||||
style: CSSProperties;
|
||||
selected: {
|
||||
record?: TableViewRecord;
|
||||
indexs?: (string | number)[];
|
||||
};
|
||||
onExit?: () => void | Promise<void>;
|
||||
onDelete?: (indexs: (string | number)[]) => void | Promise<void>;
|
||||
// 行操作编辑行的回调
|
||||
onEdit?: (
|
||||
record: TableViewRecord,
|
||||
index: string | number,
|
||||
) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export const EditMenu = ({
|
||||
configs,
|
||||
visible,
|
||||
style,
|
||||
selected,
|
||||
onExit,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: EditMenuProps) => {
|
||||
const menuConfigs = getRowOpConfig({
|
||||
selected,
|
||||
onEdit,
|
||||
onDelete,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fn = (_e: Event) => {
|
||||
if (onExit) {
|
||||
onExit();
|
||||
}
|
||||
};
|
||||
window.addEventListener('click', fn);
|
||||
return () => window.removeEventListener('click', fn);
|
||||
}, []);
|
||||
|
||||
if (visible && configs && configs.length) {
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className={classNames(
|
||||
styles['table-edit-menu'],
|
||||
'context-menu-disabled',
|
||||
)}
|
||||
>
|
||||
<Menu.SubMenu mode="menu">
|
||||
{configs.map(config => {
|
||||
const { text, onClick, icon } = menuConfigs[config];
|
||||
return (
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
onClick();
|
||||
}}
|
||||
icon={icon}
|
||||
>
|
||||
{I18n.t(text as I18nKeysNoOptionsType)}
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</Menu.SubMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <div className="context-menu-disabled"></div>;
|
||||
};
|
||||
|
||||
export const EditToolBar = ({
|
||||
configs,
|
||||
visible,
|
||||
selected,
|
||||
onExit,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: EditMenuProps) => {
|
||||
const menuConfigs = getRowOpConfig({
|
||||
selected,
|
||||
onEdit,
|
||||
onDelete,
|
||||
});
|
||||
const { indexs } = selected;
|
||||
return (
|
||||
<>
|
||||
{visible ? (
|
||||
<div
|
||||
className={styles['table-edit-toolbar']}
|
||||
style={{
|
||||
marginLeft: `${
|
||||
(selected?.indexs || []).length > 1 ? '-145px' : '-203.5px'
|
||||
}`,
|
||||
}}
|
||||
>
|
||||
<ButtonGroup className={styles['button-group']}>
|
||||
{selected ? (
|
||||
<div className={styles['selected-count']}>
|
||||
{I18n.t('table_view_002', {
|
||||
n: indexs?.length,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
<Divider layout="vertical" margin={'8px'} />
|
||||
{configs.length > 0 ? (
|
||||
<Space spacing={8}>
|
||||
{configs.map(config => {
|
||||
const { text, onClick } = menuConfigs[config];
|
||||
return (
|
||||
<Button onClick={onClick} color="primary">
|
||||
{I18n.t(text as I18nKeysNoOptionsType)}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
) : null}
|
||||
|
||||
<Divider layout="vertical" margin={'8px'} />
|
||||
|
||||
<Button
|
||||
icon={<IconClose />}
|
||||
onClick={onExit}
|
||||
color="secondary"
|
||||
></Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,313 @@
|
||||
/* stylelint-disable max-nesting-depth */
|
||||
/* stylelint-disable declaration-no-important */
|
||||
|
||||
|
||||
.data-table-view {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.table-wrapper {
|
||||
:global {
|
||||
/** 公共样式 **/
|
||||
|
||||
/** 重置table背景色 */
|
||||
.semi-table-tbody>.semi-table-row,
|
||||
.semi-table-thead>.semi-table-row>.semi-table-row-head,
|
||||
.semi-table-tbody>.semi-table-row>.semi-table-cell-fixed-left,
|
||||
.semi-table-thead>.semi-table-row>.semi-table-row-head.semi-table-cell-fixed-left::before,
|
||||
.semi-table-tbody>.semi-table-row>.semi-table-cell-fixed-right,
|
||||
.semi-table-thead>.semi-table-row>.semi-table-row-head.semi-table-cell-fixed-right::before {
|
||||
background: var(--coz-bg-max) !important;
|
||||
}
|
||||
|
||||
.semi-table-scroll-position-left .semi-table-tbody>.semi-table-row>.semi-table-cell-fixed-left-last,
|
||||
.semi-table-scroll-position-left .semi-table-thead>.semi-table-row>.semi-table-cell-fixed-left-last,
|
||||
.semi-table-scroll-position-right .semi-table-tbody>.semi-table-row>.semi-table-cell-fixed-right-last,
|
||||
.semi-table-scroll-position-right .semi-table-thead>.semi-table-row>.semi-table-cell-fixed-right-last {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.semi-table-wrapper[data-column-fixed="true"] {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.semi-table-row>.semi-table-cell-fixed-right-first {
|
||||
box-shadow: -2px 0 3px 0 rgb(0 0 0 / 8%);
|
||||
}
|
||||
|
||||
|
||||
/** table header样式 **/
|
||||
.semi-table-thead {
|
||||
// 拖拽列宽度的图标样式
|
||||
&:hover {
|
||||
.react-resizable:not(.semi-table-cell-fixed-left, .resizing, .not-resize-handle) {
|
||||
.react-resizable-handle {
|
||||
bottom: 10px;
|
||||
|
||||
width: 7px;
|
||||
height: 18px;
|
||||
|
||||
border-right: 2px solid var(--coz-stroke-plus);
|
||||
border-left: 1px solid var(--coz-stroke-plus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.semi-table-row-head {
|
||||
font-size: 12px;
|
||||
color: var(--coz-fg-secondary);
|
||||
&:first-child {
|
||||
padding-left: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-table-row-head .semi-typography {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
color: var(--coz-fg-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&>.semi-table-row>.semi-table-row-head.semi-table-cell-fixed-left-last {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.semi-table-row {
|
||||
.react-resizable-handle {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-checkbox {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/** table body部分样式 **/
|
||||
.semi-table-tbody {
|
||||
.semi-table-row {
|
||||
>.semi-table-row-cell {
|
||||
/**
|
||||
* table 开启虚拟滚动后 单元格会加上 overflow: hidden
|
||||
* 未开启虚拟滚动情况下正常展示溢出内容
|
||||
*/
|
||||
overflow: visible;
|
||||
border-top: 1px solid var(--coz-stroke-primary);
|
||||
border-bottom: 1px solid transparent;
|
||||
|
||||
&:not(.semi-table-cell-fixed-left) {
|
||||
padding: 12px 0 12px 16px !important;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 32px !important;
|
||||
|
||||
.cell-text-area-wrapper {
|
||||
left: 16px;
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&:first-child {
|
||||
>.semi-table-row-cell {
|
||||
border-top: 1px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
>.semi-table-row-cell {
|
||||
border-bottom: 1px solid var(--coz-stroke-primary);
|
||||
}
|
||||
}
|
||||
|
||||
>.semi-table-cell-fixed-left-last {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
>.semi-table-row-cell {
|
||||
min-height: 55px;
|
||||
background-color: var(--coz-mg-secondary-hovered) !important;
|
||||
border-top: 1px solid transparent;
|
||||
|
||||
&::after,
|
||||
&.semi-table-cell-fixed-right::after,
|
||||
&::before,
|
||||
&.semi-table-cell-fixed-left::before {
|
||||
content: '';
|
||||
display: none !important;
|
||||
|
||||
}
|
||||
}
|
||||
& + .semi-table-row {
|
||||
>.semi-table-row-cell {
|
||||
border-top: 1px solid transparent; // 去掉当前行下一行的上边框
|
||||
}
|
||||
}
|
||||
|
||||
.semi-checkbox {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.table-view-actions {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-checkbox,
|
||||
.table-view-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.semi-table-row-selected {
|
||||
// background: var(--coz-mg-hglt-hovered);
|
||||
border-bottom-left-radius: 4px !important;
|
||||
|
||||
>.semi-table-row-cell {
|
||||
margin-top: 1px;
|
||||
border-top: 1px solid transparent;
|
||||
}
|
||||
|
||||
.semi-table-row-cell:first-child {
|
||||
border-top-left-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
}
|
||||
|
||||
.semi-table-row-cell:last-child {
|
||||
border-top-right-radius: 8px !important;
|
||||
border-bottom-right-radius: 8px !important;
|
||||
}
|
||||
|
||||
&:hover .semi-table-row-cell:has(.text-render-wrapper) {
|
||||
background-color: var(--coz-mg-secondary-hovered) !important;
|
||||
|
||||
&>div:hover {
|
||||
// background: rgb(28 28 35 / 6%);
|
||||
// background: var(--coz-mg-secondary-hovered);
|
||||
}
|
||||
}
|
||||
|
||||
.semi-checkbox,
|
||||
&:hover .semi-checkbox {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.semi-table-row-cell,
|
||||
.semi-table-column-selection {
|
||||
background: var(--coz-mg-hglt-hovered) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-table-column-selection {
|
||||
padding-left: 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
// 固定列背景颜色调整
|
||||
.light {
|
||||
:global {
|
||||
.semi-table-tbody {
|
||||
.semi-table-row {
|
||||
&:hover {
|
||||
>.semi-table-row-cell {
|
||||
&.semi-table-cell-fixed-left,
|
||||
&.semi-table-cell-fixed-left::before,
|
||||
&.semi-table-cell-fixed-right,
|
||||
&.semi-table-cell-fixed-right::before {
|
||||
// background-color: #DCDCDE!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.semi-table-row-selected {
|
||||
>.semi-table-row-cell {
|
||||
&.semi-table-cell-fixed-left,
|
||||
&.semi-table-cell-fixed-left::before,
|
||||
&.semi-table-cell-fixed-right,
|
||||
&.semi-table-cell-fixed-right::before {
|
||||
// background-color: #D3D5FB!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
:global {
|
||||
.semi-table-tbody {
|
||||
.semi-table-row {
|
||||
&:hover {
|
||||
>.semi-table-row-cell {
|
||||
&.semi-table-cell-fixed-left,
|
||||
&.semi-table-cell-fixed-left::before,
|
||||
&.semi-table-cell-fixed-right,
|
||||
&.semi-table-cell-fixed-right::before {
|
||||
// background-color: #29303B!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.semi-table-row-selected {
|
||||
>.semi-table-row-cell {
|
||||
&.semi-table-cell-fixed-left,
|
||||
&.semi-table-cell-fixed-left::before,
|
||||
&.semi-table-cell-fixed-right,
|
||||
&.semi-table-cell-fixed-right::before {
|
||||
// background-color: #2A2F70!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-edit-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
border: 0.5px solid var(--coz-stroke-primary);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px 0 rgba(0, 0, 0, 16%), 0 16px 48px 0 rgba(0, 0, 0, 8%);
|
||||
:global {
|
||||
.semi-dropdown-menu.coz-menu {
|
||||
min-width: 160px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-edit-toolbar {
|
||||
position: fixed;
|
||||
bottom: 14px;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
padding: 8px;
|
||||
|
||||
background: var(--coz-bg-max);
|
||||
border: 0.5px solid var(--coz-stroke-primary);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 8%), 0 8px 24px 0 rgba(0, 0, 0, 4%);
|
||||
}
|
||||
|
||||
.selected-count {
|
||||
padding: 0 4px;
|
||||
font-size: 14px;
|
||||
color: var(--coz-fg-secondary);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @coze-arch/max-line-per-function */
|
||||
import React, {
|
||||
useState,
|
||||
useMemo,
|
||||
type ReactNode,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useDebounceFn } from 'ahooks';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { useTheme } from '@coze-arch/coze-design';
|
||||
import {
|
||||
type RowSelectionProps,
|
||||
type TableProps,
|
||||
type OnCellReturnObject,
|
||||
type VirtualizedOnScrollArgs,
|
||||
} from '@coze-arch/bot-semi/Table';
|
||||
import { UIEmpty, UITable } from '@coze-arch/bot-semi';
|
||||
import { AutoSizer } from '@coze-common/virtual-list';
|
||||
import { IllustrationNoResult } from '@douyinfe/semi-illustrations';
|
||||
|
||||
import {
|
||||
type TableViewRecord,
|
||||
EditMenuItem,
|
||||
type TableViewColumns,
|
||||
type TableViewValue,
|
||||
} from '../types';
|
||||
import { TextRender } from '../renders';
|
||||
import { resizeFn, getRowKey } from './utils';
|
||||
import { colWidthCacheService } from './service';
|
||||
import { EditMenu, EditToolBar } from './edit-menu';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface TableViewProps {
|
||||
// 唯一标识表,且会作为列宽缓存map中的key值
|
||||
tableKey?: string;
|
||||
// 类名,用于样式覆盖
|
||||
className?: string;
|
||||
// 编辑配置
|
||||
editProps?: {
|
||||
// 数据删除的回调,支持批量
|
||||
onDelete?: (indexs: (string | number)[]) => void;
|
||||
// 行操作编辑行的回调
|
||||
onEdit?: (record: TableViewRecord, index: string | number) => void;
|
||||
};
|
||||
// 滚动到底部的回调
|
||||
scrollToBottom?: () => void | Promise<void>;
|
||||
// 拖拽钩子
|
||||
onResize?: (col: TableViewColumns) => void;
|
||||
// 是否开启虚拟滚动,默认为false
|
||||
isVirtualized?: boolean;
|
||||
// 是否开启伸缩列,默认为false
|
||||
resizable?: boolean;
|
||||
// 是否开启行选择,默认为false
|
||||
rowSelect?: boolean;
|
||||
// 是否支持行操作,默认为false
|
||||
rowOperation?: boolean;
|
||||
// 数据
|
||||
dataSource: TableViewRecord[];
|
||||
// 表头项
|
||||
columns: TableViewColumns[];
|
||||
// 数据为空的兜底展示
|
||||
empty?: ReactNode;
|
||||
// loading
|
||||
loading?: boolean;
|
||||
// 不消费,仅用于触发渲染的state,需优化
|
||||
resizeTriState?: number;
|
||||
// 额外 tableProps
|
||||
tableProps?: TableProps;
|
||||
}
|
||||
export interface TableViewMethods {
|
||||
resetSelected: () => void;
|
||||
getTableHeight: () => number;
|
||||
}
|
||||
export interface TableWrapperProps {
|
||||
isVirtualized: boolean;
|
||||
children: (props?: TableProps) => ReactNode;
|
||||
onScroll: (args: VirtualizedOnScrollArgs & { height: number }) => void;
|
||||
}
|
||||
|
||||
const ITEM_SIZE = 56;
|
||||
const HEADER_SIZE = 41;
|
||||
const MOUSE_LEFT_BTN = 1;
|
||||
const MOUSE_RIGHT_BTN = 2;
|
||||
const SAFEY = 36;
|
||||
const SAFEX = 176;
|
||||
|
||||
const TableWrapper = ({
|
||||
isVirtualized,
|
||||
onScroll,
|
||||
children,
|
||||
}: TableWrapperProps) => {
|
||||
if (isVirtualized) {
|
||||
return (
|
||||
<AutoSizer>
|
||||
{({ width, height }: { width: number; height: number }) =>
|
||||
children({
|
||||
scroll: { y: height - HEADER_SIZE, x: width },
|
||||
style: {
|
||||
width,
|
||||
},
|
||||
virtualized: {
|
||||
itemSize: ITEM_SIZE,
|
||||
onScroll: scrollProps => onScroll({ ...scrollProps, height }),
|
||||
overScanCount: 30,
|
||||
},
|
||||
})
|
||||
}
|
||||
</AutoSizer>
|
||||
);
|
||||
}
|
||||
return <React.Fragment>{children()}</React.Fragment>;
|
||||
};
|
||||
|
||||
const EmptyStatus = () => (
|
||||
<UIEmpty
|
||||
empty={{
|
||||
icon: <IllustrationNoResult />,
|
||||
description: I18n.t('dataset_segment_empty_desc'),
|
||||
}}
|
||||
></UIEmpty>
|
||||
);
|
||||
|
||||
export const TableView = forwardRef<TableViewMethods, TableViewProps>(
|
||||
(
|
||||
{
|
||||
tableKey,
|
||||
editProps = {},
|
||||
isVirtualized = false,
|
||||
rowSelect = false,
|
||||
rowOperation = false,
|
||||
resizable = false,
|
||||
dataSource,
|
||||
columns,
|
||||
loading = false,
|
||||
className,
|
||||
scrollToBottom,
|
||||
empty,
|
||||
onResize,
|
||||
tableProps: extraTableProps = {},
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { onEdit, onDelete } = editProps;
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
const [menuStyle, setMenuStyle] = useState({});
|
||||
const [selected, setSelected] = useState<(string | number)[]>([]);
|
||||
const [focusRow, setFocusRow] = useState<number>();
|
||||
const { theme } = useTheme();
|
||||
const currentThemeClassName = useMemo(
|
||||
() => (theme === 'dark' ? styles.dark : styles.light),
|
||||
[theme],
|
||||
);
|
||||
const toolBarVisible = useMemo(() => !!selected?.length, [selected]);
|
||||
const tableData = useMemo(
|
||||
() =>
|
||||
dataSource.map((data, index) => ({
|
||||
...data,
|
||||
tableViewKey: String(index),
|
||||
})),
|
||||
[dataSource],
|
||||
);
|
||||
const menuConfigs = useMemo(() => {
|
||||
if (selected?.length && selected?.length > 1) {
|
||||
return [EditMenuItem.DELETEALL];
|
||||
}
|
||||
return [EditMenuItem.EDIT, EditMenuItem.DELETE];
|
||||
}, [selected]);
|
||||
const columnsHandler = (cols: TableViewColumns) =>
|
||||
cols.map(
|
||||
(col: TableViewColumns): TableViewColumns => ({
|
||||
...col,
|
||||
onCell: (
|
||||
_record?: TableViewRecord,
|
||||
rowIndex?: number,
|
||||
): OnCellReturnObject => ({
|
||||
onContextMenu: (e: { preventDefault: () => void }) => {
|
||||
e.preventDefault();
|
||||
},
|
||||
onMouseDown: (e: React.MouseEvent) => {
|
||||
if (e.button === MOUSE_LEFT_BTN) {
|
||||
setMenuVisible(false);
|
||||
}
|
||||
if (e.button === MOUSE_RIGHT_BTN && rowOperation) {
|
||||
e.preventDefault();
|
||||
const { offsetWidth, offsetHeight } = document.body;
|
||||
// 如果右键位置非选中项,取消选中
|
||||
if (
|
||||
rowIndex &&
|
||||
selected?.length &&
|
||||
!selected.includes(String(rowIndex))
|
||||
) {
|
||||
setSelected([]);
|
||||
}
|
||||
// 右键展示菜单
|
||||
setFocusRow(rowIndex);
|
||||
setMenuVisible(true);
|
||||
setMenuStyle({
|
||||
position: 'fixed',
|
||||
top:
|
||||
e.pageY + SAFEY * menuConfigs.length > offsetHeight
|
||||
? e.pageY - SAFEY * menuConfigs.length
|
||||
: e.pageY,
|
||||
left:
|
||||
e.pageX + SAFEX > offsetWidth ? e.pageX - SAFEX : e.pageX,
|
||||
zIndex: 100,
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
render: col.render
|
||||
? col.render
|
||||
: (
|
||||
text: TableViewValue,
|
||||
record: TableViewRecord,
|
||||
index: number,
|
||||
) => <TextRender value={text} record={record} index={index} />,
|
||||
}),
|
||||
);
|
||||
const [newColumns, setNewColumns] = useState<TableViewColumns[]>(
|
||||
columnsHandler(columns),
|
||||
);
|
||||
const rowSelection = useMemo(
|
||||
(): RowSelectionProps<TableViewRecord> => ({
|
||||
width: 38,
|
||||
fixed: true,
|
||||
selectedRowKeys: selected,
|
||||
onChange: selectedRowKeys => {
|
||||
setMenuVisible(false);
|
||||
setSelected(selectedRowKeys ?? []);
|
||||
},
|
||||
}),
|
||||
[selected, setSelected],
|
||||
);
|
||||
|
||||
const publicEditProps = {
|
||||
selected: {
|
||||
record: focusRow ? tableData[focusRow] : {},
|
||||
indexs: selected?.length ? selected : [Number(focusRow)],
|
||||
},
|
||||
style: menuStyle,
|
||||
configs: menuConfigs,
|
||||
onDelete,
|
||||
onEdit,
|
||||
};
|
||||
|
||||
const debounceScrollToBottom = useDebounceFn(
|
||||
() => {
|
||||
scrollToBottom?.();
|
||||
},
|
||||
{
|
||||
wait: 100,
|
||||
},
|
||||
);
|
||||
const onScroll = ({
|
||||
scrollDirection,
|
||||
scrollOffset,
|
||||
scrollUpdateWasRequested,
|
||||
height,
|
||||
}: VirtualizedOnScrollArgs & { height: number }) => {
|
||||
setMenuVisible(false);
|
||||
if (
|
||||
scrollDirection === 'forward' &&
|
||||
scrollOffset &&
|
||||
/**
|
||||
* 这一行一点余量都没留 可能在不同浏览器渲染下会有 bad case 导致无法满足条件
|
||||
* 如果有遇到类似反馈可以优先排查这里
|
||||
*/
|
||||
scrollOffset + height - HEADER_SIZE >= tableData.length * ITEM_SIZE &&
|
||||
!scrollUpdateWasRequested &&
|
||||
debounceScrollToBottom
|
||||
) {
|
||||
debounceScrollToBottom.run();
|
||||
}
|
||||
};
|
||||
const getTableHeight = () => {
|
||||
const bodyH = ITEM_SIZE * (tableData?.length || 0);
|
||||
return bodyH + HEADER_SIZE;
|
||||
};
|
||||
useImperativeHandle(ref, () => ({
|
||||
resetSelected: () => setSelected([]),
|
||||
getTableHeight,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
colWidthCacheService.initWidthMap();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
setNewColumns(columnsHandler(columns));
|
||||
}, [columns]);
|
||||
useEffect(() => {
|
||||
setNewColumns(columnsHandler(newColumns));
|
||||
}, [menuConfigs.length]);
|
||||
|
||||
return (
|
||||
<div className={classNames([styles['data-table-view']], className)}>
|
||||
{tableData.length || loading ? (
|
||||
<>
|
||||
<TableWrapper isVirtualized={isVirtualized} onScroll={onScroll}>
|
||||
{(tableProps?: TableProps) => (
|
||||
<UITable
|
||||
key={tableKey}
|
||||
wrapperClassName={`${styles['table-wrapper']} ${currentThemeClassName} table-wrapper`}
|
||||
tableProps={{
|
||||
...(tableProps || {}),
|
||||
...extraTableProps,
|
||||
rowKey: getRowKey,
|
||||
resizable: resizable
|
||||
? {
|
||||
onResize: col =>
|
||||
onResize ? onResize(col) : resizeFn(col),
|
||||
onResizeStop: col => {
|
||||
// resize完后缓存列宽
|
||||
const resizedCols = newColumns.map(oCol => {
|
||||
if (oCol.dataIndex === col.dataIndex) {
|
||||
return col;
|
||||
}
|
||||
return oCol;
|
||||
});
|
||||
setNewColumns(resizedCols);
|
||||
const widthMap: Record<string, number> = {};
|
||||
resizedCols.forEach(resizedCol => {
|
||||
if (resizedCol.dataIndex) {
|
||||
widthMap[resizedCol.dataIndex] =
|
||||
resizedCol.width;
|
||||
}
|
||||
});
|
||||
colWidthCacheService.setWidthMap(
|
||||
widthMap,
|
||||
tableKey,
|
||||
);
|
||||
},
|
||||
}
|
||||
: false,
|
||||
loading,
|
||||
rowSelection: rowSelect ? rowSelection : false,
|
||||
pagination: false,
|
||||
dataSource: tableData,
|
||||
columns: newColumns,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</TableWrapper>
|
||||
|
||||
<EditMenu
|
||||
{...publicEditProps}
|
||||
visible={menuVisible}
|
||||
onExit={() => setMenuVisible(false)}
|
||||
/>
|
||||
<EditToolBar
|
||||
{...publicEditProps}
|
||||
visible={toolBarVisible}
|
||||
onExit={() => setSelected([])}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{!dataSource.length && !loading ? (
|
||||
empty ? (
|
||||
empty
|
||||
) : (
|
||||
<EmptyStatus />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { REPORT_EVENTS } from '@coze-arch/report-events';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
|
||||
/**
|
||||
* 缓存列宽的方法类
|
||||
*/
|
||||
|
||||
class ColWidthCacheService {
|
||||
public mapName: string;
|
||||
public capacity: number;
|
||||
|
||||
constructor() {
|
||||
this.mapName = 'TABLE_VIEW_COL_WIDTH_MAP';
|
||||
this.capacity = 20;
|
||||
}
|
||||
private mapToString(map: Map<string, Record<string, number>>) {
|
||||
const mapArr = Array.from(map);
|
||||
return JSON.stringify(mapArr);
|
||||
}
|
||||
|
||||
private stringToMap(v: string) {
|
||||
const mapArr = JSON.parse(v);
|
||||
return mapArr.reduce(
|
||||
(
|
||||
map: Map<string, Record<string, number>>,
|
||||
[key, value]: [string, Record<string, number>],
|
||||
) => map.set(key, value),
|
||||
new Map(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化伸缩列缓存
|
||||
*/
|
||||
initWidthMap() {
|
||||
const widthMap = window.localStorage.getItem(this.mapName);
|
||||
if (!widthMap) {
|
||||
// 利用Map可记录键值对顺序的特性完成一个简易的LRU
|
||||
window.localStorage.setItem(this.mapName, this.mapToString(new Map()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置列宽缓存,若超过缓存个数,删除map中最近未使用的值
|
||||
*/
|
||||
setWidthMap(widthMap: Record<string, number>, tableKey?: string) {
|
||||
if (!tableKey) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const cacheWidthMap = this.stringToMap(
|
||||
window.localStorage.getItem(this.mapName) || '',
|
||||
);
|
||||
if (cacheWidthMap.has(tableKey)) {
|
||||
// 存在即更新(删除后加入)
|
||||
cacheWidthMap.delete(tableKey);
|
||||
} else if (cacheWidthMap.size >= this.capacity) {
|
||||
// 不存在即加入
|
||||
// 缓存超过最大值,则移除最近没有使用的
|
||||
cacheWidthMap.delete(cacheWidthMap.keys().next().value);
|
||||
}
|
||||
cacheWidthMap.set(tableKey, widthMap);
|
||||
window.localStorage.setItem(
|
||||
this.mapName,
|
||||
this.mapToString(cacheWidthMap),
|
||||
);
|
||||
} catch (err) {
|
||||
throw new CustomError(
|
||||
REPORT_EVENTS.KnowledgeTableViewSetColWidth,
|
||||
`table view set width map fail: ${err}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 以表维度查询列宽缓存信息
|
||||
* @param tableKey
|
||||
*/
|
||||
getTableWidthMap(tableKey: string) {
|
||||
try {
|
||||
const cacheWidthMap = this.stringToMap(
|
||||
window.localStorage.getItem(this.mapName) || '',
|
||||
);
|
||||
// 存在即更新
|
||||
const temp = cacheWidthMap.get(tableKey);
|
||||
cacheWidthMap.delete(tableKey);
|
||||
cacheWidthMap.set(tableKey, temp);
|
||||
return temp;
|
||||
} catch (err) {
|
||||
throw new CustomError(
|
||||
REPORT_EVENTS.KnowledgeTableViewGetColWidth,
|
||||
`table view get width map fail: ${err}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const colWidthCacheService = new ColWidthCacheService();
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { IconCozEdit, IconCozTrashCan } from '@coze-arch/coze-design/icons';
|
||||
import { type RowKey } from '@coze-arch/bot-semi/Table';
|
||||
|
||||
import {
|
||||
type TableViewRecord,
|
||||
type TableViewColumns,
|
||||
EditMenuItem,
|
||||
} from '../types';
|
||||
|
||||
const FIXED_COLUMN_WIDTH = 38;
|
||||
const MIN_COLUMN_WIDTH = 100;
|
||||
|
||||
export interface GetRowOpConfig {
|
||||
selected: {
|
||||
record?: TableViewRecord;
|
||||
indexs?: (string | number)[];
|
||||
};
|
||||
onEdit?: (
|
||||
record: TableViewRecord,
|
||||
index: string | number,
|
||||
) => void | Promise<void>;
|
||||
onDelete?: (indexs: (string | number)[]) => void | Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格列伸缩时的回调,用于限制伸缩边界
|
||||
* @param column
|
||||
* @returns
|
||||
*/
|
||||
export const resizeFn = (column: TableViewColumns): TableViewColumns => {
|
||||
if (column.fixed || column.key === 'column-selection') {
|
||||
return {
|
||||
...column,
|
||||
resizable: false,
|
||||
width: FIXED_COLUMN_WIDTH,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...column,
|
||||
width:
|
||||
Number(column.width) < MIN_COLUMN_WIDTH
|
||||
? MIN_COLUMN_WIDTH
|
||||
: Number(column.width),
|
||||
};
|
||||
};
|
||||
|
||||
export const getRowKey: RowKey<TableViewRecord> = (record?: TableViewRecord) =>
|
||||
record?.tableViewKey || '';
|
||||
|
||||
/**
|
||||
* 获取行操作配置
|
||||
* @param record
|
||||
* @param indexs
|
||||
* @param onEdit
|
||||
* @param onDelete
|
||||
* @returns
|
||||
*/
|
||||
export const getRowOpConfig = ({
|
||||
selected,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: GetRowOpConfig) => {
|
||||
const { record, indexs } = selected;
|
||||
const DeleteFn = () => {
|
||||
if (onDelete && indexs) {
|
||||
onDelete(indexs);
|
||||
}
|
||||
};
|
||||
const deleteConfig = {
|
||||
text: 'knowledge_tableview_02',
|
||||
icon: <IconCozTrashCan />,
|
||||
onClick: DeleteFn,
|
||||
};
|
||||
const editMenuConfig = {
|
||||
[EditMenuItem.EDIT]: {
|
||||
text: 'knowledge_tableview_01',
|
||||
icon: <IconCozEdit />,
|
||||
onClick: () => {
|
||||
if (onEdit && record && indexs) {
|
||||
onEdit(record, indexs[0]);
|
||||
}
|
||||
},
|
||||
},
|
||||
[EditMenuItem.DELETE]: deleteConfig,
|
||||
[EditMenuItem.DELETEALL]: deleteConfig,
|
||||
};
|
||||
return editMenuConfig;
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ColumnProps } from '@coze-arch/bot-semi/Table';
|
||||
|
||||
export type TableViewValue = string | number | undefined;
|
||||
export type TableViewRecord = {
|
||||
tableViewKey?: string;
|
||||
} & Record<string, TableViewValue>;
|
||||
export type TableViewColumns = ColumnProps<TableViewRecord>;
|
||||
|
||||
export enum TableViewMode {
|
||||
READ = 'read',
|
||||
EDIT = 'edit',
|
||||
}
|
||||
|
||||
export enum EditMenuItem {
|
||||
EDIT = 'edit',
|
||||
DELETE = 'delete',
|
||||
DELETEALL = 'deleteAll',
|
||||
}
|
||||
export interface ValidatorProps {
|
||||
validate?: (
|
||||
value: string,
|
||||
record?: TableViewRecord,
|
||||
index?: number,
|
||||
) => boolean;
|
||||
errorMsg?: string;
|
||||
}
|
||||
34
frontend/packages/components/table-view/src/index.ts
Normal file
34
frontend/packages/components/table-view/src/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import './main.css';
|
||||
|
||||
export { TableView, type TableViewMethods } from './components/table-view';
|
||||
export {
|
||||
TextRender,
|
||||
EditHeaderRender,
|
||||
TagRender,
|
||||
ActionsRender,
|
||||
ImageRender,
|
||||
} from './components/renders';
|
||||
|
||||
export {
|
||||
TableViewValue,
|
||||
TableViewColumns,
|
||||
TableViewRecord,
|
||||
} from './components/types';
|
||||
|
||||
export { colWidthCacheService } from './components/table-view/service';
|
||||
20
frontend/packages/components/table-view/src/typings.d.ts
vendored
Normal file
20
frontend/packages/components/table-view/src/typings.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare module '*.less' {
|
||||
const resource: { [key: string]: string };
|
||||
export = resource;
|
||||
}
|
||||
Reference in New Issue
Block a user