Bug问题

发布于:2025-06-09 ⋅ 阅读:(20) ⋅ 点赞:(0)

一、list 页面

import React, { useEffect, useState } from 'react';
import { shallowEqual, useHistory, useSelector } from 'dva';
import { Button, message } from 'choerodon-ui/pro';
import formatterCollections from 'hzero-front/lib/utils/intl/formatterCollections';
import {
  commonModelPrompt,
  languageConfig,
  prdTemCode,
} from '@/language/language';
import { ButtonColor } from 'choerodon-ui/pro/lib/button/enum';
import {
  getPortalConfig,
  getPortalSyncConfig,
  postPortalConfig,
} from '@/api/portalConfig/main';
import { pubPath } from '@/utils/utils';
import { useDefaultPage } from '@ino/ltc-component-paas';
import { DefaultPageMode } from '@ino/ltc-component-paas/lib/component/defaultPage/enum';
import Tabs from '@/components/Tabs/index';
import { TabItem, TableData } from '@/interface/portalConfig/main';
import { queryMapIdpValue } from 'services/api';
import { renderSync } from '@/pages/portalConfig/list/hook';
import { dataSourceTabs, languageTabs } from '@/pages/portalConfig/list';
import {
  mergeConfigData,
  portalConfigAddComponentLevelAndSort,
} from '@/utils/portalConfig/main';
import Banner from '../components/Banner/main';
import Resources from '../components/Resources/main';
import FooterBanner from '../components/FooterBanner/main';
import { loadingModel } from './hook';
import '@/assets/styles/c7n.less';
import styles from './main.less';
import { configDefault } from './store';

const PortanConfig = () => {
  const history = useHistory();

  /** 定义提交成功页面 */
  const openDefaultSuccess = useDefaultPage({
    mode: DefaultPageMode.handleSuccess,
    renderDom: {
      current: document.getElementsByClassName(
        'hzero-common-layout-content',
      )[0],
    },
    isClose: true,
    closeWait: 3,
    onClose: () => {
      fetchConfig();
    },
  });

  /** 跳转到:没有权限页面 */
  const openNoPermissions = useDefaultPage({
    mode: DefaultPageMode.noPermissions,
    renderDom: {
      current: document.getElementsByClassName(
        'hzero-common-layout-content',
      )[0],
    },
    isClose: true,
    closeWait: 3,
    onClose: () => {
      fetchConfig();
    },
  });

  /** 系统错误 */
  const openDefault = useDefaultPage({
    mode: DefaultPageMode.sysError,
    renderDom: {
      current: document.getElementsByClassName(
        'hzero-common-layout-content',
      )[0],
    },
    isClose: true,
    closeWait: 3,
    onClose: () => {
      fetchConfig();
    },
  });

  const [loading, setLoading] = useState(true); // 初始状态设为true,确保开始时显示loading
  const [isDataLoaded, setIsDataLoaded] = useState(false); // 新增状态,用于判断数据是否已加载

  const [tabKey, setTabKey] = useState<string>('zh_CN'); // 语言模式
  const [dataSource, setDataSource] = useState<string>('pc'); // 数据源(PC/移动)
  const [operation, setOperation] = useState<boolean>(false); // 操作模式
  const [rule, setRule] = useState([]); // 添加规则上限
  const [configData, setConfigData] = useState<TableData[]>(configDefault); // 配置数据

  /** 当前tab页 */
  const activeTabKey = useSelector(
    (state: any) => state?.global?.activeTabKey,
    shallowEqual,
  );

  /** 获取配置 */
  const fetchConfig = async (sourse?: string) => {
    setLoading(true);

    try {
      const res = await getPortalConfig({
        language: tabKey,
        dataSource: sourse ? sourse : dataSource,
        componentPlate: 'DEVELOPER',
      });

      if (res.failed) {
        message.error(
          languageConfig('tips.fetchError', '获取配置失败'),
          1.5,
          'top',
        );
        setIsDataLoaded(false);
        res?.code === '401' ? openNoPermissions.open() : openDefault.open();
        return;
      }

      // console.log('获取配置', dataSource, res);
      // console.log('原始数据', configData);

      // 合并数据
      const result = mergeConfigData(configDefault, res || []);

      setConfigData(result);
      setIsDataLoaded(true);
    } catch (error) {
      message.error(
        languageConfig('tips.fetchError', '获取配置失败'),
        1.5,
        'top',
      );
      setIsDataLoaded(false);
    } finally {
      setLoading(false);
    }
  };

  /** 移动端:一键同步 */
  const handleSync = async () => {
    setLoading(true);
    const res = await getPortalSyncConfig({
      language: tabKey,
      componentPlate: 'DEVELOPER',
      componentSys: 'app',
    });
    if (res.failed) {
      message.error(
        languageConfig('tips.syncFetchDataError', '同步失败'),
        1.5,
        'top',
      );
      setIsDataLoaded(false);
      res?.code === '401' ? openNoPermissions.open() : openDefault.open();
    } else {
      setConfigData(res || []);
      setIsDataLoaded(true);
    }
    setLoading(false);
  };

  /** tabs 切换 */
  const handleTabChange = (selectedTab: TabItem) => {
    // console.log('当前选中的标签数据:', selectedTab);
    setTabKey(selectedTab.key);
    setDataSource('pc');
    setLoading(true); // 切换 tabs 时设置 loading 为 true
    setOperation(false); // 切换 tabs 时设置 operation 为 false
  };

  /** 数据源: 移动端、PC端切换 */
  const handleTabTypeChange = () => {
    setDataSource(dataSource === 'pc' ? 'app' : 'pc');
    setOperation(false); // 切换 tabs 时设置 operation 为 false
  };

  /** 提交 */
  const handleSubmit = async () => {
    // 1、componentLevel 和 componentSort
    const submitParams = portalConfigAddComponentLevelAndSort(configData);
    console.log('submitParams', submitParams);

    // 2、提交数据
    setLoading(true);
    const params = {
      componentSys: dataSource,
      containerComponentList: submitParams,
    };
    const res = await postPortalConfig(tabKey, params);
    setLoading(false);
    if (res.failed) {
      message.warning(res.message, undefined, undefined, 'top');
      return;
    }
    message.success(languageConfig('tips.success', '保存成功'), 1.5, 'top');

    // 3、跳转到成功页面
    setOperation(false); // 操作状态关闭
    openDefaultSuccess.open(); // 跳转到成功页面
  };

  /** 回调 */
  const handleAction = (val: TableData[]) => {
    // console.log('这里是回调', val);
    setConfigData(val);
  };

  useEffect(() => {
    if (activeTabKey) {
      fetchConfig();
    }
  }, [activeTabKey, tabKey, dataSource]);

  // 获取数量上限
  useEffect(() => {
    const fetchRule = async () => {
      const res = await queryMapIdpValue(['INO_TAI_HOME_PAGE_MODULE']);
      // console.log('rule', res[0]);
      setRule(res?.[0] || []);
    };
    fetchRule();
  }, []);

  return (
    <>
      {loading && !isDataLoaded && loadingModel()}

      <div
        className="ltc-c7n-style"
        style={{
          overflow: 'auto',
          height: '100%',
        }}
      >
        <div className={styles.portalConfig}>
          <div className={styles.portalConfig_content}>
            {/* Tabs:切换中英文 */}
            <div className={styles.portalConfig_content_tabs}>
              <Tabs tabs={languageTabs} onTabChange={handleTabChange} />
              <div
                className={styles.portalConfig_content_tabs_preview}
                onClick={() => history.push(`${pubPath}/tal/preview/${tabKey}`)}
              >
                <img
                  src={require('@/assets/imgs/portalConfig/icon_preview.png')}
                  alt={'icon_preview'}
                />
                {languageConfig('btn.preview', '预览')}
              </div>
            </div>

            {/* 配置项:内容 */}
            <div className={styles.portalConfig_content_config}>
              {/* tabs: PC/移动端配置 */}
              <div className={styles.portalConfig_content_config_typeTabs}>
                <Tabs
                  type="card"
                  tabs={dataSourceTabs}
                  activeKey={dataSource}
                  onTabChange={handleTabTypeChange}
                />
                {renderSync(dataSource, operation, () => handleSync())}
              </div>

              <Banner
                operation={operation}
                configData={configData}
                ruleData={rule}
                dataSource={dataSource}
                onAction={val => handleAction(val)}
              />
              {/* <Resources
                operation={operation}
                configData={configData}
                ruleData={rule}
                dataSource={dataSource}
                onAction={val => handleAction(val)}
              />
              <FooterBanner
                operation={operation}
                configData={configData}
                ruleData={rule}
                dataSource={dataSource}
                onAction={val => handleAction(val)}
              /> */}
            </div>
          </div>

          {/* 底部:提交按钮 */}
          <div className={styles.portalConfig_operation}>
            {operation ? (
              <>
                <Button color={ButtonColor.primary} onClick={handleSubmit}>
                  {languageConfig('btn.submit', '提交')}
                </Button>
                <Button
                  color={ButtonColor.default}
                  onClick={() => {
                    setOperation(!operation);
                    fetchConfig();
                  }}
                >
                  {languageConfig('btn.cancel', '取消')}
                </Button>
              </>
            ) : (
              <Button
                color={ButtonColor.primary}
                onClick={() => setOperation(!operation)}
              >
                {languageConfig('btn.edit', '编辑')}
              </Button>
            )}
          </div>
        </div>
      </div>
    </>
  );
};

export default formatterCollections({
  code: [prdTemCode, commonModelPrompt],
})(PortanConfig);

二、Banner

import React, { useMemo, useState } from 'react';
import {
  languageConfig,
  PICTURE_FORMAT,
  PICTURE_MAX_SIZE,
} from '@/language/language';
import Title from '@/components/Title';
import { ModuleCreateProps, TableData } from '@/interface/portalConfig/main';
import {
  portalConfigFetchMaxLimit,
  portanConfigUpdateModuleData,
  replaceConfigData,
} from '@/utils/portalConfig/main';
import styles from '../../list/main.less';
import { renderRuleLimit, renderTitle } from '../../list/hook';
import { findZoneSize } from '../../list/store';
import Create from './create/main';
import View from './create/view';

const Banner = (props: ModuleCreateProps) => {
  const { operation, configData, ruleData, dataSource, onAction } = props;

  const [show, setShow] = useState<boolean>(true); // 管理收缩状态

  /** 获取配置数据 */
  const bannerList = useMemo(() => {
    // 1、找到这个组件数据
    const bannerModule = configData.find(
      item => item.componentModule === 'developer.bannerConfig',
    );

    if (!bannerModule?.childrenList) return [];

    // 2、获取组件下'配置项'数据
    const bannerChild = bannerModule.childrenList.find(
      child => child.componentModule === 'developer.bannerConfig.banner',
    );

    return bannerChild?.childrenList || [];
  }, [configData]);

  /** 回调:用于接收子页面传递过来的数据 */
  const handleChildDataUpdate = (newData: TableData[]) => {
    // console.log('bannerl回调', newData);
    // console.log('configData', configData);

    // 1、给模块添加'提示'标头
    const resultData = newData?.map((item, index) => ({
      ...item,
      componentName: `${languageConfig(
        'developer.banner.label.bannerImage',
        'banner图片',
      )}${index + 1}`,
    }));

    // 2、更新:从父组件'configData'中获取的本模块数据
    const bannerModule = configData.find(
      item => item.componentModule === 'developer.bannerConfig',
    );
    const updatedTableData = portanConfigUpdateModuleData(
      bannerModule,
      resultData,
      'developer.bannerConfig.banner',
    );
    // console.log('更新后的 tableData:', updatedTableData);

    // 2、合并:更新过后的'本模块'和父组件中配置数据进行合并
    const result = replaceConfigData(configData, updatedTableData);
    onAction(result);
  };

  return (
    <div className={styles.portalConfig_banner}>
      <Title
        title={languageConfig(
          'developer.banner.label.bannerConfig',
          'banner配置',
        )}
        desc={
          <div>
            <img
              src={require('@/assets/imgs/portalConfig/demo_banner.png')}
              alt={'banner示例'}
            />
          </div>
        }
        isExpanded={show}
        onToggle={() => setShow(!show)}
      />

      {show && (
        <div className={styles.portalConfig_card}>
          {/* 左侧:标题、规则限制 */}
          <div className={styles.portalConfig_card_left}>
            {renderTitle(
              languageConfig(
                'developer.banner.label.bannerImage',
                'banner图片',
              ),
            )}
            {renderRuleLimit(ruleData, 'developer.bannerConfig.banner')}
          </div>
          {/* 右侧 */}
          <div className={styles.portalConfig_card_right}>
            {operation ? (
              <Create
                list={bannerList}
                maxNum={portalConfigFetchMaxLimit(
                  ruleData,
                  'developer.bannerConfig.banner',
                )}
                tips={
                  <>
                    {PICTURE_FORMAT}
                    <br />
                    {findZoneSize('developer.bannerConfig', dataSource)}
                    <br />
                    {PICTURE_MAX_SIZE}
                    <br />
                  </>
                }
                onSelect={handleChildDataUpdate}
              />
            ) : (
              <View detail={bannerList} />
            )}
          </div>
        </div>
      )}
    </div>
  );
};

export default Banner;

三、create

import React, { useCallback } from 'react';
import {
  Button,
  Form,
  Icon,
  useDataSet,
  message,
  Attachment,
  TextField,
} from 'choerodon-ui/pro';
import { onBeforeUpload } from '@/utils/utils';
import { languageConfig } from '@/language/language';
import { LabelLayout } from 'choerodon-ui/pro/lib/form/enum';
import { FuncType } from 'choerodon-ui/pro/lib/button/enum';
import { Record } from 'choerodon-ui/dataset';
import '@/assets/styles/c7n.less';
import styles from '../../../list/main.less';
import { tableFields } from './store';

interface CreateProps {
  list: any[];
  maxNum: string | number; // 添加最大限制
  tips: React.ReactNode; // 提示文案
  onSelect: (val: any) => void; // 回调
}

const BannerCreate = (props: CreateProps) => {
  const { list = [], maxNum, tips = '', onSelect } = props;

  // ds
  const dataDs = useDataSet(() => {
    return {
      autoCreate: true,
      fields: tableFields(),
      data: list.map(item => new Record(item)),
      events: {
        update: () => {
          onSelect?.(dataDs.toData());
        },
      },
    };
  }, [list, onSelect]);

  /** 上传状态变化的处理 */
  const onUploadSuccess = (index, file) => {
    // console.log('上传状态变化的处理:info', index, file);
    if (file.fileUrl) {
      // 图片url添加
      dataDs?.get(index)?.set('componentPicture', file.fileUrl);
    }
  };

  /** 新增 */
  const handleAdd = async () => {
    // 1、限制上限,如里maxNumber 为'-' 表示不限制上限
    if (maxNum !== '-' && dataDs.length >= Number(maxNum)) {
      message.error(
        `${languageConfig(
          'uploadBanner.label.menuMaxNumPleaseDeleteRetry',
          'banner图片超出最大限制,请删除后重试:',
        )}${languageConfig(
          'uploadBanner.label.maxLength',
          '最多支持',
        )}${maxNum}${languageConfig('uploadBanner.label.unit', '条')}`,
        1.5,
        'top',
      );
      return;
    }

    // 3、创建新记录
    const newRecordData = {
      componentModule: 'developer.bannerConfig.banner.item',
    };
    // console.log('newRecordData', newRecordData);
    dataDs.push(new Record(newRecordData, dataDs));
    // console.log('karla', dataDs.toData());

    onSelect?.(dataDs.toData());
  };

  /** 删除 */
  const handleDelete = useCallback(
    async (index: number) => {
      try {
        await dataDs.delete(dataDs.get(index), false);
        // 手动触发 onSelect,确保父组件收到更新
        if (typeof onSelect === 'function') {
          onSelect(dataDs.toData());
        }
      } catch (error) {
        console.error('Delete failed:', error);
        message.error('删除记录时发生错误,请重试');
      }
    },
    [dataDs, onSelect],
  );

  return (
    <div className="ltc-c7n-style">
      {/* Add */}
      <div className={styles.portalConfig_card_right_add} onClick={handleAdd}>
        <Icon type="add" />
        {languageConfig(
          'developer.banner.label.bannerImageAdd',
          '添加banner图片',
        )}
      </div>

      {/* Form */}
      {dataDs.map((item: any, index: number) => {
        return (
          <div key={item} className={styles.portalConfig_card_right_item}>
            {/* 标识 */}
            <div className={styles.portalConfig_card_right_item_label}>
              {languageConfig(
                'developer.banner.label.bannerImage',
                'banner图片',
              )}
              {index + 1}
            </div>

            <Form
              columns={1}
              labelLayout={LabelLayout.none}
              record={dataDs.get(index)}
              style={{ flex: 1 }}
            >
              <Form.Item>
                <div
                  style={{
                    display: 'flex',
                    gap: '16px',
                    background: '#F5F5F5',
                  }}
                >
                  {/* 图片 */}
                  <div className={styles.portalConfig_upload}>
                    <Attachment
                      name="remark"
                      labelLayout={'float'}
                      listType="picture-card"
                      max={1}
                      beforeUpload={onBeforeUpload}
                      onUploadSuccess={file => onUploadSuccess(index, file)}
                      onRemove={() => {
                        dataDs.get(index)?.set('componentPicture', '');
                        dataDs.get(index)?.set('remark', '');
                      }}
                    />
                  </div>

                  {/* Tips */}
                  <div className={styles.portalConfig_tips}>{tips}</div>

                  {/* 删除:大于1条时显示  */}
                  <div
                    style={{
                      marginTop: '40px',
                    }}
                  >
                    {dataDs.toData()?.length > 1 && (
                      <Button
                        funcType={FuncType.link}
                        onClick={() => {
                          handleDelete(index);
                        }}
                        className={styles.uploadConfig_card_right_delete}
                      >
                        {languageConfig('btn.remove', '移除')}
                      </Button>
                    )}
                  </div>
                </div>
              </Form.Item>

              <Form.Item>
                <div
                  style={{
                    display: 'flex',
                    gap: '8px',
                  }}
                >
                  {/* 网页链接 */}
                  <div>
                    <TextField
                      name="componentLink"
                      clearButton
                      style={{ width: '652px' }}
                    />
                  </div>
                </div>
              </Form.Item>
            </Form>
          </div>
        );
      })}
    </div>
  );
};

export default BannerCreate;

store.js

import { languageConfig } from '@/language/language';
import { bucketInfo } from '@/utils/utils';
import { FieldType } from 'choerodon-ui/dataset/data-set/enum';

/** ds table */
export const tableFields = () => {
  return [
    {
      name: 'remark',
      type: FieldType.string,
      // bucketName: 'inovance-tai-pub-test',
      // bucketDirectory: '/portalConfig',
      // storageCode: 'INOTAL',
      bucketName: bucketInfo.bucketName,
      bucketDirectory: bucketInfo.bucketDirectory,
      storageCode: bucketInfo.storageCode,
      defaultValue: '',
    },
    {
      name: 'componentLink',
      type: FieldType.string,
      label: languageConfig('portalConfig.menu.label.link', '网页链接'),
      placeholder: languageConfig(
        'portalConfig.menu.placeholder.pleaseInputLink',
        '请输入网页链接',
      ),
      defaultValue: '',
    },
    {
      name: 'componentModule',
      type: FieldType.string,
      defaultValue: 'developer.bannerConfig.banner.item',
    },
  ];
};