MES/ERP 工序 BOM 协同场景调度维护组件

以下是基于 MES/ERP 工序 BOM 协同场景的完整 WinForm 模块化实现代码,涵盖工序路径、BOM 物料、主子调度台、GDI 图表报表、本地 SQLite 数据维护等核心功能,同时强化 GDI 图表的标准化与丰富性,支持 8-9 组后台数据的本地维护编辑:

一、核心基础层:LocalDB(增强数据维护能力)

csharp

运行

复制代码
using System.Data;
using System.Data.SQLite;
using System.IO;
using System.Windows.Forms;

public static class LocalDB
{
    private static string dbFile = Path.Combine(Application.StartupPath, "MESData.db");
    public static string Conn => $"Data Source={dbFile};Version=3;";

    #region 初始化(含基础表+8-9组测试数据)
    public static void Init()
    {
        if (!File.Exists(dbFile)) SQLiteConnection.CreateFile(dbFile);
        using (var conn = new SQLiteConnection(Conn))
        {
            conn.Open();
            // 核心表结构(工序、路径、BOM、任务、物料、产线、资源、人员、设备 共9组核心数据)
            new SQLiteCommand($@"
                CREATE TABLE IF NOT EXISTS WorkStation(Id INTEGER PRIMARY KEY,Name TEXT,Role TEXT,Remark TEXT);
                CREATE TABLE IF NOT EXISTS ProcessRoute(Id INTEGER PRIMARY KEY,Name TEXT,StationId INT,Sort INT,IsEnabled BIT);
                CREATE TABLE IF NOT EXISTS BOM(Id INTEGER PRIMARY KEY,ProductCode TEXT,Material TEXT,Spec TEXT,UseQty DECIMAL,Loss DECIMAL,Unit TEXT);
                CREATE TABLE IF NOT EXISTS WorkTask(Id INTEGER PRIMARY KEY,TaskNo TEXT,StationId INT,StationName TEXT,Status TEXT,PlanQty INT,ActualQty INT,CreateTime DATETIME);
                CREATE TABLE IF NOT EXISTS Material(Id INTEGER PRIMARY KEY,Code TEXT,Name TEXT,StockQty INT,Unit TEXT,Supplier TEXT);
                CREATE TABLE IF NOT EXISTS ProductionLine(Id INTEGER PRIMARY KEY,LineCode TEXT,LineName TEXT,StationIds TEXT,Manager TEXT);
                CREATE TABLE IF NOT EXISTS Resource(Id INTEGER PRIMARY KEY,ResType TEXT,ResName TEXT,StationId INT,Qty INT,Status TEXT);
                CREATE TABLE IF NOT EXISTS Employee(Id INTEGER PRIMARY KEY,EmpNo TEXT,Name TEXT,StationId INT,Role TEXT,Phone TEXT);
                CREATE TABLE IF NOT EXISTS Equipment(Id INTEGER PRIMARY KEY,EqCode TEXT,Name TEXT,StationId INT,Status TEXT,MaintainDate DATETIME);
            ", conn).ExecuteNonQuery();
        }
        InitSampleData(); // 初始化9组测试数据
    }

    private static void InitSampleData()
    {
        // 清空旧数据
        Execute("DELETE FROM WorkStation");
        Execute("DELETE FROM ProcessRoute");
        Execute("DELETE FROM BOM");
        Execute("DELETE FROM WorkTask");
        Execute("DELETE FROM Material");
        Execute("DELETE FROM ProductionLine");
        Execute("DELETE FROM Resource");
        Execute("DELETE FROM Employee");
        Execute("DELETE FROM Equipment");

        // 1. 工站数据
        Execute("INSERT INTO WorkStation VALUES(1,'SMT工站','操作员','表面贴装')");
        Execute("INSERT INTO WorkStation VALUES(2,'QC工站','质检员','质量检测')");
        Execute("INSERT INTO WorkStation VALUES(3,'包装工站','包装员','成品包装')");
        Execute("INSERT INTO WorkStation VALUES(4,'测试工站','测试员','功能测试')");
        Execute("INSERT INTO WorkStation VALUES(5,'插件工站','操作员','元件插件')");
        Execute("INSERT INTO WorkStation VALUES(6,'焊工站','技术员','焊接加工')");

        // 2. 工序路径
        Execute("INSERT INTO ProcessRoute VALUES(1,'SMT贴片',1,1,1)");
        Execute("INSERT INTO ProcessRoute VALUES(2,'元件插件',5,2,1)");
        Execute("INSERT INTO ProcessRoute VALUES(3,'焊接',6,3,1)");
        Execute("INSERT INTO ProcessRoute VALUES(4,'QC检测',2,4,1)");
        Execute("INSERT INTO ProcessRoute VALUES(5,'功能测试',4,5,1)");
        Execute("INSERT INTO ProcessRoute VALUES(6,'成品包装',3,6,1)");

        // 3. BOM物料
        Execute("INSERT INTO BOM VALUES(1,'PROD001','PCB主板','FR-4',1,0.03,'片')");
        Execute("INSERT INTO BOM VALUES(2,'PROD001','主控芯片','STM32F103',2,0.02,'颗')");
        Execute("INSERT INTO BOM VALUES(3,'PROD001','电容','0805 100uF',5,0.01,'个')");
        Execute("INSERT INTO BOM VALUES(4,'PROD002','外壳','ABS材质',1,0.02,'套')");
        Execute("INSERT INTO BOM VALUES(5,'PROD002','显示屏','2.4寸TFT',1,0.01,'片')");
        Execute("INSERT INTO BOM VALUES(6,'PROD002','电池','3.7V 2000mAh',1,0.005,'节')");

        // 4. 工作任务
        Execute("INSERT INTO WorkTask VALUES(1,'T001',1,'SMT工站','待执行',100,0,'2026-01-01 08:00:00')");
        Execute("INSERT INTO WorkTask VALUES(2,'T002',5,'插件工站','执行中',150,80,'2026-01-01 09:00:00')");
        Execute("INSERT INTO WorkTask VALUES(3,'T003',6,'焊工站','待执行',200,0,'2026-01-01 10:00:00')");
        Execute("INSERT INTO WorkTask VALUES(4,'T004',2,'QC工站','已完成',180,180,'2026-01-01 11:00:00')");
        Execute("INSERT INTO WorkTask VALUES(5,'T005',4,'测试工站','执行中',120,90,'2026-01-01 14:00:00')");
        Execute("INSERT INTO WorkTask VALUES(6,'T006',3,'包装工站','已完成',200,200,'2026-01-01 15:00:00')");

        // 5. 物料库存
        Execute("INSERT INTO Material VALUES(1,'MAT001','PCB主板',500,'片','深圳XX电子')");
        Execute("INSERT INTO Material VALUES(2,'MAT002','主控芯片',1000,'颗','上海XX半导体')");
        Execute("INSERT INTO Material VALUES(3,'MAT003','电容',5000,'个','东莞XX电子')");
        Execute("INSERT INTO Material VALUES(4,'MAT004','外壳',300,'套','惠州XX塑胶')");
        Execute("INSERT INTO Material VALUES(5,'MAT005','显示屏',200,'片','广州XX显示')");

        // 6. 产线数据
        Execute("INSERT INTO ProductionLine VALUES(1,'LINE001','主线1','1,5,6,2,4,3','张三')");
        Execute("INSERT INTO ProductionLine VALUES(2,'LINE002','主线2','1,5,2,4,3','李四')");

        // 7. 资源数据
        Execute("INSERT INTO Resource VALUES(1,'治具','SMT吸嘴',1,50,'正常')");
        Execute("INSERT INTO Resource VALUES(2,'工具','电烙铁',6,20,'正常')");
        Execute("INSERT INTO Resource VALUES(3,'治具','测试夹具',4,15,'待维修')");

        // 8. 员工数据
        Execute("INSERT INTO Employee VALUES(1,'EMP001','张三',1,'操作员','13800138000')");
        Execute("INSERT INTO Employee VALUES(2,'EMP002','李四',2,'质检员','13900139000')");
        Execute("INSERT INTO Employee VALUES(3,'EMP003','王五',6,'技术员','13700137000')");

        // 9. 设备数据
        Execute("INSERT INTO Equipment VALUES(1,'EQ001','SMT贴片机',1,'运行中','2026-02-01')");
        Execute("INSERT INTO Equipment VALUES(2,'EQ002','AOI检测仪',2,'待机','2026-01-15')");
        Execute("INSERT INTO Equipment VALUES(3,'EQ003','焊接机',6,'维护中','2026-01-20')");
    }
    #endregion

    #region 通用数据库操作(查询/增删改)
    public static DataTable Query(string sql)
    {
        var dt = new DataTable();
        using (var adp = new SQLiteDataAdapter(sql, Conn)) adp.Fill(dt);
        return dt;
    }

    public static int Execute(string sql)
    {
        using (var conn = new SQLiteConnection(Conn))
        {
            conn.Open();
            return new SQLiteCommand(sql, conn).ExecuteNonQuery();
        }
    }

    // 新增数据(通用方法)
    public static int Insert(string table, params (string col, object val)[] columns)
    {
        var cols = string.Join(",", columns.Select(c => c.col));
        var vals = string.Join(",", columns.Select(c => $"'{c.val}'"));
        return Execute($"INSERT INTO {table} ({cols}) VALUES ({vals})");
    }

    // 修改数据(通用方法)
    public static int Update(string table, int id, params (string col, object val)[] columns)
    {
        var setStr = string.Join(",", columns.Select(c => $"{c.col}='{c.val}'"));
        return Execute($"UPDATE {table} SET {setStr} WHERE Id={id}");
    }

    // 删除数据(通用方法)
    public static int Delete(string table, int id)
    {
        return Execute($"DELETE FROM {table} WHERE Id={id}");
    }
    #endregion
}

二、可视化层:增强版 GdiChart(标准化 + 丰富性)

csharp

运行

复制代码
using System;
using System.Data;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

public class GdiChart : Panel
{
    // 可配置属性
    public DataTable DataSource { get; set; }
    public string XAxisName { get; set; } = "工站/物料";
    public string YAxisName { get; set; } = "数量";
    public Color[] BarColors { get; set; } = { Brushes.CornflowerBlue.Color, Brushes.LightSalmon.Color, Brushes.LightGreen.Color, Brushes.Orange.Color, Brushes.Purple.Color, Brushes.Teal.Color };
    public Font TitleFont { get; set; } = new Font("微软雅黑", 12, FontStyle.Bold);
    public Font AxisFont { get; set; } = new Font("微软雅黑", 9);
    public Font LabelFont { get; set; } = new Font("Consolas", 10);

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);
        if (DataSource == null || DataSource.Rows.Count == 0) return;

        var g = e.Graphics;
        g.SmoothingMode = SmoothingMode.AntiAlias; // 抗锯齿
        g.Clear(BackColor);

        // 1. 绘制标题
        var titleSize = g.MeasureString($"{XAxisName}-{YAxisName}统计", TitleFont);
        g.DrawString($"{XAxisName}-{YAxisName}统计", TitleFont, Brushes.Black, (ClientSize.Width - titleSize.Width) / 2, 10);

        // 2. 定义绘图区域(留边距)
        int marginLeft = 80, marginRight = 20, marginTop = 50, marginBottom = 60;
        int drawWidth = ClientSize.Width - marginLeft - marginRight;
        int drawHeight = ClientSize.Height - marginTop - marginBottom;

        // 3. 计算数据极值
        int maxVal = 0;
        foreach (DataRow row in DataSource.Rows)
        {
            if (int.TryParse(row[1].ToString(), out int val) && val > maxVal) maxVal = val;
        }
        maxVal = maxVal == 0 ? 100 : maxVal; // 避免除零

        // 4. 绘制Y轴(含刻度+标注)
        int yTickCount = 5; // Y轴刻度数
        int yTickStep = maxVal / yTickCount;
        for (int i = 0; i <= yTickCount; i++)
        {
            int y = marginTop + drawHeight - (i * drawHeight / yTickCount);
            string tickText = (i * yTickStep).ToString();
            var tickSize = g.MeasureString(tickText, AxisFont);
            
            // 刻度线
            g.DrawLine(Pens.Gray, marginLeft - 5, y, marginLeft, y);
            // 刻度值
            g.DrawString(tickText, AxisFont, Brushes.Gray, marginLeft - 5 - tickSize.Width, y - tickSize.Height / 2);
        }
        // Y轴名称
        g.RotateTransform(-90);
        g.DrawString(YAxisName, AxisFont, Brushes.Black, -ClientSize.Height / 2, 20);
        g.ResetTransform();

        // 5. 绘制X轴(含刻度+标注)
        int barCount = DataSource.Rows.Count;
        int barWidth = Math.Min(70, drawWidth / (barCount * 2)); // 自适应柱宽
        int barSpacing = drawWidth / (barCount + 1); // 柱间距

        for (int i = 0; i < barCount; i++)
        {
            int x = marginLeft + (i + 1) * barSpacing - barWidth / 2;
            string xLabel = DataSource.Rows[i][0].ToString();
            var labelSize = g.MeasureString(xLabel, AxisFont);
            
            // 刻度线
            g.DrawLine(Pens.Gray, x + barWidth / 2, ClientSize.Height - marginBottom + 5, x + barWidth / 2, ClientSize.Height - marginBottom);
            // X轴标签(自动换行/截断)
            string showLabel = xLabel.Length > 6 ? $"{xLabel.Substring(0, 6)}..." : xLabel;
            g.DrawString(showLabel, AxisFont, Brushes.DarkRed, x - labelSize.Width / 2, ClientSize.Height - marginBottom + 10);
        }
        // X轴名称
        var xAxisSize = g.MeasureString(XAxisName, AxisFont);
        g.DrawString(XAxisName, AxisFont, Brushes.Black, (ClientSize.Width - xAxisSize.Width) / 2, ClientSize.Height - marginBottom + 30);

        // 6. 绘制柱状图(多颜色+边框+数值标注)
        for (int i = 0; i < barCount; i++)
        {
            int val = int.Parse(DataSource.Rows[i][1].ToString());
            int barHeight = val * drawHeight / maxVal;
            int x = marginLeft + (i + 1) * barSpacing - barWidth / 2;
            int y = marginTop + drawHeight - barHeight;

            // 柱体填充(循环颜色)
            using (var brush = new SolidBrush(BarColors[i % BarColors.Length]))
            {
                g.FillRectangle(brush, x, y, barWidth, barHeight);
            }
            // 柱体边框
            g.DrawRectangle(Pens.Black, x, y, barWidth, barHeight);
            // 柱顶数值标注
            string valText = val.ToString();
            var valSize = g.MeasureString(valText, LabelFont);
            g.DrawString(valText, LabelFont, Brushes.Black, x + barWidth / 2 - valSize.Width / 2, y - valSize.Height - 5);
        }

        // 7. 绘制图例
        int legendX = ClientSize.Width - marginRight - 150;
        int legendY = marginTop;
        g.DrawRectangle(Pens.Gray, legendX, legendY, 140, barCount * 20);
        g.DrawString("图例", AxisFont, Brushes.Black, legendX + 5, legendY);
        for (int i = 0; i < Math.Min(barCount, 6); i++)
        {
            string itemText = DataSource.Rows[i][0].ToString();
            itemText = itemText.Length > 8 ? $"{itemText.Substring(0, 8)}..." : itemText;
            g.FillRectangle(new SolidBrush(BarColors[i]), legendX + 10, legendY + 20 + i * 20, 15, 15);
            g.DrawString(itemText, AxisFont, Brushes.Black, legendX + 30, legendY + 20 + i * 20);
        }

        // 日志输出
        DashboardView._logBox?.AppendText($"{DateTime.Now:HH:mm:ss} 柱状图绘制完成:{barCount}个维度,最大值{maxVal} \r\n");
    }

    // 重载刷新方法
    public new void Refresh()
    {
        Invalidate();
        base.Refresh();
    }

    // 导出图表为图片
    public void ExportToImage(string path)
    {
        using (var bmp = new Bitmap(ClientSize.Width, ClientSize.Height))
        {
            using (var g = Graphics.FromImage(bmp))
            {
                OnPaint(new PaintEventArgs(g, new Rectangle(0, 0, bmp.Width, bmp.Height)));
            }
            bmp.Save(path, System.Drawing.Imaging.ImageFormat.Png);
            DashboardView._logBox?.AppendText($"{DateTime.Now:HH:mm:ss} 图表已导出至:{path} \r\n");
        }
    }
}

三、业务视图层:工序路径管理组件

csharp

运行

复制代码
using System;
using System.Data;
using System.Windows.Forms;

public static class ProcessRouteView
{
    public static TabPage Create()
    {
        var page = new TabPage("工序路径管理");
        var split = new SplitContainer { Dock = DockStyle.Fill, Orientation = Orientation.Horizontal, SplitterDistance = 80 };

        // 顶部操作区
        var pnlTop = new Panel { Dock = DockStyle.Fill };
        var btnAdd = new Button { Text = "新增工序", Dock = DockStyle.Left, Width = 100 };
        var btnEdit = new Button { Text = "编辑工序", Dock = DockStyle.Left, Width = 100 };
        var btnDel = new Button { Text = "删除工序", Dock = DockStyle.Left, Width = 100 };
        var btnRefresh = new Button { Text = "刷新", Dock = DockStyle.Left, Width = 80 };
        pnlTop.Controls.Add(btnAdd);
        pnlTop.Controls.Add(btnEdit);
        pnlTop.Controls.Add(btnDel);
        pnlTop.Controls.Add(btnRefresh);
        split.Panel1.Controls.Add(pnlTop);

        // 底部数据展示区
        var dgv = new DataGridView { Dock = DockStyle.Fill, AutoGenerateColumns = false };
        // 配置列
        dgv.Columns.AddRange(new[]
        {
            new DataGridViewTextBoxColumn { Name = "Id", HeaderText = "ID", DataPropertyName = "Id", Width = 60 },
            new DataGridViewTextBoxColumn { Name = "Name", HeaderText = "工序名称", DataPropertyName = "Name", Width = 150 },
            new DataGridViewTextBoxColumn { Name = "StationId", HeaderText = "工站ID", DataPropertyName = "StationId", Width = 80 },
            new DataGridViewTextBoxColumn { Name = "Sort", HeaderText = "排序", DataPropertyName = "Sort", Width = 80 },
            new DataGridViewCheckBoxColumn { Name = "IsEnabled", HeaderText = "启用", DataPropertyName = "IsEnabled", Width = 80 }
        });
        split.Panel2.Controls.Add(dgv);
        page.Controls.Add(split);

        // 加载数据
        Action loadData = () => dgv.DataSource = LocalDB.Query("SELECT * FROM ProcessRoute ORDER BY Sort");
        loadData();

        // 新增工序
        btnAdd.Click += (s, e) =>
        {
            var frm = new ProcessRouteEditForm(null);
            if (frm.ShowDialog() == DialogResult.OK)
            {
                loadData();
                DashboardView._logBox.AppendText($"{DateTime.Now:HH:mm:ss} 新增工序路径完成 \r\n");
            }
        };

        // 编辑工序
        btnEdit.Click += (s, e) =>
        {
            if (dgv.CurrentRow == null) return;
            int id = int.Parse(dgv.CurrentRow.Cells["Id"].Value.ToString());
            var row = LocalDB.Query($"SELECT * FROM ProcessRoute WHERE Id={id}").Rows[0];
            var frm = new ProcessRouteEditForm(row);
            if (frm.ShowDialog() == DialogResult.OK)
            {
                loadData();
                DashboardView._logBox.AppendText($"{DateTime.Now:HH:mm:ss} 编辑工序路径(ID:{id})完成 \r\n");
            }
        };

        // 删除工序
        btnDel.Click += (s, e) =>
        {
            if (dgv.CurrentRow == null) return;
            int id = int.Parse(dgv.CurrentRow.Cells["Id"].Value.ToString());
            if (MessageBox.Show($"确认删除ID为{id}的工序路径?", "提示", MessageBoxButtons.YesNo) == DialogResult.Yes)
            {
                LocalDB.Delete("ProcessRoute", id);
                loadData();
                DashboardView._logBox.AppendText($"{DateTime.Now:HH:mm:ss} 删除工序路径(ID:{id})完成 \r\n");
            }
        };

        // 刷新
        btnRefresh.Click += (s, e) => loadData();

        return page;
    }

    // 工序路径编辑窗体
    public class ProcessRouteEditForm : Form
    {
        private readonly DataRow _row;
        private TextBox txtName = new TextBox { Dock = DockStyle.Top, PlaceholderText = "工序名称" };
        private TextBox txtStationId = new TextBox { Dock = DockStyle.Top, PlaceholderText = "工站ID" };
        private TextBox txtSort = new TextBox { Dock = DockStyle.Top, PlaceholderText = "排序号" };
        private CheckBox chkEnabled = new CheckBox { Text = "启用", Dock = DockStyle.Top };
        private Button btnSave = new Button { Text = "保存", Dock = DockStyle.Bottom };
        private Button btnCancel = new Button { Text = "取消", Dock = DockStyle.Bottom };

        public ProcessRouteEditForm(DataRow row)
        {
            _row = row;
            Text = row == null ? "新增工序路径" : "编辑工序路径";
            Size = new Size(300, 250);
            StartPosition = FormStartPosition.CenterParent;

            // 布局
            var pnlMain = new Panel { Dock = DockStyle.Fill, Padding = new Padding(10) };
            pnlMain.Controls.Add(txtName);
            pnlMain.Controls.Add(txtStationId);
            pnlMain.Controls.Add(txtSort);
            pnlMain.Controls.Add(chkEnabled);
            var pnlBtn = new Panel { Dock = DockStyle.Bottom, Height = 40 };
            pnlBtn.Controls.Add(btnSave);
            pnlBtn.Controls.Add(btnCancel);

            Controls.Add(pnlMain);
            Controls.Add(pnlBtn);

            // 加载已有数据
            if (row != null)
            {
                txtName.Text = row["Name"].ToString();
                txtStationId.Text = row["StationId"].ToString();
                txtSort.Text = row["Sort"].ToString();
                chkEnabled.Checked = bool.Parse(row["IsEnabled"].ToString());
            }

            // 保存
            btnSave.Click += (s, e) =>
            {
                if (string.IsNullOrWhiteSpace(txtName.Text))
                {
                    MessageBox.Show("请输入工序名称");
                    return;
                }
                if (!int.TryParse(txtStationId.Text, out int stationId))
                {
                    MessageBox.Show("工站ID必须为数字");
                    return;
                }
                if (!int.TryParse(txtSort.Text, out int sort))
                {
                    MessageBox.Show("排序号必须为数字");
                    return;
                }

                if (_row == null)
                {
                    // 新增
                    LocalDB.Insert("ProcessRoute",
                        ("Name", txtName.Text),
                        ("StationId", stationId),
                        ("Sort", sort),
                        ("IsEnabled", chkEnabled.Checked ? 1 : 0));
                }
                else
                {
                    // 编辑
                    int id = int.Parse(_row["Id"].ToString());
                    LocalDB.Update("ProcessRoute", id,
                        ("Name", txtName.Text),
                        ("StationId", stationId),
                        ("Sort", sort),
                        ("IsEnabled", chkEnabled.Checked ? 1 : 0));
                }
                DialogResult = DialogResult.OK;
                Close();
            };

            // 取消
            btnCancel.Click += (s, e) => Close();
        }
    }
}

四、业务视图层:BOM 物料维护组件(增强版)

csharp

运行

复制代码
using System;
using System.Data;
using System.Windows.Forms;

public static class BomView
{
    public static TabPage Create()
    {
        var page = new TabPage("BOM物料调配");
        var split = new SplitContainer { Dock = DockStyle.Fill, Orientation = Orientation.Horizontal, SplitterDistance = 100 };

        // 顶部操作区
        var pnlTop = new Panel { Dock = DockStyle.Fill };
        var cboProduct = new ComboBox { Dock = DockStyle.Left, Width = 150, DropDownStyle = ComboBoxStyle.DropDownList };
        var btnCalc = new Button { Text = "计算产量需求", Dock = DockStyle.Left, Width = 120 };
        var txtQty = new TextBox { Dock = DockStyle.Left, Width = 100, PlaceholderText = "输入产量(如1000)" };
        var btnEditBom = new Button { Text = "维护BOM数据", Dock = DockStyle.Left, Width = 120 };
        var btnExport = new Button { Text = "导出BOM", Dock = DockStyle.Left, Width = 100 };
        pnlTop.Controls.Add(cboProduct);
        pnlTop.Controls.Add(txtQty);
        pnlTop.Controls.Add(btnCalc);
        pnlTop.Controls.Add(btnEditBom);
        pnlTop.Controls.Add(btnExport);
        split.Panel1.Controls.Add(pnlTop);

        // 底部数据展示区
        var dgv = new DataGridView { Dock = DockStyle.Fill, AutoGenerateColumns = false };
        dgv.Columns.AddRange(new[]
        {
            new DataGridViewTextBoxColumn { Name = "Id", HeaderText = "ID", DataPropertyName = "Id", Width = 60 },
            new DataGridViewTextBoxColumn { Name = "ProductCode", HeaderText = "产品编码", DataPropertyName = "ProductCode", Width = 100 },
            new DataGridViewTextBoxColumn { Name = "Material", HeaderText = "物料名称", DataPropertyName = "Material", Width = 150 },
            new DataGridViewTextBoxColumn { Name = "Spec", HeaderText = "规格", DataPropertyName = "Spec", Width = 120 },
            new DataGridViewTextBoxColumn { Name = "UseQty", HeaderText = "单用量", DataPropertyName = "UseQty", Width = 80 },
            new DataGridViewTextBoxColumn { Name = "Loss", HeaderText = "损耗率", DataPropertyName = "Loss", Width = 80 },
            new DataGridViewTextBoxColumn { Name = "Unit", HeaderText = "单位", DataPropertyName = "Unit", Width = 60 },
            new DataGridViewTextBoxColumn { Name = "TotalNeed", HeaderText = "总需求", DataPropertyName = "TotalNeed", Width = 100, DefaultCellStyle = { ForeColor = Color.Red } }
        });
        split.Panel2.Controls.Add(dgv);
        page.Controls.Add(split);

        // 加载产品编码
        var productDt = LocalDB.Query("SELECT DISTINCT ProductCode FROM BOM");
        cboProduct.Items.AddRange(productDt.Rows.Cast<DataRow>().Select(r => r["ProductCode"].ToString()).ToArray());
        if (cboProduct.Items.Count > 0) cboProduct.SelectedIndex = 0;

        // 计算产量需求
        btnCalc.Click += (s, e) =>
        {
            if (cboProduct.SelectedItem == null || string.IsNullOrWhiteSpace(txtQty.Text))
            {
                MessageBox.Show("请选择产品并输入产量");
                return;
            }
            if (!int.TryParse(txtQty.Text, out int qty))
            {
                MessageBox.Show("产量必须为数字");
                return;
            }

            var product = cboProduct.SelectedItem.ToString();
            var dt = LocalDB.Query($"SELECT * FROM BOM WHERE ProductCode='{product}'");
            if (!dt.Columns.Contains("TotalNeed"))
            {
                dt.Columns.Add("TotalNeed", typeof(decimal));
            }

            foreach (DataRow r in dt.Rows)
            {
                decimal useQty = decimal.Parse(r["UseQty"].ToString());
                decimal loss = decimal.Parse(r["Loss"].ToString());
                decimal total = BomComponent.Calc(qty, useQty, loss);
                r["TotalNeed"] = total;
            }

            dgv.DataSource = dt;
            DashboardView._logBox.AppendText($"{DateTime.Now:HH:mm:ss} 产品[{product}]产量[{qty}]的BOM需求计算完成 \r\n");
        };

        // 维护BOM数据
        btnEditBom.Click += (s, e) =>
        {
            var frm = new BomEditForm();
            frm.ShowDialog();
            // 重新加载产品编码
            cboProduct.Items.Clear();
            var newProductDt = LocalDB.Query("SELECT DISTINCT ProductCode FROM BOM");
            cboProduct.Items.AddRange(newProductDt.Rows.Cast<DataRow>().Select(r => r["ProductCode"].ToString()).ToArray());
            if (cboProduct.Items.Count > 0) cboProduct.SelectedIndex = 0;
        };

        // 导出BOM
        btnExport.Click += (s, e) =>
        {
            if (dgv.DataSource == null) return;
            var dt = (DataTable)dgv.DataSource;
            using (var sfd = new SaveFileDialog { Filter = "Excel文件|*.xlsx|CSV文件|*.csv", Title = "导出BOM数据" })
            {
                if (sfd.ShowDialog() == DialogResult.OK)
                {
                    // 简化版导出(实际可使用NPOI/EPPlus)
                    if (sfd.FilterIndex == 2)
                    {
                        var lines = new[] { string.Join(",", dt.Columns.Cast<DataColumn>().Select(c => c.ColumnName)) }
                            .Concat(dt.Rows.Cast<DataRow>().Select(r => string.Join(",", r.ItemArray)));
                        System.IO.File.WriteAllLines(sfd.FileName, lines);
                    }
                    DashboardView._logBox.AppendText($"{DateTime.Now:HH:mm:ss} BOM数据已导出至:{sfd.FileName} \r\n");
                }
            }
        };

        return page;
    }

    // BOM编辑窗体
    public class BomEditForm : Form
    {
        private DataGridView dgv = new DataGridView { Dock = DockStyle.Fill, AutoGenerateColumns = false };
        private Button btnAdd = new Button { Text = "新增", Dock = DockStyle.Bottom, Width = 80 };
        private Button btnDel = new Button { Text = "删除", Dock = DockStyle.Bottom, Width = 80 };
        private Button btnSave = new Button { Text = "保存", Dock = DockStyle.Bottom, Width = 80 };

        public BomEditForm()
        {
            Text = "BOM物料维护";
            Size = new Size(800, 400);
            StartPosition = FormStartPosition.CenterParent;

            // 配置列
            dgv.Columns.AddRange(new[]
            {
                new DataGridViewTextBoxColumn { Name = "Id", HeaderText = "ID", DataPropertyName = "Id", Width = 60 },
                new DataGridViewTextBoxColumn { Name = "ProductCode", HeaderText = "产品编码", DataPropertyName = "ProductCode", Width = 100 },
                new DataGridViewTextBoxColumn { Name = "Material", HeaderText = "物料名称", DataPropertyName = "Material", Width = 150 },
                new DataGridViewTextBoxColumn { Name = "Spec", HeaderText = "规格", DataPropertyName = "Spec", Width = 120 },
                new DataGridViewTextBoxColumn { Name = "UseQty", HeaderText = "单用量", DataPropertyName = "UseQty", Width = 80 },
                new DataGridViewTextBoxColumn { Name = "Loss", HeaderText = "损耗率", DataPropertyName = "Loss", Width = 80 },
                new DataGridViewTextBoxColumn { Name = "Unit", HeaderText = "单位", DataPropertyName = "Unit", Width = 60 }
            });
            dgv.DataSource = LocalDB.Query("SELECT * FROM BOM");

            // 布局
            var pnlBtn = new Panel { Dock = DockStyle.Bottom, Height = 40 };
            pnlBtn.Controls.Add(btnAdd);
            pnlBtn.Controls.Add(btnDel);
            pnlBtn.Controls.Add(btnSave);

            Controls.Add(dgv);
            Controls.Add(pnlBtn);

            // 新增行
            btnAdd.Click += (s, e) =>
            {
                var dt = (DataTable)dgv.DataSource;
                var newRow = dt.NewRow();
                newRow["UseQty"] = 1;
                newRow["Loss"] = 0.01;
                dt.Rows.Add(newRow);
                dgv.CurrentCell = dgv.Rows[dgv.Rows.Count - 1].Cells["ProductCode"];
            };

            // 删除行
            btnDel.Click += (s, e) =>
            {
                if (dgv.CurrentRow == null || dgv.CurrentRow.IsNewRow) return;
                int id = int.TryParse(dgv.CurrentRow.Cells["Id"].Value?.ToString(), out int i) ? i : 0;
                if (id > 0) LocalDB.Delete("BOM", id);
                dgv.Rows.Remove(dgv.CurrentRow);
            };

            // 保存修改
            btnSave.Click += (s, e) =>
            {
                var dt = (DataTable)dgv.DataSource;
                foreach (DataRow row in dt.Rows)
                {
                    if (row.RowState == DataRowState.Added)
                    {
                        LocalDB.Insert("BOM",
                            ("ProductCode", row["ProductCode"]),
                            ("Material", row["Material"]),
                            ("Spec", row["Spec"]),
                            ("UseQty", row["UseQty"]),
                            ("Loss", row["Loss"]),
                            ("Unit", row["Unit"]));
                    }
                    else if (row.RowState == DataRowState.Modified)
                    {
                        int id = int.Parse(row["Id"].ToString());
                        LocalDB.Update("BOM", id,
                            ("ProductCode", row["ProductCode"]),
                            ("Material", row["Material"]),
                            ("Spec", row["Spec"]),
                            ("UseQty", row["UseQty"]),
                            ("Loss", row["Loss"]),
                            ("Unit", row["Unit"]));
                    }
                }
                MessageBox.Show("保存成功");
                Close();
            };
        }
    }
}

五、核心整合层:MainForm(主窗体,组装所有模块)

csharp

运行

复制代码
using System;
using System.Drawing;
using System.Windows.Forms;

public partial class MainForm : Form
{
    private readonly GdiChart _chart = new GdiChart
    {
        XAxisName = "工站",
        YAxisName = "计划产量",
        BackColor = Color.White,
        BarColors = new[] { Color.CornflowerBlue, Color.LightSalmon, Color.LightGreen, Color.Orange, Color.Purple, Color.Teal }
    };

    public MainForm()
    {
        LocalDB.Init(); // 初始化数据库+测试数据
        InitializeComponent();
        InitializeLayout();
        BindEvents();
        Text = "MES/ERP 工序BOM协同系统";
        WindowState = FormWindowState.Maximized;
        Icon = SystemIcons.Application;
    }

    private void InitializeComponent()
    {
        this.SuspendLayout();
        this.Name = "MainForm";
        this.Text = "MES/ERP 工序BOM协同系统";
        this.Size = new Size(1200, 800);
        this.ResumeLayout(false);
    }

    private void InitializeLayout()
    {
        var tabControl = new TabControl { Dock = DockStyle.Fill, Font = new Font("微软雅黑", 10) };
        
        // 组装所有功能标签页
        tabControl.TabPages.Add(TaskNodeView.Create());          // 分节点执行端
        tabControl.TabPages.Add(BomView.Create());               // BOM物料调配
        tabControl.TabPages.Add(ProcessRouteView.Create());     // 工序路径管理
        tabControl.TabPages.Add(ChartView.Create(_chart));      // GDI图表报表
        tabControl.TabPages.Add(MasterSlaveView.Create());      // 主子调度台
        tabControl.TabPages.Add(DashboardView.Create());        // 汇总工作台
        tabControl.TabPages.Add(DataMaintenanceView.Create());  // 数据维护中心

        Controls.Add(tabControl);
    }

    private void BindEvents()
    {
        // 全局刷新事件
        MesEventBus.OnRefreshUI += () =>
        {
            foreach (TabPage page in ((TabControl)Controls[0]).TabPages)
            {
                foreach (Control c in page.Controls)
                {
                    if (c is DataGridView dgv) dgv.DataSource = null;
                }
                // 重新加载各页面数据(简化版,实际可按需优化)
                if (page == ((TabControl)Controls[0]).TabPages["分节点工作端"])
                {
                    TaskNodeView._dgv.DataSource = TaskComponent.GetTasks();
                }
            }
            _chart.Refresh();
        };
    }
}

// 通用数据维护视图(管理9组核心数据)
public static class DataMaintenanceView
{
    public static TabPage Create()
    {
        var page = new TabPage("数据维护中心");
        var cboTable = new ComboBox { Dock = DockStyle.Top, DropDownStyle = ComboBoxStyle.DropDownList, Height = 30 };
        var dgv = new DataGridView { Dock = DockStyle.Fill, AutoGenerateColumns = true };
        var btnRefresh = new Button { Text = "刷新", Dock = DockStyle.Top, Height = 30 };

        // 绑定9组核心数据表
        cboTable.Items.AddRange(new[]
        {
            "WorkStation(工站)", "ProcessRoute(工序路径)", "BOM(物料清单)", "WorkTask(工作任务)",
            "Material(物料库存)", "ProductionLine(产线)", "Resource(资源)", "Employee(员工)", "Equipment(设备)"
        });
        cboTable.SelectedIndexChanged += (s, e) =>
        {
            if (cboTable.SelectedItem == null) return;
            string tableName = cboTable.SelectedItem.ToString().Split('(')[0];
            dgv.DataSource = LocalDB.Query($"SELECT * FROM {tableName}");
        };
        btnRefresh.Click += (s, e) =>
        {
            if (cboTable.SelectedItem != null)
            {
                string tableName = cboTable.SelectedItem.ToString().Split('(')[0];
                dgv.DataSource = LocalDB.Query($"SELECT * FROM {tableName}");
            }
        };

        // 布局
        var split = new SplitContainer { Dock = DockStyle.Fill, Orientation = Orientation.Horizontal, SplitterDistance = 60 };
        var pnlTop = new Panel { Dock = DockStyle.Fill };
        pnlTop.Controls.Add(btnRefresh);
        pnlTop.Controls.Add(cboTable);
        split.Panel1.Controls.Add(pnlTop);
        split.Panel2.Controls.Add(dgv);
        page.Controls.Add(split);

        return page;
    }
}

六、辅助层:补充核心组件

csharp

运行

复制代码
// 1. BOM计算组件
public static class BomComponent
{
    public static DataTable GetBom(string product)
    {
        return LocalDB.Query($"SELECT * FROM BOM WHERE ProductCode='{product}'");
    }

    public static decimal Calc(int qty, decimal useQty, decimal loss)
    {
        // 公式:总需求 = 产量 × 单用量 × (1 + 损耗率)
        return Math.Round(qty * useQty * (1 + loss), 2);
    }
}

// 2. 任务组件
public static class TaskComponent
{
    public static DataTable GetTasks() => LocalDB.Query("SELECT * FROM WorkTask");

    public static void UpdateStatus(int taskId, string status)
    {
        LocalDB.Execute($"UPDATE WorkTask SET Status='{status}' WHERE Id={taskId}");
        MesEventBus.SendTask(taskId.ToString(), "分节点执行端", status);
    }

    public static (DataTable left, DataTable right) SplitTasks()
    {
        var left = LocalDB.Query("SELECT * FROM WorkTask WHERE Status!='已完成'");
        var right = LocalDB.Query("SELECT * FROM WorkTask WHERE Status='已完成'");
        return (left, right);
    }
}

// 3. 事件总线
public static class MesEventBus
{
    public delegate void TaskOperateHandler(string taskId, string station, string status);
    public delegate void RefreshUIHandler();
    public delegate void BomCalcHandler(string product, decimal total);
    public delegate void DataEditHandler(string table);

    public static event TaskOperateHandler OnTaskOperated;
    public static event RefreshUIHandler OnRefreshUI;
    public static event BomCalcHandler OnBomCalculated;
    public static event DataEditHandler OnDataEdited;

    public static void SendTask(string taskId, string station, string status)
    {
        OnTaskOperated?.Invoke(taskId, station, status);
    }

    public static void TriggerRefresh() => OnRefreshUI?.Invoke();
    public static void SendBom(string product, decimal total) => OnBomCalculated?.Invoke(product, total);
    public static void SendEdit(string table) => OnDataEdited?.Invoke(table);
}

// 4. 汇总工作台(优化版)
public static class DashboardView
{
    public static TextBox _logBox = new TextBox();

    public static TabPage Create()
    {
        var page = new TabPage("汇总工作台");
        _logBox = new TextBox
        {
            Dock = DockStyle.Fill,
            Multiline = true,
            ReadOnly = true,
            BackColor = Color.Black,
            ForeColor = Color.Lime,
            Font = new Font("Consolas", 10),
            ScrollBars = ScrollBars.Vertical,
            WordWrap = false
        };

        // 初始化日志
        _logBox.AppendText($"{DateTime.Now:HH:mm:ss} MES/ERP协同系统启动完成 \r\n");
        _logBox.AppendText($"{DateTime.Now:HH:mm:ss} 本地数据库路径:{LocalDB.Conn.Split('=')[1].Split(';')[0]} \r\n");

        page.Controls.Add(_logBox);
        return page;
    }
}

// 5. 图表视图(增强版)
public static class ChartView
{
    public static TabPage Create(GdiChart chart)
    {
        var page = new TabPage("图表报表");
        var pnlTop = new Panel { Dock = DockStyle.Top, Height = 80 };
        var btnLoadTask = new Button { Text = "加载任务产量图", Dock = DockStyle.Left, Width = 120 };
        var btnLoadBom = new Button { Text = "加载BOM需求图", Dock = DockStyle.Left, Width = 120 };
        var btnExport = new Button { Text = "导出图表", Dock = DockStyle.Left, Width = 100 };
        var cboType = new ComboBox { Dock = DockStyle.Left, Width = 150, DropDownStyle = ComboBoxStyle.DropDownList };
        cboType.Items.AddRange(new[] { "工站产量统计", "物料需求统计" });
        cboType.SelectedIndex = 0;

        pnlTop.Controls.Add(btnLoadTask);
        pnlTop.Controls.Add(btnLoadBom);
        pnlTop.Controls.Add(btnExport);
        pnlTop.Controls.Add(cboType);

        chart.Dock = DockStyle.Fill;
        page.Controls.Add(chart);
        page.Controls.Add(pnlTop);

        // 加载任务产量图
        btnLoadTask.Click += (s, e) =>
        {
            chart.XAxisName = "工站";
            chart.YAxisName = "计划产量";
            chart.DataSource = LocalDB.Query("SELECT StationName, SUM(PlanQty) Qty FROM WorkTask GROUP BY StationName");
            chart.Refresh();
        };

        // 加载BOM需求图
        btnLoadBom.Click += (s, e) =>
        {
            chart.XAxisName = "物料";
            chart.YAxisName = "总需求(1000产量)";
            var dt = LocalDB.Query("SELECT Material, UseQty*1000*(1+Loss) Qty FROM BOM WHERE ProductCode='PROD001'");
            chart.DataSource = dt;
            chart.Refresh();
        };

        // 导出图表
        btnExport.Click += (s, e) =>
        {
            using (var sfd = new SaveFileDialog { Filter = "PNG图片|*.png", Title = "导出图表" })
            {
                if (sfd.ShowDialog() == DialogResult.OK)
                {
                    chart.ExportToImage(sfd.FileName);
                }
            }
        };

        return page;
    }
}

七、程序入口

csharp

运行

复制代码
using System;
using System.Windows.Forms;

namespace PraticMES_DULTASKEXe
{
    internal static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.ThreadException += (s, e) =>
            {
                MessageBox.Show($"系统异常:{e.Exception.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
                DashboardView._logBox?.AppendText($"{DateTime.Now:HH:mm:ss} 异常:{e.Exception.Message} \r\n");
            };
            Application.Run(new MainForm());
        }
    }
}

功能说明

  1. 本地数据库:基于 SQLite 实现 9 组核心数据(工站、工序、BOM、任务、物料、产线、资源、员工、设备)的本地化存储,支持增删改查;
  2. GDI 图表:标准化柱状图,支持抗锯齿、多颜色柱体、坐标轴标注、图例、数值标注、图表导出,可配置字体 / 颜色 / 坐标轴名称;
  3. 业务模块
    • 分节点执行端:任务状态变更(待执行 / 执行中 / 已完成);
    • BOM 物料调配:产量需求计算、BOM 数据维护、导出;
    • 工序路径管理:工序的新增 / 编辑 / 删除 / 排序;
    • 主子调度台:工站 - 任务的主子表联动;
    • 汇总工作台:实时日志输出;
    • 数据维护中心:统一维护 9 组核心数据;
    • 图表报表:多维度数据可视化、导出;
  4. 交互体验:支持数据校验、异常捕获、日志记录、文件导出等。

部署说明

  1. 确保项目引用 System.Data.SQLite(可通过 NuGet 安装 System.Data.SQLite.Core);
  2. 目标框架设置为 .NET Framework 4.8.1
  3. 编译后会自动生成 MESData.db 本地数据库文件,包含预置的 9 组测试数据;
  4. 支持 Windows 系统直接运行,无需额外安装数据库。

该实现完整覆盖了 MES/ERP 工序 BOM 协同场景的核心需求,模块化设计便于扩展,本地化数据库满足离线使用需求,GDI 图表满足可视化报表需求,数据维护模块支持业务数据的灵活编辑。

相关推荐
鱼人1 小时前
Vue 3 组合式 API 最佳实践:如何写出可维护的代码
前端
wuhen_n1 小时前
LangChain 自定义 Tool 封装:打造专属 AI 能力工具集
前端·langchain·ai编程
长大19881 小时前
彻底搞懂 JavaScript 事件循环
前端
橘猫走江湖1 小时前
Web 前端本地存储:localStorage 与 IndexedDB
前端·javascript·indexeddb
小强19881 小时前
CSS 布局进化史:从 Float 到 Flexbox 再到 Grid
前端
AKA__老方丈1 小时前
删除确认 Hook - 统一管理单删/批量删除的确认弹窗与执行
前端·javascript·vue.js
假如让我当三天老蒯2 小时前
React+TS 项目结构(自学项目用)
前端·react.js
yingyima2 小时前
Celery 分布式任务队列:我差点被这行代码坑死
前端
用户125758524362 小时前
XYGo Admin 即时通讯模块解析:基于 WebSocket 的企业级消息架构实践
前端