501 lines
15 KiB
TypeScript
Raw Normal View History

2025-06-17 19:07:13 +08:00
import React, { useState, useEffect, useMemo, useCallback, memo } from 'react';
import { bitable, CurrencyCode, FieldType, ICurrencyField, ICurrencyFieldMeta } from '@lark-base-open/js-sdk';
import { Card, Modal, Checkbox, message } from 'antd';
import './App.css';
// 选项类型定义
interface OptionGroupDef {
title: string;
options: string[];
required: boolean;
level?: number;
parentOption?: string;
condition?: (checkedList: string[]) => boolean;
resetOn?: string[];
}
// 选项分组配置
const OPTION_GROUPS: OptionGroupDef[] = [
{
title: '单据类型',
options: ['首单', '翻单'],
required: true,
level: 1
},
{
title: '是否要打板',
options: ['需要打板', '不需要打板'],
required: false,
level: 2,
parentOption: '首单',
condition: (checkedList) => checkedList.includes('首单'),
resetOn: ['翻单']
},
{
title: '翻单变动',
options: ['无变动不需要修改', '有变动需要修改'],
required: false,
level: 2,
parentOption: '翻单',
condition: (checkedList) => checkedList.includes('翻单'),
resetOn: ['首单']
},
{
title: '特殊订单',
options: ['换料寄面料样', '换料重新打板', '加色', '改尺寸不打版', '改尺寸重新打板'],
required: false,
level: 3,
parentOption: '有变动需要修改',
condition: (checkedList) => checkedList.includes('有变动需要修改'),
resetOn: ['首单', '无变动不需要修改']
},
{
title: '批色样',
options: ['要批色样', '不要批色样'],
required: true,
level: 4,
parentOption: '加色',
condition: (checkedList) => checkedList.includes('加色'),
resetOn: ['首单', '无变动不需要修改', '需要打板', '不需要打板']
},
{
title: '批色样',
options: ['要批色样', '不要批色样'],
required: true,
level: 3,
parentOption: '不需要打板',
condition: (checkedList) => checkedList.includes('首单') && checkedList.includes('不需要打板'),
resetOn: ['翻单', '有变动需要修改', '无变动不需要修改']
},
{
title: '品类',
options: ['牛仔', '时装'],
required: true
},
{
title: '复杂度',
options: ['简单款', '基础款', '复杂款'],
required: true
},
{
title: '二次工艺',
options: ['绣花', '印花'],
required: false
}
];
const FABRIC_TEST_OPTIONS = ['需要面料测试', '不需要面料测试'];
// 优化预计算选项集合避免重复includes操作
const createOptionSets = (checkedList: string[]) => {
const checkedSet = new Set(checkedList);
return {
checkedSet,
hasFirstOrder: checkedSet.has('首单'),
hasReorder: checkedSet.has('翻单'),
hasNoChange: checkedSet.has('无变动不需要修改'),
hasChange: checkedSet.has('有变动需要修改'),
hasAddColor: checkedSet.has('加色'),
hasNoPlate: checkedSet.has('不需要打板')
};
};
// 优化使用memo包装OptionGroup组件
interface OptionGroupProps {
group: OptionGroupDef;
checkedList: string[];
onChange: (newList: string[]) => void;
lockedOptions?: string[];
level?: number;
}
const OptionGroup = memo<OptionGroupProps>(({
group,
checkedList,
onChange,
lockedOptions = [],
level = 1
}) => {
const isMulti = group.title === '二次工艺';
// 优化使用useMemo缓存计算结果
const groupChecked = useMemo(() =>
checkedList.filter(v => group.options.includes(v)),
[checkedList, group.options]
);
const indentStyle = useMemo(() => ({
marginLeft: level > 1 ? (level - 1) * 24 : 0,
marginTop: level > 1 ? 8 : 0,
borderLeft: level > 1 ? `${level > 2 ? 'dashed' : 'solid'} 2px #eee` : 'none',
paddingLeft: level > 1 ? 12 : 0
}), [level]);
const titleColor = useMemo(() => {
switch(level) {
case 1: return '#000';
case 2: return '#888';
case 3: return '#b36d00';
default: return '#888';
}
}, [level]);
const options = useMemo(() =>
group.options.map(opt => ({
label: opt,
value: opt,
disabled: lockedOptions.includes(opt)
})),
[group.options, lockedOptions]
);
const handleChange = useCallback((list: string[]) => {
const others = checkedList.filter(v => !group.options.includes(v));
let newList;
if (isMulti) {
newList = [...others, ...list];
} else {
newList = [...others, list.slice(-1)[0]].filter(Boolean);
}
onChange(newList);
}, [checkedList, group.options, isMulti, onChange]);
return (
<div key={group.title} style={{ ...indentStyle, marginBottom: 12 }}>
<div style={{ fontWeight: 'bold', marginBottom: 4, color: titleColor }}>
{group.title}
{group.required && <span style={{ color: 'red', marginLeft: 4 }}>*</span>}
</div>
<Checkbox.Group
options={options}
value={groupChecked}
onChange={handleChange}
/>
</div>
);
});
// 优化使用memo包装OrderConfigSelector组件
interface OrderConfigSelectorProps {
selectedRecordId: string | null;
checkedList: string[];
setCheckedList: (list: string[]) => void;
onSubmit: (options: string[]) => void;
onCancel: () => void;
loading: boolean;
}
const OrderConfigSelector = memo<OrderConfigSelectorProps>(({
selectedRecordId,
checkedList,
setCheckedList,
onSubmit,
onCancel,
loading
}) => {
// 优化使用useMemo缓存可见的选项组
const visibleGroups = useMemo(() => {
return OPTION_GROUPS.filter(group => {
if (!group.condition) return true;
return group.condition(checkedList);
});
}, [checkedList]);
// 优化使用useCallback缓存事件处理函数
const handleCheckedListChange = useCallback((newList: string[]) => {
const addedOptions = newList.filter(opt => !checkedList.includes(opt));
if (addedOptions.length > 0) {
const resetGroups = OPTION_GROUPS.filter(group =>
group.resetOn?.some(resetOpt => addedOptions.includes(resetOpt))
);
if (resetGroups.length > 0) {
const resetGroupsSet = new Set(resetGroups);
const filteredOptions = newList.filter(opt => {
const group = OPTION_GROUPS.find(g => g.options.includes(opt));
return !resetGroupsSet.has(group as OptionGroupDef);
});
setCheckedList(filteredOptions);
return;
}
}
setCheckedList(newList);
}, [checkedList, setCheckedList]);
// 优化使用useCallback缓存验证函数
const validateRequired = useCallback(() => {
for (const group of OPTION_GROUPS) {
if (group.required && group.condition?.(checkedList) !== false) {
const has = checkedList.some(v => group.options.includes(v));
if (!has) {
message.error(`请至少选择一项【${group.title}`);
return false;
}
}
}
// 特殊验证:如果选择了"不需要打板",必须选择是否批色样
if (checkedList.includes('首单') && checkedList.includes('不需要打板')) {
const hasColorSample = checkedList.some(v => v === '要批色样' || v === '不要批色样');
if (!hasColorSample) {
message.error('请选择是否要批色样');
return false;
}
}
return true;
}, [checkedList]);
const needFabricTestDialog = useCallback(() => {
return !(checkedList.includes('翻单') && checkedList.includes('无变动不需要修改'));
}, [checkedList]);
const [showFabricTestModal, setShowFabricTestModal] = useState(false);
const [fabricTestSelection, setFabricTestSelection] = useState<string[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [finalOptions, setFinalOptions] = useState<string[]>([]);
const handleSubmit = useCallback(() => {
if (!validateRequired()) return;
setFinalOptions([...checkedList]);
if (needFabricTestDialog()) {
setFabricTestSelection([]);
setShowFabricTestModal(true);
} else {
onSubmit([...checkedList]);
}
}, [validateRequired, checkedList, needFabricTestDialog, onSubmit]);
const handleFabricTestSubmit = useCallback(() => {
if (fabricTestSelection.length === 0) {
message.error('请选择是否需要面料测试');
return;
}
setIsSubmitting(true);
onSubmit([...finalOptions, ...fabricTestSelection]);
setShowFabricTestModal(false);
}, [fabricTestSelection, finalOptions, onSubmit]);
const handleCancel = useCallback(() => {
onCancel();
setCheckedList([]);
}, [onCancel, setCheckedList]);
const handleFabricTestChange = useCallback((values: string[]) => {
if (values.length > 0) {
setFabricTestSelection([values[values.length - 1]]);
} else {
setFabricTestSelection([]);
}
}, []);
useEffect(() => {
if (!showFabricTestModal && isSubmitting) {
setIsSubmitting(false);
}
}, [showFabricTestModal, isSubmitting]);
return (
<>
<Modal
title="订单配置"
open={!!selectedRecordId}
onOk={handleSubmit}
onCancel={handleCancel}
okText="确定"
cancelText="取消"
confirmLoading={loading}
>
{visibleGroups.map(group => (
<OptionGroup
key={group.title}
group={group}
checkedList={checkedList}
onChange={handleCheckedListChange}
level={group.level}
/>
))}
</Modal>
<Modal
title="面料测试"
open={showFabricTestModal}
onOk={handleFabricTestSubmit}
onCancel={() => setShowFabricTestModal(false)}
okText="确定"
cancelText="取消"
confirmLoading={isSubmitting}
>
<div>
2025-06-17 19:35:59 +08:00
<p>:</p>
2025-06-17 19:07:13 +08:00
<Checkbox.Group
options={FABRIC_TEST_OPTIONS}
value={fabricTestSelection}
onChange={handleFabricTestChange}
/>
</div>
</Modal>
</>
);
});
// 优化:主应用组件
const App: React.FC = () => {
const [selectedRecordId, setSelectedRecordId] = useState<string | null>(null);
const [checkedList, setCheckedList] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [currentSelection, setCurrentSelection] = useState<{
tableId: string | null;
recordId: string | null;
}>({ tableId: null, recordId: null });
// 优化:缓存字段选项映射,避免重复查找
const [fieldOptionsMap, setFieldOptionsMap] = useState<Map<string, {id: string, name: string}>>(new Map());
// 优化使用useCallback缓存函数
const resetOptions = useCallback(() => {
setSelectedRecordId(null);
setCheckedList([]);
}, []);
const saveOptions = useCallback(async () => {
if (!selectedRecordId || !currentSelection.tableId) {
message.warning('没有有效的选中记录或表格信息');
return;
}
setLoading(true);
const FIELD_ID_TO_SAVE = 'fldTtRHwlo';
try {
const table = await bitable.base.getTableById(currentSelection.tableId);
if (!table) {
message.error('无法获取表格对象');
return;
}
const field = await table.getFieldById(FIELD_ID_TO_SAVE);
if (!field) {
message.error('无法获取字段对象');
return;
}
// 优化:只在字段选项映射为空时才重新获取
let optionsMap = fieldOptionsMap;
if (optionsMap.size === 0) {
const fieldMeta = await field.getMeta();
console.log('字段元数据:', fieldMeta);
const fieldOptions = fieldMeta.property?.options || [];
optionsMap = new Map(fieldOptions.map(opt => [opt.name, opt]));
setFieldOptionsMap(optionsMap);
}
let dataToSave = null;
if (checkedList.length > 0) {
console.log('字段中的所有选项:', Array.from(optionsMap.keys()));
console.log('要保存的选项:', checkedList);
// 优化使用Map查找O(1)时间复杂度
dataToSave = checkedList.map(optionText => {
const matchedOption = optionsMap.get(optionText);
if (!matchedOption) {
console.warn(`未找到选项 "${optionText}" 对应的ID`);
console.log('可能的匹配选项:', Array.from(optionsMap.keys()).filter(key =>
key.includes(optionText) || optionText.includes(key)
));
return null;
}
console.log(`找到匹配: "${optionText}" -> ID: ${matchedOption.id}`);
return {
id: matchedOption.id,
text: matchedOption.name
};
}).filter(Boolean);
}
console.log('准备保存的数据:', dataToSave);
await field.setValue(selectedRecordId, dataToSave);
const savedValue = await field.getValue(selectedRecordId);
console.log('保存后的字段值:', savedValue);
if (savedValue) {
console.log('数据成功写入单元格');
message.success('已保存选项');
} else {
throw new Error('数据写入后未能读取到值');
}
} catch (e: any) {
console.error('保存失败:', e);
message.error(`保存失败: ${e.message || '未知错误'}`);
} finally {
setLoading(false);
resetOptions();
setCurrentSelection({ tableId: null, recordId: null });
}
}, [selectedRecordId, currentSelection.tableId, checkedList, fieldOptionsMap, resetOptions]);
useEffect(() => {
const unsubscribe = bitable.base.onSelectionChange(async (event: any) => {
try {
const { data } = event;
if (data && data.tableId && data.recordId) {
const isNewCell = (
data.tableId !== currentSelection.tableId ||
data.recordId !== currentSelection.recordId
);
if (isNewCell) {
resetOptions();
setCurrentSelection({
tableId: data.tableId,
recordId: data.recordId
});
// 优化:切换单元格时清空字段选项缓存
setFieldOptionsMap(new Map());
}
const table = await bitable.base.getTableById(data.tableId);
const cellValue = await table.getCellValue('fldTtRHwlo', data.recordId);
setSelectedRecordId(data.recordId);
if (typeof cellValue === 'string' && cellValue.trim() !== '') {
setCheckedList(cellValue.split(',').map(item => item.trim()));
} else if (Array.isArray(cellValue)) {
setCheckedList(cellValue.map(item => String(item)));
} else {
setCheckedList([]);
}
}
} catch (e: any) {
message.error('获取选中记录失败: ' + (e.message || e));
}
});
return unsubscribe;
}, [currentSelection, resetOptions]);
return (
<div style={{ padding: '16px' }}>
<Card>
<div></div>
</Card>
<OrderConfigSelector
selectedRecordId={selectedRecordId}
checkedList={checkedList}
setCheckedList={setCheckedList}
onSubmit={saveOptions}
onCancel={resetOptions}
loading={loading}
/>
</div>
);
};
export default App;