关于: "AutoEM":

data structure

in product_service.py

def save_simulation_result

关键看这个函数!
其他的都是大包裹式的字典!


n5321 | 2025年12月18日 00:41

数据流更改建议

这是一个非常典型的桌面应用程序架构演进问题。目前的架构中,数据在 数据库 (SQLite)UI 控件状态DataService (内存) 三者之间流转,确实存在“多头管理”和“同步不及时”的风险。

以下我将深入分析你当前的 数据流现状,并给出基于 DataService 作为唯一可信源 (Single Source of Truth) 的重构建议。


一、 当前数据流现状分析 (Current Data Flow)

目前的流向是混合的,主要痛点在于:数据是谁的?

1. 加载流程 (Loading)

  • 路径: Database -> ProductController -> CustomerRequirementsPage (UI) -> FieldWidget

  • 问题: 数据直接填入了 UI 控件。虽然 BLDCPage 里也顺手更新了一下 DataService,但主流程是依赖 UI 控件存储数据的。

2. 修改流程 (Editing)

  • 路径 A (Design Target Page): 用户打字 -> on_text_changed -> 直接调用 DataService.update...。

  • 路径 B (Input Panel): 用户打字 -> _on_text_changed -> 直接调用 DataService.update_maxwell_parameters。

  • 问题: UI 直接操作 Service,耦合度高。而且如果代码里有 setText 操作,会触发信号,导致不必要的递归更新。

3. 仿真启动流程 (Simulation Start)

  • 路径: InputPanel 点击开始 -> 从 DataService.customer_requirements 和 DataService.maxwell_parameters 拼凑参数 -> 发送给 Controller -> Worker。

  • 问题: maxwell_parameters 和 customer_requirements 之间界限模糊。有些参数(如额定电压)既是客户需求,又是 Maxwell 输入,目前是简单粗暴的 update() 覆盖,容易弄丢数据来源。

4. 结果回填流程 (Result Backfill)

  • 路径: Worker -> MaxwellAutomation -> DataService.update_result_detail (写入) -> Controller -> View.on_simulation_success -> View 读取 DataService 或直接使用传参。

  • 问题: 这是一个比较好的部分,已经通过 Service 中转了,但 Worker 内部隐式实例化 DataService 有点依赖全局单例的副作用。


二、 改进建议:DataService 中枢化重构

目标:UI 只是数据的展示层,Database 是持久化层,DataService 是运行时的唯一核心。

1. 重新定义变量含义

首先理清 DataService 中那几个字典的具体职责:

变量名当前定义建议改进定义
product_info产品基础信息(ID, Model)身份信息:当前选中产品的 ID、型号、系列ID。
customer_requirements客户需求(电压、扭矩等)设计目标(Target):只读或通过"设计目标页"修改。代表“我想要达到的指标”。
maxwell_parameters发送给 Maxwell 的参数仿真输入(Input):实际发给 Maxwell 的几何/物理参数。默认从 Target 继承,但允许在 InputPanel 覆盖修改。
maxwell_para_ranges优化范围优化配置:AI 算法的上下限配置。
result_detail仿真结果仿真输出(Output):Maxwell 算完回填的标量数据。

2. 架构重构:引入信号驱动机制

修改 DataService,使其继承自 QObject,并在数据变更时发送信号。

修改后的 services/data_service.py 结构建议:

codePython

from PyQt6.QtCore import QObject, pyqtSignal, QMutex, QMutexLocker

class DataService(QObject):
  _instance = None
  _create_mutex = QMutex()

  # === 定义信号,通知 UI 刷新 ===
  # 这样 UI 不需要知道是谁改了数据,只要数据变了,UI 就自动刷新
  data_changed = pyqtSignal(str, dict) # (category, new_data)

  def __init__(self):
      super().__init__()
      if not hasattr(self, '_initialized'):
          self._data_mutex = QMutex()
           
          # 数据容器
          self._data = {
              "product_info": {},
              "project_info": {},
              "customer_requirements": {}, # 对应设计目标
              "maxwell_parameters": {},     # 对应仿真输入
              "process_limits": {},
              "maxwell_para_ranges": {},
              "result_detail": {}
          }
          self._initialized = True

  # === 通用更新方法 ===
  def _update_category(self, category: str, data: dict):
      with QMutexLocker(self._data_mutex):
          self._data[category].update(data)
          # 拿到更新后的完整副本
          full_data = self._data[category].copy()
       
      # 发射信号 (在锁外部,防止死锁)
      self.data_changed.emit(category, full_data)

  # === 具体业务方法 ===
  def set_active_product(self, product_dict: dict):
      """加载新产品时调用,清空旧状态,填入新状态"""
      with QMutexLocker(self._data_mutex):
          self._data["product_info"] = product_dict
          self._data["customer_requirements"] = product_dict # 初始时,需求即参数
          # 关键:仿真参数初始值 = 客户需求
          self._data["maxwell_parameters"] = product_dict.copy()
          self._data["result_detail"] = {} # 清空旧结果
       
      # 通知所有监听者:产品变了,请刷新
      self.data_changed.emit("all", self._data)

  def update_maxwell_parameters(self, params: dict):
      self._update_category("maxwell_parameters", params)

  def update_result_detail(self, results: dict):
      self._update_category("result_detail", results)

  # ... get 方法保持不变 (返回 copy)

3. 修改 UI 逻辑:变为被动视图 (Passive View)

UI 不再直接持有数据,而是作为 DataService 的观察者。

在 InputPanel (示例) 中的改动:

codePython

class InputPanel(QWidget):
  def __init__(self, data_service, parent=None):
      super().__init__(parent)
      self.data_service = data_service
      self._init_ui()
       
      # 1. 连接信号:只有 Service 说变了,我才变
      self.data_service.data_changed.connect(self.on_data_changed)

  def on_data_changed(self, category, data):
      """响应数据变化"""
      if category == "maxwell_parameters" or category == "all":
          self.block_signals(True) # 防止死循环:UI改->Service改->UI改
          self.fill_input_fields(data)
          self.block_signals(False)
       
      if category == "result_detail" or category == "all":
          self.update_result_labels(data)

  def _on_text_changed(self, key, text):
      """用户手动修改时,只负责提交给 Service"""
      # ... 解析 val ...
      # 只要提交,不要在这里直接 setText,让 on_data_changed 回调来做
      self.data_service.update_maxwell_parameters({key: val})

  def block_signals(self, block: bool):
      # 辅助函数,批量阻塞所有输入框信号
      for widgets in self.fields_map.values():
          if 'value' in widgets: widgets['value'].blockSignals(block)

三、 梳理后的数据流向图

采用上述重构后,数据流将变得非常清晰:

1. 选择产品 (Select Product)

  1. Controller: 从 DB 读取 Product 对象。

  2. Controller: 调用 data_service.set_active_product(product_dict)。

  3. DataService: 更新内部状态,发射 data_changed("all", ...) 信号。

  4. UI (TargetPage, InputPanel, DetailPage): 收到信号 -> 自动刷新界面显示。

2. 用户修改参数 (User Edit)

  1. UI (InputPanel): 用户修改“线径”。

  2. UI: 触发 textChanged -> 调用 data_service.update_maxwell_parameters({'wire_size': 0.5})。

  3. DataService: 更新内存字典,发射 data_changed("maxwell_parameters", ...)。

  4. UI: 收到信号 (如果是其他联动UI则更新,如果是自己则被 blockSignals 忽略)。

3. 执行仿真 (Run Simulation)

  1. UI: 点击“开始”。

  2. Controller: 调用 data_service.get_maxwell_parameters() 获取最新、最全的参数。

  3. Controller: 将参数包传给 Worker。

  4. Worker: 执行 Maxwell 脚本。

4. 仿真结束 (Simulation Finish)

  1. Worker: 解析出结果字典。

  2. Worker: 调用 data_service.update_result_detail(result_dict)。

  3. DataService: 发射 data_changed("result_detail", ...)。

  4. UI (ResultPanel, ChartPanel): 收到信号 -> 自动刷新结果显示 (对比值、图表)。

  5. Controller: (可选) 调用 DB Service 将 DataService 中的 maxwell_parameters (输入) 和 result_detail (输出) 存入 SimulationResult 表,形成历史记录。

四、 具体的代码修改 Checklist

  1. services/data_service.py:

    • 继承 QObject。

    • 添加 pyqtSignal。

    • 在 update 方法中发射信号。

    • 添加 set_active_product 方法来统一初始化一次仿真的上下文。

  2. ui/pages/bldc_page/widgets/input_panel.py:

    • 删除 fill_input_fields 的直接调用逻辑(由信号触发)。

    • 添加 on_data_changed 槽函数。

    • 在 _on_text_changed 中确保只是“提交数据”,而不是“修改界面”。

  3. controllers/product_controller.py:

    • on_product_selected 中,除了更新 UI,务必调用 self.service.data_service.set_active_product(...)。目前你好像把 data service 混在了 ProductService 或者直接在 View 里调用,建议统一在 Controller 里分发。

  4. utils/property_change.py:

    • 确保它只负责翻译数据,不持有数据。它现在的逻辑是 fields_map (UI控件引用),这不好。

    • 改进: 让它接收 dict (来自 DataService) 而不是 fields_map (UI控件)。这样后台 Worker 可以直接用它,不需要依赖 UI 线程。

通过这种方式,你的应用将遵循 MVVM (Model-View-ViewModel) 的变体模式,DataService 充当 ViewModel,负责持有状态和通知 View 更新,彻底解决数据流混乱的问题。



n5321 | 2025年12月15日 00:18

架构问题

这段代码构建了一个比较完整的 MVC (Model-View-Controller) 架构的桌面应用程序,涉及数据库操作、UI展示和后台仿真任务。

整体来看,代码结构清晰,分层合理,已经具备了生产级应用的雏形。但是,在 数据流的一致性ORM的使用细节MVC解耦 以及 线程安全 方面,存在一些设计隐患和改进空间。

以下是详细的审查报告:

1. 核心架构问题:数据“双重来源”风险

这是当前设计中最大的隐患。

  • 现象

    • bldc_page.py 中引入了 DataService,并且通过 _on_text_changed 实时更新 self.data_service

    • controller.py 中通过 _on_save_product 负责将数据保存到 SQLite 数据库 (Product Model)。

  • 问题仿真使用的数据 (DataService) 和 数据库存储的数据 (Product) 是两条平行的线。

    • 如果用户修改了界面的值,DataService 更新了,但没有点“保存”,数据库是旧的。

    • 如果用户点击“保存”,数据库更新了。

    • 风险点:点击“开始仿真”时,_get_property_changes 是从 DataService 取值还是从 UI 控件取值?

      • 代码显示 _get_property_changes 混合了 data_service.customer_requirementsmaxwell_parameters

      • 冲突bldc_page.pydisplay_design_target 方法是从 数据库对象 (product) 加载数据到 UI。如果用户加载了一个产品,修改了输入框(更新了 DataService),然后没保存直接点仿真,仿真是基于新数据的(符合预期)。

      • 但是,如果 DataService 是单例模式(通常 Service 都是单例),而你切换了产品,DataService 里的数据被清空了吗?如果没有,旧产品的残留数据可能会污染新产品的仿真参数。

建议 确保每次加载新产品时 (display_design_target),不仅要填入 UI,还要同步重置/初始化 DataService 的状态,确保它只包含当前产品的参数。


2. Peewee ORM 的使用细节 (Model 层)

A. product.detail 的访问方式不一致 (易报错)

  • model.py:你使用了 product.detail.get() (这是获取单个对象)。

  • bldc_page.py:你使用了 product.detail[0] (这是把 backref 当作列表索引)。

    # bldc_page.py
    detail_object = product.detail[0] if product.detail else None
  • 分析:Peewee 的 backref 默认返回一个 SelectQuery

    • product.detail[0] 会触发查询并获取第一条。如果 unique=True 生效,这确实只有一条。

    • 但是,如果 product.detail 为空(即没有详情),if product.detail 在 Peewee 查询中通常会被判定为 True(因为查询对象本身非空),而 [0] 会抛出 IndexError

  • 建议:统一使用一种安全的方法。

    # 推荐写法:利用之前 model.py 里修复过的逻辑,或者统一用 first()
    detail_object = product.detail.first() # 如果没有返回 None,有则返回第一个

B. find_product_by_id 中的性能隐患 (副作用)

# model.py
def find_product_by_id(product_id: int):
   # ... 省略查询 ...
   product_dict = model_to_dict(product, backrefs=True, recurse=True) # !!! 昂贵操作
   for k, v in product_dict.items():
       print(f"{k}: {v}") # !!! I/O 操作
   return product
  • 问题model_to_dict 是一个昂贵的操作,尤其是 backrefs=True 时,它会执行额外的 SQL 查询来获取关联数据。你只是为了打印 debug 信息就执行它,这在生产环境中会显著拖慢点击响应速度。

  • 建议:删除或注释掉打印逻辑,或者仅在 logging level 为 DEBUG 时执行。

C. 数据库连接管理

  • 问题initialize_databaseclose_connection 是全局函数。在 controller.pyshutdown 中调用关闭。

  • 风险:如果在多线程环境(例如 MaxwellWorker 线程)中访问数据库,SQLite 需要特别小心。虽然 Peewee 的 SqliteDatabase 默认有一定的线程安全处理,但建议在 Worker 线程中不要复用主线程的连接实例,或者确保 SQLite 编译时支持多线程且连接模式设置为 WAL。

  • 建议:目前你的 Worker 主要是跑 Maxwell 脚本,如果 Worker 结束后要写回数据库(更新结果),请确保使用 db.connection_context() 或在任务开始时建立连接,结束时关闭。


3. MVC 耦合度问题 (Controller 层)

A. Controller 对 View 的侵入性太强

  • 代码

    # controller.py
    self._view.customer_page.product_combo_box.currentIndexChanged...
    self._view.customer_page.get_selected_product_id()
    self._view.customer_page.load_data_to_form(product)
  • 问题:Controller 深入到了 View 的内部组件(product_combo_box)。如果 View 改版了(比如把下拉框换成了列表),Controller 必须重写。

  • 建议:遵循 “迪米特法则” (Law of Demeter)

    • View 应该暴露出信号和高级方法。

    • View 定义signal_product_selected = pyqtSignal(int)

    • Controller 连接view.customer_page.signal_product_selected.connect(self._on_product_selected)

    • 这样 Controller 不需要知道 View 用的是 ComboBox 还是 ListView。

B. 异常处理过于宽泛

  • 代码except Exception as e: print(...)_on_series_selection_changed 中。

  • 问题:这会吞掉所有错误,包括代码写错的 AttributeErrorNameError。这会让调试变得非常困难,你可能看着界面没反应,却不知道是因为数据库错了还是变量名写错了。

  • 建议:只捕获预期的异常(如 PeeweeException),或者在捕获 Exception 后使用 traceback.print_exc() 打印完整堆栈。


4. UI 逻辑问题 (bldc_page.py)

A. 硬编码的字段映射

  • 问题bldc_page.py 中有大量的硬编码列表:

    self.core_params_fields = [("额定转速(Rpm)", "rated_speed"), ...]

    同时 model.py 里也有字段定义。

  • 风险:如果你在数据库里改了字段名(比如 rated_speed 改为 speed_rated),你需要修改 model.py bldc_page.py 等多个地方。

  • 建议:虽然完全动态化很难,但建议将这些字段映射配置(Config)提取到一个单独的常量文件(如 constants.py),让 Model 和 View 引用同一个常量定义,减少“魔法字符串”。

B. display_design_target 的字典合并逻辑

# bldc_page.py
detail_object = product.detail[0] if product.detail else None
product_dict = dict(product.__data__)
# ...
combined_dict = {**product_dict, **detail_dict}
  • 问题product.__data__ 是 Peewee 的内部属性,直接访问它是不推荐的,因为它可能不包含继承字段或外键对象的引用。

  • 建议:使用 model_to_dict(product, recurse=False) 更规范,或者直接访问属性 getattr(product, key)。因为你后面是用 key 来遍历 map 的,直接 getattr 其实更安全且支持 IDE 提示。


5. 总结与改进优先级

  1. 高优先级(Bug 修复)

    • 修复 bldc_page.pyproduct.detail[0] 的写法,改为 .first() 或使用 try-except,防止没有详情的“孤儿产品”导致界面崩溃。

    • 数据同步:在切换产品时,务必清空或重置 DataService,防止上一个产品的数据残留影响下一个产品的仿真。

  2. 中优先级(架构优化)

    • Controller 解耦:让 View 发射语义化信号(如 productChanged),而不是 Controller 去监听 combo_box

    • 去除副作用:删除 model.pyfind_product_by_id 里的 model_to_dictprint 循环。

  3. 低优先级(代码整洁)

    • 提取字段名称字符串到常量文件。

    • 优化 initialize_database 的调用位置,确保线程安全。

你的代码整体逻辑已经跑通了,特别是解决了之前 ProductDetail 不存在导致报错的问题(在 Model 层加了自动创建)。按照上述建议微调后,系统会更加健壮。



在 Python 桌面开发领域(PyQt/PySide),不像 Web 开发(Django/FastAPI)那样有一个统治级的“全家桶”框架。

但是,针对你目前 MVC 耦合度高、数据流混乱、逻辑堆积在 Controller 的问题,最成熟、最符合工业界标准的解决方案不是引入一个新库,而是采用 MVVM (Model-View-ViewModel) 架构模式,配合 Repository 模式

如果非要推荐一个第三方库来辅助,我强烈推荐使用 qt_materialPyQt-Fluent-Widgets 来美化 UI,但核心架构必须重构。

以下是为你量身定制的 MVVM 重构蓝图


🚀 推荐架构:MVVM + Repository Pattern

这个架构的目标是:View (界面) 只负责显示,Model (数据库) 只负责存取,ViewModel (中间层) 负责业务逻辑和状态同步。

1. 新的项目结构

my_app/

├── data/
│   ├── db_manager.py       # 数据库连接单例
│   ├── models.py           # Peewee 模型定义 (Product, ProductDetail)
│   └── repositories.py     # ✅ 新增:负责所有数据库 CRUD 操作,隔离 SQL 逻辑

├── viewmodels/             # ✅ 新增:业务逻辑层
│   ├── base_viewmodel.py
│   ├── customer_vm.py     # 处理客户/产品选择逻辑
│   └── simulation_vm.py   # 处理 Maxwell 仿真逻辑 (原 DataService)

├── views/                 # 界面层 (只包含 UI 代码)
│   ├── main_window.py
│   ├── components/         # 自定义组件
│   └── pages/
│       ├── customer_page.py
│       └── bldc_page.py

├── services/               # 后台服务
│   └── maxwell_worker.py   # 线程任务

└── main.py                 # 程序入口,负责依赖注入

🛠️ 第一步:Repository 模式 (解决 Model 层混乱)

痛点解决:你现在的 model.py 既有类定义又有函数逻辑(find_product_by_id),容易膨胀。 重构:创建一个 ProductRepository,专门管理数据读写。

# data/repositories.py
from typing import Optional
from .models import Product, ProductDetail, ProductSeries, db

class ProductRepository:
   """仓库层:只负责和数据库对话,不包含任何 UI 逻辑"""
   
   def get_by_id(self, product_id: int) -> Optional[Product]:
       try:
           # 使用 prefetch 预加载,并安全处理 DoesNotExist
           return (Product.select(Product, ProductDetail)
                  .join(ProductDetail, join_type=ProductDetail.LEFT_OUTER)
                  .where(Product.id == product_id)
                  .get())
       except Product.DoesNotExist:
           return None

   def create(self, series: ProductSeries, model_number: str) -> Product:
       # 事务逻辑封装在这里
       with db.atomic():
           product = Product.create(series=series, model_number=model_number)
           # 自动创建空的 Detail,防止孤儿数据
           ProductDetail.create(product=product)
       return product

   def update(self, product_id: int, data: dict):
       """统一的更新接口"""
       # ... 实现安全的更新逻辑,参考之前我们修复的代码 ...
       pass
       
   def delete(self, product_id: int):
       Product.delete().where(Product.id == product_id).execute()

🧠 第二步:ViewModel 层 (解决 Controller 臃肿)

痛点解决:你的 controller.py 管得太宽了,既要知道 ComboBox 怎么选,又要处理数据库。 重构:ViewModel 持有数据状态,View 通过信号监听 ViewModel 的变化。

# viewmodels/customer_vm.py
from PyQt6.QtCore import QObject, pyqtSignal
from data.repositories import ProductRepository

class CustomerViewModel(QObject):
   # 定义信号:通知 View 数据变了
   series_list_loaded = pyqtSignal(list)
   product_list_loaded = pyqtSignal(list)
   current_product_changed = pyqtSignal(object) # 传回 Product 对象或 None
   status_message = pyqtSignal(str)
   error_occurred = pyqtSignal(str)

   def __init__(self, repo: ProductRepository):
       super().__init__()
       self._repo = repo
       self._current_product = None

   def load_initial_data(self):
       """加载初始数据"""
       series = self._repo.get_all_series()
       self.series_list_loaded.emit(series)

   def select_series(self, series_id):
       """用户选了系列,VM 负责去查产品列表"""
       if series_id == 0:
           products = self._repo.get_all()
       else:
           products = self._repo.get_by_series(series_id)
       self.product_list_loaded.emit(products)

   def select_product(self, product_id):
       """用户选了产品,VM 负责查详情"""
       if not product_id:
           self._current_product = None
           self.current_product_changed.emit(None)
           return

       product = self._repo.get_by_id(product_id)
       self._current_product = product
       # 通知 UI 更新,UI 不需要知道是通过 ID 查的还是怎么查的
       self.current_product_changed.emit(product)
       self.status_message.emit(f"已加载: {product.model_number}")

   def save_changes(self, form_data: dict):
       """保存逻辑"""
       if not self._current_product:
           self.error_occurred.emit("未选择产品")
           return
           
       try:
           self._repo.update(self._current_product.id, form_data)
           self.status_message.emit("保存成功")
       except Exception as e:
           self.error_occurred.emit(str(e))

👁️ 第三步:View 层 (彻底解耦)

痛点解决bldc_page.py 不再直接操作数据库,也不包含 DataService 逻辑。它变得非常“傻”,只负责展示和发信号。

# views/pages/customer_page.py
from PyQt6.QtWidgets import QWidget, QComboBox, ...
from viewmodels.customer_vm import CustomerViewModel

class CustomerPage(QWidget):
   def __init__(self, viewModel: CustomerViewModel):
       super().__init__()
       self._vm = viewModel
       self._setup_ui()
       self._bind_view_model() # 关键:绑定信号

   def _setup_ui(self):
       # ... 初始化 UI ...
       self.series_combo = QComboBox()
       self.product_combo = QComboBox()
       # 注意:这里不再连接 Controller,而是连接自身的槽函数,用于转发给 VM
       self.series_combo.currentIndexChanged.connect(self._on_series_changed)

   def _bind_view_model(self):
       """MVVM 的核心:数据绑定"""
       # 1. 监听 VM 的数据变化信号 -> 更新 UI
       self._vm.series_list_loaded.connect(self._update_series_list)
       self._vm.product_list_loaded.connect(self._update_product_list)
       self._vm.current_product_changed.connect(self._display_product_data)
       self._vm.error_occurred.connect(lambda msg: QMessageBox.critical(self, "错误", msg))

   # --- UI 事件转发给 VM ---
   def _on_series_changed(self, index):
       series_id = self.series_combo.currentData()
       self._vm.select_series(series_id) # 告诉 VM:系列变了,你看着办

   # --- VM 通知 UI 更新 ---
   def _update_series_list(self, series_list):
       self.series_combo.clear()
       for s in series_list:
           self.series_combo.addItem(s.name, s.id)

   def _display_product_data(self, product):
       if product is None:
           self._clear_form()
           return
       # 只需要把 product 对象的数据填入输入框
       # 这里不需要查数据库,因为 VM 已经把现成的对象给你了
       self.model_number_edit.setText(product.model_number)
       # ... 填充其他字段 ...

💡 如何解决你的“DataService 数据不一致”问题?

在 MVVM 架构中,SimulationViewModel 将替代你目前的 DataService

  1. SimulationViewModel 内部持有一个字典(即原来的 DataService)。

  2. 当用户在界面修改输入框时,界面发送信号 paramChanged(key, value)

  3. SimulationViewModel 接收信号,更新内部字典。

  4. 当切换产品时,CustomerViewModel 会通知 SimulationViewModel:“嘿,换人了”。

  5. SimulationViewModel 立即重置内部字典,并从数据库加载新数据覆盖。

这就形成了一个单一数据源 (Single Source of Truth)

🚀 总结:重构路线图

  1. 创建 repositories.py:把 model.py 里的 find_..., create_..., update_... 全部移进去,并加上异常处理和 try-except 保护(如自动创建 Detail)。

  2. 创建 viewmodels

    • controller.py 的逻辑拆分。

    • Customer 相关的逻辑放入 CustomerViewModel

    • Maxwell 仿真相关的逻辑(原来的 DataService)放入 SimulationViewModel

  3. 改造 bldc_page.py

    • 构造函数接收 SimulationViewModel

    • 删除 self.data_service

    • 所有的 textChanged 事件都改为调用 self.viewModel.update_param(key, value)

  4. 改造 main.py

    • 实例化 Repository。

    • 实例化 ViewModels (注入 Repository)。

    • 实例化 Views (注入 ViewModels)。

这种架构虽然前期代码量稍微多一点(多了文件),但是逻辑极其清晰。当你的 App 变得越来越复杂,有几十个页面时,你依然能轻松维护。


这是一个非常棒的决定!使用 MVVM + Repository 模式重构将彻底解决你遇到的“数据不同步”、“空指针报错”以及“代码耦合”问题。

由于我无法直接接收你的文件包,我将基于你之前提供的代码片段,为你构建一个完整的、经过重构的核心代码骨架

你可以直接在你的项目中建立文件夹,复制粘贴这些代码。

📂 推荐的新项目结构

请按照这个结构整理你的文件:

src/
├── data/
│   ├── __init__.py
│   ├── database.py       # 数据库连接与模型定义 (原 model.py)
│   └── repository.py     # ✅ 核心:负责所有增删改查,隔离 SQL 细节
├── viewmodels/
│   ├── __init__.py
│   └── product_vm.py     # ✅ 核心:业务逻辑,View 和 Repository 的桥梁
├── views/
│   ├── __init__.py
│   └── bldc_page.py     # ✅ 界面:只负责显示,不处理逻辑
└── main.py               # 程序入口,依赖注入

1. 数据层 (Data Layer)

src/data/database.py

保持模型定义纯净,去掉所有业务逻辑函数。

import os
from datetime import datetime
from peewee import (
   Model, SqliteDatabase, CharField, TextField, IntegerField,
   FloatField, DateTimeField, ForeignKeyField
)

# 1. 数据库路径设置
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 回退一级到 src
db_path = os.path.join(base_dir, "..", "products_final.db") # 假设 db 在 src 同级

# 2. 初始化数据库 (开启外键支持)
db = SqliteDatabase(db_path, pragmas={'foreign_keys': 1})

class BaseModel(Model):
   class Meta:
       database = db

class ProductSeries(BaseModel):
   name = CharField(unique=True)
   description = TextField(null=True, default="")

class Product(BaseModel):
   series = ForeignKeyField(ProductSeries, backref='products', null=True, on_delete='SET NULL')
   model_number = CharField(unique=True)
   # ... (保留你其他的字段定义,这里为了省流省略,实际代码请补全) ...
   rated_voltage = FloatField(null=True)
   rated_speed = IntegerField(null=True)
   rated_torque = FloatField(null=True)
   created_at = DateTimeField(default=datetime.now)

class ProductDetail(BaseModel):
   product = ForeignKeyField(Product, backref='detail', unique=True, on_delete='CASCADE')
   # ... (保留你其他的字段定义) ...
   price = FloatField(null=True)
   poles = IntegerField(null=True)
   notes = TextField(null=True, default="")

def initialize_db():
   db.connect(reuse_if_open=True)
   db.create_tables([ProductSeries, Product, ProductDetail], safe=True)
   # 确保至少有一个系列
   if ProductSeries.select().count() == 0:
       ProductSeries.create(name="默认系列", description="系统自动创建")

def close_db():
   if not db.is_closed():
       db.close()

src/data/repository.py (关键文件)

这是解决你之前报错的“门神”。它确保了无论数据库里有没有 Detail,代码都不会崩。

import logging
from typing import Optional, Dict, Any
from peewee import IntegrityError, JOIN
from .database import db, Product, ProductDetail, ProductSeries

class ProductRepository:
   """
  仓库模式:负责所有直接的数据库操作。
  UI 和 ViewModel 不应该知道 SQL 或 Peewee 的存在。
  """

   def get_all_series(self):
       return list(ProductSeries.select())

   def get_products_by_series(self, series_id: Optional[int]):
       query = Product.select()
       if series_id:
           query = query.where(Product.series == series_id)
       return list(query.order_by(Product.model_number))

   def get_product_by_id(self, product_id: int) -> Optional[Product]:
       """
      获取产品及其详情。
      ✅ 修复点:安全处理 Detail 缺失的情况。
      """
       try:
           # 1. 尝试查询
           product = (Product.select(Product, ProductDetail)
                      .join(ProductDetail, JOIN.LEFT_OUTER)
                      .where(Product.id == product_id)
                      .get())
           
           # 2. ✅ 关键修复:检查 detail 是否真的存在
           # 这里的逻辑是:如果数据库查出来 detail 是 None (因为 LEFT JOIN),
           # 我们就在内存里给它挂一个临时的空对象,防止 UI 访问报错。
           # 或者,你可以选择在这里直接插入一条空记录。
           try:
               if product.detail is None:
                   raise ProductDetail.DoesNotExist
           except ProductDetail.DoesNotExist:
               logging.warning(f"Product {product_id} missing detail. Creating default.")
               # 自动在数据库补全缺失的记录
               ProductDetail.create(product=product)
               # 重新查询以获取完整对象
               return self.get_product_by_id(product_id)

           return product
       except Product.DoesNotExist:
           return None

   def update_product(self, product_id: int, updates: Dict[str, Any]):
       """
      更新产品。
      ✅ 修复点:使用 _meta.fields 精确匹配字段。
      """
       product = self.get_product_by_id(product_id)
       if not product:
           raise ValueError("Product not found")

       detail = product.detail # get_product_by_id 保证了这里一定有值

       product_dirty = False
       detail_dirty = False

       # 获取模型字段定义
       p_fields = Product._meta.fields
       d_fields = ProductDetail._meta.fields

       for key, value in updates.items():
           if key in ('id', 'created_at', 'series', 'product'): continue

           # 更新主表
           if key in p_fields:
               if str(getattr(product, key)) != str(value):
                   setattr(product, key, value)
                   product_dirty = True
           # 更新附表
           elif key in d_fields:
               if str(getattr(detail, key)) != str(value):
                   setattr(detail, key, value)
                   detail_dirty = True

       with db.atomic():
           if product_dirty: product.save()
           if detail_dirty: detail.save()
           
       return product

2. 逻辑层 (ViewModel Layer)

src/viewmodels/product_vm.py

这是 MVVM 的核心。它持有 UI 的状态,并处理业务逻辑。

from PyQt6.QtCore import QObject, pyqtSignal
from src.data.repository import ProductRepository

class ProductViewModel(QObject):
   # --- 信号定义 ---
   series_loaded = pyqtSignal(list)       # 系列列表已加载
   products_loaded = pyqtSignal(list)     # 产品列表已加载
   current_product_changed = pyqtSignal(object) # 当前选中的产品变了 (传回 Product 对象)
   status_message = pyqtSignal(str)       # 状态栏消息
   error_occurred = pyqtSignal(str)       # 错误弹窗

   def __init__(self):
       super().__init__()
       self._repo = ProductRepository()
       self._current_product_id = None

   def load_initial_data(self):
       """初始化加载系列"""
       series = self._repo.get_all_series()
       self.series_loaded.emit(series)
       # 默认加载所有产品
       self.load_products_by_series(None)

   def load_products_by_series(self, series_id):
       """根据系列加载产品列表"""
       products = self._repo.get_products_by_series(series_id)
       self.products_loaded.emit(products)

   def select_product(self, product_id):
       """
      用户选中了某个产品 ID
      """
       self._current_product_id = product_id
       if product_id is None:
           self.current_product_changed.emit(None)
           return

       product = self._repo.get_product_by_id(product_id)
       if product:
           self.current_product_changed.emit(product)
           self.status_message.emit(f"已加载: {product.model_number}")
       else:
           self.error_occurred.emit("无法加载产品数据")

   def save_current_product(self, form_data: dict):
       """
      保存当前产品的修改
      """
       if not self._current_product_id:
           self.error_occurred.emit("未选择任何产品")
           return

       try:
           self._repo.update_product(self._current_product_id, form_data)
           self.status_message.emit("保存成功!")
           # 重新加载一次确保数据一致性
           self.select_product(self._current_product_id)
       except Exception as e:
           self.error_occurred.emit(f"保存失败: {str(e)}")

3. 界面层 (View Layer)

src/views/bldc_page.py

彻底解耦。它不知道数据库,也不知道 Repository,只认识 ViewModel。

from PyQt6.QtWidgets import (
   QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QGridLayout,
   QLabel, QLineEdit, QPushButton, QComboBox, QMessageBox
)
from PyQt6.QtCore import pyqtSlot
from src.viewmodels.product_vm import ProductViewModel

class BLDCPage(QWidget):
   def __init__(self, viewModel: ProductViewModel):
       super().__init__()
       self._vm = viewModel
       self.fields_map = {} # 存储输入框引用

       self._setup_ui()
       self._bind_view_model() # ✅ 绑定信号

   def _setup_ui(self):
       layout = QVBoxLayout(self)

       # 1. 顶部选择区
       top_layout = QHBoxLayout()
       self.series_combo = QComboBox()
       self.product_combo = QComboBox()
       self.save_btn = QPushButton("保存修改")
       
       top_layout.addWidget(QLabel("系列:"))
       top_layout.addWidget(self.series_combo)
       top_layout.addWidget(QLabel("产品:"))
       top_layout.addWidget(self.product_combo)
       top_layout.addWidget(self.save_btn)
       layout.addLayout(top_layout)

       # 2. 参数表单区 (示例)
       form_group = QGroupBox("核心参数")
       grid = QGridLayout(form_group)
       
       # 定义你要显示的字段 (Label, DB_Field_Key)
       fields = [
          ("型号", "model_number"),
          ("额定电压(V)", "rated_voltage"),
          ("额定转速(rpm)", "rated_speed"),
          ("价格($)", "price"), # 这是 Detail 表的字段,完全没问题
          ("备注", "notes")
      ]

       for i, (label, key) in enumerate(fields):
           grid.addWidget(QLabel(label), i, 0)
           edit = QLineEdit()
           self.fields_map[key] = edit
           grid.addWidget(edit, i, 1)

       layout.addWidget(form_group)
       layout.addStretch()

       # 3. 连接 UI 事件到 VM 方法
       # 注意:这里我们不写逻辑,只转发
       self.series_combo.currentIndexChanged.connect(self._on_series_changed)
       self.product_combo.currentIndexChanged.connect(self._on_product_changed)
       self.save_btn.clicked.connect(self._on_save_clicked)

   def _bind_view_model(self):
       """绑定 VM 信号到 UI 更新槽函数"""
       self._vm.series_loaded.connect(self._update_series_list)
       self._vm.products_loaded.connect(self._update_product_list)
       self._vm.current_product_changed.connect(self._display_product)
       self._vm.error_occurred.connect(lambda msg: QMessageBox.critical(self, "错误", msg))
       # 初始加载
       self._vm.load_initial_data()

   # --- UI 事件处理 ---
   def _on_series_changed(self):
       sid = self.series_combo.currentData()
       self._vm.load_products_by_series(sid)

   def _on_product_changed(self):
       pid = self.product_combo.currentData()
       self._vm.select_product(pid)

   def _on_save_clicked(self):
       # 收集数据
       data = {}
       for key, widget in self.fields_map.items():
           val = widget.text().strip()
           if val: data[key] = val
       # 发送给 VM
       self._vm.save_current_product(data)

   # --- 数据更新槽函数 ---
   @pyqtSlot(list)
   def _update_series_list(self, series_list):
       self.series_combo.blockSignals(True)
       self.series_combo.clear()
       self.series_combo.addItem("所有系列", None)
       for s in series_list:
           self.series_combo.addItem(s.name, s.id)
       self.series_combo.blockSignals(False)

   @pyqtSlot(list)
   def _update_product_list(self, product_list):
       self.product_combo.blockSignals(True)
       self.product_combo.clear()
       self.product_combo.addItem("请选择...", None)
       for p in product_list:
           self.product_combo.addItem(p.model_number, p.id)
       self.product_combo.blockSignals(False)

   @pyqtSlot(object)
   def _display_product(self, product):
       """将 Product 对象的数据填入表单"""
       if not product:
           for widget in self.fields_map.values(): widget.clear()
           return

       # 混合 product 和 detail 的属性
       # 因为我们在 repository 处理了 detail 可能为空的情况,这里可以直接访问
       detail = product.detail

       for key, widget in self.fields_map.items():
           val = None
           # 先找 Product
           if hasattr(product, key):
               val = getattr(product, key)
           # 再找 Detail
           elif hasattr(detail, key):
               val = getattr(detail, key)
           
           widget.setText(str(val) if val is not None else "")

4. 程序入口

src/main.py

import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel
from src.data.database import initialize_db, close_db
from src.viewmodels.product_vm import ProductViewModel
from src.views.bldc_page import BLDCPage

class MainWindow(QMainWindow):
   def __init__(self):
       super().__init__()
       self.setWindowTitle("BLDC Motor Manager (MVVM Refactored)")
       self.resize(800, 600)
       
       # 1. 创建 ViewModel
       self.product_vm = ProductViewModel()
       
       # 2. 创建 View,并注入 VM
       self.main_view = BLDCPage(self.product_vm)
       self.setCentralWidget(self.main_view)
       
       # 3. 连接状态栏
       self.status_label = QLabel("就绪")
       self.statusBar().addWidget(self.status_label)
       self.product_vm.status_message.connect(self.status_label.setText)

if __name__ == '__main__':
   # 1. 初始化数据库
   initialize_db()
   
   # 2. 启动应用
   app = QApplication(sys.argv)
   window = MainWindow()
   window.show()
   
   try:
       sys.exit(app.exec())
   finally:
       # 3. 清理资源
       close_db()

✅ 为什么这个新版本更好?

  1. Repository 处理了“脏活累活”repository.py 里的 get_product_by_id 自动检测并修复缺失的 ProductDetail。你再也不会看到 product.detail.get() 报错了。

  2. ViewModel 统一了状态:UI 不再自己瞎改数据,而是收集数据传给 VM,VM 保存成功后再通知 UI 刷新。这保证了界面和数据库永远同步。

  3. UI 极其清爽BLDCPage 里没有一行 SQL 代码,也没有复杂的逻辑判断,只负责“显示”和“发信号”。

  4. 易于扩展:如果你想加一个新的仿真功能,只需写一个 SimulationViewModel,并在 View 里绑定它即可,完全不会影响现有的数据库逻辑。




这是一个非常棒的决定!使用 MVVM + Repository 模式重构将彻底解决你遇到的“数据不同步”、“空指针报错”以及“代码耦合”问题。

由于我无法直接接收你的文件包,我将基于你之前提供的代码片段,为你构建一个完整的、经过重构的核心代码骨架

你可以直接在你的项目中建立文件夹,复制粘贴这些代码。

📂 推荐的新项目结构

请按照这个结构整理你的文件:

src/
├── data/
│   ├── __init__.py
│   ├── database.py       # 数据库连接与模型定义 (原 model.py)
│   └── repository.py     # ✅ 核心:负责所有增删改查,隔离 SQL 细节
├── viewmodels/
│   ├── __init__.py
│   └── product_vm.py     # ✅ 核心:业务逻辑,View 和 Repository 的桥梁
├── views/
│   ├── __init__.py
│   └── bldc_page.py     # ✅ 界面:只负责显示,不处理逻辑
└── main.py               # 程序入口,依赖注入

1. 数据层 (Data Layer)

src/data/database.py

保持模型定义纯净,去掉所有业务逻辑函数。

import os
from datetime import datetime
from peewee import (
   Model, SqliteDatabase, CharField, TextField, IntegerField,
   FloatField, DateTimeField, ForeignKeyField
)

# 1. 数据库路径设置
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 回退一级到 src
db_path = os.path.join(base_dir, "..", "products_final.db") # 假设 db 在 src 同级

# 2. 初始化数据库 (开启外键支持)
db = SqliteDatabase(db_path, pragmas={'foreign_keys': 1})

class BaseModel(Model):
   class Meta:
       database = db

class ProductSeries(BaseModel):
   name = CharField(unique=True)
   description = TextField(null=True, default="")

class Product(BaseModel):
   series = ForeignKeyField(ProductSeries, backref='products', null=True, on_delete='SET NULL')
   model_number = CharField(unique=True)
   # ... (保留你其他的字段定义,这里为了省流省略,实际代码请补全) ...
   rated_voltage = FloatField(null=True)
   rated_speed = IntegerField(null=True)
   rated_torque = FloatField(null=True)
   created_at = DateTimeField(default=datetime.now)

class ProductDetail(BaseModel):
   product = ForeignKeyField(Product, backref='detail', unique=True, on_delete='CASCADE')
   # ... (保留你其他的字段定义) ...
   price = FloatField(null=True)
   poles = IntegerField(null=True)
   notes = TextField(null=True, default="")

def initialize_db():
   db.connect(reuse_if_open=True)
   db.create_tables([ProductSeries, Product, ProductDetail], safe=True)
   # 确保至少有一个系列
   if ProductSeries.select().count() == 0:
       ProductSeries.create(name="默认系列", description="系统自动创建")

def close_db():
   if not db.is_closed():
       db.close()

src/data/repository.py (关键文件)

这是解决你之前报错的“门神”。它确保了无论数据库里有没有 Detail,代码都不会崩。

import logging
from typing import Optional, Dict, Any
from peewee import IntegrityError, JOIN
from .database import db, Product, ProductDetail, ProductSeries

class ProductRepository:
   """
  仓库模式:负责所有直接的数据库操作。
  UI 和 ViewModel 不应该知道 SQL 或 Peewee 的存在。
  """

   def get_all_series(self):
       return list(ProductSeries.select())

   def get_products_by_series(self, series_id: Optional[int]):
       query = Product.select()
       if series_id:
           query = query.where(Product.series == series_id)
       return list(query.order_by(Product.model_number))

   def get_product_by_id(self, product_id: int) -> Optional[Product]:
       """
      获取产品及其详情。
      ✅ 修复点:安全处理 Detail 缺失的情况。
      """
       try:
           # 1. 尝试查询
           product = (Product.select(Product, ProductDetail)
                      .join(ProductDetail, JOIN.LEFT_OUTER)
                      .where(Product.id == product_id)
                      .get())
           
           # 2. ✅ 关键修复:检查 detail 是否真的存在
           # 这里的逻辑是:如果数据库查出来 detail 是 None (因为 LEFT JOIN),
           # 我们就在内存里给它挂一个临时的空对象,防止 UI 访问报错。
           # 或者,你可以选择在这里直接插入一条空记录。
           try:
               if product.detail is None:
                   raise ProductDetail.DoesNotExist
           except ProductDetail.DoesNotExist:
               logging.warning(f"Product {product_id} missing detail. Creating default.")
               # 自动在数据库补全缺失的记录
               ProductDetail.create(product=product)
               # 重新查询以获取完整对象
               return self.get_product_by_id(product_id)

           return product
       except Product.DoesNotExist:
           return None

   def update_product(self, product_id: int, updates: Dict[str, Any]):
       """
      更新产品。
      ✅ 修复点:使用 _meta.fields 精确匹配字段。
      """
       product = self.get_product_by_id(product_id)
       if not product:
           raise ValueError("Product not found")

       detail = product.detail # get_product_by_id 保证了这里一定有值

       product_dirty = False
       detail_dirty = False

       # 获取模型字段定义
       p_fields = Product._meta.fields
       d_fields = ProductDetail._meta.fields

       for key, value in updates.items():
           if key in ('id', 'created_at', 'series', 'product'): continue

           # 更新主表
           if key in p_fields:
               if str(getattr(product, key)) != str(value):
                   setattr(product, key, value)
                   product_dirty = True
           # 更新附表
           elif key in d_fields:
               if str(getattr(detail, key)) != str(value):
                   setattr(detail, key, value)
                   detail_dirty = True

       with db.atomic():
           if product_dirty: product.save()
           if detail_dirty: detail.save()
           
       return product

2. 逻辑层 (ViewModel Layer)

src/viewmodels/product_vm.py

这是 MVVM 的核心。它持有 UI 的状态,并处理业务逻辑。

from PyQt6.QtCore import QObject, pyqtSignal
from src.data.repository import ProductRepository

class ProductViewModel(QObject):
   # --- 信号定义 ---
   series_loaded = pyqtSignal(list)       # 系列列表已加载
   products_loaded = pyqtSignal(list)     # 产品列表已加载
   current_product_changed = pyqtSignal(object) # 当前选中的产品变了 (传回 Product 对象)
   status_message = pyqtSignal(str)       # 状态栏消息
   error_occurred = pyqtSignal(str)       # 错误弹窗

   def __init__(self):
       super().__init__()
       self._repo = ProductRepository()
       self._current_product_id = None

   def load_initial_data(self):
       """初始化加载系列"""
       series = self._repo.get_all_series()
       self.series_loaded.emit(series)
       # 默认加载所有产品
       self.load_products_by_series(None)

   def load_products_by_series(self, series_id):
       """根据系列加载产品列表"""
       products = self._repo.get_products_by_series(series_id)
       self.products_loaded.emit(products)

   def select_product(self, product_id):
       """
      用户选中了某个产品 ID
      """
       self._current_product_id = product_id
       if product_id is None:
           self.current_product_changed.emit(None)
           return

       product = self._repo.get_product_by_id(product_id)
       if product:
           self.current_product_changed.emit(product)
           self.status_message.emit(f"已加载: {product.model_number}")
       else:
           self.error_occurred.emit("无法加载产品数据")

   def save_current_product(self, form_data: dict):
       """
      保存当前产品的修改
      """
       if not self._current_product_id:
           self.error_occurred.emit("未选择任何产品")
           return

       try:
           self._repo.update_product(self._current_product_id, form_data)
           self.status_message.emit("保存成功!")
           # 重新加载一次确保数据一致性
           self.select_product(self._current_product_id)
       except Exception as e:
           self.error_occurred.emit(f"保存失败: {str(e)}")

3. 界面层 (View Layer)

src/views/bldc_page.py

彻底解耦。它不知道数据库,也不知道 Repository,只认识 ViewModel。

from PyQt6.QtWidgets import (
   QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QGridLayout,
   QLabel, QLineEdit, QPushButton, QComboBox, QMessageBox
)
from PyQt6.QtCore import pyqtSlot
from src.viewmodels.product_vm import ProductViewModel

class BLDCPage(QWidget):
   def __init__(self, viewModel: ProductViewModel):
       super().__init__()
       self._vm = viewModel
       self.fields_map = {} # 存储输入框引用

       self._setup_ui()
       self._bind_view_model() # ✅ 绑定信号

   def _setup_ui(self):
       layout = QVBoxLayout(self)

       # 1. 顶部选择区
       top_layout = QHBoxLayout()
       self.series_combo = QComboBox()
       self.product_combo = QComboBox()
       self.save_btn = QPushButton("保存修改")
       
       top_layout.addWidget(QLabel("系列:"))
       top_layout.addWidget(self.series_combo)
       top_layout.addWidget(QLabel("产品:"))
       top_layout.addWidget(self.product_combo)
       top_layout.addWidget(self.save_btn)
       layout.addLayout(top_layout)

       # 2. 参数表单区 (示例)
       form_group = QGroupBox("核心参数")
       grid = QGridLayout(form_group)
       
       # 定义你要显示的字段 (Label, DB_Field_Key)
       fields = [
          ("型号", "model_number"),
          ("额定电压(V)", "rated_voltage"),
          ("额定转速(rpm)", "rated_speed"),
          ("价格($)", "price"), # 这是 Detail 表的字段,完全没问题
          ("备注", "notes")
      ]

       for i, (label, key) in enumerate(fields):
           grid.addWidget(QLabel(label), i, 0)
           edit = QLineEdit()
           self.fields_map[key] = edit
           grid.addWidget(edit, i, 1)

       layout.addWidget(form_group)
       layout.addStretch()

       # 3. 连接 UI 事件到 VM 方法
       # 注意:这里我们不写逻辑,只转发
       self.series_combo.currentIndexChanged.connect(self._on_series_changed)
       self.product_combo.currentIndexChanged.connect(self._on_product_changed)
       self.save_btn.clicked.connect(self._on_save_clicked)

   def _bind_view_model(self):
       """绑定 VM 信号到 UI 更新槽函数"""
       self._vm.series_loaded.connect(self._update_series_list)
       self._vm.products_loaded.connect(self._update_product_list)
       self._vm.current_product_changed.connect(self._display_product)
       self._vm.error_occurred.connect(lambda msg: QMessageBox.critical(self, "错误", msg))
       # 初始加载
       self._vm.load_initial_data()

   # --- UI 事件处理 ---
   def _on_series_changed(self):
       sid = self.series_combo.currentData()
       self._vm.load_products_by_series(sid)

   def _on_product_changed(self):
       pid = self.product_combo.currentData()
       self._vm.select_product(pid)

   def _on_save_clicked(self):
       # 收集数据
       data = {}
       for key, widget in self.fields_map.items():
           val = widget.text().strip()
           if val: data[key] = val
       # 发送给 VM
       self._vm.save_current_product(data)

   # --- 数据更新槽函数 ---
   @pyqtSlot(list)
   def _update_series_list(self, series_list):
       self.series_combo.blockSignals(True)
       self.series_combo.clear()
       self.series_combo.addItem("所有系列", None)
       for s in series_list:
           self.series_combo.addItem(s.name, s.id)
       self.series_combo.blockSignals(False)

   @pyqtSlot(list)
   def _update_product_list(self, product_list):
       self.product_combo.blockSignals(True)
       self.product_combo.clear()
       self.product_combo.addItem("请选择...", None)
       for p in product_list:
           self.product_combo.addItem(p.model_number, p.id)
       self.product_combo.blockSignals(False)

   @pyqtSlot(object)
   def _display_product(self, product):
       """将 Product 对象的数据填入表单"""
       if not product:
           for widget in self.fields_map.values(): widget.clear()
           return

       # 混合 product 和 detail 的属性
       # 因为我们在 repository 处理了 detail 可能为空的情况,这里可以直接访问
       detail = product.detail

       for key, widget in self.fields_map.items():
           val = None
           # 先找 Product
           if hasattr(product, key):
               val = getattr(product, key)
           # 再找 Detail
           elif hasattr(detail, key):
               val = getattr(detail, key)
           
           widget.setText(str(val) if val is not None else "")

4. 程序入口

src/main.py

我已经仔细阅读了你提供的所有源代码。代码量很大,逻辑比较完整,但正如之前分析的,存在**Controller 负担过重****DataService  Database 数据双重来源****View 直接处理逻辑**等架构问题。

既然你已经提供了源码,我不需要你再提供任何东西了。我将基于你现有的代码,为你生成一套**核心的重构代码**

**注意:** 为了保持回答的长度可读且可执行,我不会复制粘贴原本用于“创建布局”的几百行 UI 代码(例如 `_create_form_grid`),因为那些不需要改。我主要重写**数据流向****逻辑控制**的部分。

请按照以下步骤,在你的项目中创建文件夹并填入代码。

---

### 📂 1. 新的项目结构

请务必按照这个结构来组织文件:

```text
src/
├── main.py                     # 程序入口
├── data/                       # 数据层
  ├── __init__.py
  ├── model.py                # (原 model.py) 只保留 Class 定义,删除下面的所有函数
  └── repository.py           # ✅ 新增:负责所有增删改查
├── viewmodels/                 # 逻辑层
  ├── __init__.py
  ├── product_vm.py           # ✅ 新增:管理 CustomerRequirementsPage 的逻辑
  └── simulation_vm.py        # ✅ 新增:替代 DataService,管理 BLDCPage 和 Worker
├── views/                      # 界面层
  ├── __init__.py
  ├── main_view.py            # (原 ui/main_view.py)
  ├── design_target_page.py   # (原 design_target_page.py) 修改对接 VM
  ├── bldc_page.py            # (原 bldc_page.py) 修改对接 VM
  └── product_detail_page.py  # (原 product_detail_page.py)
└── utils/                      # 工具层 (保持你现有的不变)
   ├── worker.py
   ├── normalize.py
   └── maxwell_modules/ ...
```

---

### 🧱 2. Data Layer (数据仓库层)

#### `src/data/model.py`
**操作**:保留你原来的 `BaseModel`, `ProductSeries`, `Product`, `ProductDetail`, `initialize_database`, `close_connection`
**删除**:删除文件下方所有的 `find_product_by_id`, `create_full_product` 等函数,因为我们要把它们移到 `repository.py` 中。

#### `src/data/repository.py` (新增)
这是解决空指针报错和统一数据访问的关键。

```python
import logging
from peewee import JOIN, IntegrityError, DoesNotExist
from .model import db, Product, ProductDetail, ProductSeries

class ProductRepository:
   """负责所有数据库交互,View 和 ViewModel 不直接操作 Model"""

   def get_all_series(self):
       return list(ProductSeries.select().order_by(ProductSeries.name))

   def get_products_by_series(self, series_id):
       query = Product.select()
       if series_id and series_id != 0:
           query = query.where(Product.series == series_id)
       return list(query.order_by(Product.model_number))

   def get_product_by_id(self, product_id: int):
       """获取产品,如果详情不存在则自动修复"""
       try:
           # 左连接查询
           product = (Product.select(Product, ProductDetail)
                      .join(ProductDetail, JOIN.LEFT_OUTER)
                      .where(Product.id == product_id)
                      .get())
           
           # ✅ 自动修复逻辑:如果 detail 为空,自动创建
           # 注意:Peewee 的 LEFT JOIN 如果没查到,访问 product.detail 可能会抛错或为 None
           try:
               if product.detail is None:
                   raise DoesNotExist
           except (DoesNotExist, AttributeError):
               logging.warning(f"Product {product_id} missing detail. Auto-creating.")
               ProductDetail.create(product=product)
               # 重新查询以确保对象完整
               return self.get_product_by_id(product_id)

           return product
       except DoesNotExist:
           return None

   def create_product(self, series_id: int, model_number: str):
       """创建新产品"""
       series = ProductSeries.get_by_id(series_id)
       with db.atomic():
           product = Product.create(series=series, model_number=model_number)
           ProductDetail.create(product=product) # 必须同时创建详情
       return product

   def update_product(self, product_id: int, updates: dict):
       """更新产品,使用 _meta.fields 过滤字段"""
       product = self.get_product_by_id(product_id)
       if not product: return False

       detail = product.detail
       p_dirty, d_dirty = False, False
       
       # 字段映射
       p_fields = Product._meta.fields
       d_fields = ProductDetail._meta.fields

       for key, value in updates.items():
           if key in ('id', 'created_at', 'series', 'product'): continue
           
           # 转为字符串比较,防止 10 != 10.0 的问题
           str_val = str(value) if value is not None else ""

           if key in p_fields:
               old_val = str(getattr(product, key)) if getattr(product, key) is not None else ""
               if old_val != str_val:
                   setattr(product, key, value)
                   p_dirty = True
           elif key in d_fields:
               old_val = str(getattr(detail, key)) if getattr(detail, key) is not None else ""
               if old_val != str_val:
                   setattr(detail, key, value)
                   d_dirty = True

       with db.atomic():
           if p_dirty: product.save()
           if d_dirty: detail.save()
       return True

   def delete_product(self, product_id: int):
       Product.delete().where(Product.id == product_id).execute()
```

---

### 🧠 3. ViewModel Layer (逻辑层)

#### `src/viewmodels/product_vm.py` (新增)
这个类替代了 `Controller` 中关于“客户需求页”的所有逻辑。

```python
from PyQt6.QtCore import QObject, pyqtSignal
from src.data.repository import ProductRepository
from src.data.model import Product

class ProductViewModel(QObject):
   # 信号
   series_list_updated = pyqtSignal(list)
   product_list_updated = pyqtSignal(list)
   current_product_changed = pyqtSignal(object) # 发送 Product 对象
   status_msg = pyqtSignal(str)
   error_msg = pyqtSignal(str)

   def __init__(self):
       super().__init__()
       self.repo = ProductRepository()
       self._current_product_id = None

   def load_init_data(self):
       series = self.repo.get_all_series()
       self.series_list_updated.emit(series)
       self.filter_products_by_series(0) # 0 代表全部

   def filter_products_by_series(self, series_id):
       products = self.repo.get_products_by_series(series_id)
       self.product_list_updated.emit(products)

   def select_product(self, product_id):
       self._current_product_id = product_id
       if not product_id:
           self.current_product_changed.emit(None)
           self.status_msg.emit("就绪")
           return
       
       product = self.repo.get_product_by_id(product_id)
       if product:
           self.current_product_changed.emit(product)
           self.status_msg.emit(f"已加载: {product.model_number}")
       else:
           self.error_msg.emit("加载产品失败")

   def create_new_product(self, series_id, model_number):
       try:
           new_prod = self.repo.create_product(series_id, model_number)
           self.filter_products_by_series(series_id) # 刷新列表
           self.status_msg.emit(f"产品 {model_number} 创建成功")
           return new_prod.id
       except Exception as e:
           self.error_msg.emit(f"创建失败: {str(e)}")
           return None

   def save_changes(self, form_data: dict):
       if not self._current_product_id:
           return
       try:
           self.repo.update_product(self._current_product_id, form_data)
           self.status_msg.emit("保存成功")
           # 重新加载以刷新界面显示
           self.select_product(self._current_product_id)
       except Exception as e:
           self.error_msg.emit(f"保存失败: {str(e)}")

   def delete_current_product(self):
       if self._current_product_id:
           self.repo.delete_product(self._current_product_id)
           self.filter_products_by_series(0) # 刷新
           self.select_product(None)
           self.status_msg.emit("产品已删除")
```

#### `src/viewmodels/simulation_vm.py` (新增)
这个类替代了 `DataService` `bldc_page` 中的逻辑部分。它实现了**单一数据源**

```python
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot
from src.utils.worker import MaxwellWorker
from src.utils.maxwell_modules.maxwell_config import MaxwellConfig
from src.data.model import Product
from pathlib import Path

class SimulationViewModel(QObject):
   # 信号
   parameter_changed = pyqtSignal(str, object) # key, value
   log_received = pyqtSignal(str)
   progress_updated = pyqtSignal(int)
   simulation_finished = pyqtSignal(object) # fig
   simulation_error = pyqtSignal(str)

   def __init__(self):
       super().__init__()
       # 替代 DataService,这里是所有参数的唯一真实来源
       self.parameters = {}
       self.worker = MaxwellWorker()
       
       # 连接 Worker 信号
       self.worker.log_message.connect(self.log_received)
       self.worker.progress_updated.connect(self.progress_updated)
       self.worker.finished.connect(self.simulation_finished)
       self.worker.error.connect(self.simulation_error)

   def load_product(self, product: Product):
       """当用户在主界面切换产品时调用"""
       self.parameters.clear()
       if not product: return

       # 从 Product 对象加载数据到 parameters 字典
       # 这里你可以复用 normalize.py 或者手动映射
       d_dict = product.detail.__data__ if product.detail else {}
       p_dict = product.__data__
       
       # 合并并存入 self.parameters
       self.parameters.update(p_dict)
       self.parameters.update(d_dict)
       
       # 触发 UI 更新(如果有需要的话,或者 View 直接监听 ProductVM)
       
   def update_parameter(self, key, value):
       """UI 输入框变化时调用"""
       self.parameters[key] = value
       # 这里可以加逻辑:如果 value 变了,是否需要通知其他地方?

   def start_simulation(self, project_path, project_name):
       if self.worker.isRunning():
           return

       config = MaxwellConfig(project_path, Path(project_path).parent,
                              project_name, "RMxprtDesign", "Setup")
       
       # 准备数据:self.parameters 就是最新的数据,因为 input 改动时同步更新了
       self.worker.start()
       self.worker.execute_simulation_task(config, self.parameters)

   def cleanup(self):
       self.worker.cleanup()
```

---

### 🎨 4. View Layer (界面层改造)

#### `src/views/design_target_page.py` (改造要点)

1.  **构造函数**:接收 `ProductViewModel`
2.  **删除**:删除 `DataService`
3.  **连接**
   *   下拉框 `currentIndexChanged` -> 调用 `vm.select_product(id)`
   *   保存按钮 `clicked` -> 收集 `self.fields_map` 数据 -> 调用 `vm.save_changes(data)`
   *   VM 信号 `current_product_changed` -> 触发 `load_data_to_form(product)`

```python
# 代码片段示例
class CustomerRequirementsPage(QWidget):
   def __init__(self, viewModel):
       super().__init__()
       self.vm = viewModel
       # ... UI 初始化 ...
       
       # 绑定
       self.vm.product_list_updated.connect(self.update_product_combo_box)
       self.vm.current_product_changed.connect(self.load_data_to_form)
       
       self.save_button.clicked.connect(self._on_save)

   def _on_save(self):
       data, _ = self.get_data_from_form() # 复用你现有的收集逻辑
       # 把两个字典合二为一传给 VM
       full_data = {**data[0], **data[1]}
       self.vm.save_changes(full_data)
```

#### `src/views/bldc_page.py` (改造要点)

1.  **构造函数**:接收 `SimulationViewModel`
2.  **删除**:删除 `DataService`, `MaxwellWorker` (因为移到了 VM)
3.  **修改输入框**`textChanged` 事件不再更新 `DataService`,而是调用 `self.vm.update_parameter(key, value)`
4.  **修改开始按钮**:调用 `self.vm.start_simulation(...)`

```python
# 代码片段示例
class MaxwellApp(QWidget):
   def __init__(self, sim_vm):
       super().__init__()
       self.vm = sim_vm
       # ... UI 初始化 ...
       
       # 绑定
       self.vm.log_received.connect(self.log_output.append)
       self.vm.simulation_finished.connect(self._on_sim_finished)
       
   def _on_text_changed(self, key, text):
       # ... 类型转换 ...
       self.vm.update_parameter(key, value)
       
   def _start_simulation(self):
       # ... 检查路径 ...
       self.vm.start_simulation(self.selected_project_path, self.project_name)
```

---

### 🚀 5. Main Entry (入口)

#### `src/main.py`
这是组装所有组件的地方。

```python
import sys
from PyQt6.QtWidgets import QApplication
from src.data.model import initialize_database, close_connection
from src.viewmodels.product_vm import ProductViewModel
from src.viewmodels.simulation_vm import SimulationViewModel
from src.views.main_view import MainView

def main():
   app = QApplication(sys.argv)
   
   # 1. 初始化 DB
   initialize_database()
   
   # 2. 创建 ViewModels
   product_vm = ProductViewModel()
   sim_vm = SimulationViewModel()
   
   # 3. 关联 VM:当产品改变时,通知仿真 VM 加载新数据
   product_vm.current_product_changed.connect(sim_vm.load_product)
   
   # 4. 创建 MainView (需要修改 MainView 构造函数来接收 VMs)
   #   或者 MainView 创建子页面时,把 vm 传进去
   main_window = MainView(product_vm, sim_vm)
   main_window.show()
   
   # 5. 初始加载数据
   product_vm.load_init_data()
   
   exit_code = app.exec()
   
   # 6. 清理
   sim_vm.cleanup()
   close_connection()
   sys.exit(exit_code)

if __name__ == "__main__":
   main()
```

### 修改 `MainView` (src/views/main_view.py)

你需要稍微改一下 `MainView` `__init__` `_create_main_content_stack`

```python
class MainView(QMainWindow):
   def __init__(self, product_vm, sim_vm): # 接收 VMs
       super().__init__()
       self.product_vm = product_vm
       self.sim_vm = sim_vm
       # ... 其他初始化 ...
       self.main_content_stack = self._create_main_content_stack()
       # ...

   def _create_main_content_stack(self):
       stack = QStackedWidget()
       # 注入 VM 到子页面
       self.customer_page = CustomerRequirementsPage(self.product_vm)
       self.bldc_page = MaxwellApp(self.sim_vm)
       self.detail_page = ProductDetailPage() # 这个页面比较简单,可以后续再加绑定
       
       stack.addWidget(self.customer_page)
       stack.addWidget(self.bldc_page)
       stack.addWidget(self.detail_page)
       return stack
```

### 总结

这套方案直接使用了你提供的 Peewee Model 定义和 PyQt 界面代码,但重构了**连接方式**

1.  **Repository** 帮你自动处理了 `product.detail` 为空的情况,消灭了空指针 BUG。
2.  **ProductViewModel** 统一了数据库读写逻辑,不再依赖 `controller.py`
3.  **SimulationViewModel** 统一了仿真参数,替代了 `DataService`,解决了“数据不同步”的问题。
4.  **MainView** 负责依赖注入,让各个组件松耦合。

✅ 为什么这个新版本更好?

  1. Repository 处理了“脏活累活”repository.py 里的 get_product_by_id 自动检测并修复缺失的 ProductDetail。你再也不会看到 product.detail.get() 报错了。

  2. ViewModel 统一了状态:UI 不再自己瞎改数据,而是收集数据传给 VM,VM 保存成功后再通知 UI 刷新。这保证了界面和数据库永远同步。

  3. UI 极其清爽BLDCPage 里没有一行 SQL 代码,也没有复杂的逻辑判断,只负责“显示”和“发信号”。

  4. 易于扩展:如果你想加一个新的仿真功能,只需写一个 SimulationViewModel,并在 View 里绑定它即可,完全不会影响现有的数据库逻辑。







n5321 | 2025年11月22日 00:25

AutoEM coreparameter update

目标:增加核心参数的一次性罗列!在每一次call Ansys maxwell 完成计算之后,获取结果参数,call back 到UI上!

方式:核心参数还是用qgrid生成,保持动态特性。价值是维护方便

数据存在df_for_detail 的变量里面!

暂时的问题是对数据结构的设计,需要呈现哪些data还需要仔细的思考!



n5321 | 2025年11月2日 23:29

AutoEM chart update

一个logic 看上去很简单的update,居然花了差不多两天的时间!

目标:在matplotlib的曲线图里添加对照组。

方式:在UI里添加button,选择,读取csv文档 add traces,沿用原始的chart设置!

测试idea: 写了一个try_compare.py,UI, path齐全,分分钟搞定了!

实操:添加进AutoEM。

完全不是那么回事!

try_compare写的logic 跟AutoEM完全不同!try_compare在一段script把所有的东西都包括进去了,所以debug 也极其简单!

在AutoEM中有一个很复杂的操作,因为数据和控制要传到到N个object!他具体的logi大概如下:

  1. page默认生成一个chart! chart是在fig 里面!

  2. UI提供chart的框子——准备填matplot的figure,设置成canva

  3. worker get data

  4. plotting tool 升成figure(用新曲线完全替代)

  5. 再返回到worker

  6. UI清楚原来的figure,添加新的figure

尝试实操!

  1. 更改logic!不在替换figure,考虑用同一个figure,在figure的框架里做删改traces!

    1. 问题是figure里面对于chart的设置有大量的代码,把设置代码从plotting_tool改到init之中,后续recall的时候又会用默认设置。

    2. 放弃新idea,继续用figure替换的思路。

  2. 未做深入思考,考虑增加一个function,plotting_tool生成sim chart,增加一个ref_chart的功能几乎一致的function,来生成ref chart。然后卡住了。

    1. 晚上睡觉才想起本质上是替换的,把设置全部堆到plotting_tool才是正常操作!

  3. 增加新功能,UI里添加button,click以后,可以看到ref chart!

    1. 需要把UI get 到的data 传递到worker,worker生成fig,再传递会UI

    2. 本质上我对signal and slot的逻辑没有搞清楚。

  4. 问题:在这种细节繁复的装配体结构中,数据的传递,logic线的设计,是架构师内容。还蛮麻烦的!

  5. 自己对QT的框架理解本质上不够!

还是需要深入阅读代码!

暂时解决问题!


n5321 | 2025年11月2日 10:42

maxwell 作为com对象分析

matlab 有invoke 函数来探测COM!

在python 下面用的dir

dir(self.maxwell_app)
['Design', 'Desktop', 'Editor', 'Module', 'Project', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'analyze_design', 'connect_maxwell', 'export_reports', 'iMaxwell', 'open_project_and_design', 'release_maxwell', 'set_properties']

具体来看:

 MaxwellAutomation 类结构概览

🧩 一、核心对象属性(Attributes)

属性名类型/用途说明
DesktopAEDT 桌面对象(oDesktop)控制 Maxwell AEDT 会话、项目打开、退出等操作。
ProjectAEDT 工程对象(oProject)当前打开的 Maxwell 工程。
DesignAEDT 设计对象(oDesign)当前工程下的具体设计,如“RMxprtDesign”或“Maxwell3DDesign”。
EditorAEDT 编辑器对象(oEditor)用于修改几何、材料、属性、参数。通常是 "3D Modeler" 编辑器。
ModuleAEDT 模块对象(oModule)可调用报告、求解、场后处理等功能模块,如 "ReportSetup""FieldsReporter" 等。
iMaxwellCOM 接口对象win32com.client.Dispatch("Ansoft.ElectronicsDesktop") 获取,用于连接 AEDT。

⚙️ 二、主要方法(Methods)

方法名主要功能典型调用示例
__init__()初始化 MaxwellAutomation 实例app = MaxwellAutomation()
connect_maxwell()建立与 Maxwell AEDT 的 COM 连接app.connect_maxwell()
open_project_and_design(config)打开指定工程与设计app.open_project_and_design(config)
set_properties(property_changes)批量修改参数属性app.set_properties(property_changes)
analyze_design(setup_name)启动指定 Setup 的仿真app.analyze_design("Setup1")
export_reports(config)导出仿真结果报告或数据文件app.export_reports(config)
release_maxwell()关闭并释放 Maxwell 会话app.release_maxwell()

🧱 三、内置 Python 魔法方法(不需手动调用)

这些是 Python 自动提供的类方法,用于对象描述、比较、属性访问等,一般不手动使用:

方法功能
__init__构造函数
__str__ / __repr__控制打印输出格式
__eq__, __lt__, __gt__对象比较
__getattribute__, __setattr__控制属性访问
__dict__, __doc__存放实例属性与文档说明
__sizeof__, __reduce__, __module__内部运行支持


n5321 | 2025年10月30日 22:20

Python modify Ansys Maxwell properties!

python通过com32 来更改ansys 参数的logic 跟matlab 确实不同!

一个简单的事情,居然还花了几个小时的时间!

背景:

1. matlab基本上copy record vbscript 就可以了!但是python 用的是不一样的logic!
通过com32, 他是直接插入到Ansys对象里面去的!一个com又已经被封装了!  我熟悉的是actxserver下面对ansys 的manipulate!

2. Ansys Maxwell下面的数据结构对于不同的参数来说,是不一样的!所以他需要设计不同的控制结构!开始考虑是做成一个数组结构,然后添加索引,match 到了以后直接调用。

问题是中间debug的过程比较麻烦!因为封装了,所以有点难把问题点拎出来!

logic! 暂时用的这个code 实现!

def set_properties(self, prop_changes):
"""Applies a list of property changes to the active design or editor."""
for prop_change in prop_changes:
changed_props = ["NAME:" + prop_change['name']]
print('更改的参数', changed_props)

if prop_change['name'] == 'Wire Size':
# Wire Size 复合属性
changed_props += [
"WireSizeWireDiameter:=", prop_change['value'],
"WireSizeGauge:=", "0",
"WireSizeWireWidth:=", "0mm",
"WireSizeWireThickness:=", "0mm",
"WireSizeMixedWireRectType:=", False,
["NAME:WireSizeMixedDiameter"],
["NAME:WireSizeMixedWidth"],
["NAME:WireSizeMixedThickness"],
["NAME:WireSizeMixedThicknessMixedFillet"],
["NAME:WireSizeMixedThicknessMixedNumber"]
]
print('wiresize data is', changed_props)

# 不同类型的参数修改方式
if 'type' in prop_change:
if prop_change['type'] == 'Value':
changed_props += ["Value:=", prop_change['value']]
elif prop_change['type'] == 'Material':
changed_props += ["Material:=", prop_change['value']]
elif prop_change['type'] == 'MustBeInt':
changed_props += [
"MustBeInt:=", prop_change.get('must_be_int', False),
"Value:=", prop_change['value']
]
else:
# 默认只写 Value
changed_props += ["Value:=", prop_change['value']]

# 选择目标对象
if prop_change['target'] == 'Editor':
self.Editor.ChangeProperty([
"NAME:AllTabs",
[
f"NAME:{prop_change['tab']}",
["NAME:PropServers", prop_change['prop_server']],
["NAME:ChangedProps", changed_props]
]
])
elif prop_change['target'] == 'Design':
self.Design.ChangeProperty([
"NAME:AllTabs",
[
f"NAME:{prop_change['tab']}",
["NAME:PropServers", prop_change['prop_server']],
["NAME:ChangedProps", [
"NAME:" + prop_change['name'],
"Value:=", prop_change['value']
]]
]
])

几个点记录一下!

1, 考虑更改的参数,暂时用的prop_changes来统一。里面去changed_props来做微调。

最后通过self.editer or self.design来更改。

2, 更改的参数点,需要把所以的参数都记录进来!


更改文档
maxwell_automation.py

ai_search_page.py




 


n5321 | 2025年10月26日 11:00

潜在bug!

properties setup暂时用的这个function!
感觉是一个坑

def _get_property_changes_from_table(self) -> List[dict]:
"""
从参数表格中获取用户输入的属性修改。
返回一个字典列表,每个字典包含一个属性的修改信息。
"""
prop_changes = []
prop_map = {
"Reference Speed": {'target': 'Editor', 'tab': 'Machine', 'prop_server': 'Machine', 'type': 'Value'},
"Steel Type (Stator)": {'target': 'Editor', 'tab': 'Stator', 'prop_server': 'Stator', 'type': 'Material'},
"Steel Type (Rotor)": {'target': 'Editor', 'tab': 'Rotor', 'prop_server': 'Rotor', 'type': 'Material'},
"Embrace": {'target': 'Editor', 'tab': 'Pole', 'prop_server': 'Rotor:Pole', 'type': 'Value'},
"Magnet Thickness": {'target': 'Editor', 'tab': 'Pole', 'prop_server': 'Rotor:Pole', 'type': 'Value'},
"Rated Output Power": {'target': 'Design', 'tab': 'General', 'prop_server': 'AnalysisSetup:all', 'type': 'Value'},
"Rated Speed": {'target': 'Design', 'tab': 'General', 'prop_server': 'AnalysisSetup:Setup1', 'type': 'Value'},
}

for row in range(self.param_table.rowCount()):
name_item = self.param_table.item(row, 0)
value_item = self.param_table.item(row, 1)

if name_item and value_item:
name = name_item.text().strip()
value = value_item.text().strip()

if name in prop_map:
info = prop_map[name].copy()
info['name'] = name
info['value'] = value
prop_changes.append(info)
elif name: # 如果参数名不为空但不在映射中
self.log_output.append(f"<span style='color:orange;'>警告: 未知参数 '{name}',将跳过。</span>")
logging.warning(f"未知参数 '{name}',将跳过。")
logging.debug(f"Collected property changes: {prop_changes}")
return prop_changes




n5321 | 2025年10月21日 00:13

AutoEM desktop app design

new page

chart的字体太大了!已经从12改到10号,怎么好像还是有点不好看!

核心参数的内容需要思考一下!

重点是要把对比体现出来!user的目标是设计最好的电机,最好是比较出来的!

日志的位置可以考虑调整,参数设置的地方需要更改!chart 那里考虑做tab!同时需要看的还有其他好几个图,比如齿槽扭力的chart就值得看一眼!这个改动感觉需要一两周才搞得完啊!

debug的工作量有点大!



n5321 | 2025年10月20日 00:30

python 版AutoEM

问题:

数据获取已经搞定!

有了一个最简单的可以work的GUI版本!

问题:
结果图表需要整理!

Log,目前的log未填充文本进文档。需要添加进去!

参数不用table,用edit_line!_后续更改!

button keep one!

文档加密的问题!后续添加?keep MVP框架?!




n5321 | 2025年10月14日 00:00