一、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',
},
];
};