feat: add match material functionality to BOM server and UI
- Implemented MatchMaterialHandler to handle material matching requests. - Added new DTOs for match material requests and responses. - Updated BomHttpServer to register the new match material API endpoint. - Enhanced the UI with a new button to trigger material matching. - Added modal to display matching results and confirm updates to material codes. - Integrated API calls to fetch matching material codes from the PLM system.
This commit is contained in:
parent
b2ca9a46dd
commit
f13d646f7d
|
|
@ -140,7 +140,7 @@ namespace Laservall.Solidworks
|
|||
|
||||
private void Selections_NewSelection(IXDocument doc, Xarial.XCad.IXSelObject selObject)
|
||||
{
|
||||
m_TaskPane.Control.OnSelectChange(doc, selObject);
|
||||
m_TaskPane.Control.ViewModel.OnSelectionChanged(doc, selObject);
|
||||
}
|
||||
|
||||
public override void OnDisconnect()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Laservall.Solidworks.Common
|
||||
{
|
||||
public static class HttpClientHelper
|
||||
{
|
||||
private static readonly HttpClient _client = new HttpClient
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
|
||||
public static void SetBaseAddress(string baseUrl)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(baseUrl))
|
||||
{
|
||||
_client.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<TResponse> PostJsonAsync<TRequest, TResponse>(
|
||||
string url, TRequest body, CancellationToken ct = default(CancellationToken))
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(body);
|
||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
var response = await _client.PostAsync(url, content, ct).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
return JsonConvert.DeserializeObject<TResponse>(responseText);
|
||||
}
|
||||
|
||||
public static async Task<T> GetJsonAsync<T>(string url, CancellationToken ct = default(CancellationToken))
|
||||
{
|
||||
var response = await _client.GetAsync(url, ct).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
return JsonConvert.DeserializeObject<T>(responseText);
|
||||
}
|
||||
|
||||
public static async Task PostJsonAsync<TRequest>(
|
||||
string url, TRequest body, CancellationToken ct = default(CancellationToken))
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(body);
|
||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
var response = await _client.PostAsync(url, content, ct).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,12 +8,37 @@ using System.Windows;
|
|||
|
||||
namespace Laservall.Solidworks.Common
|
||||
{
|
||||
public static class ComboBoxDataRegistry
|
||||
{
|
||||
private static readonly Dictionary<string, List<string>> _data =
|
||||
new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static readonly List<string> _defaultItems = new List<string>();
|
||||
|
||||
public static void Register(string propertyName, List<string> items)
|
||||
{
|
||||
_data[propertyName] = items ?? new List<string>();
|
||||
}
|
||||
|
||||
public static List<string> GetItems(string propertyName)
|
||||
{
|
||||
List<string> items;
|
||||
if (_data.TryGetValue(propertyName, out items))
|
||||
{
|
||||
return items;
|
||||
}
|
||||
return _defaultItems;
|
||||
}
|
||||
}
|
||||
|
||||
public class ComboxPropertyEditor : PropertyEditorBase
|
||||
{
|
||||
public override FrameworkElement CreateElement(PropertyItem propertyItem)
|
||||
{
|
||||
var comboBox = new ComboBox();
|
||||
comboBox.ItemsSource = new List<string> { "A", "B", "C" };
|
||||
comboBox.IsEditable = true;
|
||||
var items = ComboBoxDataRegistry.GetItems(propertyItem.DisplayName);
|
||||
comboBox.ItemsSource = items;
|
||||
comboBox.SelectedItem = propertyItem.Value?.ToString();
|
||||
comboBox.SelectionChanged += (s, e) =>
|
||||
{
|
||||
|
|
@ -33,7 +58,9 @@ namespace Laservall.Solidworks.Common
|
|||
public override FrameworkElement CreateElement(PropertyItem propertyItem)
|
||||
{
|
||||
var comboBox = new ComboBox();
|
||||
comboBox.ItemsSource = new List<string> { "A", "B", "C" };
|
||||
comboBox.IsEditable = true;
|
||||
var items = ComboBoxDataRegistry.GetItems(propertyItem.DisplayName);
|
||||
comboBox.ItemsSource = items;
|
||||
comboBox.SelectedItem = propertyItem.Value?.ToString();
|
||||
comboBox.SelectionChanged += (s, e) =>
|
||||
{
|
||||
|
|
@ -47,4 +74,20 @@ namespace Laservall.Solidworks.Common
|
|||
return ComboBox.SelectedItemProperty;
|
||||
}
|
||||
}
|
||||
|
||||
public class ReadOnlyTextPropertyEditor : PropertyEditorBase
|
||||
{
|
||||
public override FrameworkElement CreateElement(PropertyItem propertyItem)
|
||||
{
|
||||
return new System.Windows.Controls.TextBlock
|
||||
{
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
}
|
||||
|
||||
public override DependencyProperty GetDependencyProperty()
|
||||
{
|
||||
return System.Windows.Controls.TextBlock.TextProperty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
using System;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Laservall.Solidworks.Common
|
||||
{
|
||||
public class RelayCommand : ICommand
|
||||
{
|
||||
private readonly Action<object> _execute;
|
||||
private readonly Func<object, bool> _canExecute;
|
||||
|
||||
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public RelayCommand(Action execute, Func<bool> canExecute = null)
|
||||
: this(_ => execute(), canExecute != null ? (Func<object, bool>)(_ => canExecute()) : null)
|
||||
{
|
||||
}
|
||||
|
||||
public event EventHandler CanExecuteChanged;
|
||||
|
||||
public bool CanExecute(object parameter)
|
||||
{
|
||||
return _canExecute == null || _canExecute(parameter);
|
||||
}
|
||||
|
||||
public void Execute(object parameter)
|
||||
{
|
||||
_execute(parameter);
|
||||
}
|
||||
|
||||
public void RaiseCanExecuteChanged()
|
||||
{
|
||||
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Laservall.Solidworks.Common
|
||||
{
|
||||
public abstract class ViewModelBase : INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
protected bool Set<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
|
||||
{
|
||||
if (Equals(field, value)) return false;
|
||||
field = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
using Laservall.Solidworks.Model;
|
||||
using Laservall.Solidworks.Model;
|
||||
using SolidWorks.Interop.sldworks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
|
@ -6,104 +6,237 @@ using System.Diagnostics;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Shapes;
|
||||
using Xarial.XCad.Documents;
|
||||
using Xarial.XCad.SolidWorks.Documents;
|
||||
|
||||
namespace Laservall.Solidworks.Extension
|
||||
{
|
||||
public static class SWDocReader
|
||||
{
|
||||
public static void ReadDocProperties(IModelDoc2 doc,PartPropModel partPropModel)
|
||||
// 特殊属性名称(需要通过SolidWorks内置API读取,而非自定义属性)
|
||||
private static readonly HashSet<string> SpecialReadProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
if (doc != null)
|
||||
"材质", "MATERIAL",
|
||||
"重量", "MASS"
|
||||
};
|
||||
|
||||
public static void ReadDocProperties(IModelDoc2 doc, PartPropModel partPropModel)
|
||||
{
|
||||
if (doc == null) return;
|
||||
|
||||
Configuration activeConfig = doc.ConfigurationManager?.ActiveConfiguration;
|
||||
CustomPropertyManager configPropMgr = activeConfig?.CustomPropertyManager;
|
||||
|
||||
CustomPropertyManager filePropMgr = null;
|
||||
try
|
||||
{
|
||||
partPropModel.GetType().GetProperties().ToList().ForEach(p =>
|
||||
filePropMgr = doc.Extension?.get_CustomPropertyManager("");
|
||||
}
|
||||
catch { }
|
||||
|
||||
partPropModel.GetType().GetProperties().ToList().ForEach(p =>
|
||||
{
|
||||
p.CustomAttributes.ToList().ForEach(attr =>
|
||||
{
|
||||
// Get DisplayName attribute
|
||||
p.CustomAttributes.ToList().ForEach(attr =>
|
||||
if (attr.AttributeType == typeof(System.ComponentModel.DisplayNameAttribute))
|
||||
{
|
||||
if (attr.AttributeType == typeof(System.ComponentModel.DisplayNameAttribute))
|
||||
var displayName = attr.ConstructorArguments[0].Value.ToString();
|
||||
|
||||
string val = ReadPropertyValue(doc, configPropMgr, filePropMgr, displayName);
|
||||
|
||||
if (val != null)
|
||||
{
|
||||
var displayName = attr.ConstructorArguments[0].Value.ToString();
|
||||
// Get custom property value by display name
|
||||
var val2 = GetDocProperty(doc, displayName);
|
||||
if (val2 != null)
|
||||
switch (p.PropertyType.Name)
|
||||
{
|
||||
// 判断属性类型
|
||||
switch (p.PropertyType.Name)
|
||||
{
|
||||
case "Double":
|
||||
if (double.TryParse(val2, out double d))
|
||||
{
|
||||
p.SetValue(partPropModel, d);
|
||||
}
|
||||
break;
|
||||
case "Int32":
|
||||
if (int.TryParse(val2, out int i))
|
||||
{
|
||||
p.SetValue(partPropModel, i);
|
||||
}
|
||||
break;
|
||||
case "Boolean":
|
||||
if (bool.TryParse(val2, out bool b))
|
||||
{
|
||||
p.SetValue(partPropModel, b);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
p.SetValue(partPropModel, val2);
|
||||
break;
|
||||
}
|
||||
case "Double":
|
||||
if (double.TryParse(val, out double d))
|
||||
{
|
||||
p.SetValue(partPropModel, d);
|
||||
}
|
||||
break;
|
||||
case "Int32":
|
||||
if (int.TryParse(val, out int i))
|
||||
{
|
||||
p.SetValue(partPropModel, i);
|
||||
}
|
||||
break;
|
||||
case "Boolean":
|
||||
// 支持 "true"/"false" 和 "1"/"0" 和 "是"/"否"
|
||||
if (bool.TryParse(val, out bool b))
|
||||
{
|
||||
p.SetValue(partPropModel, b);
|
||||
}
|
||||
else if (val == "1" || string.Equals(val, "是", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
p.SetValue(partPropModel, true);
|
||||
}
|
||||
else if (val == "0" || string.Equals(val, "否", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
p.SetValue(partPropModel, false);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
p.SetValue(partPropModel, val);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//var val = partComponent.Component.CustomPropertyManager[p.Name];
|
||||
//if (val != null)
|
||||
//{
|
||||
// p.SetValue(partPropModel, val);
|
||||
//}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取属性值:先检查特殊属性,再尝试配置级,最后回退到文件级
|
||||
/// </summary>
|
||||
private static string ReadPropertyValue(
|
||||
IModelDoc2 doc,
|
||||
CustomPropertyManager configPropMgr,
|
||||
CustomPropertyManager filePropMgr,
|
||||
string propertyName)
|
||||
{
|
||||
// 1. 特殊属性(内置属性,通过SolidWorks API直接获取)
|
||||
if (SpecialReadProperties.Contains(propertyName))
|
||||
{
|
||||
string specialVal = ReadSpecialProperty(doc, propertyName);
|
||||
if (!string.IsNullOrEmpty(specialVal))
|
||||
{
|
||||
return specialVal;
|
||||
}
|
||||
// 特殊属性读取失败时,仍尝试从自定义属性读取(用户可能手动设置了)
|
||||
}
|
||||
|
||||
// 2. 配置级自定义属性(优先)
|
||||
if (configPropMgr != null)
|
||||
{
|
||||
string configVal = GetPropertyFromManager(configPropMgr, propertyName);
|
||||
if (configVal != null)
|
||||
{
|
||||
return configVal;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 文件级自定义属性(回退)
|
||||
if (filePropMgr != null)
|
||||
{
|
||||
string fileVal = GetPropertyFromManager(filePropMgr, propertyName);
|
||||
if (fileVal != null)
|
||||
{
|
||||
return fileVal;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从指定的CustomPropertyManager读取属性值
|
||||
/// </summary>
|
||||
private static string GetPropertyFromManager(CustomPropertyManager propMgr, string propertyName)
|
||||
{
|
||||
try
|
||||
{
|
||||
int status = propMgr.Get6(propertyName, false, out string val, out string valout, out bool wasResolved, out bool _);
|
||||
|
||||
// status: swCustomInfoGetResult_e
|
||||
// 0 = swCustomInfoGetResult_NotPresent — 属性不存在
|
||||
if (status == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (wasResolved && !string.IsNullOrEmpty(valout))
|
||||
{
|
||||
return valout;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
return val;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前激活配置的自定义属性
|
||||
/// 读取特殊内置属性(材质、重量等)
|
||||
/// </summary>
|
||||
/// <param name="model"></param>
|
||||
/// <param name="propertyName"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetDocProperty(IModelDoc2 model , string propertyName)
|
||||
private static string ReadSpecialProperty(IModelDoc2 doc, string propertyName)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 材质 — 仅Part文件有材质属性
|
||||
if (string.Equals(propertyName, "材质", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(propertyName, "MATERIAL", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
PartDoc partDoc = doc as PartDoc;
|
||||
if (partDoc != null)
|
||||
{
|
||||
Configuration config = doc.ConfigurationManager?.ActiveConfiguration;
|
||||
if (config != null)
|
||||
{
|
||||
string materialName = partDoc.GetMaterialPropertyName2(config.Name, out string _);
|
||||
if (!string.IsNullOrEmpty(materialName))
|
||||
{
|
||||
return materialName;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 重量(质量) — Part和Assembly都支持
|
||||
if (string.Equals(propertyName, "重量", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(propertyName, "MASS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ModelDocExtension docExt = doc.Extension;
|
||||
if (docExt != null)
|
||||
{
|
||||
int status = 0;
|
||||
// 返回值: [CenterOfMassX, CenterOfMassY, CenterOfMassZ, Volume, Area, Mass, ...]
|
||||
double[] massProps = docExt.GetMassProperties(1, ref status) as double[];
|
||||
|
||||
if (status == 0 && massProps != null && massProps.Length > 5)
|
||||
{
|
||||
double mass = massProps[5]; // 质量,单位: kg
|
||||
return Math.Round(mass, 4).ToString();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"ReadSpecialProperty({propertyName}) 异常: {ex.Message}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取单个属性值(保留向后兼容)
|
||||
/// 优先级:特殊属性 > 配置级 > 文件级
|
||||
/// </summary>
|
||||
public static string GetDocProperty(IModelDoc2 model, string propertyName)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return "未打开任何文档";
|
||||
}
|
||||
else
|
||||
|
||||
Configuration activeConfig = model.ConfigurationManager?.ActiveConfiguration;
|
||||
CustomPropertyManager configPropMgr = activeConfig?.CustomPropertyManager;
|
||||
|
||||
CustomPropertyManager filePropMgr = null;
|
||||
try
|
||||
{
|
||||
ModelDocExtension swModelDocExt = default(ModelDocExtension);
|
||||
CustomPropertyManager swCustProp = default(CustomPropertyManager);
|
||||
string val = "";
|
||||
string valout = "";
|
||||
int status;
|
||||
|
||||
//swModel = (ModelDoc2)swApp.ActiveDoc;
|
||||
swModelDocExt = model.Extension;
|
||||
|
||||
// Get the custom property data
|
||||
swCustProp = swModelDocExt.get_CustomPropertyManager("");
|
||||
//status = swCustProp.Get4(propertyName, false, out val, out valout);
|
||||
status = swCustProp.Get6(propertyName, false, out val,out valout,out bool wasResolved,out bool _);
|
||||
|
||||
if (wasResolved)
|
||||
{
|
||||
return valout;
|
||||
}
|
||||
else
|
||||
{
|
||||
return val;
|
||||
}
|
||||
filePropMgr = model.Extension?.get_CustomPropertyManager("");
|
||||
}
|
||||
catch { }
|
||||
|
||||
return ReadPropertyValue(model, configPropMgr, filePropMgr, propertyName) ?? "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,62 +1,117 @@
|
|||
using SolidWorks.Interop.sldworks;
|
||||
using Laservall.Solidworks.Common;
|
||||
using SolidWorks.Interop.sldworks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Xarial.XCad.Data;
|
||||
|
||||
namespace Laservall.Solidworks.Extension
|
||||
{
|
||||
public static class SWDocWriter
|
||||
{
|
||||
// 根据文档与属性模型,写入自定义属性
|
||||
private static readonly HashSet<string> SkipWriteProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"材质", "MATERIAL",
|
||||
"重量", "MASS"
|
||||
};
|
||||
|
||||
public static void WriteDocProperties(IModelDoc2 doc, Model.PartPropModel partPropModel)
|
||||
{
|
||||
if (doc != null)
|
||||
if (doc == null) return;
|
||||
|
||||
Configuration activeConfig = doc.ConfigurationManager?.ActiveConfiguration;
|
||||
CustomPropertyManager configPropMgr = activeConfig?.CustomPropertyManager;
|
||||
|
||||
CustomPropertyManager filePropMgr = null;
|
||||
try
|
||||
{
|
||||
partPropModel.GetType().GetProperties().ToList().ForEach(p =>
|
||||
{
|
||||
// Get DisplayName attribute
|
||||
p.CustomAttributes.ToList().ForEach(attr =>
|
||||
{
|
||||
if (attr.AttributeType == typeof(System.ComponentModel.DisplayNameAttribute))
|
||||
{
|
||||
var displayName = attr.ConstructorArguments[0].Value.ToString();
|
||||
var val = p.GetValue(partPropModel);
|
||||
if (val != null)
|
||||
{
|
||||
// 设置自定义属性值
|
||||
SetDocProperty(doc, displayName, val.ToString());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
filePropMgr = doc.Extension?.get_CustomPropertyManager("");
|
||||
}
|
||||
catch { }
|
||||
|
||||
string[] configNames = null;
|
||||
try
|
||||
{
|
||||
configNames = configPropMgr?.GetNames() as string[];
|
||||
}
|
||||
catch { }
|
||||
|
||||
partPropModel.GetType().GetProperties().ToList().ForEach(p =>
|
||||
{
|
||||
p.CustomAttributes.ToList().ForEach(attr =>
|
||||
{
|
||||
if (attr.AttributeType == typeof(System.ComponentModel.DisplayNameAttribute))
|
||||
{
|
||||
var displayName = attr.ConstructorArguments[0].Value.ToString();
|
||||
|
||||
// ReadOnly属性(使用ReadOnlyTextPropertyEditor的)不写入
|
||||
bool isReadOnly = p.CustomAttributes.Any(a =>
|
||||
a.AttributeType == typeof(System.ComponentModel.EditorAttribute) &&
|
||||
a.ConstructorArguments.Any(c =>
|
||||
c.Value?.ToString()?.Contains(nameof(ReadOnlyTextPropertyEditor)) == true));
|
||||
if (isReadOnly) return;
|
||||
|
||||
// 内置特殊属性不通过自定义属性写入
|
||||
if (SkipWriteProperties.Contains(displayName)) return;
|
||||
|
||||
var val = p.GetValue(partPropModel);
|
||||
string strVal = val?.ToString() ?? "";
|
||||
|
||||
SetPropertyValue(configPropMgr, filePropMgr, configNames, displayName, strVal);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// 写入属性值:属性存在于配置级则写配置级,否则写文件级
|
||||
/// 与SolidWorksBomDataProvider.SaveChanges()保持一致的策略
|
||||
/// </summary>
|
||||
/// <param name="model"></param>
|
||||
/// <param name="propertyName"></param>
|
||||
/// <returns></returns>
|
||||
public static void SetDocProperty(IModelDoc2 model, string propertyName,string val)
|
||||
private static void SetPropertyValue(
|
||||
CustomPropertyManager configPropMgr,
|
||||
CustomPropertyManager filePropMgr,
|
||||
string[] configNames,
|
||||
string propertyName,
|
||||
string val)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
bool writtenToConfig = false;
|
||||
|
||||
if (configPropMgr != null && configNames != null && configNames.Contains(propertyName))
|
||||
{
|
||||
try
|
||||
{
|
||||
configPropMgr.Set2(propertyName, val);
|
||||
writtenToConfig = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"SetPropertyValue config({propertyName}) failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//swModel = (ModelDoc2)swApp.ActiveDoc;
|
||||
ModelDocExtension swModelDocExt = model.Extension;
|
||||
|
||||
// Get the custom property data
|
||||
CustomPropertyManager swCustProp = swModelDocExt.get_CustomPropertyManager("");
|
||||
|
||||
swCustProp.Set2(propertyName, val);
|
||||
|
||||
if (!writtenToConfig && filePropMgr != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
filePropMgr.Set2(propertyName, val);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"SetPropertyValue file({propertyName}) failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
else if (!writtenToConfig && configPropMgr != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
configPropMgr.Set2(propertyName, val);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"SetPropertyValue config-fallback({propertyName}) failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,6 +90,9 @@
|
|||
<Compile Include="Common\PLMConfig.cs" />
|
||||
<Compile Include="Common\PropertyEditor.cs" />
|
||||
<Compile Include="Common\SM4Helper.cs" />
|
||||
<Compile Include="Common\ViewModelBase.cs" />
|
||||
<Compile Include="Common\RelayCommand.cs" />
|
||||
<Compile Include="Common\HttpClientHelper.cs" />
|
||||
<Compile Include="Extension\SWDocReader.cs" />
|
||||
<Compile Include="Extension\SWDocWriter.cs" />
|
||||
<Compile Include="Extension\SWUtils.cs" />
|
||||
|
|
@ -104,6 +107,7 @@
|
|||
<Compile Include="Pane\PartPropPaneUserControl.xaml.cs">
|
||||
<DependentUpon>PartPropPaneUserControl.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Pane\PartPropPaneViewModel.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Windows\ColumnConfigWindow.xaml.cs">
|
||||
<DependentUpon>ColumnConfigWindow.xaml</DependentUpon>
|
||||
|
|
@ -127,6 +131,7 @@
|
|||
<Compile Include="Server\Handlers\SaveHandler.cs" />
|
||||
<Compile Include="Server\Handlers\ExportHandler.cs" />
|
||||
<Compile Include="Server\Handlers\SettingsHandler.cs" />
|
||||
<Compile Include="Server\Handlers\MatchMaterialHandler.cs" />
|
||||
<Compile Include="Server\StaThreadMarshaller.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using System.Text;
|
|||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using ReadOnlyTextPropertyEditor = Laservall.Solidworks.Common.ReadOnlyTextPropertyEditor;
|
||||
|
||||
namespace Laservall.Solidworks.Model
|
||||
{
|
||||
|
|
|
|||
|
|
@ -21,17 +21,16 @@
|
|||
<RowDefinition Height="20"/>
|
||||
</Grid.RowDefinitions>
|
||||
<hc:ButtonGroup Grid.Row="0" VerticalAlignment="Center" Margin="10,5">
|
||||
<Button FontSize="14" Content="保存" Background="#81C784" Click="Button_Click" />
|
||||
<Button FontSize="14" Content="重置" Background="#E57373" />
|
||||
<Button FontSize="14" Content="保存" Background="#81C784" Command="{Binding SaveCommand}" />
|
||||
<Button FontSize="14" Content="重置" Background="#E57373" Command="{Binding ResetCommand}" />
|
||||
</hc:ButtonGroup>
|
||||
<hc:PropertyGrid Background="Transparent"
|
||||
FontSize="14"
|
||||
Grid.Row="1"
|
||||
SelectedObject="{Binding PartPropModel}"/>
|
||||
<TextBlock Grid.Row="2"
|
||||
x:Name="VersionText"
|
||||
SelectedObject="{Binding Model}"/>
|
||||
<TextBlock Grid.Row="2"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Text="1.0.0.1"/>
|
||||
Text="{Binding VersionText}"/>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
|
|
|||
|
|
@ -1,88 +1,20 @@
|
|||
using Laservall.Solidworks.Extension;
|
||||
using Laservall.Solidworks.Model;
|
||||
using SolidWorks.Interop.sldworks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using Xarial.XCad;
|
||||
using Xarial.XCad.Documents;
|
||||
using Xarial.XCad.Features;
|
||||
using Xarial.XCad.SolidWorks.Documents;
|
||||
using System.Windows.Controls;
|
||||
using Xarial.XCad.SolidWorks.UI;
|
||||
|
||||
namespace Laservall.Solidworks.Pane
|
||||
{
|
||||
/// <summary>
|
||||
/// PartPropPaneUserControl.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class PartPropPaneUserControl : UserControl
|
||||
{
|
||||
public ISwTaskPane<PartPropPaneUserControl> pane;
|
||||
public IModelDoc2 selectedDoc;
|
||||
public IXDocument selectedIXDoc;
|
||||
public PartPropModel PartPropModel { get; set; } = new PartPropModel();
|
||||
|
||||
public PartPropPaneViewModel ViewModel { get; }
|
||||
|
||||
public PartPropPaneUserControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
DataContext = this;
|
||||
VersionText.Text = $"Ver.{Assembly.GetExecutingAssembly().GetName().Version}";
|
||||
}
|
||||
|
||||
public void OnSelectChange(IXDocument doc, IXSelObject xSelObject)
|
||||
{
|
||||
if (xSelObject.OwnerDocument is IXDocument)
|
||||
{
|
||||
if(xSelObject is ISwPartComponent partComponent)
|
||||
{
|
||||
selectedDoc = (IModelDoc2)partComponent.Component.GetModelDoc();
|
||||
//selectedIXDoc = partComponent.ReferencedDocument;
|
||||
}
|
||||
else if(xSelObject is IXFeature feature)
|
||||
{
|
||||
if(feature.Component is ISwComponent swPartComponent)
|
||||
{
|
||||
selectedDoc = swPartComponent.Component.GetModelDoc() as IModelDoc2;
|
||||
|
||||
}
|
||||
else if(feature.OwnerDocument != null)
|
||||
{
|
||||
selectedDoc = (feature.OwnerDocument as ISwDocument).Model;
|
||||
}
|
||||
}
|
||||
if (selectedDoc != null)
|
||||
{
|
||||
SWDocReader.ReadDocProperties(selectedDoc, PartPropModel);
|
||||
}
|
||||
|
||||
}
|
||||
if(xSelObject.OwnerDocument is ISwPart)
|
||||
{
|
||||
//xSelObject.OwnerDocument.Properties
|
||||
selectedDoc = null;
|
||||
}
|
||||
Debug.WriteLine($"OnSelectChange: {xSelObject.GetType()}");
|
||||
}
|
||||
|
||||
private void Button_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if(selectedDoc != null)
|
||||
{
|
||||
SWDocWriter.WriteDocProperties(selectedDoc, PartPropModel);
|
||||
}
|
||||
ViewModel = new PartPropPaneViewModel();
|
||||
DataContext = ViewModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
using Laservall.Solidworks.Common;
|
||||
using Laservall.Solidworks.Extension;
|
||||
using Laservall.Solidworks.Model;
|
||||
using SolidWorks.Interop.sldworks;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Windows.Input;
|
||||
using Xarial.XCad;
|
||||
using Xarial.XCad.Documents;
|
||||
using Xarial.XCad.Features;
|
||||
using Xarial.XCad.SolidWorks.Documents;
|
||||
|
||||
namespace Laservall.Solidworks.Pane
|
||||
{
|
||||
public class PartPropPaneViewModel : ViewModelBase
|
||||
{
|
||||
private IModelDoc2 _selectedDoc;
|
||||
|
||||
private PartPropModel _model = new PartPropModel();
|
||||
public PartPropModel Model
|
||||
{
|
||||
get => _model;
|
||||
set => Set(ref _model, value);
|
||||
}
|
||||
|
||||
private string _statusText;
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
set => Set(ref _statusText, value);
|
||||
}
|
||||
|
||||
private string _versionText;
|
||||
public string VersionText
|
||||
{
|
||||
get => _versionText;
|
||||
set => Set(ref _versionText, value);
|
||||
}
|
||||
|
||||
public ICommand SaveCommand { get; }
|
||||
public ICommand ResetCommand { get; }
|
||||
|
||||
public PartPropPaneViewModel()
|
||||
{
|
||||
VersionText = $"Ver.{Assembly.GetExecutingAssembly().GetName().Version}";
|
||||
|
||||
SaveCommand = new RelayCommand(ExecuteSave, CanSave);
|
||||
ResetCommand = new RelayCommand(ExecuteReset, CanReset);
|
||||
}
|
||||
|
||||
public void OnSelectionChanged(IXDocument doc, IXSelObject selObject)
|
||||
{
|
||||
IModelDoc2 resolvedDoc = null;
|
||||
|
||||
if (selObject.OwnerDocument is IXDocument)
|
||||
{
|
||||
if (selObject is ISwPartComponent partComponent)
|
||||
{
|
||||
resolvedDoc = (IModelDoc2)partComponent.Component.GetModelDoc();
|
||||
}
|
||||
else if (selObject is IXFeature feature)
|
||||
{
|
||||
if (feature.Component is ISwComponent swPartComponent)
|
||||
{
|
||||
resolvedDoc = swPartComponent.Component.GetModelDoc() as IModelDoc2;
|
||||
}
|
||||
else if (feature.OwnerDocument != null)
|
||||
{
|
||||
resolvedDoc = (feature.OwnerDocument as ISwDocument).Model;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selObject.OwnerDocument is ISwPart)
|
||||
{
|
||||
resolvedDoc = null;
|
||||
}
|
||||
|
||||
_selectedDoc = resolvedDoc;
|
||||
|
||||
if (_selectedDoc != null)
|
||||
{
|
||||
SWDocReader.ReadDocProperties(_selectedDoc, Model);
|
||||
StatusText = $"已加载: {_selectedDoc.GetTitle()}";
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusText = "";
|
||||
}
|
||||
|
||||
((RelayCommand)SaveCommand).RaiseCanExecuteChanged();
|
||||
((RelayCommand)ResetCommand).RaiseCanExecuteChanged();
|
||||
|
||||
Debug.WriteLine($"OnSelectionChanged: {selObject.GetType()}");
|
||||
}
|
||||
|
||||
private bool CanSave()
|
||||
{
|
||||
return _selectedDoc != null;
|
||||
}
|
||||
|
||||
private void ExecuteSave()
|
||||
{
|
||||
if (_selectedDoc == null) return;
|
||||
SWDocWriter.WriteDocProperties(_selectedDoc, Model);
|
||||
StatusText = "属性已保存";
|
||||
}
|
||||
|
||||
private bool CanReset()
|
||||
{
|
||||
return _selectedDoc != null;
|
||||
}
|
||||
|
||||
private void ExecuteReset()
|
||||
{
|
||||
if (_selectedDoc == null) return;
|
||||
SWDocReader.ReadDocProperties(_selectedDoc, Model);
|
||||
StatusText = "属性已重置";
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -21,6 +21,7 @@ namespace Laservall.Solidworks.Server
|
|||
private readonly SaveHandler _saveHandler;
|
||||
private readonly ExportHandler _exportHandler;
|
||||
private readonly SettingsHandler _settingsHandler;
|
||||
private readonly MatchMaterialHandler _matchMaterialHandler;
|
||||
private int _port;
|
||||
private bool _disposed;
|
||||
|
||||
|
|
@ -41,6 +42,7 @@ namespace Laservall.Solidworks.Server
|
|||
_saveHandler = new SaveHandler(dataProvider);
|
||||
_exportHandler = new ExportHandler(() => _bomDataHandler.LastLoadResult ?? _bomStreamHandler.LastLoadResult, dataProvider);
|
||||
_settingsHandler = new SettingsHandler(dataProvider);
|
||||
_matchMaterialHandler = new MatchMaterialHandler();
|
||||
|
||||
RegisterRoutes();
|
||||
}
|
||||
|
|
@ -138,6 +140,11 @@ namespace Laservall.Solidworks.Server
|
|||
await _settingsHandler.HandlePost(ctx, ct);
|
||||
});
|
||||
|
||||
_router.Post("/api/bom/match-material", async (ctx, ct) =>
|
||||
{
|
||||
await _matchMaterialHandler.HandleMatch(ctx, ct);
|
||||
});
|
||||
|
||||
_router.Post("/api/shutdown", async (ctx, ct) =>
|
||||
{
|
||||
ctx.Response.StatusCode = 200;
|
||||
|
|
|
|||
|
|
@ -69,4 +69,46 @@ namespace Laservall.Solidworks.Server.Dto
|
|||
public int Total { get; set; }
|
||||
public string CurrentName { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class MatchMaterialRequest
|
||||
{
|
||||
public List<MatchMaterialItemDto> Items { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class MatchMaterialItemDto
|
||||
{
|
||||
public string DrawingNo { get; set; }
|
||||
public string DocPath { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class MatchMaterialResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public List<MatchMaterialResultDto> Results { get; set; }
|
||||
public string Error { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class MatchMaterialResultDto
|
||||
{
|
||||
public string DrawingNo { get; set; }
|
||||
public string DocPath { get; set; }
|
||||
public string PartCode { get; set; }
|
||||
public string PartName { get; set; }
|
||||
public bool Matched { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class PlmFindPartsCodeRequest
|
||||
{
|
||||
public string model { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class PlmFindPartsCodeResponse
|
||||
{
|
||||
public string part_id { get; set; }
|
||||
public string part_code { get; set; }
|
||||
public string part_name { get; set; }
|
||||
public string model { get; set; }
|
||||
public string remark { get; set; }
|
||||
public string brand { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
using Laservall.Solidworks.Server.Dto;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Laservall.Solidworks.Server.Handlers
|
||||
{
|
||||
internal sealed class MatchMaterialHandler
|
||||
{
|
||||
private const string PlmApiUrl = "http://10.0.0.155:9991/api/PLM/FindPartsCode";
|
||||
private static readonly HttpClient _httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
|
||||
|
||||
public async Task HandleMatch(HttpListenerContext context, CancellationToken ct)
|
||||
{
|
||||
string body;
|
||||
using (var reader = new StreamReader(context.Request.InputStream, Encoding.UTF8))
|
||||
{
|
||||
body = await reader.ReadToEndAsync();
|
||||
}
|
||||
|
||||
var request = JsonConvert.DeserializeObject<MatchMaterialRequest>(body);
|
||||
if (request?.Items == null || request.Items.Count == 0)
|
||||
{
|
||||
await WriteJson(context.Response, new MatchMaterialResponse
|
||||
{
|
||||
Success = true,
|
||||
Results = new List<MatchMaterialResultDto>()
|
||||
}, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var plmRequestBody = request.Items
|
||||
.Select(i => new PlmFindPartsCodeRequest { model = i.DrawingNo })
|
||||
.ToList();
|
||||
|
||||
var plmJson = JsonConvert.SerializeObject(plmRequestBody);
|
||||
var httpContent = new StringContent(plmJson, Encoding.UTF8, "application/json-patch+json");
|
||||
var httpResponse = await _httpClient.PostAsync(PlmApiUrl, httpContent, ct);
|
||||
var responseText = await httpResponse.Content.ReadAsStringAsync();
|
||||
|
||||
if (!httpResponse.IsSuccessStatusCode)
|
||||
{
|
||||
await WriteJson(context.Response, new MatchMaterialResponse
|
||||
{
|
||||
Success = false,
|
||||
Results = new List<MatchMaterialResultDto>(),
|
||||
Error = $"PLM API 返回 {(int)httpResponse.StatusCode}: {responseText}"
|
||||
}, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
var plmResults = JsonConvert.DeserializeObject<List<PlmFindPartsCodeResponse>>(responseText)
|
||||
?? new List<PlmFindPartsCodeResponse>();
|
||||
|
||||
var plmLookup = plmResults
|
||||
.Where(r => !string.IsNullOrEmpty(r.part_code))
|
||||
.ToDictionary(r => r.model ?? "", r => r, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var results = new List<MatchMaterialResultDto>();
|
||||
foreach (var item in request.Items)
|
||||
{
|
||||
PlmFindPartsCodeResponse plmMatch;
|
||||
if (plmLookup.TryGetValue(item.DrawingNo ?? "", out plmMatch))
|
||||
{
|
||||
results.Add(new MatchMaterialResultDto
|
||||
{
|
||||
DrawingNo = item.DrawingNo,
|
||||
DocPath = item.DocPath,
|
||||
PartCode = plmMatch.part_code,
|
||||
PartName = plmMatch.part_name,
|
||||
Matched = true
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
results.Add(new MatchMaterialResultDto
|
||||
{
|
||||
DrawingNo = item.DrawingNo,
|
||||
DocPath = item.DocPath,
|
||||
PartCode = "",
|
||||
PartName = "",
|
||||
Matched = false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await WriteJson(context.Response, new MatchMaterialResponse
|
||||
{
|
||||
Success = true,
|
||||
Results = results
|
||||
}, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await WriteJson(context.Response, new MatchMaterialResponse
|
||||
{
|
||||
Success = false,
|
||||
Results = new List<MatchMaterialResultDto>(),
|
||||
Error = $"匹配失败: {ex.Message}"
|
||||
}, ct);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WriteJson(HttpListenerResponse response, object data, CancellationToken ct)
|
||||
{
|
||||
response.ContentType = "application/json; charset=utf-8";
|
||||
string json = JsonConvert.SerializeObject(data);
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(json);
|
||||
response.ContentLength64 = bytes.Length;
|
||||
await response.OutputStream.WriteAsync(bytes, 0, bytes.Length, ct);
|
||||
response.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ const AppContent: React.FC = () => {
|
|||
const [viewMode, setViewMode] = useState<'hierarchical' | 'flat'>('hierarchical');
|
||||
const [configVisible, setConfigVisible] = useState(false);
|
||||
|
||||
const { treeData, settings, loading, reload, updateItemsLocally } = useBomData();
|
||||
const { items, treeData, settings, loading, reload, updateItemsLocally } = useBomData();
|
||||
const { dirtyCells, setDirty, clearAll, isDirty, getDirtyChanges, dirtyCount } = useDirtyTracking();
|
||||
const {
|
||||
columns,
|
||||
|
|
@ -55,6 +55,7 @@ const AppContent: React.FC = () => {
|
|||
onOpenConfig={() => setConfigVisible(true)}
|
||||
getDirtyChanges={getDirtyChanges}
|
||||
onSaveSuccess={handleSaveSuccess}
|
||||
items={items}
|
||||
/>
|
||||
<div style={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
|
||||
{viewMode === 'hierarchical' ? (
|
||||
|
|
|
|||
|
|
@ -56,4 +56,27 @@ export interface ProgressEventDto {
|
|||
Processed: number;
|
||||
Total: number;
|
||||
CurrentName: string;
|
||||
}
|
||||
|
||||
export interface MatchMaterialItemDto {
|
||||
DrawingNo: string;
|
||||
DocPath: string;
|
||||
}
|
||||
|
||||
export interface MatchMaterialRequest {
|
||||
Items: MatchMaterialItemDto[];
|
||||
}
|
||||
|
||||
export interface MatchMaterialResultDto {
|
||||
DrawingNo: string;
|
||||
DocPath: string;
|
||||
PartCode: string;
|
||||
PartName: string;
|
||||
Matched: boolean;
|
||||
}
|
||||
|
||||
export interface MatchMaterialResponse {
|
||||
Success: boolean;
|
||||
Results: MatchMaterialResultDto[];
|
||||
Error: string | null;
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Space, Button, Segmented, Badge, message } from 'antd';
|
||||
import { Space, Button, Segmented, Badge, message, Modal, Table, Tag } from 'antd';
|
||||
import { apiPost, getExportUrl } from '../api/client';
|
||||
import type { SaveChangeDto } from '../api/types';
|
||||
import type { SaveChangeDto, BomItemDto, MatchMaterialRequest, MatchMaterialResponse, MatchMaterialResultDto } from '../api/types';
|
||||
|
||||
interface ToolBarProps {
|
||||
dirtyCount: number;
|
||||
|
|
@ -11,6 +11,7 @@ interface ToolBarProps {
|
|||
onOpenConfig: () => void;
|
||||
getDirtyChanges: () => SaveChangeDto[];
|
||||
onSaveSuccess: (changes: SaveChangeDto[]) => void;
|
||||
items: BomItemDto[];
|
||||
}
|
||||
|
||||
export const ToolBar: React.FC<ToolBarProps> = ({
|
||||
|
|
@ -21,8 +22,13 @@ export const ToolBar: React.FC<ToolBarProps> = ({
|
|||
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;
|
||||
|
|
@ -47,29 +53,153 @@ export const ToolBar: React.FC<ToolBarProps> = ({
|
|||
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}>
|
||||
保存
|
||||
<>
|
||||
<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>
|
||||
</Badge>
|
||||
<Button onClick={handleExport}>导出</Button>
|
||||
<Button onClick={onRefresh}>刷新</Button>
|
||||
<Button onClick={onOpenConfig}>列配置</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>
|
||||
<Space>
|
||||
<Segmented
|
||||
options={[
|
||||
{ label: '层级视图', value: 'hierarchical' },
|
||||
{ label: '扁平视图', value: 'flat' },
|
||||
]}
|
||||
value={viewMode}
|
||||
onChange={(val) => onViewModeChange(val as 'hierarchical' | 'flat')}
|
||||
|
||||
<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 }}
|
||||
/>
|
||||
<div style={{ marginLeft: 16, color: '#52c41a' }}>● 已连接</div>
|
||||
</Space>
|
||||
</Space>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue