feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
5
frontend/packages/community/component/.stylelintrc.js
Normal file
5
frontend/packages/community/component/.stylelintrc.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const { defineConfig } = require('@coze-arch/stylelint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
extends: [],
|
||||
});
|
||||
16
frontend/packages/community/component/README.md
Normal file
16
frontend/packages/community/component/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# @community-components/editor
|
||||
|
||||
> Project template for react component with storybook and supports publish independently.
|
||||
|
||||
## Features
|
||||
|
||||
- [x] eslint & ts
|
||||
- [x] esm bundle
|
||||
- [x] umd bundle
|
||||
- [x] storybook
|
||||
|
||||
## Commands
|
||||
|
||||
- init: `rush update`
|
||||
- dev: `npm run dev`
|
||||
- build: `npm run build`
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["./dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
14
frontend/packages/community/component/eslint.config.js
Normal file
14
frontend/packages/community/component/eslint.config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.test.ts', '**/*.test.tsx', '**/__tests__/**/*'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
103
frontend/packages/community/component/package.json
Normal file
103
frontend/packages/community/component/package.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"name": "@coze-community/components",
|
||||
"version": "0.0.1",
|
||||
"description": "business components for community",
|
||||
"license": "Apache-2.0",
|
||||
"author": "gaoding.devingao@bytedance.com",
|
||||
"maintainers": [],
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"unpkg": "./dist/umd/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
"types": "./src/index.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "npm run test -- --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coze-arch/bot-api": "workspace:*",
|
||||
"@coze-arch/bot-hooks": "workspace:*",
|
||||
"@coze-arch/bot-icons": "workspace:*",
|
||||
"@coze-arch/bot-semi": "workspace:*",
|
||||
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
|
||||
"@coze-arch/i18n": "workspace:*",
|
||||
"@coze-arch/logger": "workspace:*",
|
||||
"@coze-arch/responsive-kit": "workspace:*",
|
||||
"@coze-common/coze-mitt": "workspace:*",
|
||||
"@coze-foundation/space-store": "workspace:*",
|
||||
"@coze-studio/api-schema": "workspace:*",
|
||||
"@coze-studio/components": "workspace:*",
|
||||
"@douyinfe/semi-illustrations": "^2.36.0",
|
||||
"ahooks": "^3.7.8",
|
||||
"classnames": "^2.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coze-arch/bot-env": "workspace:*",
|
||||
"@coze-arch/bot-typings": "workspace:*",
|
||||
"@coze-arch/eslint-config": "workspace:*",
|
||||
"@coze-arch/stylelint-config": "workspace:*",
|
||||
"@coze-arch/tea": "workspace:*",
|
||||
"@coze-arch/ts-config": "workspace:*",
|
||||
"@coze-arch/vitest-config": "workspace:*",
|
||||
"@emotion/react": "11.11.1",
|
||||
"@emotion/styled": "11.11.0",
|
||||
"@mui/material": "^5",
|
||||
"@rollup/plugin-commonjs": "^24.0.0",
|
||||
"@rollup/plugin-json": "~6.0.0",
|
||||
"@rollup/plugin-node-resolve": "~15.0.1",
|
||||
"@rollup/plugin-replace": "^4.0.0",
|
||||
"@rsbuild/core": "1.1.13",
|
||||
"@swc/core": "^1.3.35",
|
||||
"@swc/helpers": "^0.4.12",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@types/dompurify": "3.0.5",
|
||||
"@types/lodash-es": "^4.17.10",
|
||||
"@types/prismjs": "1.26.3",
|
||||
"@types/react": "18.2.37",
|
||||
"@types/react-dom": "18.2.15",
|
||||
"@types/react-helmet": "^6.1.11",
|
||||
"@vitest/coverage-v8": "~3.0.5",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"core-js": "^3.37.1",
|
||||
"danmu.js": "^0.5.0",
|
||||
"debug": "^4.3.4",
|
||||
"less": "^3.13.1",
|
||||
"less-loader": "~11.1.3",
|
||||
"postcss": "^8.4.32",
|
||||
"react": "~18.2.0",
|
||||
"react-dom": "~18.2.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-is": ">= 16.8.0",
|
||||
"react-router-dom": "^6.22.0",
|
||||
"rollup": "^4.9.0",
|
||||
"rollup-plugin-cleanup": "^3.2.1",
|
||||
"rollup-plugin-node-externals": "^6.1.2",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-ts": "^3.1.1",
|
||||
"scheduler": ">=0.19.0",
|
||||
"styled-components": ">= 2",
|
||||
"tailwindcss": "~3.3.3",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-svgr": "~3.3.0",
|
||||
"vitest": "~3.0.5",
|
||||
"webpack": "~5.91.0",
|
||||
"xgplayer-subtitles": "^1.0.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.2.0",
|
||||
"react-dom": ">=18.2.0"
|
||||
},
|
||||
"// deps": "immer@^10.0.3 为脚本自动补齐,请勿改动"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
.card-button {
|
||||
cursor: pointer;
|
||||
|
||||
height: 32px;
|
||||
padding: 6px 16px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
|
||||
border: 1px solid;
|
||||
border-radius: 8px;
|
||||
|
||||
@apply coz-stroke-primary;
|
||||
@apply coz-bg-primary;
|
||||
@apply coz-fg-primary;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 FC, type PropsWithChildren } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const CardButton: FC<
|
||||
PropsWithChildren<{
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}>
|
||||
> = ({ className, onClick, children }) => (
|
||||
<button
|
||||
className={cls(styles['card-button'], className)}
|
||||
color="primary"
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
@@ -0,0 +1,42 @@
|
||||
.container {
|
||||
cursor: pointer;
|
||||
|
||||
position: relative;
|
||||
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
|
||||
border: 1px solid;
|
||||
border-radius: 8px;
|
||||
|
||||
@apply coz-stroke-primary;
|
||||
|
||||
|
||||
&:not(.skeleton):hover {
|
||||
@apply coz-bg-max;
|
||||
@apply coz-stroke-primary;
|
||||
|
||||
box-shadow: 0 6px 8px 0 rgba(28, 31, 35, 6%);
|
||||
|
||||
&.shadow-primary {
|
||||
box-shadow: 0 6px 8px 0 rgba(0, 8, 16, 12%);
|
||||
}
|
||||
}
|
||||
|
||||
&.skeleton {
|
||||
cursor: default;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.check {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.width100 {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -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 React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
const Container = (props: {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
shadowMode?: 'default' | 'primary';
|
||||
onClick?: () => void;
|
||||
}) => {
|
||||
const { className, children, onClick, shadowMode } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'coz-bg-max',
|
||||
s.container,
|
||||
s.width100,
|
||||
className,
|
||||
s[`shadow-${shadowMode}`],
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SkeletonContainer = (props: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) => (
|
||||
<div
|
||||
className={classNames(
|
||||
'coz-mg-primary',
|
||||
s.container,
|
||||
s.width100,
|
||||
s.skeleton,
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props?.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const CardContainer = Container;
|
||||
export const CardSkeletonContainer = SkeletonContainer;
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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 FC } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { AvatarName } from '@coze-studio/components';
|
||||
import { type explore } from '@coze-studio/api-schema';
|
||||
import { type UserInfo as ProductUserInfo } from '@coze-arch/bot-api/product_api';
|
||||
import { Typography } from '@coze-arch/coze-design';
|
||||
|
||||
type UserInfo = explore.product_common.UserInfo | ProductUserInfo;
|
||||
interface TemplateCardBodyProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
userInfo?: UserInfo;
|
||||
descClassName?: string;
|
||||
renderCardTag?: () => React.ReactNode;
|
||||
renderDescBottomSlot?: () => React.ReactNode;
|
||||
}
|
||||
export const CardInfo: FC<TemplateCardBodyProps> = ({
|
||||
title,
|
||||
description,
|
||||
userInfo,
|
||||
renderCardTag,
|
||||
descClassName,
|
||||
renderDescBottomSlot,
|
||||
}) => (
|
||||
<div className={cls('mt-[8px] px-[4px] grow', 'flex flex-col')}>
|
||||
<div className="flex items-center gap-[8px] overflow-hidden">
|
||||
<Typography.Text
|
||||
className="!font-medium text-[16px] leading-[22px] coz-fg-primary !max-w-[180px]"
|
||||
ellipsis={{ showTooltip: true, rows: 1 }}
|
||||
>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
{renderCardTag?.()}
|
||||
</div>
|
||||
|
||||
<AvatarName
|
||||
className="mt-[4px]"
|
||||
avatar={userInfo?.avatar_url}
|
||||
name={userInfo?.name}
|
||||
username={userInfo?.user_name}
|
||||
label={{
|
||||
name: userInfo?.user_label?.label_name,
|
||||
icon: userInfo?.user_label?.icon_url,
|
||||
href: userInfo?.user_label?.jump_link,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cls(
|
||||
'mt-[8px] flex flex-col justify-between grow',
|
||||
descClassName,
|
||||
)}
|
||||
>
|
||||
<Typography.Text
|
||||
className="min-h-[40px] leading-[20px] coz-fg-secondary"
|
||||
ellipsis={{ showTooltip: true, rows: 2 }}
|
||||
>
|
||||
{description}
|
||||
</Typography.Text>
|
||||
{renderDescBottomSlot?.()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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 { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
|
||||
import {
|
||||
IconCozBot,
|
||||
IconCozWorkflow,
|
||||
IconCozWorkspace,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { Tag, type TagProps } from '@coze-arch/coze-design';
|
||||
import { ProductEntityType } from '@coze-arch/bot-api/product_api';
|
||||
|
||||
interface IProps {
|
||||
type: ProductEntityType;
|
||||
}
|
||||
|
||||
interface TagConfig {
|
||||
icon: ReactNode;
|
||||
i18nKey: I18nKeysNoOptionsType;
|
||||
}
|
||||
|
||||
const TYPE_ICON_MAP: Partial<Record<ProductEntityType, TagConfig>> = {
|
||||
[ProductEntityType.BotTemplate]: {
|
||||
icon: <IconCozBot />,
|
||||
i18nKey: 'template_agent',
|
||||
},
|
||||
[ProductEntityType.WorkflowTemplateV2]: {
|
||||
icon: <IconCozWorkflow />,
|
||||
i18nKey: 'template_workflow',
|
||||
},
|
||||
[ProductEntityType.ImageflowTemplateV2]: {
|
||||
icon: <IconCozWorkflow />,
|
||||
i18nKey: 'template_workflow',
|
||||
},
|
||||
[ProductEntityType.ProjectTemplate]: {
|
||||
icon: <IconCozWorkspace />,
|
||||
i18nKey: 'project_store_search',
|
||||
},
|
||||
};
|
||||
|
||||
const TYPE_COLOR_MAP: Partial<Record<ProductEntityType, TagProps['color']>> = {
|
||||
[ProductEntityType.BotTemplate]: 'primary',
|
||||
[ProductEntityType.WorkflowTemplateV2]: 'primary',
|
||||
[ProductEntityType.ImageflowTemplateV2]: 'primary',
|
||||
[ProductEntityType.ProjectTemplate]: 'brand',
|
||||
};
|
||||
|
||||
export const CardTag = ({ type }: IProps) => {
|
||||
const config = TYPE_ICON_MAP[type];
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tag
|
||||
color={TYPE_COLOR_MAP[type] ?? 'primary'}
|
||||
className="h-[20px] !px-[4px] !py-[2px] coz-fg-primary font-medium shrink-0"
|
||||
>
|
||||
{config.icon}
|
||||
<span className="ml-[2px]">{I18n.t(config.i18nKey)}</span>
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
.plugin {
|
||||
margin-bottom: 0;
|
||||
|
||||
.plugin-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
display: none;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
width: 100%;
|
||||
|
||||
&.one-column-grid {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.btn-container {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.description {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.card-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 6px;
|
||||
|
||||
/*
|
||||
* 如此写边框和设置背景色的原因
|
||||
* 1、边框是内边框,处于图片的边缘上,因此需要用after来写
|
||||
* 2、背景色用before实体来写,是由于 border和背景色都是透明色,重叠会导致颜色加重,出现边框
|
||||
*/
|
||||
&::after {
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
|
||||
width: calc(100% - 2px);
|
||||
height: calc(100% - 2px);
|
||||
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 6px;
|
||||
|
||||
@apply coz-stroke-primary;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
|
||||
width: calc(100% - 2px);
|
||||
height: calc(100% - 2px);
|
||||
|
||||
@apply bg-stroke-5;
|
||||
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
& > img {
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
137
frontend/packages/community/component/src/card/plugin/index.tsx
Normal file
137
frontend/packages/community/component/src/card/plugin/index.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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 FC } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { type explore } from '@coze-studio/api-schema';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Avatar, Space, Tag, Toast, Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
import { cozeBaseUrl } from '@/const/url';
|
||||
|
||||
import { PluginAuthMode, type AuthMode } from '../type';
|
||||
import { CardInfo } from '../components/info';
|
||||
import { CardContainer, CardSkeletonContainer } from '../components/container';
|
||||
import { CardButton } from '../components/button';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface ProductInfo extends explore.ProductInfo {
|
||||
plugin_extra: explore.ProductInfo['plugin_extra'] & AuthMode;
|
||||
}
|
||||
|
||||
export type PluginCardProps = ProductInfo & {
|
||||
isInstalled?: boolean;
|
||||
isShowInstallButton?: boolean;
|
||||
};
|
||||
|
||||
export const PluginCard: FC<PluginCardProps> = props => (
|
||||
<CardContainer
|
||||
className={styles.plugin}
|
||||
shadowMode="default"
|
||||
onClick={() => {
|
||||
console.log('CardContainer...');
|
||||
}}
|
||||
>
|
||||
<div className={styles['plugin-wrapper']}>
|
||||
<PluginCardBody {...props} />
|
||||
|
||||
<Space
|
||||
className={cls(styles['btn-container'], {
|
||||
[styles['one-column-grid']]:
|
||||
props.isInstalled || !props.isShowInstallButton,
|
||||
})}
|
||||
>
|
||||
{!props.isInstalled && props.isShowInstallButton ? (
|
||||
<CardButton
|
||||
onClick={() => {
|
||||
Toast.success(I18n.t('plugin_install_success'));
|
||||
}}
|
||||
>
|
||||
{I18n.t('plugin_store_install')}
|
||||
</CardButton>
|
||||
) : null}
|
||||
<CardButton
|
||||
onClick={() => {
|
||||
window.open(
|
||||
`${cozeBaseUrl}/store/plugin/${props.meta_info?.id}?from=plugin_card`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
{I18n.t('plugin_usage_limits_modal_view_details')}
|
||||
</CardButton>
|
||||
</Space>
|
||||
</div>
|
||||
</CardContainer>
|
||||
);
|
||||
|
||||
export const PluginCardSkeleton = () => (
|
||||
<CardSkeletonContainer className={cls('h-[186px]', styles.plugin)} />
|
||||
);
|
||||
|
||||
const PluginCardBody: FC<PluginCardProps> = props => {
|
||||
const renderCardTag = () => {
|
||||
if (
|
||||
props.plugin_extra.auth_mode === PluginAuthMode.Required ||
|
||||
props.plugin_extra.auth_mode === PluginAuthMode.Supported
|
||||
) {
|
||||
return (
|
||||
<Tag
|
||||
color={'yellow'}
|
||||
className="h-[20px] !px-[4px] !py-[2px] coz-fg-primary font-medium shrink-0"
|
||||
>
|
||||
<span className="ml-[2px]">
|
||||
{I18n.t('plugin_store_unauthorized')}
|
||||
</span>
|
||||
</Tag>
|
||||
);
|
||||
} else if (props.plugin_extra.auth_mode === PluginAuthMode.Configured) {
|
||||
return (
|
||||
<Tooltip content={I18n.t('plugin_store_contact_deployer')}>
|
||||
<Tag
|
||||
color={'brand'}
|
||||
className="h-[20px] !px-[4px] !py-[2px] coz-fg-primary font-medium shrink-0"
|
||||
>
|
||||
<span className="ml-[2px]">
|
||||
{I18n.t('plugin_store_authorized')}
|
||||
</span>
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Avatar
|
||||
className={styles['card-avatar']}
|
||||
src={props.meta_info?.icon_url}
|
||||
shape="square"
|
||||
/>
|
||||
<CardInfo
|
||||
{...{
|
||||
title: props.meta_info?.name,
|
||||
description: props.meta_info?.description,
|
||||
userInfo: props.meta_info?.user_info,
|
||||
authMode: props.plugin_extra.auth_mode,
|
||||
renderCardTag,
|
||||
descClassName: styles.description,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
.template {
|
||||
margin-bottom: 0;
|
||||
|
||||
.template-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
display: none;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.btn-container {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.description {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* 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 FC, useState } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { explore } from '@coze-studio/api-schema';
|
||||
import { useSpaceList } from '@coze-foundation/space-store';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Image, Input, Modal, Space, Toast } from '@coze-arch/coze-design';
|
||||
import { ProductEntityType } from '@coze-arch/bot-api/product_api';
|
||||
|
||||
import { cozeBaseUrl } from '@/const/url';
|
||||
|
||||
import { type CardInfoProps } from '../type';
|
||||
import { CardTag } from '../components/tag';
|
||||
import { CardInfo } from '../components/info';
|
||||
import { CardContainer, CardSkeletonContainer } from '../components/container';
|
||||
import { CardButton } from '../components/button';
|
||||
|
||||
type ProductInfo = explore.ProductInfo;
|
||||
import styles from './index.module.less';
|
||||
|
||||
export type TemplateCardProps = ProductInfo;
|
||||
|
||||
const PATH_MAP: Partial<
|
||||
Record<explore.product_common.ProductEntityType, string>
|
||||
> = {
|
||||
[ProductEntityType.BotTemplate]: 'agent',
|
||||
[ProductEntityType.WorkflowTemplateV2]: 'workflow',
|
||||
[ProductEntityType.ImageflowTemplateV2]: 'workflow',
|
||||
[ProductEntityType.ProjectTemplate]: 'project',
|
||||
};
|
||||
|
||||
export const TemplateCard: FC<TemplateCardProps> = props => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
return (
|
||||
<CardContainer
|
||||
className={styles.template}
|
||||
shadowMode="default"
|
||||
onClick={() => {
|
||||
console.log('Template Click Card');
|
||||
}}
|
||||
>
|
||||
<div className={styles['template-wrapper']}>
|
||||
<TempCardBody
|
||||
{...{
|
||||
title: props.meta_info?.name,
|
||||
description: props.meta_info?.description,
|
||||
userInfo: props.meta_info?.user_info,
|
||||
entityType: props.meta_info.entity_type,
|
||||
imgSrc: props.meta_info.covers?.[0].url,
|
||||
}}
|
||||
/>
|
||||
<Space className={styles['btn-container']}>
|
||||
<CardButton
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
}}
|
||||
>
|
||||
{I18n.t('copy')}
|
||||
</CardButton>
|
||||
<CardButton
|
||||
onClick={() => {
|
||||
const pathPrefix = PATH_MAP[props.meta_info.entity_type] || '';
|
||||
const pathSuffix = [
|
||||
ProductEntityType.WorkflowTemplateV2,
|
||||
ProductEntityType.ImageflowTemplateV2,
|
||||
].includes(props.meta_info.entity_type)
|
||||
? `?entity_type=${props.meta_info.entity_type}`
|
||||
: '';
|
||||
window.open(
|
||||
`${cozeBaseUrl}/template/${pathPrefix}/${props.meta_info.id}${pathSuffix}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
{I18n.t('plugin_usage_limits_modal_view_details')}
|
||||
</CardButton>
|
||||
</Space>
|
||||
</div>
|
||||
{visible ? (
|
||||
<DuplicateModal
|
||||
productId={props.meta_info.id}
|
||||
entityType={props.meta_info.entity_type}
|
||||
defaultTitle={`${props.meta_info?.name}(${I18n.t('duplicate_rename_copy')})`}
|
||||
hide={() => setVisible(false)}
|
||||
/>
|
||||
) : null}
|
||||
</CardContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const DuplicateModal: FC<{
|
||||
defaultTitle: string;
|
||||
productId: string;
|
||||
entityType: explore.product_common.ProductEntityType;
|
||||
hide: () => void;
|
||||
}> = ({ defaultTitle, hide, productId, entityType }) => {
|
||||
const [title, setTitle] = useState(defaultTitle);
|
||||
const { spaces } = useSpaceList();
|
||||
const spaceId = spaces?.[0]?.id;
|
||||
console.log('title', title, spaces);
|
||||
return (
|
||||
<Modal
|
||||
type="modal"
|
||||
title={I18n.t('creat_project_use_template')}
|
||||
visible={true}
|
||||
onOk={async () => {
|
||||
try {
|
||||
await explore.PublicDuplicateProduct({
|
||||
product_id: productId,
|
||||
entity_type: entityType,
|
||||
space_id: spaceId,
|
||||
name: title,
|
||||
});
|
||||
Toast.success(I18n.t('copy_success'));
|
||||
hide();
|
||||
} catch (err) {
|
||||
console.error('PublicDuplicateProduct', err);
|
||||
Toast.error(I18n.t('copy_failed'));
|
||||
}
|
||||
}}
|
||||
onCancel={hide}
|
||||
cancelText={I18n.t('Cancel')}
|
||||
okText={I18n.t('Confirm')}
|
||||
>
|
||||
<Space vertical spacing={4} className="w-full">
|
||||
<Space className="w-full">
|
||||
<span className="coz-fg-primary font-medium leading-[20px]">
|
||||
{I18n.t('creat_project_project_name')}
|
||||
</span>
|
||||
<span className="coz-fg-hglt-red">*</span>
|
||||
</Space>
|
||||
<Input
|
||||
className="w-full"
|
||||
placeholder=""
|
||||
defaultValue={defaultTitle}
|
||||
onChange={value => {
|
||||
setTitle(value);
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export const TemplateCardSkeleton = () => (
|
||||
<CardSkeletonContainer className={cls('h-[278px]', styles.template)} />
|
||||
);
|
||||
|
||||
export const TempCardBody: FC<
|
||||
CardInfoProps & {
|
||||
entityType?: explore.product_common.ProductEntityType | ProductEntityType;
|
||||
renderImageBottomSlot?: () => React.ReactNode;
|
||||
renderDescBottomSlot?: () => React.ReactNode;
|
||||
}
|
||||
> = ({
|
||||
title,
|
||||
imgSrc,
|
||||
description,
|
||||
entityType,
|
||||
userInfo,
|
||||
renderImageBottomSlot,
|
||||
renderDescBottomSlot,
|
||||
}) => (
|
||||
<div>
|
||||
<div className="relative w-full h-[140px] rounded-[8px] overflow-hidden">
|
||||
<Image
|
||||
preview={false}
|
||||
src={imgSrc}
|
||||
className="w-full h-full"
|
||||
imgCls="w-full h-full object-cover object-center"
|
||||
/>
|
||||
{renderImageBottomSlot?.()}
|
||||
</div>
|
||||
<CardInfo
|
||||
{...{
|
||||
title,
|
||||
description,
|
||||
userInfo,
|
||||
renderCardTag: () =>
|
||||
entityType ? <CardTag type={entityType} /> : null,
|
||||
descClassName: styles.description,
|
||||
renderDescBottomSlot,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
43
frontend/packages/community/component/src/card/type.ts
Normal file
43
frontend/packages/community/component/src/card/type.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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 explore } from '@coze-studio/api-schema';
|
||||
import { type UserInfo as ProductUserInfo } from '@coze-arch/bot-api/product_api';
|
||||
type UserInfo = explore.product_common.UserInfo;
|
||||
|
||||
export interface CardInfoProps {
|
||||
title?: string;
|
||||
imgSrc?: string;
|
||||
description?: string;
|
||||
userInfo?: UserInfo | ProductUserInfo;
|
||||
}
|
||||
|
||||
/** for open coze */
|
||||
export enum PluginAuthMode {
|
||||
/** No authorization required */
|
||||
NoAuth = 0,
|
||||
/** Authorization is required, but not configured */
|
||||
Required = 1,
|
||||
/** Authorization is required and has been configured */
|
||||
Configured = 2,
|
||||
/** Authorization is required, but the configuration can be empty */
|
||||
Supported = 3,
|
||||
}
|
||||
|
||||
export interface AuthMode {
|
||||
/** for open coze */
|
||||
auth_mode?: PluginAuthMode;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.more {
|
||||
&:global(.coz-tag-mini) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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 { CozAvatar, Tag, Tooltip, Typography } from '@coze-arch/coze-design';
|
||||
import { type PluginConnectorInfo } from '@coze-arch/bot-api/product_api';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface ConnectorListProps {
|
||||
connectors: PluginConnectorInfo[];
|
||||
className?: string;
|
||||
visibleNum?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_VISIBLE_NUM = 3;
|
||||
|
||||
export const ConnectorList: React.FC<ConnectorListProps> = ({
|
||||
connectors,
|
||||
className,
|
||||
visibleNum = DEFAULT_VISIBLE_NUM,
|
||||
}) => {
|
||||
const moreNum = connectors.length - visibleNum;
|
||||
return (
|
||||
<div className={classNames('ml-auto flex gap-4px', className)}>
|
||||
{connectors.slice(0, visibleNum).map(item => (
|
||||
<Tooltip key={item.id} content={item.name} theme="dark">
|
||||
<CozAvatar
|
||||
className="border coz-stroke-primary border-solid"
|
||||
size="micro"
|
||||
src={item.icon}
|
||||
type="platform"
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
{moreNum > 0 ? (
|
||||
<Tooltip
|
||||
position="right"
|
||||
content={
|
||||
<div className="flex flex-col gap-8px max-w-[200px] max-h-[188px] overflow-y-auto overflow-x-hidden">
|
||||
{connectors.slice(visibleNum).map(item => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex gap-8px items-center max-w-full"
|
||||
>
|
||||
<CozAvatar
|
||||
className="border coz-stroke-primary border-solid"
|
||||
size="micro"
|
||||
src={item.icon}
|
||||
type="platform"
|
||||
/>
|
||||
<Typography.Text
|
||||
ellipsis={true}
|
||||
className="flex-1 overflow-hidden"
|
||||
>
|
||||
{item.name}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tag className={styles.more} size="mini" color="primary">
|
||||
+{moreNum}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
21
frontend/packages/community/component/src/const/url.ts
Normal file
21
frontend/packages/community/component/src/const/url.ts
Normal file
@@ -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 const cozeBaseUrl = IS_OVERSEA
|
||||
? // cp-disable-next-line
|
||||
'https://www.coze.com'
|
||||
: // cp-disable-next-line
|
||||
'https://www.coze.cn';
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
|
||||
.favorite-btn {
|
||||
color: #1D1C23;
|
||||
|
||||
:global(.semi-button-content-right) {
|
||||
margin-left: 4px;
|
||||
color: #1D1C23;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
.un-collected {
|
||||
path {
|
||||
fill: #1D1C23
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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, {
|
||||
useRef,
|
||||
useState,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
type MouseEvent,
|
||||
} from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
import { UIButton } from '@coze-arch/bot-semi';
|
||||
|
||||
import { type FavoriteIconBtnRef, FavoriteIconBtn } from '../favorite-icon-btn';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface HeaderProps {
|
||||
favoriteCount?: number;
|
||||
productId?: string;
|
||||
entityType?: number;
|
||||
isFavorite?: boolean;
|
||||
svgColor?: 'default' | 'dark';
|
||||
onReportFavorite: (action) => void;
|
||||
disabled?: boolean;
|
||||
isMobile?: boolean;
|
||||
unCollectedIconCls?: string;
|
||||
collectedIconCls?: string;
|
||||
onClickBefore?: (
|
||||
action: 'cancel' | 'add',
|
||||
event?: MouseEvent<HTMLDivElement, globalThis.MouseEvent>,
|
||||
) => boolean | Promise<boolean>;
|
||||
/**兼容UI1.0&2.0 全部替换后去除 */
|
||||
isNewStyle?: boolean;
|
||||
isForbiddenIconClick?: boolean;
|
||||
}
|
||||
|
||||
/* Plugin header */
|
||||
|
||||
export const FavoriteBtn = forwardRef((props: HeaderProps, ref) => {
|
||||
const {
|
||||
favoriteCount,
|
||||
onReportFavorite,
|
||||
productId,
|
||||
entityType,
|
||||
isFavorite,
|
||||
svgColor,
|
||||
disabled,
|
||||
isMobile,
|
||||
isNewStyle,
|
||||
collectedIconCls,
|
||||
unCollectedIconCls,
|
||||
onClickBefore,
|
||||
isForbiddenIconClick,
|
||||
} = props;
|
||||
const refFavoriteBtn = useRef<FavoriteIconBtnRef>(null);
|
||||
const [favoriteNumberAdd, setFavoriteNumberAdd] = useState(0);
|
||||
|
||||
// 该数字不能小于0, 防止出现异常数字
|
||||
const favoriteNum = Math.max(
|
||||
0,
|
||||
(Number(favoriteCount) || 0) + (Number(favoriteNumberAdd) || 0),
|
||||
);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
favorite: (event?: MouseEvent<HTMLDivElement, globalThis.MouseEvent>) =>
|
||||
refFavoriteBtn.current?.favorite(event),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
const favoriteIconButton = (
|
||||
<FavoriteIconBtn
|
||||
ref={refFavoriteBtn}
|
||||
productId={productId}
|
||||
entityType={entityType}
|
||||
isFavorite={isFavorite}
|
||||
onClickBefore={onClickBefore}
|
||||
isVisible={true}
|
||||
onReportTea={onReportFavorite}
|
||||
className={collectedIconCls}
|
||||
unCollectedIconCls={cls(styles['un-collected'], unCollectedIconCls)}
|
||||
isForbiddenClick={isForbiddenIconClick}
|
||||
onChange={value => {
|
||||
setFavoriteNumberAdd(prevNumber =>
|
||||
//该值再 1和 -1 之间。
|
||||
Math.min(Math.max(prevNumber + (Number(value) || 0), -1), 1),
|
||||
);
|
||||
}}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
);
|
||||
return isMobile ? (
|
||||
<div
|
||||
onClick={event => {
|
||||
if (!isForbiddenIconClick) {
|
||||
refFavoriteBtn.current?.favorite(event);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{favoriteIconButton}
|
||||
</div>
|
||||
) : isNewStyle ? (
|
||||
<Button
|
||||
size="large"
|
||||
color="primary"
|
||||
icon={favoriteIconButton}
|
||||
onClick={event => {
|
||||
refFavoriteBtn.current?.favorite(event);
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
{favoriteNum > 0
|
||||
? `${I18n.t('mkpl_num_favorites')}(${favoriteNum})`
|
||||
: I18n.t('mkpl_num_favorites')}
|
||||
</Button>
|
||||
) : (
|
||||
<UIButton
|
||||
theme={'light'}
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
className={cls(styles['favorite-btn'], styles[svgColor])}
|
||||
icon={favoriteIconButton}
|
||||
onClick={event => {
|
||||
refFavoriteBtn.current?.favorite(event);
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
{favoriteNum > 0 ? favoriteNum : I18n.t('mkpl_favorite')}
|
||||
</UIButton>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import {
|
||||
IconMobileCollect,
|
||||
IconMobileCollectFill,
|
||||
} from '@coze-arch/bot-icons';
|
||||
|
||||
export const FavoriteIconMobile = (props: { isFavorite?: boolean }) => {
|
||||
const { isFavorite } = props;
|
||||
return <>{isFavorite ? <IconMobileCollectFill /> : <IconMobileCollect />}</>;
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
|
||||
.icon-filled {
|
||||
width: 100%;
|
||||
color: rgba(255, 204, 18, 100%);
|
||||
}
|
||||
|
||||
.icon-stroked {
|
||||
width: 100%;
|
||||
color: rgba(29, 28, 35, 35%);
|
||||
}
|
||||
|
||||
.show-ani {
|
||||
animation-name: ani;
|
||||
animation-duration: 0.6s;
|
||||
}
|
||||
|
||||
.show-btn {
|
||||
&.icon-stroked {
|
||||
width: 100%;
|
||||
color: var(--coz-fg-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ani {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
38% {
|
||||
transform: scale(1.11);
|
||||
}
|
||||
|
||||
64% {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
74%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { IconButton } from '@coze-arch/coze-design';
|
||||
import { IconCollectFilled, IconCollectStroked } from '@coze-arch/bot-icons';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const FavoriteIcon = (props: {
|
||||
isFavorite?: boolean;
|
||||
isShowAni: boolean;
|
||||
unCollectedIconCls?: string;
|
||||
isMobile?: boolean;
|
||||
useButton?: boolean;
|
||||
className?: string;
|
||||
}) => {
|
||||
const { isFavorite, isShowAni, className, unCollectedIconCls, useButton } =
|
||||
props;
|
||||
|
||||
const iconProps = {
|
||||
className: cls(
|
||||
isFavorite ? styles['icon-filled'] : styles['icon-stroked'],
|
||||
isFavorite ? className : unCollectedIconCls,
|
||||
{
|
||||
[styles['show-ani']]: isFavorite && isShowAni,
|
||||
[styles['show-btn']]: useButton,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
const icon = isFavorite ? (
|
||||
<IconCollectFilled {...iconProps} />
|
||||
) : (
|
||||
<IconCollectStroked {...iconProps} />
|
||||
);
|
||||
|
||||
if (useButton) {
|
||||
return <IconButton size="default" color="primary" icon={icon} />;
|
||||
}
|
||||
|
||||
return icon;
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
|
||||
export const useAnimationChange = ({ isVisible }: { isVisible?: boolean }) => {
|
||||
const [isShowAni, setIsShowAni] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!isVisible) {
|
||||
setIsShowAni(false);
|
||||
}
|
||||
}, [isVisible]);
|
||||
const changeAnimationStatus = useMemoizedFn((isCurFavorite: boolean) => {
|
||||
if (!isCurFavorite) {
|
||||
setIsShowAni(true);
|
||||
} else {
|
||||
setIsShowAni(false);
|
||||
}
|
||||
});
|
||||
return { isShowAni, changeAnimationStatus };
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 { useMemoizedFn } from 'ahooks';
|
||||
import { type ProductEntityType } from '@coze-arch/bot-api/product_api';
|
||||
import { ProductApi } from '@coze-arch/bot-api';
|
||||
import { cozeMitt } from '@coze-common/coze-mitt';
|
||||
|
||||
import { type FavoriteCommParams } from '../type';
|
||||
export const useFavoriteStatusRequest = ({
|
||||
productId,
|
||||
entityType,
|
||||
entityId,
|
||||
topicId,
|
||||
onChange,
|
||||
setIsFavorite,
|
||||
}: FavoriteCommParams & {
|
||||
setIsFavorite: (isFavorite: boolean) => void;
|
||||
}) => {
|
||||
const changeFavoriteStatus = useMemoizedFn(
|
||||
async (isCurFavorite: boolean, action: string) => {
|
||||
setIsFavorite(!isCurFavorite);
|
||||
try {
|
||||
await ProductApi.PublicFavoriteProduct({
|
||||
// 后端不能处理空字符串
|
||||
product_id: productId || undefined,
|
||||
entity_type: entityType as ProductEntityType,
|
||||
is_cancel: isCurFavorite,
|
||||
entity_id: entityId,
|
||||
topic_id: topicId,
|
||||
});
|
||||
onChange?.(isCurFavorite ? -1 : 1);
|
||||
cozeMitt.emit('refreshFavList', {
|
||||
id: entityId,
|
||||
numDelta: action === 'add' ? 1 : -1,
|
||||
emitPosition: 'favorite-icon-btn',
|
||||
});
|
||||
} catch (_err) {
|
||||
setIsFavorite(isCurFavorite);
|
||||
}
|
||||
},
|
||||
);
|
||||
return { changeFavoriteStatus };
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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, useCallback, useRef, type MouseEvent } from 'react';
|
||||
|
||||
import { useMemoizedFn, useUpdateEffect } from 'ahooks';
|
||||
|
||||
import { type FavoriteCommParams } from '../type';
|
||||
import { useFavoriteStatusRequest } from './use-farvorite-request';
|
||||
import { useAnimationChange } from './use-animation-change';
|
||||
|
||||
type ClickAction = 'cancel' | 'add';
|
||||
const getClickAction = (isCurFavoriteStatus: boolean): ClickAction =>
|
||||
isCurFavoriteStatus ? 'cancel' : 'add';
|
||||
|
||||
export const useFavoriteChange = ({
|
||||
isFavoriteDefault,
|
||||
onReportTea,
|
||||
productId,
|
||||
entityId,
|
||||
entityType,
|
||||
onChange,
|
||||
onClickBefore,
|
||||
topicId,
|
||||
isVisible,
|
||||
onFavoriteStateChange,
|
||||
}: FavoriteCommParams & {
|
||||
onReportTea?: (action: 'cancel' | 'add') => void;
|
||||
isFavoriteDefault?: boolean;
|
||||
isVisible?: boolean;
|
||||
onFavoriteStateChange?: (isFavorite: boolean) => void;
|
||||
}) => {
|
||||
const [isFavorite, setIsFavorite] = useState<boolean>(
|
||||
isFavoriteDefault ?? false,
|
||||
);
|
||||
const { isShowAni, changeAnimationStatus } = useAnimationChange({
|
||||
isVisible,
|
||||
});
|
||||
const refIsChange = useRef(false);
|
||||
|
||||
// 改变状态前,先做前置请求,判断是否需要放弃本次状态变更,如果 onClickBefore 返回 false,则不进行变更。
|
||||
const onClickBeforeHandle = useMemoizedFn(
|
||||
async (
|
||||
action: ClickAction,
|
||||
event?: MouseEvent<HTMLDivElement, globalThis.MouseEvent>,
|
||||
) => (await onClickBefore?.(action, event)) !== false,
|
||||
);
|
||||
const { changeFavoriteStatus } = useFavoriteStatusRequest({
|
||||
productId,
|
||||
entityType,
|
||||
entityId,
|
||||
topicId,
|
||||
onChange,
|
||||
setIsFavorite,
|
||||
});
|
||||
|
||||
const onClick = useCallback(
|
||||
async (event?: MouseEvent<HTMLDivElement, globalThis.MouseEvent>) => {
|
||||
if (refIsChange.current) {
|
||||
// 进行中,直接返回,不做处理
|
||||
event?.stopPropagation?.();
|
||||
event?.preventDefault?.();
|
||||
return;
|
||||
}
|
||||
const action = getClickAction(isFavorite);
|
||||
refIsChange.current = true;
|
||||
try {
|
||||
if ((await onClickBeforeHandle(action, event)) !== false) {
|
||||
event?.stopPropagation?.();
|
||||
onReportTea?.(action);
|
||||
changeAnimationStatus(isFavorite);
|
||||
await changeFavoriteStatus(isFavorite, action);
|
||||
}
|
||||
} catch (_err) {
|
||||
console.error('useFavoriteChange:', _err);
|
||||
}
|
||||
refIsChange.current = false;
|
||||
},
|
||||
[isFavorite, onReportTea],
|
||||
);
|
||||
|
||||
useUpdateEffect(() => {
|
||||
onFavoriteStateChange?.(isFavorite);
|
||||
}, [isFavorite]);
|
||||
return { isFavorite, onClick, isShowAni };
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
.favorite-icon-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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, { forwardRef, useImperativeHandle } from 'react';
|
||||
|
||||
import { type FavoriteIconBtnProps } from './type';
|
||||
import { useFavoriteChange } from './hooks/use-favorite-change';
|
||||
import { FavoriteIconMobile } from './components/favorite-icon-mobile';
|
||||
import { FavoriteIcon } from './components/favorite-icon';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface FavoriteIconBtnRef {
|
||||
favorite: (event) => void;
|
||||
}
|
||||
|
||||
export const FavoriteIconBtn = forwardRef(
|
||||
(props: FavoriteIconBtnProps, ref) => {
|
||||
const {
|
||||
topicId,
|
||||
productId,
|
||||
entityType,
|
||||
entityId,
|
||||
isFavorite: isFavoriteDefault,
|
||||
onChange,
|
||||
isVisible,
|
||||
onReportTea,
|
||||
unCollectedIconCls,
|
||||
onClickBefore,
|
||||
onFavoriteStateChange,
|
||||
isMobile,
|
||||
className,
|
||||
useButton = false,
|
||||
isForbiddenClick = false,
|
||||
} = props;
|
||||
|
||||
const { isFavorite, onClick, isShowAni } = useFavoriteChange({
|
||||
isFavoriteDefault,
|
||||
onReportTea,
|
||||
productId,
|
||||
entityId,
|
||||
entityType,
|
||||
onChange,
|
||||
onClickBefore,
|
||||
topicId,
|
||||
isVisible,
|
||||
onFavoriteStateChange,
|
||||
});
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
favorite: onClick,
|
||||
}),
|
||||
[onClick],
|
||||
);
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
onClick={isForbiddenClick ? undefined : onClick}
|
||||
className={styles['favorite-icon-btn']}
|
||||
data-testid="bot-card-favorite-icon"
|
||||
>
|
||||
{isMobile ? (
|
||||
<FavoriteIconMobile isFavorite={isFavorite} />
|
||||
) : (
|
||||
<FavoriteIcon
|
||||
useButton={useButton}
|
||||
isFavorite={isFavorite}
|
||||
isShowAni={isShowAni}
|
||||
unCollectedIconCls={unCollectedIconCls}
|
||||
className={className}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 MouseEvent } from 'react';
|
||||
|
||||
export interface FavoriteCommParams {
|
||||
topicId?: string;
|
||||
productId?: string;
|
||||
entityType?: number;
|
||||
isFavorite?: boolean;
|
||||
useButton?: boolean;
|
||||
entityId?: string;
|
||||
onClickBefore?: (
|
||||
action: 'cancel' | 'add',
|
||||
event?: MouseEvent<HTMLDivElement, globalThis.MouseEvent>,
|
||||
) => boolean | Promise<boolean>;
|
||||
onChange?: (num) => void; // 当收藏状态真正变化的时候,回调
|
||||
}
|
||||
|
||||
export interface FavoriteIconBtnProps extends FavoriteCommParams {
|
||||
onFavoriteStateChange?: (isFavorite: boolean) => void; // 当收藏icon的显示状态变化的时候,回调
|
||||
isVisible: boolean;
|
||||
onReportTea?: (action: 'cancel' | 'add') => void;
|
||||
unCollectedIconCls?: string;
|
||||
isMobile?: boolean;
|
||||
isForbiddenClick?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
41
frontend/packages/community/component/src/index.ts
Normal file
41
frontend/packages/community/component/src/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 { OfficialLabel } from './official-label';
|
||||
|
||||
export { InfiniteList } from './infinite-list';
|
||||
export {
|
||||
type InfiniteListDataProps,
|
||||
type InfiniteListRef,
|
||||
type EmptyProps,
|
||||
} from './infinite-list/type';
|
||||
export { ConnectorList } from './connector-list';
|
||||
|
||||
export { FavoriteBtn } from './favorite-button';
|
||||
export { FavoriteIconBtn, type FavoriteIconBtnRef } from './favorite-icon-btn';
|
||||
export { SubMenuItem } from './sub-menu-item';
|
||||
export {
|
||||
TemplateCard,
|
||||
TemplateCardSkeleton,
|
||||
TempCardBody,
|
||||
type TemplateCardProps,
|
||||
} from './card/template';
|
||||
export { CardTag } from './card/components/tag';
|
||||
export {
|
||||
PluginCard,
|
||||
PluginCardSkeleton,
|
||||
type PluginCardProps,
|
||||
} from './card/plugin';
|
||||
@@ -0,0 +1,44 @@
|
||||
.empty {
|
||||
overflow: visible;
|
||||
height: 100%;
|
||||
|
||||
|
||||
|
||||
.spin {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
:global {
|
||||
.semi-spin-wrapper {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.semi-tabs-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.semi-spin-children {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-left: 8px;
|
||||
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: var(--semi-color-text-3, rgba(29, 28, 35, 35%));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { UIEmpty, Spin } from '@coze-arch/bot-semi';
|
||||
import { IllustrationFailure } from '@douyinfe/semi-illustrations';
|
||||
|
||||
import { type EmptyProps } from '../../type';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
/* Plugin header */
|
||||
|
||||
function Index(props: EmptyProps) {
|
||||
const {
|
||||
isLoading,
|
||||
isSearching,
|
||||
loadRetry,
|
||||
isError,
|
||||
renderEmpty,
|
||||
text,
|
||||
btn,
|
||||
icon,
|
||||
} = props;
|
||||
return (
|
||||
<div className={s.empty}>
|
||||
{renderEmpty?.(props) ||
|
||||
(!isError ? (
|
||||
isLoading ? (
|
||||
<Spin
|
||||
tip={
|
||||
<span className={s['loading-text']}>{I18n.t('Loading')}</span>
|
||||
}
|
||||
wrapperClassName={s.spin}
|
||||
size="middle"
|
||||
/>
|
||||
) : (
|
||||
<UIEmpty
|
||||
isNotFound={!!isSearching}
|
||||
empty={{
|
||||
title: text?.emptyTitle || I18n.t('inifinit_list_empty_title'),
|
||||
description: text?.emptyTitle ? text?.emptyDesc : '',
|
||||
btnText: btn?.emptyText,
|
||||
btnOnClick: btn?.emptyClick,
|
||||
icon,
|
||||
}}
|
||||
notFound={{
|
||||
title:
|
||||
text?.searchEmptyTitle || I18n.t('inifinit_search_not_found'),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<UIEmpty
|
||||
empty={{
|
||||
title: I18n.t('inifinit_list_load_fail'),
|
||||
icon: <IllustrationFailure />,
|
||||
btnText: loadRetry && I18n.t('inifinit_list_retry'),
|
||||
btnOnClick: () => {
|
||||
loadRetry?.();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Index;
|
||||
@@ -0,0 +1,55 @@
|
||||
.footer-container {
|
||||
padding: 12px 0 28px;
|
||||
text-align: center;
|
||||
|
||||
* {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.error-retry {
|
||||
margin-left: 10px;
|
||||
line-height: 20px;
|
||||
color: var(--semi-color-text-3, rgba(29, 28, 35, 35%));
|
||||
}
|
||||
|
||||
.error-retry {
|
||||
cursor: pointer;
|
||||
color: var(--semi-color-focus-border, #4d53e8);
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-spin-middle > .semi-spin-wrapper {
|
||||
height: 16px;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.load-more-btn {
|
||||
font-weight: 600;
|
||||
background: #fff;
|
||||
border-radius: 40px;
|
||||
|
||||
span {
|
||||
color: #1d1c23;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #fff;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.responsive-foot-container {
|
||||
padding: 0 0 16px;
|
||||
|
||||
.load-more-btn {
|
||||
height: 40px;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Spin, UIButton } from '@coze-arch/bot-semi';
|
||||
import { useIsResponsive } from '@coze-arch/bot-hooks';
|
||||
|
||||
import { type FooterProps } from '../../type';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
/* Plugin header */
|
||||
|
||||
function Index(props: FooterProps) {
|
||||
const {
|
||||
isLoading,
|
||||
loadRetry,
|
||||
isError,
|
||||
renderFooter,
|
||||
isNeedBtnLoadMore,
|
||||
noMore,
|
||||
} = props;
|
||||
const isResponsive = useIsResponsive();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(s['footer-container'], {
|
||||
[s['responsive-foot-container']]: isResponsive,
|
||||
})}
|
||||
>
|
||||
{renderFooter?.(props) ||
|
||||
(isLoading ? (
|
||||
<>
|
||||
<Spin />
|
||||
<span className={s.loading}>{I18n.t('Loading')}</span>
|
||||
</>
|
||||
) : isError ? (
|
||||
<>
|
||||
<Spin />
|
||||
<span className={s['error-retry']} onClick={loadRetry}>
|
||||
{I18n.t('inifinit_list_retry')}
|
||||
</span>
|
||||
</>
|
||||
) : isNeedBtnLoadMore && !noMore ? (
|
||||
<UIButton
|
||||
onClick={loadRetry}
|
||||
className={s['load-more-btn']}
|
||||
theme="borderless"
|
||||
>
|
||||
{I18n.t('mkpl_load_btn')}
|
||||
</UIButton>
|
||||
) : null)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Index;
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* 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,
|
||||
useRef,
|
||||
useEffect,
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
useInfiniteScroll,
|
||||
useUpdateEffect,
|
||||
useMemoizedFn,
|
||||
useDebounceFn,
|
||||
} from 'ahooks';
|
||||
|
||||
import { type ScrollProps, type InfiniteListDataProps } from '../type';
|
||||
|
||||
/* 滚动Hooks */
|
||||
|
||||
function useForwardFunc<T>(
|
||||
dataInfo: InfiniteListDataProps<T>,
|
||||
mutate: Dispatch<SetStateAction<InfiniteListDataProps<T>>>,
|
||||
) {
|
||||
// 手动插入数据,不通过接口
|
||||
const insertData = (item, index) => {
|
||||
dataInfo.list.splice(index, 0, item);
|
||||
mutate({
|
||||
...dataInfo,
|
||||
list: [...(dataInfo?.list || [])],
|
||||
});
|
||||
};
|
||||
|
||||
// 手动删除数据,不通过接口
|
||||
const removeData = index => {
|
||||
dataInfo.list.splice(index, 1);
|
||||
mutate({
|
||||
...dataInfo,
|
||||
list: [...(dataInfo?.list || [])],
|
||||
});
|
||||
};
|
||||
|
||||
const getDataList = () => dataInfo?.list;
|
||||
|
||||
return { insertData, removeData, getDataList };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function, @coze-arch/max-line-per-function -- 看了下代码行数不太好优化
|
||||
function useScroll<T>(props: ScrollProps<T>) {
|
||||
const {
|
||||
targetRef,
|
||||
loadData,
|
||||
threshold,
|
||||
reloadDeps,
|
||||
isNeedBtnLoadMore,
|
||||
resetDataIfReload = true,
|
||||
} = props;
|
||||
const [isLoadingError, setIsLoadingError] = useState<boolean>(false);
|
||||
const refFetchNo = useRef<number>(0);
|
||||
const refResolve = useRef<(value) => void>();
|
||||
const {
|
||||
loading,
|
||||
data: dataInfo,
|
||||
loadingMore,
|
||||
loadMore,
|
||||
noMore,
|
||||
cancel,
|
||||
mutate,
|
||||
reload,
|
||||
} = useInfiniteScroll<InfiniteListDataProps<T>>(
|
||||
async current => {
|
||||
// 此处逻辑如此复杂,是解决Scroll中的bug。
|
||||
// useInfiniteScroll中的cancel只是取消了一次请求,但是数据会根据current重新设置一遍。
|
||||
const fetchNo = refFetchNo.current;
|
||||
if (refResolve.current) {
|
||||
// 保证顺序执行,如果有当前方法,就取消上一次的请求,防止出现由于网络原因导致数据覆盖问题
|
||||
// 同时发出A1,A2,三次请求,但是A1先到达,然后请求了B1, 但是A1过慢,导致了A1覆盖了B1的请求。
|
||||
refResolve.current({
|
||||
...(current || {}),
|
||||
list: [],
|
||||
});
|
||||
}
|
||||
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
refResolve.current = resolve;
|
||||
loadData(current)
|
||||
.then(value => resolve(value))
|
||||
.catch(err => reject(err));
|
||||
});
|
||||
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
refResolve.current = null;
|
||||
|
||||
// 切换Tab的时候,如果此时正在请求,防止数据的残留界面显示
|
||||
if (refFetchNo.current !== fetchNo) {
|
||||
if (current) {
|
||||
current.list = [];
|
||||
}
|
||||
return {
|
||||
list: [],
|
||||
nextPage: 1,
|
||||
};
|
||||
}
|
||||
return result as InfiniteListDataProps<T>;
|
||||
},
|
||||
{
|
||||
target: isLoadingError || isNeedBtnLoadMore ? null : targetRef, //失败的时候,通过去掉target的事件绑定,禁止滚动加载。
|
||||
threshold,
|
||||
onBefore: () => {
|
||||
//setIsLoadingError(false);
|
||||
},
|
||||
isNoMore: data => data?.hasMore !== undefined && !data?.hasMore,
|
||||
onSuccess: () => {
|
||||
if (isLoadingError) {
|
||||
setIsLoadingError(false);
|
||||
}
|
||||
},
|
||||
onError: e => {
|
||||
// 如果在请求第一页数据时发生错误,并且当前列表不为空,则reset数据
|
||||
// 这个case只有当resetDataIfReload设置为false时才会发生
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
if (dataInfo.nextPage === 1 && (dataInfo?.list?.length ?? 0) > 0) {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
mutate({
|
||||
...dataInfo,
|
||||
list: [],
|
||||
});
|
||||
}
|
||||
setIsLoadingError(true);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { insertData, removeData, getDataList } = useForwardFunc(
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
dataInfo,
|
||||
mutate,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isNeedBtnLoadMore && !(loading || loadingMore)) {
|
||||
reload();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const reloadData = useMemoizedFn(() => {
|
||||
mutate({
|
||||
list: resetDataIfReload ? [] : (dataInfo?.list ?? []),
|
||||
hasMore: undefined,
|
||||
nextPage: 1,
|
||||
});
|
||||
cancel();
|
||||
setIsLoadingError(false);
|
||||
reload();
|
||||
});
|
||||
|
||||
useUpdateEffect(() => {
|
||||
refFetchNo.current++;
|
||||
reloadData();
|
||||
}, [...(reloadDeps || [])]);
|
||||
const isLoading = loading || loadingMore || props.isLoading;
|
||||
const { run: loadMoreDebounce } = useDebounceFn(
|
||||
() => {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
if (!isNeedBtnLoadMore) {
|
||||
loadMore();
|
||||
}
|
||||
},
|
||||
{ wait: 500 },
|
||||
);
|
||||
useEffect(() => {
|
||||
const resize = () => {
|
||||
loadMoreDebounce();
|
||||
};
|
||||
window.addEventListener('resize', resize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', resize);
|
||||
};
|
||||
}, []);
|
||||
const { list } = dataInfo || {};
|
||||
return {
|
||||
dataList: list,
|
||||
isLoading,
|
||||
loadMore: () => {
|
||||
if (!isLoading) {
|
||||
//如果已经有数据加载中了,需要禁止重复加载。
|
||||
loadMore();
|
||||
}
|
||||
},
|
||||
reload: reloadData,
|
||||
noMore,
|
||||
cancel,
|
||||
isLoadingError,
|
||||
mutate,
|
||||
insertData,
|
||||
removeData,
|
||||
getDataList,
|
||||
};
|
||||
}
|
||||
|
||||
export default useScroll;
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
.height-whole-100 {
|
||||
overflow: visible;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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 {
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
type RefObject,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { ResponsiveList } from '@coze-arch/responsive-kit';
|
||||
import { List } from '@coze-arch/bot-semi';
|
||||
|
||||
import { type InfiniteListProps, type InfiniteListRef } from './type';
|
||||
import useScroll from './hooks/use-scroll';
|
||||
import Footer from './components/footer';
|
||||
import Empty from './components/empty';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
/* Plugin header */
|
||||
|
||||
function Index<T extends object>(props: InfiniteListProps<T>, ref) {
|
||||
const {
|
||||
isSearching,
|
||||
className,
|
||||
emptyContent,
|
||||
grid,
|
||||
renderItem,
|
||||
itemClassName,
|
||||
renderFooter,
|
||||
scrollConf,
|
||||
emptyConf,
|
||||
onChangeState,
|
||||
canShowData = true,
|
||||
isNeedBtnLoadMore = false,
|
||||
isResponsive,
|
||||
retryFunc,
|
||||
responsiveConf,
|
||||
containerClassName,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
dataList,
|
||||
isLoading,
|
||||
loadMore,
|
||||
noMore,
|
||||
isLoadingError,
|
||||
mutate,
|
||||
reload,
|
||||
insertData,
|
||||
removeData,
|
||||
getDataList,
|
||||
} = useScroll<T>({ ...scrollConf, isNeedBtnLoadMore });
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({ mutate, reload, insertData, removeData, getDataList }),
|
||||
[mutate, reload, insertData, removeData, getDataList],
|
||||
);
|
||||
useEffect(() => {
|
||||
onChangeState?.(isLoading, dataList);
|
||||
}, [dataList, isLoading]);
|
||||
|
||||
// 根据白名单对列表移动端进行移动端适配
|
||||
|
||||
return (
|
||||
<div className={cls(s['height-whole-100'], containerClassName)}>
|
||||
{!dataList?.length || !canShowData ? (
|
||||
/** 数据为空的时候,操作如何显示空页面 */
|
||||
<Empty
|
||||
isError={canShowData ? isLoadingError : false}
|
||||
isSearching={isSearching}
|
||||
isLoading={canShowData ? isLoading : true}
|
||||
loadRetry={retryFunc || loadMore}
|
||||
{...emptyConf}
|
||||
/>
|
||||
) : isResponsive ? (
|
||||
<ResponsiveList<T>
|
||||
className={className}
|
||||
emptyContent={isLoading ? <></> : emptyContent}
|
||||
dataSource={dataList}
|
||||
renderItem={(item, number) => renderItem?.(item, number)}
|
||||
gridCols={responsiveConf?.gridCols}
|
||||
gridGapXs={{
|
||||
basic: 4,
|
||||
}}
|
||||
footer={
|
||||
<div className="text-sm px-6 py-3">
|
||||
<Footer
|
||||
isError={isLoadingError}
|
||||
noMore={noMore}
|
||||
isLoading={isLoading}
|
||||
loadRetry={retryFunc || loadMore}
|
||||
renderFooter={renderFooter}
|
||||
isNeedBtnLoadMore={isNeedBtnLoadMore}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<List
|
||||
{...{ className, emptyContent, grid }}
|
||||
emptyContent={isLoading ? <></> : emptyContent}
|
||||
dataSource={dataList}
|
||||
split={false}
|
||||
renderItem={(item, number) => (
|
||||
<List.Item
|
||||
className={
|
||||
typeof itemClassName === 'string'
|
||||
? itemClassName
|
||||
: itemClassName?.(item) // 支持动态行className
|
||||
}
|
||||
>
|
||||
{renderItem?.(item, number)}
|
||||
</List.Item>
|
||||
)}
|
||||
footer={
|
||||
<Footer
|
||||
isError={isLoadingError}
|
||||
noMore={noMore}
|
||||
isLoading={isLoading}
|
||||
loadRetry={retryFunc || loadMore}
|
||||
renderFooter={renderFooter}
|
||||
isNeedBtnLoadMore={isNeedBtnLoadMore}
|
||||
dataNum={dataList?.length}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const InfiniteList = forwardRef(Index) as <T>(
|
||||
props: InfiniteListProps<T> & { ref?: RefObject<InfiniteListRef> },
|
||||
) => JSX.Element;
|
||||
107
frontend/packages/community/component/src/infinite-list/type.ts
Normal file
107
frontend/packages/community/component/src/infinite-list/type.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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 ReactElement, type RefObject } from 'react';
|
||||
|
||||
import {
|
||||
type ResponsiveTokenMap,
|
||||
type ScreenRange,
|
||||
} from '@coze-arch/responsive-kit';
|
||||
import { type ListProps } from '@coze-arch/bot-semi/List';
|
||||
|
||||
export interface EmptyProps {
|
||||
isError?: boolean;
|
||||
isLoading?: boolean;
|
||||
isSearching?: boolean;
|
||||
loadRetry?: () => void; //重试加载
|
||||
text?: {
|
||||
emptyTitle?: string;
|
||||
emptyDesc?: string;
|
||||
searchEmptyTitle?: string;
|
||||
};
|
||||
btn?: {
|
||||
emptyClick?: () => void; //
|
||||
emptyText?: string;
|
||||
};
|
||||
icon?: ReactElement;
|
||||
|
||||
renderEmpty?: (
|
||||
emptyProps: Omit<EmptyProps, 'renderEmpty'>,
|
||||
) => React.ReactNode | null;
|
||||
}
|
||||
export interface FooterProps {
|
||||
isError?: boolean; // 是否加载出错
|
||||
isLoading?: boolean; // 是否加载中
|
||||
noMore?: boolean; //没有更多数据
|
||||
isNeedBtnLoadMore?: boolean;
|
||||
dataNum?: number;
|
||||
loadRetry?: () => void; //重试加载
|
||||
renderFooter?: (
|
||||
footerProps: Omit<FooterProps, 'renderFooter'>,
|
||||
) => React.ReactNode | null;
|
||||
}
|
||||
|
||||
export interface InfiniteListDataProps<T> {
|
||||
list: T[];
|
||||
hasMore?: boolean;
|
||||
nextPage: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ScrollProps<T> {
|
||||
threshold?: number; //距离下方多长距离,开始加载数据
|
||||
targetRef?: RefObject<HTMLDivElement>; // 监听滚动的Dom 引用
|
||||
loadData: (current) => Promise<InfiniteListDataProps<T>>; // 加载更多数据
|
||||
reloadDeps?: unknown[]; // 重新加载数据依赖
|
||||
isNeedBtnLoadMore?: boolean;
|
||||
isLoading?: boolean; // 是否加载中
|
||||
resetDataIfReload?: boolean; // 当reload时,是否先reset列表已存在数据,默认为true
|
||||
}
|
||||
|
||||
export interface InfiniteListProps<T>
|
||||
extends Pick<
|
||||
ListProps<T>,
|
||||
'className' | 'emptyContent' | 'grid' | 'renderItem'
|
||||
> {
|
||||
containerClassName?: string;
|
||||
canShowData?: boolean; //是否能够显示数据了
|
||||
isSearching?: boolean; // 是否搜索中,主要是用于错误显示的时候,选择文案使用
|
||||
itemClassName?: string | ((item: T) => string);
|
||||
isNeedBtnLoadMore?: boolean;
|
||||
isResponsive?: boolean;
|
||||
emptyConf: {
|
||||
renderEmpty?: EmptyProps['renderEmpty'];
|
||||
text?: EmptyProps['text'];
|
||||
btn?: EmptyProps['btn'];
|
||||
icon?: EmptyProps['icon'];
|
||||
};
|
||||
renderFooter?: FooterProps['renderFooter'];
|
||||
scrollConf: ScrollProps<T>;
|
||||
rowKey?: string;
|
||||
retryFunc?: () => void;
|
||||
onChangeState?: (loading, data) => void;
|
||||
responsiveConf?: {
|
||||
gridCols?: ResponsiveTokenMap<ScreenRange>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InfiniteListRef {
|
||||
mutate: (data) => void;
|
||||
reload: () => void;
|
||||
insertData: (item, index) => void;
|
||||
removeData: (index) => void;
|
||||
getDataList: () => unknown[]; // 获取当前列表数据
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
.official-label {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
transform: translate(25%, 25%);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.default {
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&.small {
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Tooltip } from '@coze-arch/coze-design';
|
||||
import { IconOfficialLabel } from '@coze-arch/bot-icons';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
/**
|
||||
* small 16px
|
||||
* default 20px
|
||||
* large 32px
|
||||
*/
|
||||
export type OfficialLabelSize = 'small' | 'default' | 'large';
|
||||
|
||||
export interface OfficialLabelProps {
|
||||
size?: OfficialLabelSize;
|
||||
visible: boolean;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const OfficialLabelSizeMap = {
|
||||
small: styles.small,
|
||||
default: styles.default,
|
||||
large: styles.large,
|
||||
};
|
||||
|
||||
export const OfficialLabel: React.FC<OfficialLabelProps> = ({
|
||||
size = 'default',
|
||||
children,
|
||||
visible,
|
||||
className,
|
||||
}) => (
|
||||
<div className="relative w-fit h-fit">
|
||||
<Tooltip
|
||||
spacing={12}
|
||||
trigger={visible ? 'hover' : 'custom'}
|
||||
content={I18n.t('mkpl_plugin_tooltip_official')}
|
||||
>
|
||||
{visible ? (
|
||||
<IconOfficialLabel
|
||||
className={classNames(
|
||||
styles['official-label'],
|
||||
OfficialLabelSizeMap[size],
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
</Tooltip>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -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 { type FC, type ReactNode } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Typography } from '@coze-arch/coze-design';
|
||||
|
||||
export interface SubMenuItemProps {
|
||||
icon?: ReactNode;
|
||||
title?: string;
|
||||
activeIcon?: ReactNode;
|
||||
isActive: boolean;
|
||||
suffix?: ReactNode;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const SubMenuItem: FC<SubMenuItemProps> = ({
|
||||
icon = null,
|
||||
title,
|
||||
activeIcon = null,
|
||||
isActive,
|
||||
suffix,
|
||||
onClick,
|
||||
}) => (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
'flex items-center gap-[8px]',
|
||||
'transition-colors',
|
||||
'rounded-[8px]',
|
||||
'h-[32px] w-full',
|
||||
'px-[8px]',
|
||||
'cursor-pointer',
|
||||
'hover:coz-mg-primary-hovered',
|
||||
isActive ? 'coz-bg-primary coz-fg-plus' : 'coz-fg-primary coz-bg-max',
|
||||
)}
|
||||
>
|
||||
<div className="text-[16px] leading-none leading-none w-[16px] h-[16px]">
|
||||
{isActive ? activeIcon : icon}
|
||||
</div>
|
||||
<Typography.Text
|
||||
ellipsis={{ showTooltip: true, rows: 1 }}
|
||||
fontSize="14px"
|
||||
weight={500}
|
||||
className="flex-1 text-[14px] leading-[20px] font-[500]"
|
||||
>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
{suffix}
|
||||
</div>
|
||||
);
|
||||
17
frontend/packages/community/component/src/typings.d.ts
vendored
Normal file
17
frontend/packages/community/component/src/typings.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
21
frontend/packages/community/component/tailwind.config.ts
Normal file
21
frontend/packages/community/component/tailwind.config.ts
Normal file
@@ -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.
|
||||
*/
|
||||
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
export default {
|
||||
content: ['./src/**/*.{ts,tsx}', '../../packages/**/src/**/*.{ts,tsx}'],
|
||||
} satisfies Config;
|
||||
71
frontend/packages/community/component/tsconfig.build.json
Normal file
71
frontend/packages/community/component/tsconfig.build.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"compilerOptions": {
|
||||
"rootDirs": ["./"],
|
||||
"types": [],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src", "src/**/*.json"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../arch/api-schema/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-api/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-env/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-hooks/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-typings/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/i18n/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/logger/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/responsive-kit/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/tea/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../common/coze-mitt/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../components/bot-icons/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../components/bot-semi/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/eslint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/stylelint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/ts-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/vitest-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../foundation/space-store/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../studio/components/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/packages/community/component/tsconfig.json
Normal file
15
frontend/packages/community/component/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
],
|
||||
"exclude": ["**/*"]
|
||||
}
|
||||
27
frontend/packages/community/component/tsconfig.misc.json
Normal file
27
frontend/packages/community/component/tsconfig.misc.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"include": [
|
||||
"__tests__",
|
||||
"stories",
|
||||
"vitest.config.ts",
|
||||
"vitest.setup.ts",
|
||||
"tailwind.config.ts",
|
||||
"../pages/src/flow-trial/store-preview"
|
||||
],
|
||||
"exclude": ["./dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"rootDirs": ["./"],
|
||||
"outDir": "./dist",
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"rootDir": "./"
|
||||
}
|
||||
}
|
||||
38
frontend/packages/community/component/vitest.config.ts
Normal file
38
frontend/packages/community/component/vitest.config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 svgr from 'vite-plugin-svgr';
|
||||
import { defineConfig } from '@coze-arch/vitest-config';
|
||||
|
||||
export default defineConfig(
|
||||
{
|
||||
dirname: __dirname,
|
||||
preset: 'web',
|
||||
plugins: [
|
||||
// @ts-expect-error Incompatible svgr types
|
||||
svgr({
|
||||
include: ['**/*.svg'],
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
setupFiles: ['./vitest.setup.ts'],
|
||||
reporters: ['verbose'],
|
||||
},
|
||||
},
|
||||
{
|
||||
fixSemi: true,
|
||||
},
|
||||
);
|
||||
17
frontend/packages/community/component/vitest.setup.ts
Normal file
17
frontend/packages/community/component/vitest.setup.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 '@testing-library/jest-dom/vitest';
|
||||
Reference in New Issue
Block a user