206 lines
5.9 KiB
TypeScript
206 lines
5.9 KiB
TypeScript
import React, { useState } from 'react';
|
||
import { Space, Button, Segmented, Badge, message, Modal, Table, Tag } from 'antd';
|
||
import { apiPost, getExportUrl } from '../api/client';
|
||
import type { SaveChangeDto, BomItemDto, MatchMaterialRequest, MatchMaterialResponse, MatchMaterialResultDto } from '../api/types';
|
||
|
||
interface ToolBarProps {
|
||
dirtyCount: number;
|
||
viewMode: 'hierarchical' | 'flat';
|
||
onViewModeChange: (mode: 'hierarchical' | 'flat') => void;
|
||
onRefresh: () => void;
|
||
onOpenConfig: () => void;
|
||
getDirtyChanges: () => SaveChangeDto[];
|
||
onSaveSuccess: (changes: SaveChangeDto[]) => void;
|
||
items: BomItemDto[];
|
||
}
|
||
|
||
export const ToolBar: React.FC<ToolBarProps> = ({
|
||
dirtyCount,
|
||
viewMode,
|
||
onViewModeChange,
|
||
onRefresh,
|
||
onOpenConfig,
|
||
getDirtyChanges,
|
||
onSaveSuccess,
|
||
items,
|
||
}) => {
|
||
const [saving, setSaving] = useState(false);
|
||
const [matching, setMatching] = useState(false);
|
||
const [matchResults, setMatchResults] = useState<MatchMaterialResultDto[]>([]);
|
||
const [matchModalVisible, setMatchModalVisible] = useState(false);
|
||
const [matchSaving, setMatchSaving] = useState(false);
|
||
|
||
const handleSave = async () => {
|
||
if (dirtyCount === 0) return;
|
||
setSaving(true);
|
||
try {
|
||
const changes = getDirtyChanges();
|
||
const res = await apiPost<any>('/api/bom/save', { Changes: changes });
|
||
if (res.Success) {
|
||
message.success(`保存成功,共更新 ${res.SavedCount} 项`);
|
||
onSaveSuccess(changes);
|
||
} else {
|
||
message.error(`保存失败: ${res.Error}`);
|
||
}
|
||
} catch (e: any) {
|
||
message.error(`保存失败: ${e.message}`);
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleExport = () => {
|
||
window.open(getExportUrl(viewMode === 'flat'), '_blank');
|
||
};
|
||
|
||
const handleMatchMaterial = async () => {
|
||
const emptyItems = items.filter(
|
||
(item) => !item.IsAssembly && !item.Props?.['物料编码']
|
||
);
|
||
|
||
if (emptyItems.length === 0) {
|
||
message.info('所有零件的物料编码均已填写');
|
||
return;
|
||
}
|
||
|
||
setMatching(true);
|
||
try {
|
||
const request: MatchMaterialRequest = {
|
||
Items: emptyItems.map((item) => ({
|
||
DrawingNo: item.DrawingNo,
|
||
DocPath: item.DocPath,
|
||
})),
|
||
};
|
||
|
||
const res = await apiPost<MatchMaterialResponse>('/api/bom/match-material', request);
|
||
if (!res.Success) {
|
||
message.error(`匹配失败: ${res.Error}`);
|
||
return;
|
||
}
|
||
|
||
const matched = res.Results.filter((r) => r.Matched);
|
||
if (matched.length === 0) {
|
||
message.info('未找到匹配的物料编码');
|
||
return;
|
||
}
|
||
|
||
setMatchResults(matched);
|
||
setMatchModalVisible(true);
|
||
} catch (e: any) {
|
||
message.error(`匹配失败: ${e.message}`);
|
||
} finally {
|
||
setMatching(false);
|
||
}
|
||
};
|
||
|
||
const handleConfirmMatch = async () => {
|
||
setMatchSaving(true);
|
||
try {
|
||
const changes: SaveChangeDto[] = matchResults.map((r) => ({
|
||
DocPath: r.DocPath,
|
||
Key: '物料编码',
|
||
Value: r.PartCode,
|
||
}));
|
||
|
||
const res = await apiPost<any>('/api/bom/save', { Changes: changes });
|
||
if (res.Success) {
|
||
message.success(`写入成功,共更新 ${res.Results?.length ?? changes.length} 项物料编码`);
|
||
onSaveSuccess(changes);
|
||
setMatchModalVisible(false);
|
||
setMatchResults([]);
|
||
} else {
|
||
message.error(`写入失败: ${res.Error}`);
|
||
}
|
||
} catch (e: any) {
|
||
message.error(`写入失败: ${e.message}`);
|
||
} finally {
|
||
setMatchSaving(false);
|
||
}
|
||
};
|
||
|
||
const matchColumns = [
|
||
{
|
||
title: '图号',
|
||
dataIndex: 'DrawingNo',
|
||
key: 'DrawingNo',
|
||
width: 260,
|
||
},
|
||
{
|
||
title: '物料编码',
|
||
dataIndex: 'PartCode',
|
||
key: 'PartCode',
|
||
width: 180,
|
||
},
|
||
{
|
||
title: 'PLM零件名称',
|
||
dataIndex: 'PartName',
|
||
key: 'PartName',
|
||
width: 180,
|
||
},
|
||
{
|
||
title: '状态',
|
||
key: 'status',
|
||
width: 80,
|
||
render: (_: unknown, record: MatchMaterialResultDto) =>
|
||
record.Matched ? <Tag color="green">已匹配</Tag> : <Tag color="red">未匹配</Tag>,
|
||
},
|
||
];
|
||
|
||
return (
|
||
<>
|
||
<Space style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', width: '100%' }}>
|
||
<Space>
|
||
<Badge count={dirtyCount}>
|
||
<Button type="primary" onClick={handleSave} disabled={dirtyCount === 0} loading={saving}>
|
||
保存
|
||
</Button>
|
||
</Badge>
|
||
<Button onClick={handleExport}>导出</Button>
|
||
<Button onClick={onRefresh}>刷新</Button>
|
||
<Button onClick={onOpenConfig}>列配置</Button>
|
||
<Button onClick={handleMatchMaterial} loading={matching}>
|
||
匹配物料编号
|
||
</Button>
|
||
</Space>
|
||
<Space>
|
||
<Segmented
|
||
options={[
|
||
{ label: '层级视图', value: 'hierarchical' },
|
||
{ label: '扁平视图', value: 'flat' },
|
||
]}
|
||
value={viewMode}
|
||
onChange={(val) => onViewModeChange(val as 'hierarchical' | 'flat')}
|
||
/>
|
||
<div style={{ marginLeft: 16, color: '#52c41a' }}>● 已连接</div>
|
||
</Space>
|
||
</Space>
|
||
|
||
<Modal
|
||
title={`匹配物料编号 (${matchResults.length} 项匹配)`}
|
||
open={matchModalVisible}
|
||
onOk={handleConfirmMatch}
|
||
onCancel={() => {
|
||
setMatchModalVisible(false);
|
||
setMatchResults([]);
|
||
}}
|
||
okText="确认写入"
|
||
cancelText="取消"
|
||
confirmLoading={matchSaving}
|
||
width={760}
|
||
>
|
||
<p style={{ marginBottom: 12, color: '#666' }}>
|
||
以下零件已匹配到物料编码,确认后将写入SolidWorks文件属性。
|
||
</p>
|
||
<Table
|
||
dataSource={matchResults}
|
||
columns={matchColumns}
|
||
rowKey="DocPath"
|
||
size="small"
|
||
pagination={false}
|
||
scroll={{ y: 400 }}
|
||
/>
|
||
</Modal>
|
||
</>
|
||
);
|
||
};
|