antd + react model自定义footer_你想知道的React组件设计模式这里都有(下)

本文深入探讨了React中的六种关键组件设计模式,包括Context模式、组合组件、继承模式等,通过具体示例帮助读者理解和掌握这些模式的应用场景与实践技巧。
65452ca7-4c13-eb11-8da9-e4434bdf6706.gif66452ca7-4c13-eb11-8da9-e4434bdf6706.gif本文梳理了context模式、组合组件、组合与继承这三类React组件设计模式往期回顾:你想知道的React组件设计模式这里都有(上)66452ca7-4c13-eb11-8da9-e4434bdf6706.gif上一篇介绍了三种设计模式,包括:
  • 容器与展示组件;

  • 高阶组件;

  • render props。

这篇我们继续介绍三种设计模式,包括:
  • context模式 ;

  • 组合组件 ;

  • 继承模式。

为了更好的理解,你可以将相应源码下载下来查看:https://github.com/imalextu/learn-react-patterns

一、Context模式

>>>>

概念介绍

React 的 Context 接口提供了一个无需为每层组件手动添加 props ,就能在组件树间进行数据传递的方法。

在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

>>>>

示例

React v16.3.0前后的Context相关API不同,这边只介绍新版本的Context使用方法。
第一步:新建createContext
首先,要用新提供的 createContext 函数创造一个“上下文”对象。
const ThemeContext = React.createContext();
第二步:生成Provider 和 Consumer

接着,我们用ThemeContext生成两个属性,分别是Provider和Consumer。从字面意思即可理解。Provider供数据提供者使用,Consumer供数据消费者使用。

const ThemeProvider = ThemeContext.Provider;const ThemeConsumer = ThemeContext.Consumer;
第三步:使用ThemeProvider给数据提供者
const Context = () => {  return (    <div>      <ThemeProvider value={{ mainColor: 'blue', textColor: 'pink' }} >        <Page />      ThemeProvider>    div>  )}// 调用contextconst Page = () => (  <div>    <Title>标题Title>    <Content>      内容    Content>  div>);
第四步:使用ThemeConsumer给数据接收者
// 这里演示一个class组件。Counsumer使用了renderProps模式哦。class Title extends React.Component {  render() {    return (      <ThemeConsumer>        {          (theme) => (            <h1 style={{ color: theme.mainColor }}>              {this.props.children}            h1>          )        }      ThemeConsumer>    );  }}// 这里演示一个函数式组件const Content = (props, context) => {  return (    <ThemeConsumer>      {        (theme) => (          <p style={{ color: theme.textColor }}>            {props.children}          p>        )      }    ThemeConsumer>  );};
>>>>

模式所解决的问题

Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。如下图,组件a、组件g、组件f需要共享数据,则只需要在最外层套上Provider,需要共享的组件使用Consumer即可。

68452ca7-4c13-eb11-8da9-e4434bdf6706.png

>>>>

使用注意事项

因为 context 会使用参考标识(reference identity)来决定何时进行渲染,这里可能会有一些陷阱,当 provider 的父组件进行重渲染时,可能会在 consumers 组件中触发意外的渲染。举个例子,当每一次 Provider 重渲染时,以下的代码会重渲染所有下面的 consumers 组件,因为 value 属性总是被赋值为新的对象:
class App extends React.Component {  render() {    return (      <Provider value={{something: 'something'}}>        <Toolbar />      Provider>    );  }}
为了防止这种情况,将 value 状态提升到父节点的 state 里:
class App extends React.Component {  constructor(props) {    super(props);    this.state = {      value: {something: 'something'},    };  }  render() {    return (      <Provider value={this.state.value}>        <Toolbar />      Provider>    );  }}

二、组合组件

>>>>

概念介绍

Compound Component 翻译为组合组件。借用组合组件,使用者只需要传递子组件,子组件所需要的props在父组件会封装好,引用子组件的时候就没必要传递所有props了。组合组件核心的两个方法是React.Children.map和React.cloneElement。React.Children.map 用来遍历获得组件的子元素。React.cloneElement 则用来复制元素,这个函数第一个参数就是被复制的元素,第二个参数可以增加新产生元素的 props ,我们就是利用这个函数,把想要的 props 传入子元素。>>>>

示例

我们设计一个类似于 antd 中的 Tabs 组件,提供tab切换功能,而被选中的TabItem需要高亮。

69452ca7-4c13-eb11-8da9-e4434bdf6706.png

如果我们使用常规写法,用 Tabs 中一个 state 记录当前被选中的 Tabitem 序号,然后根据这个 state 传递 props 给 TabItem,还需要传递一个 onClick 事件进去,捕获点击选择事件。

<TabItem active={true} onClick={this.onClick}>OneTabItem><TabItem active={false} onClick={this.onClick}>TwoTabItem><TabItem active={false} onClick={this.onClick}>ThreeTabItem> 

每次增加一个TabItem,是不是都需要传递active和onClick,这太繁琐了!我们用compound模式解决这个问题。

const TabItem = (props) => {  const {active, onClick} = props;  const tabStyle = {    'max-width': '150px',    color: active ? 'red' : 'green',    border: active ? '1px red solid' : '0px',  };  return (    <h1 style={tabStyle} onClick={onClick}>      {props.children}    h1>  );};// jsx调用Tabs以及TabItemconst Compound = (props, context) => {  return (    <Tabs>      <TabItem>OneTabItem>      <TabItem>TwoTabItem>      <TabItem>ThreeTabItem>      <TabItem>FourTabItem>    Tabs>  );};
上面的代码展示了我们最终调用Tabs以及TabItem的样子。重点在于Tabs我们要如何实现:
class Tabs extends React.Component {  state = {    activeIndex:  0  }  render() {    const newChildren = React.Children.map(this.props.children, (child, index) => {      if (child.type) {        return React.cloneElement(child, {          active: this.state.activeIndex === index,          onClick: () => this.setState({activeIndex: index})        });      } else {        return child;      }    });    return (      <Fragment>        {newChildren}      Fragment>    );  }}
原本我们要如此调用:
<TabItem active={false} onClick={this.onClick}>OneTabItem>
现在我们这样调用就可以了:
<TabItem>OneTabItem>
通过组合使用React.Children.map和React.cloneElement,我们让TabItem获得了它想要的属性,简化了TabItem的使用,是不是很神奇?>>>>

模式所解决的问题

组合组件设计模式一般应用在一些共享组件上。如 select 和 option , Tab 和TabItem 等,通过组合组件,使用者只需要传递子组件,子组件所需要的 props 在父组件会封装好,引用子组件的时候就没必要传递所有 props 了。我们可以在共享的组件中运用这种模式,简化组件使用者的调用方式,antd 当中你就能看到许多组合组件的使用。

三、继承模式

>>>>

概念介绍

说了那么多的模式,我们最后来谈谈很熟悉的继承模式。如果组件定义为class组件,那么我们当然可以使用继承的模式来实现组件的复用。>>>>

示例

我们通过一个基类来实现一些通用的逻辑,然后再通过继承分别实现两个子类。
class Base extends React.PureComponent {  getAlbumItem = () => {    return null  }  render () {    return (      <div style={{border:'1px solid red',margin:5,width:300}}>        {this.getAlbumItem()}        <div>通用逻辑写这里div>      div>    )  }}class Mobile extends Base {  getAlbumItem = () => {    return <span>mobilespan>  }}class Pc extends Base {  getAlbumItem = () => {    return <span>pcspan>  }}
我们具体看下Provider组件是如何定义的。通过这段代码props.children(allProps),我们调用了传入的函数。
const Provider = (props) => {  // 判断是否是女性用户  let isWoman = Math.random() > 0.5 ? true : false  if (isWoman) {    const allProps = { add: '高阶组件增加的属性', ...props }    return props.children(allProps)  } else {    return <div>女士专用,男士无权浏览div>;  }}
我们可以看到Mobile和Pc共享了Base的逻辑,实现了复用。‍>>>>

组合与继承

如果你刚使用React,可能继承的方式对大家来说更熟悉的。因为继承看起来很方便,也很好理解。但是React官方并不推荐使用继承,因为各种组合的模式完全足够使用,上面的例子我们完全可以用组合的思想去实现。为什么不推荐使用继承?继承有两个缺点,其一是,父类的属性和方法,子类是无条件继承的。也就是说,不管子类愿意不愿意,都必须继承父类所有的属性和方法,这样就不够灵活了。其二是,js中class并不直接支持多继承。这两个缺点使得继承相对于组合组件缺少了灵活性以及可扩展性。请记住,组合优于继承!组件的复用请第一时间想到使用组合而非继承。

尾声

到这里,六种React组件设计模式就就讲完了。这六种模式已经覆盖了绝大多数的组件使用场景。随着React的更新,也许将来会有更多组件设计模式出现。但是思想都是想通的,比如“责任分离”、“不要重复自己”(DRY,Don't Repeat Yourself) 等等。明白这些代码设计思想,将来我们也能很快地掌握新的组件设计模式。参考文档:
  • React官方文档

      (http://t.cn/AiYGz4Na)
  • React Component Patterns

      (http://t.cn/EvsJ8gj)
  • React实战:设计模式和最佳实践

      (http://t.cn/EUy09Ml)
  • Presentational and Container Components

     (http://t.cn/RqMyfwV)
  • React组件「设计模式」快速指南

    http://t.cn/AiThDOqG

  • 为什么老鸟要告诉你优先使用组合而不是继承?

    http://t.cn/AiThD8E4

6a452ca7-4c13-eb11-8da9-e4434bdf6706.png长的帅的人都“在看”6b452ca7-4c13-eb11-8da9-e4434bdf6706.gif
import { AiToolImageCoverCard } from '@aiComponents/CardItems'; import GridList from '@aiComponents/GridList'; import AiToolOptionItem from '@aiComponents/OptionItems'; import IconFont from '@components/Iconfont'; import ProductAuth from '@components/ProductAuth'; import ThemeButton from '@components/ThemeButton'; import { userDeclineAuth } from '@utils/auth.ts'; import { CoolImage } from '@wander/common-components'; import { useMount } from 'ahooks'; import { Modal } from 'antd'; import classNames from 'classnames/bind'; import PubSub from 'pubsub-js'; import { forwardRef, useImperativeHandle } from 'react'; import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import type { ImageSelectorModalRef } from '@/components/ImageSelectorModal'; import ImageSelectorModal from '@/components/ImageSelectorModal'; import useGetConfigValue from '@/hooks/useGetConfigValue.ts'; import { SCENERY_LIGHTING_TOOL_STYLE_PICTURES_KEY } from '@/pages/AiTools/SceneryLighting/config.tsx'; import { useStore } from '@/store/createStore.ts'; import type { UserRoleType } from '@/types/user'; import styles from './index.module.less'; const cx = classNames.bind(styles); type CategoriesItem = { alias: string; // 展示封面 coverUrl: string; // 参考图链接 referImageUrl: string; // 效果模式风格参考图权重 weight?: number; // 可用用户类型re availableType?: UserRoleType; }; type ImageItem = { // 风格名称 alias: string; key: string; modelName: string; categories: CategoriesItem[]; // 可用用户类型re availableType?: UserRoleType; }; // type Props< // T extends { // name: string; // // 展示封面 // coverUrl: string; // // 可用用户类型re // availableType?: UserRoleType; // }, // > = { // hidden?: boolean; // list: T[]; // onChange: (item: T, index: number) => void; // onUploadChange: (item: T, index: number) => void; // }; type Props = { hidden?: boolean; }; export type SceneryLightingComfyStyleImageListRef = { // 获取当前选中项 getComfyCurrentSelectItem: () => CategoriesItem & { modelName: string; }; }; const SceneryLightingComfyStyleImageList = forwardRef<SceneryLightingComfyStyleImageListRef, Props>( ({ hidden = false }, ref) => { const { t } = useTranslation(); const { productType } = useStore((state) => state.productInfo); // 配置的图片列表 const imageList = useGetConfigValue<ImageItem[]>({ productKeys: { JZXZ: SCENERY_LIGHTING_TOOL_STYLE_PICTURES_KEY, JZDS: SCENERY_LIGHTING_TOOL_STYLE_PICTURES_KEY, }, }) || []; // 二级类目索引 const [secondaryCategoryActiveIndex, setSecondaryCategoryActiveIndex] = useState(0); // 当前用户上传的图片链接 const [uploadImageUrl, setUploadImageUrl] = useState(''); // 风格配置弹窗限制 const [styleConfigLimit, setStyleConfigLimit] = useState<UserRoleType | 'NONE'>('NONE'); // 上传参考图限制弹窗 const [uploadImageLimitOpen, setUploadImageLimitOpen] = useState(false); // 一级类目索引 const [firstCategoryIndex, setFirstCategoryIndex] = useState(0); // 当前显示的二级分类列表 const [currentSubCategories, setCurrentSubCategories] = useState<CategoriesItem[]>([]); const styleImageModelName = imageList[firstCategoryIndex].modelName; // 风格参考图上传弹框ref const imageSelectorModalRef = useRef<ImageSelectorModalRef>(null); const onFirstCategoryChange = (index: number) => { setFirstCategoryIndex(index); // 设置当前显示的二级分类 setCurrentSubCategories(imageList[index].categories || []); // 重置二级分类选中索引 setSecondaryCategoryActiveIndex(0); }; useImperativeHandle( ref, () => ({ getComfyCurrentSelectItem: () => { if (secondaryCategoryActiveIndex === currentSubCategories.length && uploadImageUrl) { return { alias: t('AiTools-SceneryLighting-Operate-StyleImageList-QmcW'), coverUrl: uploadImageUrl, referImageUrl: uploadImageUrl, weight: currentSubCategories[secondaryCategoryActiveIndex]?.weight, modelName: styleImageModelName, }; } return { ...currentSubCategories[secondaryCategoryActiveIndex], referImageUrl: currentSubCategories[secondaryCategoryActiveIndex]?.coverUrl, modelName: styleImageModelName, }; }, }), [secondaryCategoryActiveIndex, t, uploadImageUrl, currentSubCategories, styleImageModelName], ); useMount(() => { if (imageList.length > 0) { setCurrentSubCategories(imageList[0].categories); } }); return ( <> <div hidden={hidden}> <div className={cx('title')}> <GridList list={imageList} renderItem={(item, index) => { return ( <AiToolOptionItem active={index === firstCategoryIndex} onClick={() => { onFirstCategoryChange(index); }} label={item.alias} /> ); }} /> </div> <div className={cx('grid_wrap')}> {currentSubCategories.map((item, index) => ( <AiToolImageCoverCard key={index} coverUrl={item.coverUrl} active={index === secondaryCategoryActiveIndex} // availableType={item.availableType} label={item.alias} onClick={() => { setSecondaryCategoryActiveIndex(index); }} /> ))} <div className={cx('image_item', productType.toLowerCase(), { active: uploadImageUrl && secondaryCategoryActiveIndex === currentSubCategories.length, })} > {/* .square_box > .inset_box用于等比例盒子构建 */} <div className={cx('square_box')}> <div className={cx('inset_box')}> {/* 图片上传后 显示用户上传的图片层级 */} {uploadImageUrl ? ( <div className={cx('cover_image_container')} onClick={() => { // 自定义上传的参考图选中索引 预设图片数量 setSecondaryCategoryActiveIndex(currentSubCategories.length); }} > <CoolImage className={cx('cover_image')} src={uploadImageUrl} alt="" /> <div className={cx('extra_node')} hidden={ !uploadImageUrl || secondaryCategoryActiveIndex !== currentSubCategories.length } > <IconFont type="micro-icon-category-selected" className={cx('select_icon')} /> <IconFont type="micro-icon-retry" className={cx('re_upload_icon')} onClick={(event) => { event.stopPropagation(); imageSelectorModalRef.current?.setModalStatus('SELECT_MODAL'); }} /> </div> </div> ) : ( // 图片上传按钮 <div className={cx('upload_box', productType.toLowerCase())} onClick={() => { if (!userDeclineAuth('FOREVER_VIP')) { setUploadImageLimitOpen(true); return; } imageSelectorModalRef.current?.setModalStatus('SELECT_MODAL'); }} > <IconFont type="micro-icon-add" className={cx('add_icon')} /> </div> )} </div> </div> <div className={cx('label')}> {!uploadImageUrl ? ( <span>{t('AiTools-SceneryLighting-Operate-StyleImageList-Rw5M')}</span> ) : ( <span>{t('AiTools-SceneryLighting-Operate-StyleImageList-QmcW')}</span> )} </div> </div> </div> </div> {/* 风格库选择弹窗 */} <ImageSelectorModal ref={imageSelectorModalRef} queryTagImages={{ categoryList: [ { name: t('AiTools-SceneryLighting-Operate-StyleImageList-XQd5'), tag: '夜景效果图', sourceType: 'JZXZ', }, ], iconClassName: cx('style_refer_image'), introduce: t('AiTools-SceneryLighting-Operate-StyleImageList-csA5'), modalTitle: t('AiTools-SceneryLighting-Operate-StyleImageList-csA5'), }} onFinish={(item) => { setUploadImageUrl(item.largeUrl); setSecondaryCategoryActiveIndex(currentSubCategories.length); }} /> {/* 风格图片列表限制弹窗 */} <Modal open={styleConfigLimit !== 'NONE'} width="fit-content" centered footer={null} onCancel={() => { setStyleConfigLimit('NONE'); }} > <div className={cx('modal_title')}>{t('AiTools-VideoRecordModal-kPTR')}</div> </Modal> {/* 参考图限制弹窗 */} <Modal open={uploadImageLimitOpen} width="fit-content" centered footer={null} onCancel={() => { setUploadImageLimitOpen(false); }} > <div className={cx('modal_title')}>{t('AiTools-VideoRecordModal-kPTR')}</div> <ProductAuth productTypes={['JZXZ']}> <p className={cx('notice_text')}> 由于算力影响,上传自定义参考图为专业/企业会员功能,请办理会员后再使用! </p> </ProductAuth> <ProductAuth productTypes={['JZDS']}> <p className={cx('notice_text')}> {t('AiTools-SceneryLighting-Operate-StyleImageList-8bTf')} </p> </ProductAuth> <ProductAuth productTypes={['JZXZ']}> <div className={cx('btn_group')}> <ThemeButton className={cx('btn')} ghost shape="circle" onClick={() => { setUploadImageLimitOpen(false); }} > 取消 </ThemeButton> <ThemeButton className={cx('btn', 'guide')} shape="circle" onClick={() => { PubSub.publish('onOpenVip', 'personal'); }} > 开通专业会员 </ThemeButton> </div> </ProductAuth> <ProductAuth productTypes={['JZDS']}> <div className={cx('btn_group')}> <ThemeButton className={cx('btn')} ghost onClick={() => { setUploadImageLimitOpen(false); }} > {t('ThemeModal-Ff4a')} </ThemeButton> <ThemeButton className={cx('btn')} onClick={() => { PubSub.publish('onOpenVip', 'personal'); }} > {t('VipTag-7Jdr')} </ThemeButton> </div> </ProductAuth> </Modal> </> ); }, ); export default SceneryLightingComfyStyleImageList; import { AiToolImageCoverCard } from '@aiComponents/CardItems'; import IconFont from '@components/Iconfont'; import ProductAuth from '@components/ProductAuth'; import ThemeButton from '@components/ThemeButton'; import { userDeclineAuth } from '@utils/auth.ts'; import { CoolImage } from '@wander/common-components'; import { Modal } from 'antd'; import classNames from 'classnames/bind'; import PubSub from 'pubsub-js'; import { forwardRef, useImperativeHandle } from 'react'; import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import type { ImageSelectorModalRef } from '@/components/ImageSelectorModal'; import ImageSelectorModal from '@/components/ImageSelectorModal'; import useGetConfigValue from '@/hooks/useGetConfigValue.ts'; import { useStore } from '@/store/createStore.ts'; import type { GeneratePriorityType } from '@/types/draw.ts'; import type { UserRoleType } from '@/types/user'; import styles from './index.module.less'; const cx = classNames.bind(styles); type ImageItem = { // 展示封面 coverUrl: string; // 参考图链接 referImageUrl: string; // 风格名称 name: string; // 可用用户类型re availableType?: UserRoleType; // 默认选中优先级 BALANCE 平衡 | PROMPTS 描述词优先 | CONTROLNET 模型优先 generatePriorityType: GeneratePriorityType; // 重绘幅度 denoisingStrength: number; // 效果模式底图权重 promptsBaseImageWeight: number; // 效果模式风格参考图权重 promptsStyleReferImageWeight: number; // 准确模式底图权重 controlNetBaseImageWeight: number; }; type SceneryLightingStyleImageListProps = { hidden?: boolean; }; export type SceneryLightingStyleImageListRef = { // 获取当前选中项 getCurrentSelectItem: () => ImageItem; }; const customerUploadConfig = { generatePriorityType: 'CONTROLNET' as const, denoisingStrength: 0.8, promptsBaseImageWeight: 1, promptsStyleReferImageWeight: 0.85, controlNetBaseImageWeight: 0.6, }; const SceneryLightingStyleImageList = forwardRef< SceneryLightingStyleImageListRef, SceneryLightingStyleImageListProps >((_props, ref) => { const { t } = useTranslation(); const { productType } = useStore((state) => state.productInfo); const { hidden = false } = _props; // 配置的图片列表 const imageList = useGetConfigValue<ImageItem[]>({ productKeys: { JZXZ: 'jzxz_scenery_lighting_category_config', JZDS: 'jzds_scenery_lighting_category_config', }, }) || []; const [activeIndex, setActiveIndex] = useState(0); // 当前用户上传的图片链接 const [uploadImageUrl, setUploadImageUrl] = useState(''); // 风格配置弹窗限制 const [styleConfigLimit, setStyleConfigLimit] = useState<UserRoleType | 'NONE'>('NONE'); // 上传参考图限制弹窗 const [uploadImageLimitOpen, setUploadImageLimitOpen] = useState(false); const limitTypeText: Record<string, string> = { COMMON_VIP: t('AiTools-SceneryLighting-Operate-StyleImageList-hw7d'), /* i18n-ignore */ FOREVER_VIP: productType === 'JZDS' ? t('AiTools-SceneryLighting-Operate-StyleImageList-hw7d') : '专业', /* i18n-ignore */ ORGANIZATION_VIP: '企业', }; // 风格参考图上传弹框ref const imageSelectorModalRef = useRef<ImageSelectorModalRef>(null); useImperativeHandle( ref, () => ({ getCurrentSelectItem: () => { if (activeIndex === imageList.length && uploadImageUrl) { return { ...customerUploadConfig, name: t('AiTools-SceneryLighting-Operate-StyleImageList-QmcW'), coverUrl: uploadImageUrl, referImageUrl: uploadImageUrl, }; } return imageList[activeIndex]; }, hidden, }), [activeIndex, imageList, t, uploadImageUrl, hidden], ); return ( <> <div className={cx('grid_wrap')} hidden={hidden}> {imageList.map((item, index) => ( <AiToolImageCoverCard key={index} coverUrl={item.coverUrl} active={index === activeIndex} availableType={item.availableType} label={item.name} onClick={() => { if (item.availableType && !userDeclineAuth(item.availableType)) { setStyleConfigLimit(item.availableType); return; } setActiveIndex(index); }} /> ))} <div className={cx('image_item', productType.toLowerCase(), { active: uploadImageUrl && activeIndex === imageList.length, })} > {/* .square_box > .inset_box用于等比例盒子构建 */} <div className={cx('square_box')}> <div className={cx('inset_box')}> {/* 图片上传后 显示用户上传的图片层级 */} {uploadImageUrl ? ( <div className={cx('cover_image_container')} onClick={() => { // 自定义上传的参考图选中索引 预设图片数量 setActiveIndex(imageList.length); }} > <CoolImage className={cx('cover_image')} src={uploadImageUrl} alt="" /> <div className={cx('extra_node')} hidden={!uploadImageUrl || activeIndex !== imageList.length} > <IconFont type="micro-icon-category-selected" className={cx('select_icon')} /> <IconFont type="micro-icon-retry" className={cx('re_upload_icon')} onClick={(event) => { event.stopPropagation(); imageSelectorModalRef.current?.setModalStatus('SELECT_MODAL'); }} /> </div> </div> ) : ( // 图片上传按钮 <div className={cx('upload_box', productType.toLowerCase())} onClick={() => { if (!userDeclineAuth('FOREVER_VIP')) { setUploadImageLimitOpen(true); return; } imageSelectorModalRef.current?.setModalStatus('SELECT_MODAL'); }} > <IconFont type="micro-icon-add" className={cx('add_icon')} /> </div> )} </div> </div> <div className={cx('label')}> {!uploadImageUrl ? ( <span>{t('AiTools-SceneryLighting-Operate-StyleImageList-Rw5M')}</span> ) : ( <span>{t('AiTools-SceneryLighting-Operate-StyleImageList-QmcW')}</span> )} </div> </div> </div> {/* 风格库选择弹窗 */} <ImageSelectorModal ref={imageSelectorModalRef} queryTagImages={{ categoryList: [ { name: t('AiTools-SceneryLighting-Operate-StyleImageList-XQd5'), tag: '夜景效果图', sourceType: 'JZXZ', }, ], iconClassName: cx('style_refer_image'), introduce: t('AiTools-SceneryLighting-Operate-StyleImageList-csA5'), modalTitle: t('AiTools-SceneryLighting-Operate-StyleImageList-csA5'), }} onFinish={(item) => { setUploadImageUrl(item.largeUrl); setActiveIndex(imageList.length); }} /> {/* 风格图片列表限制弹窗 */} <Modal open={styleConfigLimit !== 'NONE'} width="fit-content" centered footer={null} onCancel={() => { setStyleConfigLimit('NONE'); }} > <div className={cx('modal_title')}>{t('AiTools-VideoRecordModal-kPTR')}</div> <p className={cx('notice_text')}> {t('AiTools-SceneryLighting-Operate-StyleImageList-xyjh', { vipText: limitTypeText[styleConfigLimit], })} </p> <ProductAuth productTypes={['JZXZ']}> <div className={cx('btn_group')}> <ThemeButton className={cx('btn')} ghost shape="circle" onClick={() => { setStyleConfigLimit('NONE'); }} > 取消 </ThemeButton> <ThemeButton className={cx('btn', 'guide')} shape="circle" onClick={() => { PubSub.publish('onOpenVip', 'personal'); }} > 开通{limitTypeText[styleConfigLimit]}会员 </ThemeButton> </div> </ProductAuth> <ProductAuth productTypes={['JZDS']}> <div className={cx('btn_group')}> <ThemeButton className={cx('btn')} ghost onClick={() => { setStyleConfigLimit('NONE'); }} > {t('ThemeModal-Ff4a')} </ThemeButton> <ThemeButton className={cx('btn')} onClick={() => { PubSub.publish('onOpenVip', 'personal'); }} > {t('VipTag-7Jdr')} </ThemeButton> </div> </ProductAuth> </Modal> {/* 参考图限制弹窗 */} <Modal open={uploadImageLimitOpen} width="fit-content" centered footer={null} onCancel={() => { setUploadImageLimitOpen(false); }} > <div className={cx('modal_title')}>{t('AiTools-VideoRecordModal-kPTR')}</div> <ProductAuth productTypes={['JZXZ']}> <p className={cx('notice_text')}> 由于算力影响,上传自定义参考图为专业/企业会员功能,请办理会员后再使用! </p> </ProductAuth> <ProductAuth productTypes={['JZDS']}> <p className={cx('notice_text')}> {t('AiTools-SceneryLighting-Operate-StyleImageList-8bTf')} </p> </ProductAuth> <ProductAuth productTypes={['JZXZ']}> <div className={cx('btn_group')}> <ThemeButton className={cx('btn')} ghost shape="circle" onClick={() => { setUploadImageLimitOpen(false); }} > 取消 </ThemeButton> <ThemeButton className={cx('btn', 'guide')} shape="circle" onClick={() => { PubSub.publish('onOpenVip', 'personal'); }} > 开通专业会员 </ThemeButton> </div> </ProductAuth> <ProductAuth productTypes={['JZDS']}> <div className={cx('btn_group')}> <ThemeButton className={cx('btn')} ghost onClick={() => { setUploadImageLimitOpen(false); }} > {t('ThemeModal-Ff4a')} </ThemeButton> <ThemeButton className={cx('btn')} onClick={() => { PubSub.publish('onOpenVip', 'personal'); }} > {t('VipTag-7Jdr')} </ThemeButton> </div> </ProductAuth> </Modal> </> ); }); export default SceneryLightingStyleImageList; 以上两个组件的 <div className={cx('image_item', productType.toLowerCase(), { active: uploadImageUrl && activeIndex === imageList.length, })} > {/* .square_box > .inset_box用于等比例盒子构建 */} <div className={cx('square_box')}> <div className={cx('inset_box')}> {/* 图片上传后 显示用户上传的图片层级 */} {uploadImageUrl ? ( <div className={cx('cover_image_container')} onClick={onUploadImageSelect}> <CoolImage className={cx('cover_image')} src={uploadImageUrl} alt="" /> <div className={cx('extra_node')} hidden={!uploadImageUrl || activeIndex !== imageList.length} > <IconFont type="micro-icon-category-selected" className={cx('select_icon')} /> <IconFont type="micro-icon-retry" className={cx('re_upload_icon')} onClick={(event) => { event.stopPropagation(); onReUploadClick(); }} /> </div> </div> ) : ( // 图片上传按钮 <div className={cx('upload_box', productType.toLowerCase())} onClick={onUploadClick} > <IconFont type="micro-icon-add" className={cx('add_icon')} /> </div> )} </div> </div> <div className={cx('label')}> {!uploadImageUrl ? ( <span>{t('AiTools-SceneryLighting-Operate-StyleImageList-Rw5M')}</span> ) : ( <span>{t('AiTools-SceneryLighting-Operate-StyleImageList-QmcW')}</span> )} </div> </div> </div> </div> {/* 风格库选择弹窗 */} <ImageSelectorModal ref={imageSelectorModalRef} queryTagImages={{ categoryList: [ { name: t('AiTools-SceneryLighting-Operate-StyleImageList-XQd5'), tag: '夜景效果图', sourceType: 'JZXZ', }, ], iconClassName: cx('style_refer_image'), introduce: t('AiTools-SceneryLighting-Operate-StyleImageList-csA5'), modalTitle: t('AiTools-SceneryLighting-Operate-StyleImageList-csA5'), }} onFinish={(item) => { // setUploadImageUrl(item.largeUrl); // setActiveIndex(imageList.length); }} /> {/* 风格图片列表限制弹窗 */} <Modal open={styleConfigLimit !== 'NONE'} width="fit-content" centered footer={null} onCancel={() => { setStyleConfigLimit('NONE'); }} > <div className={cx('modal_title')}>{t('AiTools-VideoRecordModal-kPTR')}</div> </Modal> {/* 参考图限制弹窗 */} <Modal open={uploadImageLimitOpen} width="fit-content" centered footer={null} onCancel={() => { setUploadImageLimitOpen(false); }} > <div className={cx('modal_title')}>{t('AiTools-VideoRecordModal-kPTR')}</div> <ProductAuth productTypes={['JZXZ']}> <p className={cx('notice_text')}> 由于算力影响,上传自定义参考图为专业/企业会员功能,请办理会员后再使用! </p> </ProductAuth> <ProductAuth productTypes={['JZDS']}> <p className={cx('notice_text')}> {t('AiTools-SceneryLighting-Operate-StyleImageList-8bTf')} </p> </ProductAuth> <ProductAuth productTypes={['JZXZ']}> <div className={cx('btn_group')}> <ThemeButton className={cx('btn')} ghost shape="circle" onClick={() => { setUploadImageLimitOpen(false); }} > 取消 </ThemeButton> <ThemeButton className={cx('btn', 'guide')} shape="circle" onClick={() => { PubSub.publish('onOpenVip', 'personal'); }} > 开通专业会员 </ThemeButton> </div> </ProductAuth> <ProductAuth productTypes={['JZDS']}> <div className={cx('btn_group')}> <ThemeButton className={cx('btn')} ghost onClick={() => { setUploadImageLimitOpen(false); }} > {t('ThemeModal-Ff4a')} </ThemeButton> <ThemeButton className={cx('btn')} onClick={() => { PubSub.publish('onOpenVip', 'personal'); }} > {t('VipTag-7Jdr')} </ThemeButton> </div> </ProductAuth> </Modal>这一部分网格布局和modal弹框一起抽离出来封装成一个组件贡以上两个组件使用
最新发布
06-06
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值