Merge remote-tracking branch 'origin/dev-opt' into dev-opt

This commit is contained in:
cyc 2026-04-03 16:00:45 +08:00
commit de4dcf33e9
14 changed files with 250 additions and 100 deletions

View File

@ -82,6 +82,7 @@
"echarts": "^5.5.0",
"echarts-gl": "~2.0.9",
"fabric": "^5.3.0",
"gray-matter": "~4.0.3",
"highlight.js": "^11.7.0",
"lodash": "^4.17.21",
"motion": "~12.23.12",
@ -122,7 +123,7 @@
"@testing-library/react": "^14.0.0",
"@types/antd": "^1.0.0",
"@types/express": "^4.17.14",
"@types/jest": "^29.5.1",
"@types/jest": "~29.5.14",
"@types/lodash": "^4.14.194",
"@types/react": "^18.0.38",
"@types/react-dom": "^18.0.11",

View File

@ -92,11 +92,5 @@
&__content {
flex: 1;
min-height: 0;
:global {
.wmde-markdown {
border-radius: 8px !important;
}
}
}
}

View File

@ -1,6 +1,7 @@
import KFIcon from '@/components/KFIcon';
import KFMdEditor from '@/components/KFMdEditor';
import DownloadSkillModal from '@/pages/Skill/components/DownloadModal';
import FrontmatterCard from '@/pages/Skill/components/FrontmatterCard';
import { SkillData } from '@/pages/Skill/types';
import {
cancelPraiseSkillReq,
@ -16,7 +17,9 @@ import { to } from '@/utils/promise';
import { useModel, useParams } from '@umijs/max';
import { Button, Flex, message } from 'antd';
import classNames from 'classnames';
import matter from 'gray-matter';
import { useCallback, useEffect, useState } from 'react';
import AddSkillModal from '../components/AddSkillModal';
import styles from './index.less';
function SkillInfo() {
@ -63,35 +66,60 @@ function SkillInfo() {
}
}, [id, info]);
// 处理下载
const handleDownload = useCallback(async () => {
if (info!.file_tree_json) {
const treeData = parseJsonText(info!.file_tree_json);
const { close } = openAntdModal(DownloadSkillModal, {
treeData,
onOk: () => {
close();
downloadSkill(info!.file_path);
},
});
} else {
downloadSkill(info!.file_path);
}
}, [info]);
const downloadSkill = async (url: string) => {
const downloadSkill = useCallback(async (url: string) => {
if (url) {
const [res] = await to(downloadSkillReq(url));
if (res && res.data) {
downloadFileWithUrl(res.data);
}
}
}, []);
// 处理下载
const handleDownload = useCallback(async () => {
const addLeaf = (treeData: any[]) => {
treeData.forEach((item) => {
item.isLeaf = !item.children;
if (item.children) {
addLeaf(item.children);
}
});
};
if (info!.file_tree_json) {
const treeData = parseJsonText(info!.file_tree_json);
if (Array.isArray(treeData) && treeData.length > 0) {
addLeaf(treeData);
const { close } = openAntdModal(DownloadSkillModal, {
treeData,
onOk: () => {
close();
downloadSkill(info!.file_path);
},
});
return;
}
}
downloadSkill(info!.file_path);
}, [info, downloadSkill]);
const handleEdit = () => {
const { close } = openAntdModal(AddSkillModal, {
id: id as string,
onOk: () => {
close();
getSkillInfo();
},
});
};
if (!info) {
return null;
}
const { data: frontmatter, content: mdContent } = matter(info!.md_content || '');
return (
<div className={styles['skill-info']}>
<div className={styles['skill-info__top']}>
@ -120,17 +148,32 @@ function SkillInfo() {
</Button>
)}
<Button color="default" variant="filled" onClick={handleDownload}>
{isMine && (
<Button
color="default"
variant="filled"
className={styles['skill-info__top__btn']}
onClick={handleEdit}
>
</Button>
)}
<Button
color="default"
variant="filled"
className={styles['skill-info__top__btn']}
onClick={handleDownload}
>
</Button>
<div
className={classNames(styles['skill-info__top__praise-btn'], {
[styles['skill-info__top__praise-btn--praised']]: info?.prraised,
[styles['skill-info__top__praise-btn--praised']]: info?.praised,
})}
onClick={handlePraise}
>
<KFIcon
type={info?.prraised ? 'icon-a-dianzanicon1' : 'icon-dianzanicon'}
type={info?.praised ? 'icon-a-dianzanicon1' : 'icon-dianzanicon'}
font={16}
style={{ marginRight: '6px' }}
/>
@ -140,9 +183,10 @@ function SkillInfo() {
</Flex>
<div className={styles['skill-info__top__desc']}>{info?.description || ''}</div>
</div>
<FrontmatterCard data={frontmatter} />
<KFMdEditor
className={styles['skill-info__content']}
defaultValue={info?.md_content || ''}
defaultValue={mdContent || ''}
canEdit={false}
noDataTitle={'没有内容'}
placeholder="请输入"

View File

@ -1,7 +1,7 @@
import { CommonTabKeys } from '@/enums';
import { useCacheState } from '@/hooks/useCacheState';
import { SkillCategoryData } from '@/pages/Skill/types';
import { getSkillTypesReq } from '@/services/skill/index';
import { getAssetIcon } from '@/services/dataset/index.js';
import { to } from '@/utils/promise';
import { type TabsProps } from 'antd';
import { useCallback, useEffect, useRef, useState } from 'react';
@ -9,11 +9,13 @@ import CategoryList from '../components/CategoryList';
import ResourceList, { ResourceListRef } from '../components/ResourceList';
import styles from './index.less';
export const ALL_TYPE = '全部分类';
function SkillPage() {
const [cacheState, setCacheState] = useCacheState();
const [activeTab, setActiveTab] = useState<string>(cacheState?.activeTab ?? CommonTabKeys.Public);
const [typeList, setTypeList] = useState<SkillCategoryData[]>([]);
const [activeTypes, setActiveTypes] = useState<string[]>(cacheState?.activeType ?? []);
const [activeType, setActiveType] = useState<string | undefined>(cacheState?.activeType);
const dataListRef = useRef<ResourceListRef>(null);
const [typeSearchText, setTypeSearchText] = useState<string>('');
@ -23,10 +25,11 @@ function SkillPage() {
name: typeSearchText,
page: 0,
size: 10000,
category_id: 3,
};
const [res] = await to(getSkillTypesReq(params));
const [res] = await to(getAssetIcon(params));
if (res && res.data) {
setTypeList(res.data);
setTypeList([{ id: '', name: ALL_TYPE, path: 'icon-quanbuyuyan' }, ...res.data]);
}
}, [typeSearchText]);
@ -42,14 +45,7 @@ function SkillPage() {
// 选择类型
const chooseType = (type: string) => {
dataListRef.current?.resetPage();
setActiveTypes([type]);
// setActiveTypes((prev) => {
// if (prev.includes(type)) {
// return prev.filter((v) => v !== type);
// } else {
// return [...prev, type];
// }
// });
setActiveType(type);
};
// 切换 Tab重置数据
@ -63,7 +59,7 @@ function SkillPage() {
<div className={styles['skill-page']}>
<CategoryList
typeList={typeList}
activeTypes={activeTypes}
activeType={activeType}
onTypeSelect={chooseType}
currentTab={activeTab}
tabItems={[
@ -81,9 +77,8 @@ function SkillPage() {
/>
<ResourceList
ref={dataListRef}
skillTypes={typeList}
isPublic={isPublic}
dataTypes={activeTypes}
dataType={activeType}
initialSearchText={cacheState?.searchText}
initialPagination={cacheState?.pagination}
setCacheState={setCacheState}

View File

@ -3,7 +3,8 @@ import ImagePicker, { ImagePickerServerType } from '@/components/ImagePicker';
import KFModal from '@/components/KFModal';
import UploadBlock from '@/components/UploadBlock';
import { SkillCategoryData } from '@/pages/Skill/types';
import { SkillUploadUrl, createSkillReq } from '@/services/skill';
import { getAssetIcon } from '@/services/dataset/index.js';
import { SkillUploadUrl, createSkillReq, getSkillInfoReq, updateSkillReq } from '@/services/skill';
import { to } from '@/utils/promise';
import { getFileListFromEvent, limitUploadFileType, validateUploadFiles } from '@/utils/ui';
import {
@ -17,17 +18,17 @@ import {
type UploadProps,
} from 'antd';
import { omit } from 'lodash';
import { useState } from 'react';
import { useEffect, useState } from 'react';
interface AddVersionModalProps extends Omit<ModalProps, 'onOk'> {
skillTypes: SkillCategoryData[];
id?: number | string;
onOk: () => void;
}
function AddSkillModal({ skillTypes, onOk, ...rest }: AddVersionModalProps) {
function AddSkillModal({ id, onOk, ...rest }: AddVersionModalProps) {
const [uuid] = useState(Date.now());
const [form] = Form.useForm();
// const [sliptUploadRequest, cancelUpload] = useSplitUpload();
const [typeList, setTypeList] = useState<SkillCategoryData[]>([]);
// 上传组件参数
const uploadProps: UploadProps = {
@ -45,11 +46,46 @@ function AddSkillModal({ skillTypes, onOk, ...rest }: AddVersionModalProps) {
maxCount: 1,
};
// 获取分类
useEffect(() => {
const getSkillTypes = async () => {
const params = {
page: 0,
size: 10000,
category_id: 3,
};
const [res] = await to(getAssetIcon(params));
if (res && res.data) {
setTypeList(res.data);
}
};
getSkillTypes();
}, []);
// 获取 Skill 详情
useEffect(() => {
const getSkillInfo = async (id: number | string) => {
const [res] = await to(getSkillInfoReq(id));
if (res && res.data) {
form.setFieldsValue(res.data);
}
};
if (id) {
getSkillInfo(id);
}
}, [id, form]);
// 上传请求
const createDatasetVersion = async (params: any) => {
const [res] = await to(createSkillReq(params));
if (id) {
params.id = id;
}
const request = id ? updateSkillReq : createSkillReq;
const [res] = await to(request(params));
if (res) {
message.success('创建成功');
message.success(id ? '编辑成功' : '创建成功');
onOk?.();
}
};
@ -57,20 +93,24 @@ function AddSkillModal({ skillTypes, onOk, ...rest }: AddVersionModalProps) {
// 提交
const onFinish = (formData: any) => {
const fileList: UploadFile[] = formData['fileList'] ?? [];
if (validateUploadFiles(fileList)) {
const data = fileList[0]?.response?.data ?? {};
const params = {
...omit(formData, 'fileList'),
...data,
};
createDatasetVersion(params);
if (id) {
createDatasetVersion(formData);
} else {
if (validateUploadFiles(fileList)) {
const data = fileList[0]?.response?.data ?? {};
const params = {
...omit(formData, 'fileList'),
...data,
};
createDatasetVersion(params);
}
}
};
return (
<KFModal
{...rest}
title="创建 Skill"
title={id ? '编辑 Skill' : '创建 Skill'}
width={608}
okButtonProps={{
htmlType: 'submit',
@ -99,7 +139,7 @@ function AddSkillModal({ skillTypes, onOk, ...rest }: AddVersionModalProps) {
},
]}
>
<Input placeholder={`请输入 Skill 名称`} />
<Input placeholder={`请输入 Skill 名称`} disabled={id !== undefined} />
</Form.Item>
<Form.Item
@ -133,7 +173,7 @@ function AddSkillModal({ skillTypes, onOk, ...rest }: AddVersionModalProps) {
>
<Select
placeholder="请选择 Skill 类型"
options={skillTypes}
options={typeList}
fieldNames={{ label: 'name', value: 'name' }}
optionFilterProp="name"
showSearch
@ -153,22 +193,24 @@ function AddSkillModal({ skillTypes, onOk, ...rest }: AddVersionModalProps) {
<ImagePicker serverType={ImagePickerServerType.Skill} uuid={uuid}></ImagePicker>
</Form.Item>
<Form.Item
label="Skill 文件"
name="fileList"
valuePropName="fileList"
getValueFromEvent={getFileListFromEvent}
rules={[
{
required: true,
message: '请上传 Skill 文件',
},
]}
>
<Upload {...uploadProps} data={{ uuid: uuid }} className={'common-upload-component'}>
<UploadBlock desc={'请上传 SKILL.md 或包含 SKILL.md 的 .zip 文件'}></UploadBlock>
</Upload>
</Form.Item>
{!id && (
<Form.Item
label="Skill 文件"
name="fileList"
valuePropName="fileList"
getValueFromEvent={getFileListFromEvent}
rules={[
{
required: true,
message: '请上传 Skill 文件',
},
]}
>
<Upload {...uploadProps} data={{ uuid: uuid }} className={'common-upload-component'}>
<UploadBlock desc={'请上传 SKILL.md 或包含 SKILL.md 的 .zip 文件'}></UploadBlock>
</Upload>
</Form.Item>
)}
</Form>
</KFModal>
);

View File

@ -48,6 +48,7 @@
color: @text-color;
font-size: 15px;
border-radius: 8px;
cursor: pointer;
&:hover {
background-color: rgba(81, 76, 249, 0.1);

View File

@ -9,7 +9,7 @@ import styles from './index.less';
type CategoryProps = {
typeList: SkillCategoryData[];
activeTypes: string[];
activeType: string | undefined;
tabItems: KFRadioItem[];
currentTab: string;
onTabChange: (value: string) => void;
@ -19,7 +19,7 @@ type CategoryProps = {
function CategoryList({
typeList,
activeTypes,
activeType,
tabItems,
currentTab,
onTabChange,
@ -53,11 +53,15 @@ function CategoryList({
key={item.id}
align="center"
className={classNames(styles['category-list__content__title'], {
[styles['category-list__content__title--active']]: activeTypes.includes(item.name),
[styles['category-list__content__title--active']]: activeType === item.name,
})}
onClick={() => onTypeSelect(item.name)}
onClick={() => {
if (item.name !== activeType) {
onTypeSelect(item.name);
}
}}
>
<KFIcon type={'icon-diancuihuacailiao'} width={16} height={16} />
<KFIcon type={item.path} width={16} height={16} />
<div style={{ marginLeft: 8 }}>{item.name}</div>
</Flex>
))}

View File

@ -26,7 +26,12 @@ function DownloadSkillModal({ treeData, onOk, ...rest }: AddVersionModalProps) {
>
<DirectoryTree
treeData={treeData}
defaultExpandAll={true}
fieldNames={{
title: 'name',
key: 'name',
}}
defaultExpandedKeys={[treeData[0]?.name || '']}
selectable={false}
titleRender={(record: DownloadTreeDataNode) => {
const label = record.name + (record.fileSize ? `${record.fileSize}` : '');
return <span style={{ fontSize: 14 }}>{label}</span>;

View File

@ -0,0 +1,22 @@
.frontmatter-card {
padding: 12px;
background-color: @background-color;
font-size: @font-size-input;
color: @text-color;
display: flex;
flex-direction: column;
gap: 10px;
border-radius: 8px 8px 0 0;
border-bottom: 1px solid hsl(210, 18%, 87%);
;
&__title {
flex: none;
width: 80px;
}
&__value {
flex: 1;
font-weight: bold;
}
}

View File

@ -0,0 +1,27 @@
import { Flex } from 'antd';
import styles from './index.less';
type FrontmatterCardProps = {
data: Record<string, string>;
};
function FrontmatterCard({ data }: FrontmatterCardProps) {
const { name, description } = data;
if (!name && !description) {
return null;
}
return (
<div className={styles['frontmatter-card']}>
<Flex align="flex-start">
<span className={styles['frontmatter-card__title']}>name: </span>
<span className={styles['frontmatter-card__value']}>{name}</span>
</Flex>
<Flex align="flex-start">
<span className={styles['frontmatter-card__title']}>description: </span>
<span className={styles['frontmatter-card__value']}>{description}</span>
</Flex>
</div>
);
}
export default FrontmatterCard;

View File

@ -1,7 +1,8 @@
import KFEmpty, { EmptyType } from '@/components/KFEmpty';
import KFSearch from '@/components/KFSearch';
import { CommonTabKeys } from '@/enums';
import { SkillCategoryData, SkillData } from '@/pages/Skill/types';
import { ALL_TYPE } from '@/pages/Skill/List';
import { SkillData } from '@/pages/Skill/types';
import { deleteSkillReq, getSkillListReq } from '@/services/skill';
import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise';
@ -12,14 +13,14 @@ import { Ref, forwardRef, useCallback, useEffect, useImperativeHandle, useState
import AddSkillModal from '../AddSkillModal';
import ResourceItem from '../ResourceItem';
import styles from './index.less';
export type ResourceListRef = {
reset: () => void;
resetPage: () => void;
};
type ResourceListProps = {
skillTypes?: SkillCategoryData[];
dataTypes?: string[];
dataType?: string | undefined;
isPublic: boolean;
initialSearchText?: string;
initialPagination?: PaginationProps;
@ -27,14 +28,7 @@ type ResourceListProps = {
};
function ResourceList(
{
skillTypes,
dataTypes,
isPublic,
initialSearchText,
initialPagination,
setCacheState,
}: ResourceListProps,
{ dataType, isPublic, initialSearchText, initialPagination, setCacheState }: ResourceListProps,
ref: Ref<ResourceListRef>,
) {
const navigate = useNavigate();
@ -57,7 +51,7 @@ function ResourceList(
size: pagination.pageSize,
is_public: isPublic,
name: searchText || undefined,
is_hot_stone: true,
type: dataType === ALL_TYPE ? undefined : dataType,
};
const [res] = await to(getSkillListReq(params));
if (res && res.data && res.data.content) {
@ -67,7 +61,7 @@ function ResourceList(
setDataList([]);
setTotal(0);
}
}, [pagination, searchText, isPublic]);
}, [pagination, searchText, isPublic, dataType]);
useEffect(() => {
getDataList();
@ -142,7 +136,7 @@ function ResourceList(
activeTab: isPublic ? CommonTabKeys.Public : CommonTabKeys.Private,
pagination,
searchText,
activeType: dataTypes,
activeType: dataType,
});
navigate(`/dataset/skills/info/${record.id}`);
};
@ -158,7 +152,6 @@ function ResourceList(
// 新建弹框
const showModal = () => {
const { close } = openAntdModal(AddSkillModal, {
skillTypes: skillTypes || [],
onOk: () => {
close();
getDataList();

View File

@ -1,6 +1,7 @@
export type SkillCategoryData = {
id: string;
name: string;
path: string;
};
export type SkillData = {
@ -14,10 +15,8 @@ export type SkillData = {
description: string;
type: string;
md_content: string;
prraised: boolean;
prraises_count: number;
is_public: boolean;
praised: boolean;
is_public: boolean;
file_tree_json: string;
file_path: string;
};

View File

@ -153,6 +153,27 @@ function ApprovalModal({ record, onOk, ...rest }: ApprovalModalProps) {
},
];
break;
case ApprovalType.SKILL:
items = [
{
label: 'Skill 名称',
value: content.name,
},
{
label: 'Skill 描述',
value: content.description,
},
{
label: 'Skill 类型',
value: content.type,
},
{
label: '创建时间',
value: content.createTime,
format: formatDate,
},
];
break;
default:
items = [];
break;

View File

@ -47,6 +47,7 @@ export enum ApprovalType {
IMAGE = 'IMAGE',
CODE = 'CODE',
SERVICE = 'SERVICE',
SKILL = 'SKILL',
}
const approvalTypeOptions = [
@ -55,6 +56,7 @@ const approvalTypeOptions = [
{ label: '镜像', value: ApprovalType.IMAGE },
{ label: '代码配置', value: ApprovalType.CODE },
{ label: '应用', value: ApprovalType.SERVICE },
{ label: 'Skill', value: ApprovalType.SKILL },
];
const statusOptions = [{ label: '全部', value: '' }, ...approvalStatusOptions];