2026-03-20 09:33:27 +08:00
|
|
|
|
import React, { useState } from 'react';
|
2026-04-09 17:06:24 +08:00
|
|
|
|
import { Space, Button, Segmented, Badge, message, Modal, Table, Tag } from 'antd';
|
2026-03-20 09:33:27 +08:00
|
|
|
|
import { apiPost, getExportUrl } from '../api/client';
|
2026-04-09 17:06:24 +08:00
|
|
|
|
import type { SaveChangeDto, BomItemDto, MatchMaterialRequest, MatchMaterialResponse, MatchMaterialResultDto } from '../api/types';
|
2026-03-20 09:33:27 +08:00
|
|
|
|
|
|
|
|
|
|
interface ToolBarProps {
|
|
|
|
|
|
dirtyCount: number;
|
|
|
|
|
|
viewMode: 'hierarchical' | 'flat';
|
|
|
|
|
|
onViewModeChange: (mode: 'hierarchical' | 'flat') => void;
|
|
|
|
|
|
onRefresh: () => void;
|
|
|
|
|
|
onOpenConfig: () => void;
|
|
|
|
|
|
getDirtyChanges: () => SaveChangeDto[];
|
|
|
|
|
|
onSaveSuccess: (changes: SaveChangeDto[]) => void;
|
2026-04-09 17:06:24 +08:00
|
|
|
|
items: BomItemDto[];
|
2026-03-20 09:33:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const ToolBar: React.FC<ToolBarProps> = ({
|
|
|
|
|
|
dirtyCount,
|
|
|
|
|
|
viewMode,
|
|
|
|
|
|
onViewModeChange,
|
|
|
|
|
|
onRefresh,
|
|
|
|
|
|
onOpenConfig,
|
|
|
|
|
|
getDirtyChanges,
|
|
|
|
|
|
onSaveSuccess,
|
2026-04-09 17:06:24 +08:00
|
|
|
|
items,
|
2026-03-20 09:33:27 +08:00
|
|
|
|
}) => {
|
|
|
|
|
|
const [saving, setSaving] = useState(false);
|
2026-04-09 17:06:24 +08:00
|
|
|
|
const [matching, setMatching] = useState(false);
|
|
|
|
|
|
const [matchResults, setMatchResults] = useState<MatchMaterialResultDto[]>([]);
|
|
|
|
|
|
const [matchModalVisible, setMatchModalVisible] = useState(false);
|
|
|
|
|
|
const [matchSaving, setMatchSaving] = useState(false);
|
2026-03-20 09:33:27 +08:00
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-09 17:06:24 +08:00
|
|
|
|
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>,
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-03-20 09:33:27 +08:00
|
|
|
|
return (
|
2026-04-09 17:06:24 +08:00
|
|
|
|
<>
|
|
|
|
|
|
<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}>
|
|
|
|
|
|
匹配物料编号
|
2026-03-20 09:33:27 +08:00
|
|
|
|
</Button>
|
2026-04-09 17:06:24 +08:00
|
|
|
|
</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>
|
2026-03-20 09:33:27 +08:00
|
|
|
|
</Space>
|
2026-04-09 17:06:24 +08:00
|
|
|
|
|
|
|
|
|
|
<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 }}
|
2026-03-20 09:33:27 +08:00
|
|
|
|
/>
|
2026-04-09 17:06:24 +08:00
|
|
|
|
</Modal>
|
|
|
|
|
|
</>
|
2026-03-20 09:33:27 +08:00
|
|
|
|
);
|
2026-04-09 17:06:24 +08:00
|
|
|
|
};
|