Przeglądaj źródła

1、优化软床报价业务逻辑,模板报价自动生成一套标准配置资料
2、新增软床报价自定义配置弹窗

MY 2 tygodni temu
rodzic
commit
e9459016cf

+ 1 - 0
JLHHJSvr/Com/Model/u_configure_codemx.cs

@@ -66,6 +66,7 @@ namespace JLHHJSvr.Com.Model
         public decimal? useqty { get; set; }
         public string erp_mtrlcode { get; set; }
         public string wrkgrpcode2 { get; set; }
+        public List<u_configure_codemxbom> bomList { get; set; }
 
         #region 辅助
         public string pzname { get; set; }

+ 2 - 0
JLHHJSvr/Com/Model/u_softbed_mx.cs

@@ -35,7 +35,9 @@ namespace JLHHJSvr.Com.Model
         public decimal cost_price { get; set; }
         public decimal cost_amt { get; set; }
         public string pzname { get; set; }
+        public string pznamemx { get; set; }
         public string parts_type { get; set; }
+        public int pzmxid { get; set; }
         #region 辅助
         public string formulaname { get; set; }
         #endregion

+ 2 - 1
JLHHJSvr/DBA/ParkDBVersion.cs

@@ -30,7 +30,7 @@ namespace JLHHJSvr.DBA
     {
         protected override string currentVersion
         {
-            get { return "1.0.251211"; }
+            get { return "1.0.260427"; }
         }
 
         protected override string dbname
@@ -341,6 +341,7 @@ new Script("1.0.251211", @"CREATE TABLE u_bill_relation (
 )", ""),
 new Script("1.0.251211", @"CREATE INDEX IX_u_bill_relation_src
 ON u_bill_relation (src_keyword, src_billid)", ""),
+new Script("1.0.260427", @"ALTER TABLE u_softbed_mx ADD pzmxid INT NOT NULL CONSTRAINT DF_u_softbed_mx_pzmxid DEFAULT(0)", ""),
                 };
             }
         }

+ 3 - 0
JLHHJSvr/DataStore/web_configure_typelist.xml

@@ -11,6 +11,9 @@ SELECT u_configure_type.contfigtypeid
 FROM u_configure_type
   </selectstr>
   <where>
+	<when notempty="@arg_search">
+		u_configure_type.contfigtypename LIKE '%' + @arg_search + '%'
+	</when>
   </where>
   <orderstr>
   </orderstr>

+ 655 - 24
JLHHJSvr/Helper/SoftBedHelper.cs

@@ -7,17 +7,23 @@ using JLHHJSvr.Tools;
 using LJLib.DAL.SQL;
 using NPOI.HSSF.Record;
 using NPOI.SS.Formula.Functions;
+using NPOI.Util;
 using System;
 using System.Collections;
 using System.Collections.Generic;
 using System.Data;
+using System.Diagnostics;
 using System.Linq;
+using System.Security.Cryptography;
 using System.Text;
 using System.Text.RegularExpressions;
 using System.Threading.Tasks;
 using System.Web;
 using System.Web.Configuration;
+using System.Web.UI.WebControls;
 using static JLHHJSvr.Helper.CacheHelper;
+using static System.Runtime.CompilerServices.RuntimeHelpers;
+using static System.Windows.Forms.VisualStyles.VisualStyleElement.Menu;
 using static System.Windows.Forms.VisualStyles.VisualStyleElement.Tab;
 
 namespace JLHHJSvr.Helper
@@ -49,7 +55,7 @@ namespace JLHHJSvr.Helper
         public List<u_softbed_mx> GetSoftBedMxList(int billid,string fields = null)
         {
             fields = fields ?? @"softbed_id,printid,formulaid,pzid,mtrlid,mtrlname,mtrlcode,mtrlmode,unit,has_type,allow_edit,cutting_length,cutting_width,cutting_qty,
-                                useqty,use_formula,use_formula_str,actual_useqty,loss_rate,price,price_formula,price_formula_str,cost_price,cost_amt,pzname,formulaname,parts_type";
+                                useqty,use_formula,use_formula_str,actual_useqty,loss_rate,price,price_formula,price_formula_str,cost_price,cost_amt,pzname,formulaname,parts_type,pzmxid,pznamemx";
             var mxlist = new List<u_softbed_mx>();
 
             var selectStr = @"SELECT u_softbed_mx.softbed_id
@@ -79,8 +85,12 @@ namespace JLHHJSvr.Helper
 									,ISNULL(u_configure_code.name,u_softbed_mx.pzname) AS pzname
 									,u_softbed_mx.parts_type
 									,ISNULL(u_softbed_formula.formulaname,'') AS formulaname
+									,u_softbed_mx.pzmxid
+									,ISNULL(u_configure_codemx.namemx,'') AS pznamemx
 								FROM u_softbed_mx
 								LEFT JOIN u_configure_code ON u_softbed_mx.pzid = u_configure_code.pzid
+                                LEFT JOIN u_configure_codemx ON u_softbed_mx.pzid = u_configure_codemx.pzid
+                                    AND u_softbed_mx.pzmxid = u_configure_codemx.printid
 								LEFT JOIN u_softbed_formula ON u_softbed_mx.formulaid = u_softbed_formula.formulaid
 								";
 
@@ -433,7 +443,9 @@ namespace JLHHJSvr.Helper
         /// <param name="softbed"></param>
         private void SaveSoftBedTemplate(u_softbed softbed)
 		{
-			if (softbed.is_template == 0) return;
+            SaveSoftBedTemplateV2(softbed);
+            return;
+            if (softbed.is_template == 0) return;
 
 			if(string.IsNullOrEmpty(softbed.softbed_code))
 			{
@@ -442,20 +454,10 @@ namespace JLHHJSvr.Helper
 
 			string prefix = $"{softbed.softbed_code}|";
 			var configureList = GetSoftBedConfigureName(softbed).Values.ToList();
-
-            var baseInfoHelper = GetHelper<BasicInfoHelper>(cmd, context);
 			foreach(var configure in configureList)
             {
-                // 判断是否已存在
-                cmd.CommandText = @"SELECT COUNT(*) FROM u_configure_type WHERE LTRIM(RTRIM(contfigtypename)) = @contfigtypename";
-                cmd.Parameters.Clear();
-				cmd.Parameters.AddWithValue("@contfigtypename", configure.contfigtypename);
-				var cnt = Convert.ToInt32(cmd.ExecuteScalar());
-
-				if (cnt > 0) continue;
-                baseInfoHelper.SaveConfigureType(configure);
+                SaveSoftBedConfigure(configure);
             }
-
 			// 生成标准选配项值ifdft
 			if(softbed.codeMxList != null && softbed.codeMxList.Count > 0)
 			{
@@ -498,8 +500,9 @@ namespace JLHHJSvr.Helper
 					contfigtypename = $"{prefix}床头",
 					contfigtype = 0,
 					usechflag = 1,
-					flag = 0
-				});
+					flag = 0,
+                    codeList = GetSoftBedConfigureCode(softbed.mxList,1)
+                });
             }
 
             if (softbed.has_nightstand == 1)
@@ -510,7 +513,8 @@ namespace JLHHJSvr.Helper
                     contfigtypename = $"{prefix}床头柜",
                     contfigtype = 0,
                     usechflag = 1,
-                    flag = 0
+                    flag = 0,
+                    codeList = GetSoftBedConfigureCode(softbed.mxList, 2)
                 });
             }
 
@@ -522,19 +526,183 @@ namespace JLHHJSvr.Helper
                     contfigtypename = $"{prefix}床架",
                     contfigtype = 0,
                     usechflag = 1,
-                    flag = 0
+                    flag = 0,
+                    codeList = GetSoftBedConfigureCode(softbed.mxList, 4)
                 });
             }
 
 			return dict;
+        }/// <summary>
+         /// 返回软床报价相关配置项
+         /// </summary>
+         /// <param name="softbed"></param>
+         /// <returns></returns>
+        private List<u_configure_code> GetSoftBedConfigureCode(List<u_softbed_mx> mxList,byte has_type)
+        {
+            var tempMxList = mxList.Where(t => t.has_type == has_type);
+
+            var codeMap = new Dictionary<string, List<u_configure_codemx>> ();
+            var codeMxMap = new Dictionary<Tuple<string,string>, List<u_softbed_mx>>();
+            foreach(var mx in tempMxList)
+            {
+                // 
+                var codeMxKey = Tuple.Create(mx.pzname,mx.pznamemx);
+                // 
+                if (!codeMxMap.ContainsKey(codeMxKey)) codeMxMap.Add(codeMxKey, new List<u_softbed_mx>());
+                codeMxMap[codeMxKey].Add(mx);
+            }
+
+            var codeMxList = new List<u_configure_codemx>();
+            foreach(var item in codeMxMap)
+            {
+                if (item.Value.Count <= 0) continue;
+                var key = item.Key;
+                var configureCodeMx = new u_configure_codemx()
+                {
+                    pzid = item.Value.First().pzid,
+                    namemx = key.Item2,
+                    printid = item.Value.First().pzmxid,
+                    ifdft = 1
+                };
+                var bomList = new List<u_configure_codemxbom>();
+
+                foreach(var mx in item.Value)
+                {
+                    bomList.Add(new u_configure_codemxbom()
+                    {
+                        pzid = mx.pzid,
+                        printid = mx.pzmxid,
+                        pid = 0,
+                        mtrlid = mx.mtrlid,
+                        default_length = mx.cutting_length,
+                        default_width = mx.cutting_width,
+                        default_qty = mx.cutting_qty,
+                        price = mx.price,
+                        price_formula = mx.price_formula,
+                        sonscale = mx.useqty,
+                        sonscale_formula = mx.use_formula,
+                        sonloss = mx.loss_rate
+                    });
+                }
+
+                configureCodeMx.bomList = bomList;
+                codeMxList.Add(configureCodeMx);
+
+                if (!codeMap.ContainsKey(key.Item1)) codeMap.Add(key.Item1, codeMxList);
+            }
+
+            var resultList = new List<u_configure_code>();
+            foreach(var item in codeMap)
+            {
+                if (item.Value.Count <= 0) continue;
+                var key = item.Key;
+                resultList.Add(new u_configure_code()
+                {
+                    name = key,
+                    typeid = 0,
+                    pzid = item.Value.First().pzid,
+                    ifuse = 1,
+                    codeMxList = item.Value
+                });
+            }
+
+            return resultList;
         }
-		/// <summary>
-		/// 获取软床报价配置
-		/// </summary>
-		/// <param name="softbed"></param>
-		/// <param name="extraWheres"></param>
-		/// <returns></returns>
-		public List<u_configure_type> GetSoftBedConfigureList(u_softbed softbed,List<string> extraWheres)
+        private void SaveSoftBedConfigure(u_configure_type configure)
+        {
+            var baseInfoHelper = GetHelper<BasicInfoHelper>(cmd, context);
+
+            // 判断配置是否已存在
+            cmd.CommandText = @"SELECT COUNT(*) AS printid FROM u_configure_type INNER JOIN u_configure_code ON u_configure_type.contfigtypeid = u_configure_code.typeid WHERE LTRIM(RTRIM(contfigtypename)) = @contfigtypename";
+            cmd.Parameters.Clear();
+            cmd.Parameters.AddWithValue("@contfigtypename", configure.contfigtypename);
+            int cnt = Convert.ToInt32(cmd.ExecuteScalar());
+            if (cnt == 0)
+            {
+                baseInfoHelper.SaveConfigureType(configure);
+            }
+
+            // 处理配置项
+            var codeList = new List<u_configure_code>();
+            var codeMxList = new List<u_configure_codemx>();
+            var codeMxBomList = new List<u_configure_codemxbom>();
+            int codeNum = cnt == 0 ? 1 : cnt;
+            foreach (var code in configure.codeList)
+            {
+                int printid = 1;
+                if (code.pzid > 0)
+                {
+                    cmd.CommandText = @"SELECT ISNULL(MAX(printid),0) AS printid FROM u_configure_codemx WHERE pzid = @pzid";
+                    cmd.Parameters.Clear();
+                    cmd.Parameters.AddWithValue("@pzid", code.pzid);
+                    using (var reader = cmd.ExecuteReader())
+                    {
+                        if (reader.Read())
+                        {
+                            printid = Convert.ToInt32(reader["printid"]);
+                        }
+                    }
+                }
+                //
+                code.typeid = configure.contfigtypeid;
+                code.pzid = code.pzid > 0 ? code.pzid : BllHelper.GetID(cmd, "u_configure_code");
+                code.pzcode = FormatCode(codeNum, 4);
+
+                foreach(var codeMx in code.codeMxList)
+                {
+                    codeMx.pzid = code.pzid;
+                    codeMx.printid = printid++;
+                    codeMx.pzcodemx = $"{code.pzcode}_{FormatCode(printid, 3)}";
+                    codeMxList.Add(codeMx);
+                }
+            }
+
+            foreach(var codeMx in codeMxList)
+            {
+                int pid = 1;
+                if(cnt > 0)
+                {
+                    cmd.CommandText = @"SELECT ISNULL(MAX(pid),0) AS printid FROM u_configure_codemxbom WHERE pzid = @pzid AND printid = @printid";
+                    cmd.Parameters.Clear();
+                    cmd.Parameters.AddWithValue("@pzid", codeMx.pzid);
+                    cmd.Parameters.AddWithValue("@printid", codeMx.printid);
+                    using (var reader = cmd.ExecuteReader())
+                    {
+                        if (reader.Read())
+                        {
+                            pid = Convert.ToInt32(reader["printid"]);
+                        }
+                    }
+                }
+                foreach (var bom in codeMx.bomList)
+                {
+                    bom.printid = codeMx.printid.Value;
+                    bom.pid = pid++;
+                    codeMxBomList.Add(bom);
+                }
+            }
+            DbSqlHelper.BulkInsert(cmd, codeList, "u_configure_code", null, "typeid,pzid,pzcode,inputtype,name,configtype,ifcross,ifcheck,ifuse,rulestr,ifnum,decnum,maxnum,minnum,pricestr,priceratestr,ifpack,zj_special");
+            DbSqlHelper.BulkInsert(cmd, codeMxList, "u_configure_codemx", null, "pzid,printid,pzcodemx,namemx,gradestr,mtrlcode,price,ifdft,MCostRate,ProfitRate,dscrp,ifuse,ifnoch,pricerate,packqty,packvol,price_pz,grade");
+            DbSqlHelper.BulkInsert(cmd, codeMxBomList, "u_configure_codemxbom", null, "pzid,printid,pid,mtrlid,sonscale,sonscale_formula,mng_cost_rate,profit_rate,realqty,sonloss,sonloss_formula,sondecloss,sondecloss_formula,default_length,default_width,default_qty");
+        }
+        public static string FormatCode(int number, int length)
+        {
+            int max = (int)Math.Pow(10, length) - 1;
+
+            if (number > max)
+            {
+                throw new Exception($"编码超出长度限制,{length}位最多只能到 {max}");
+            }
+
+            return number.ToString($"D{length}");
+        }
+        /// <summary>
+        /// 获取软床报价配置
+        /// </summary>
+        /// <param name="softbed"></param>
+        /// <param name="extraWheres"></param>
+        /// <returns></returns>
+        public List<u_configure_type> GetSoftBedConfigureList(u_softbed softbed,List<string> extraWheres)
 		{
 			var codeMxList = new List<u_configure_codemx>();
 
@@ -973,6 +1141,469 @@ namespace JLHHJSvr.Helper
             DbSqlHelper.BulkInsert(cmd, codeMxBom_copyList, "u_configure_codemxbom",null, "pzid,printid,pid,mtrlid,sonscale,sonscale_formula,mng_cost_rate,profit_rate,realqty,sonloss,sonloss_formula,sondecloss,sondecloss_formula,default_length,default_width,default_qty");
         }
 
+        /// <summary>
+        /// 新版模板核价资料保存。
+        /// 仅处理模板报价,将报价明细同步到核价配置资料,并将生成的 pzid/pzmxid 回写到报价明细。
+        /// </summary>
+        /// <param name="softbed"></param>
+        public void SaveSoftBedTemplateV2(u_softbed softbed)
+        {
+            if (softbed == null) throw new LJCommonException("模板核价保存失败,数据异常!");
+            if (softbed.is_template == 0) return;
+            if (string.IsNullOrWhiteSpace(softbed.softbed_code)) throw new LJCommonException("报价未生成唯一码,无法生成模板配置");
+            if (softbed.mxList == null || softbed.mxList.Count <= 0) return;
+
+            var typeNameMap = GetSoftBedTemplateTypeNameMapV2(softbed);
+            foreach (var item in typeNameMap)
+            {
+                var typeMxList = softbed.mxList
+                    .Where(t => t.has_type == item.Key)
+                    .OrderBy(t => t.printid)
+                    .ToList();
+
+                if (typeMxList.Count <= 0) continue;
+
+                ValidateSoftBedTemplateMxListV2(item.Value, typeMxList);
+
+                int typeId = EnsureSoftBedTemplateTypeV2(item.Value);
+                var codeMxGroups = typeMxList
+                    .GroupBy(t => new
+                    {
+                        PzName = (t.pzname ?? string.Empty).Trim(),
+                        PzNameMx = (t.pznamemx ?? string.Empty).Trim()
+                    })
+                    .ToList();
+
+                foreach (var group in codeMxGroups)
+                {
+                    var currentMxList = group.OrderBy(t => t.printid).ToList();
+                    var firstMx = currentMxList.First();
+
+                    int pzid = EnsureSoftBedTemplateCodeV2(typeId, firstMx.pzid, group.Key.PzName);
+                    int pzmxid = EnsureSoftBedTemplateCodeMxV2(pzid, firstMx.pzmxid, group.Key.PzNameMx);
+
+                    foreach (var mx in currentMxList)
+                    {
+                        mx.pzid = pzid;
+                        mx.pzmxid = pzmxid;
+                        mx.pzname = group.Key.PzName;
+                        mx.pznamemx = group.Key.PzNameMx;
+                    }
+
+                    ReplaceSoftBedTemplateBomV2(pzid, pzmxid, currentMxList);
+                }
+            }
+
+            BackWriteSoftBedTemplateMxIdsV2(softbed);
+        }
+
+        private Dictionary<byte, string> GetSoftBedTemplateTypeNameMapV2(u_softbed softbed)
+        {
+            var dict = new Dictionary<byte, string>();
+            string prefix = $"{softbed.softbed_code}|";
+
+            if (softbed.has_headboard == 1) dict[1] = $"{prefix}床头";
+            if (softbed.has_nightstand == 1) dict[2] = $"{prefix}床头柜";
+            if (softbed.has_bedframe == 1) dict[4] = $"{prefix}床架";
+
+            return dict;
+        }
+
+        private void ValidateSoftBedTemplateMxListV2(string typeName, List<u_softbed_mx> mxList)
+        {
+            foreach (var mx in mxList)
+            {
+                if (string.IsNullOrWhiteSpace(mx.pzname))
+                {
+                    throw new LJCommonException($"模板核价保存失败,类型:{typeName},明细行:{mx.printid} 未设置部件选配项名称(pzname)");
+                }
+
+                if (string.IsNullOrWhiteSpace(mx.pznamemx))
+                {
+                    throw new LJCommonException($"模板核价保存失败,类型:{typeName},明细行:{mx.printid} 未设置部件选配明细名称(pznamemx)");
+                }
+
+                if (mx.mtrlid <= 0)
+                {
+                    throw new LJCommonException($"模板核价保存失败,类型:{typeName},明细行:{mx.printid} 物料ID无效");
+                }
+            }
+        }
+
+        private int EnsureSoftBedTemplateTypeV2(string typeName)
+        {
+            cmd.CommandText = @"SELECT TOP 1 contfigtypeid 
+                                FROM u_configure_type 
+                                WHERE LTRIM(RTRIM(contfigtypename)) = LTRIM(RTRIM(@contfigtypename))";
+            cmd.Parameters.Clear();
+            cmd.Parameters.AddWithValue("@contfigtypename", typeName);
+            object result = cmd.ExecuteScalar();
+            if (result != null && result != DBNull.Value)
+            {
+                return Convert.ToInt32(result);
+            }
+
+            var configure = new u_configure_type()
+            {
+                contfigtypeid = BllHelper.GetID(cmd, "u_configure_type"),
+                contfigtypename = typeName,
+                contfigtype = 0,
+                usechflag = 1,
+                flag = 0
+            };
+            DbSqlHelper.Insert(cmd, "u_configure_type", null, configure, "contfigtypeid,contfigtype,contfigtypename,usechflag,flag");
+            return configure.contfigtypeid.Value;
+        }
+
+        private int EnsureSoftBedTemplateCodeV2(int typeId, int currentPzid, string codeName)
+        {
+            if (string.IsNullOrWhiteSpace(codeName)) throw new LJCommonException("模板核价保存失败,配置项名称不能为空");
+
+            string pzcode = string.Empty;
+            if (currentPzid > 0)
+            {
+                bool isExists = false;
+                int dbTypeId = 0;
+                string dbName = string.Empty;
+
+                cmd.CommandText = @"SELECT TOP 1 typeid,pzcode,name 
+                                    FROM u_configure_code 
+                                    WHERE pzid = @pzid";
+                cmd.Parameters.Clear();
+                cmd.Parameters.AddWithValue("@pzid", currentPzid);
+                using (var reader = cmd.ExecuteReader())
+                {
+                    if (reader.Read())
+                    {
+                        isExists = true;
+                        pzcode = Convert.ToString(reader["pzcode"]);
+                        dbTypeId = Convert.ToInt32(reader["typeid"]);
+                        dbName = Convert.ToString(reader["name"]);
+                    }
+                }
+
+                if (isExists)
+                {
+                    if (dbTypeId != typeId || !string.Equals((dbName ?? string.Empty).Trim(), codeName.Trim(), StringComparison.Ordinal))
+                    {
+                        cmd.CommandText = @"UPDATE u_configure_code
+                                            SET typeid = @typeid,
+                                                pzcode = @pzcode,
+                                                name = @name,
+                                                inputtype = @inputtype,
+                                                configtype = @configtype,
+                                                ifcross = @ifcross,
+                                                ifcheck = @ifcheck,
+                                                ifuse = @ifuse,
+                                                ifnum = @ifnum,
+                                                decnum = @decnum,
+                                                ifpack = @ifpack
+                                            WHERE pzid = @pzid";
+                        cmd.Parameters.Clear();
+                        cmd.Parameters.AddWithValue("@typeid", typeId);
+                        cmd.Parameters.AddWithValue("@pzcode", pzcode ?? string.Empty);
+                        cmd.Parameters.AddWithValue("@name", codeName);
+                        cmd.Parameters.AddWithValue("@inputtype", 0);
+                        cmd.Parameters.AddWithValue("@configtype", 0);
+                        cmd.Parameters.AddWithValue("@ifcross", 0);
+                        cmd.Parameters.AddWithValue("@ifcheck", 0);
+                        cmd.Parameters.AddWithValue("@ifuse", 1);
+                        cmd.Parameters.AddWithValue("@ifnum", 0);
+                        cmd.Parameters.AddWithValue("@decnum", 0);
+                        cmd.Parameters.AddWithValue("@ifpack", 0);
+                        cmd.Parameters.AddWithValue("@pzid", currentPzid);
+                        cmd.ExecuteNonQuery();
+                    }
+
+                    return currentPzid;
+                }
+            }
+
+            cmd.CommandText = @"SELECT TOP 1 pzid 
+                                FROM u_configure_code
+                                WHERE typeid = @typeid
+                                  AND LTRIM(RTRIM(name)) = LTRIM(RTRIM(@name))";
+            cmd.Parameters.Clear();
+            cmd.Parameters.AddWithValue("@typeid", typeId);
+            cmd.Parameters.AddWithValue("@name", codeName);
+            object result = cmd.ExecuteScalar();
+            if (result != null && result != DBNull.Value)
+            {
+                return Convert.ToInt32(result);
+            }
+
+            int nextPzid = BllHelper.GetID(cmd, "u_configure_code");
+            pzcode = GetNextSoftBedTemplateCodeNoV2(typeId);
+
+            var insertCode = new u_configure_code()
+            {
+                typeid = typeId,
+                pzid = nextPzid,
+                pzcode = pzcode,
+                name = codeName,
+                inputtype = 0,
+                configtype = 0,
+                ifcross = 0,
+                ifcheck = 0,
+                ifuse = 1,
+                ifnum = 0,
+                decnum = 0,
+                ifpack = 0
+            };
+            DbSqlHelper.Insert(cmd, "u_configure_code", null, insertCode, "typeid,pzid,pzcode,inputtype,name,configtype,ifcross,ifcheck,ifuse,ifnum,decnum,ifpack");
+            return nextPzid;
+        }
+
+        private int EnsureSoftBedTemplateCodeMxV2(int pzid, int currentPrintId, string codeMxName)
+        {
+            if (string.IsNullOrWhiteSpace(codeMxName)) throw new LJCommonException("模板核价保存失败,配置项明细名称不能为空");
+
+            string pzcode = GetSoftBedTemplateCodeNoV2(pzid);
+
+            if (currentPrintId > 0)
+            {
+                bool isExists = false;
+                string dbName = string.Empty;
+
+                cmd.CommandText = @"SELECT TOP 1 namemx
+                                    FROM u_configure_codemx
+                                    WHERE pzid = @pzid AND printid = @printid";
+                cmd.Parameters.Clear();
+                cmd.Parameters.AddWithValue("@pzid", pzid);
+                cmd.Parameters.AddWithValue("@printid", currentPrintId);
+                using (var reader = cmd.ExecuteReader())
+                {
+                    if (reader.Read())
+                    {
+                        isExists = true;
+                        dbName = Convert.ToString(reader["namemx"]);
+                    }
+                }
+
+                if (isExists)
+                {
+                    if (!string.Equals((dbName ?? string.Empty).Trim(), codeMxName.Trim(), StringComparison.Ordinal))
+                    {
+                        cmd.CommandText = @"UPDATE u_configure_codemx
+                                            SET pzcodemx = @pzcodemx,
+                                                namemx = @namemx,
+                                                ifuse = @ifuse
+                                            WHERE pzid = @pzid
+                                              AND printid = @printid";
+                        cmd.Parameters.Clear();
+                        cmd.Parameters.AddWithValue("@pzcodemx", $"{pzcode}_{FormatCode(currentPrintId, 3)}");
+                        cmd.Parameters.AddWithValue("@namemx", codeMxName);
+                        cmd.Parameters.AddWithValue("@ifuse", 1);
+                        cmd.Parameters.AddWithValue("@pzid", pzid);
+                        cmd.Parameters.AddWithValue("@printid", currentPrintId);
+                        cmd.ExecuteNonQuery();
+                    }
+
+                    return currentPrintId;
+                }
+            }
+
+            cmd.CommandText = @"SELECT TOP 1 printid 
+                                FROM u_configure_codemx
+                                WHERE pzid = @pzid
+                                  AND LTRIM(RTRIM(namemx)) = LTRIM(RTRIM(@namemx))";
+            cmd.Parameters.Clear();
+            cmd.Parameters.AddWithValue("@pzid", pzid);
+            cmd.Parameters.AddWithValue("@namemx", codeMxName);
+            object result = cmd.ExecuteScalar();
+            if (result != null && result != DBNull.Value)
+            {
+                return Convert.ToInt32(result);
+            }
+
+            int nextPrintId = GetNextSoftBedTemplateCodeMxIdV2(pzid);
+            byte defaultFlag = HasSoftBedTemplateDefaultCodeMxV2(pzid) ? (byte)0 : (byte)1;
+
+            var insertMx = new u_configure_codemx()
+            {
+                pzid = pzid,
+                printid = nextPrintId,
+                pzcodemx = $"{pzcode}_{FormatCode(nextPrintId, 3)}",
+                namemx = codeMxName,
+                ifdft = defaultFlag,
+                ifuse = 1
+            };
+            DbSqlHelper.Insert(cmd, "u_configure_codemx", null, insertMx, "pzid,printid,pzcodemx,namemx,ifdft,ifuse");
+            return nextPrintId;
+        }
+
+        private void ReplaceSoftBedTemplateBomV2(int pzid, int printid, List<u_softbed_mx> mxList)
+        {
+            var dbBomMap = new Dictionary<int, u_configure_codemxbom>();
+            cmd.CommandText = @"SELECT pzid,printid,pid,mtrlid,sonscale,sonscale_formula,mng_cost_rate,profit_rate,realqty,sonloss,sonloss_formula,
+                                       sondecloss,sondecloss_formula,default_length,default_width,default_qty
+                                FROM u_configure_codemxbom
+                                WHERE pzid = @pzid AND printid = @printid
+                                ORDER BY pid";
+            cmd.Parameters.Clear();
+            cmd.Parameters.AddWithValue("@pzid", pzid);
+            cmd.Parameters.AddWithValue("@printid", printid);
+            using (var reader = cmd.ExecuteReader())
+            {
+                while (reader.Read())
+                {
+                    int mtrlid = Convert.ToInt32(reader["mtrlid"]);
+                    if (dbBomMap.ContainsKey(mtrlid)) continue;
+
+                    dbBomMap.Add(mtrlid, new u_configure_codemxbom()
+                    {
+                        pzid = Convert.ToInt32(reader["pzid"]),
+                        printid = Convert.ToInt32(reader["printid"]),
+                        pid = Convert.ToInt32(reader["pid"]),
+                        mtrlid = mtrlid,
+                        sonscale = Convert.ToDecimal(reader["sonscale"]),
+                        sonscale_formula = Convert.ToString(reader["sonscale_formula"]),
+                        mng_cost_rate = Convert.ToDecimal(reader["mng_cost_rate"]),
+                        profit_rate = Convert.ToDecimal(reader["profit_rate"]),
+                        realqty = Convert.ToDecimal(reader["realqty"]),
+                        sonloss = Convert.ToDecimal(reader["sonloss"]),
+                        sonloss_formula = Convert.ToString(reader["sonloss_formula"]),
+                        sondecloss = Convert.ToDecimal(reader["sondecloss"]),
+                        sondecloss_formula = Convert.ToString(reader["sondecloss_formula"]),
+                        default_length = Convert.ToDecimal(reader["default_length"]),
+                        default_width = Convert.ToDecimal(reader["default_width"]),
+                        default_qty = Convert.ToDecimal(reader["default_qty"])
+                    });
+                }
+            }
+
+            int nextPid = GetNextSoftBedTemplateBomPidV2(pzid, printid);
+            foreach (var mx in mxList.OrderBy(t => t.printid))
+            {
+                if (dbBomMap.TryGetValue(mx.mtrlid, out var dbBom))
+                {
+                    cmd.CommandText = @"UPDATE u_configure_codemxbom
+                                        SET sonscale = @sonscale,
+                                            sonscale_formula = @sonscale_formula,
+                                            sonloss = @sonloss,
+                                            default_length = @default_length,
+                                            default_width = @default_width,
+                                            default_qty = @default_qty
+                                        WHERE pzid = @pzid
+                                          AND printid = @printid
+                                          AND pid = @pid";
+                    cmd.Parameters.Clear();
+                    cmd.Parameters.AddWithValue("@sonscale", mx.useqty);
+                    cmd.Parameters.AddWithValue("@sonscale_formula", mx.use_formula ?? string.Empty);
+                    cmd.Parameters.AddWithValue("@sonloss", mx.loss_rate);
+                    cmd.Parameters.AddWithValue("@default_length", mx.cutting_length);
+                    cmd.Parameters.AddWithValue("@default_width", mx.cutting_width);
+                    cmd.Parameters.AddWithValue("@default_qty", mx.cutting_qty);
+                    cmd.Parameters.AddWithValue("@pzid", pzid);
+                    cmd.Parameters.AddWithValue("@printid", printid);
+                    cmd.Parameters.AddWithValue("@pid", dbBom.pid);
+                    cmd.ExecuteNonQuery();
+                    continue;
+                }
+
+                var bom = new u_configure_codemxbom()
+                {
+                    pzid = pzid,
+                    printid = printid,
+                    pid = nextPid++,
+                    mtrlid = mx.mtrlid,
+                    sonscale = mx.useqty,
+                    sonscale_formula = mx.use_formula,
+                    mng_cost_rate = 0,
+                    profit_rate = 0,
+                    realqty = 0,
+                    sonloss = mx.loss_rate,
+                    sonloss_formula = string.Empty,
+                    sondecloss = 0,
+                    sondecloss_formula = string.Empty,
+                    price = mx.price,
+                    price_formula = mx.price_formula,
+                    default_length = mx.cutting_length,
+                    default_width = mx.cutting_width,
+                    default_qty = mx.cutting_qty
+                };
+                DbSqlHelper.Insert(cmd, "u_configure_codemxbom", null, bom, "pzid,printid,pid,mtrlid,sonscale,sonscale_formula,mng_cost_rate,profit_rate,realqty,sonloss,sonloss_formula,sondecloss,sondecloss_formula,default_length,default_width,default_qty");
+            }
+        }
+
+        private void BackWriteSoftBedTemplateMxIdsV2(u_softbed softbed)
+        {
+            if (softbed.softbed_id <= 0 || softbed.mxList == null || softbed.mxList.Count <= 0) return;
+
+            foreach (var mx in softbed.mxList.Where(t => t.has_type == 1 || t.has_type == 2 || t.has_type == 4))
+            {
+                cmd.CommandText = @"UPDATE u_softbed_mx
+                                    SET pzid = @pzid,
+                                        pzmxid = @pzmxid,
+                                        pzname = @pzname
+                                    WHERE softbed_id = @softbed_id
+                                      AND printid = @printid";
+                cmd.Parameters.Clear();
+                cmd.Parameters.AddWithValue("@pzid", mx.pzid);
+                cmd.Parameters.AddWithValue("@pzmxid", mx.pzmxid);
+                cmd.Parameters.AddWithValue("@pzname", mx.pzname ?? string.Empty);
+                cmd.Parameters.AddWithValue("@softbed_id", softbed.softbed_id);
+                cmd.Parameters.AddWithValue("@printid", mx.printid);
+                cmd.ExecuteNonQuery();
+            }
+        }
+
+        private string GetNextSoftBedTemplateCodeNoV2(int typeId)
+        {
+            int maxNum = 0;
+            cmd.CommandText = @"SELECT pzcode FROM u_configure_code WHERE typeid = @typeid";
+            cmd.Parameters.Clear();
+            cmd.Parameters.AddWithValue("@typeid", typeId);
+            using (var reader = cmd.ExecuteReader())
+            {
+                while (reader.Read())
+                {
+                    string pzcode = Convert.ToString(reader["pzcode"]).Trim();
+                    if (int.TryParse(pzcode, out int currentNum) && currentNum > maxNum)
+                    {
+                        maxNum = currentNum;
+                    }
+                }
+            }
+
+            return FormatCode(maxNum + 1, 4);
+        }
+
+        private string GetSoftBedTemplateCodeNoV2(int pzid)
+        {
+            cmd.CommandText = @"SELECT TOP 1 pzcode FROM u_configure_code WHERE pzid = @pzid";
+            cmd.Parameters.Clear();
+            cmd.Parameters.AddWithValue("@pzid", pzid);
+            object result = cmd.ExecuteScalar();
+            return result == null || result == DBNull.Value ? string.Empty : Convert.ToString(result).Trim();
+        }
+
+        private int GetNextSoftBedTemplateCodeMxIdV2(int pzid)
+        {
+            cmd.CommandText = @"SELECT ISNULL(MAX(printid),0) FROM u_configure_codemx WHERE pzid = @pzid";
+            cmd.Parameters.Clear();
+            cmd.Parameters.AddWithValue("@pzid", pzid);
+            object result = cmd.ExecuteScalar();
+            return Convert.ToInt32(result) + 1;
+        }
+
+        private bool HasSoftBedTemplateDefaultCodeMxV2(int pzid)
+        {
+            cmd.CommandText = @"SELECT COUNT(*) FROM u_configure_codemx WHERE pzid = @pzid AND ifdft = 1";
+            cmd.Parameters.Clear();
+            cmd.Parameters.AddWithValue("@pzid", pzid);
+            return Convert.ToInt32(cmd.ExecuteScalar()) > 0;
+        }
+
+        private int GetNextSoftBedTemplateBomPidV2(int pzid, int printid)
+        {
+            cmd.CommandText = @"SELECT ISNULL(MAX(pid),0) FROM u_configure_codemxbom WHERE pzid = @pzid AND printid = @printid";
+            cmd.Parameters.Clear();
+            cmd.Parameters.AddWithValue("@pzid", pzid);
+            cmd.Parameters.AddWithValue("@printid", printid);
+            return Convert.ToInt32(cmd.ExecuteScalar()) + 1;
+        }
+
         #region 通用公式
         private CalculateFormula formula = new CalculateFormula();
 		private string BillKeyWord = "u_softbed";

+ 16 - 13
JLHWEB/src/components/LjDialog/index-new.vue

@@ -3,6 +3,7 @@
     v-model="drawer"
     class="lj-dialog"
     :class="[globalStore.assemblySize, ifoverflow ? 'lj-dialog-overflow' : '']"
+    :style="dialogStyle"
     v-bind="{ ...drawerDefineProp, ...$attrs }"
     @closed="closed"
   >
@@ -14,14 +15,7 @@
       </slot>
     </template>
     <template #default>
-      <template v-if="Object.keys(dlStyle).length">
-        <div :style="dlStyle">
-          <slot />
-        </div>
-      </template>
-      <template v-else>
-        <slot />
-      </template>
+      <slot />
     </template>
     <template #footer v-if="slots.footer || confirmed">
       <slot name="footer" v-if="slots.footer"></slot>
@@ -65,7 +59,7 @@ const globalStore = useGlobalStore();
 /**
  * @description 高度样式
  */
-const dlStyle = computed(() => {
+const dialogStyle = computed(() => {
   let style: any = {};
   if (!props.height) return style;
   if (typeof props.height == "number") {
@@ -119,14 +113,22 @@ defineExpose({ show: showFunc, hide });
 <style lang="scss">
 .lj-dialog {
   border-radius: $br-md;
+  display: flex;
+  flex-direction: column;
   &.is-fullscreen {
-    display: flex;
-    flex-direction: column;
     height: 100%;
   }
+  .el-dialog__header {
+    flex-shrink: 0;
+  }
   .el-dialog__body {
     flex: 1;
+    min-height: 0;
     padding: $space-b2 $space-b3 $space-b3 $space-b3;
+    overflow: auto;
+  }
+  .el-dialog__footer {
+    flex-shrink: 0;
   }
   &.lj-dialog-overflow {
     overflow: hidden;
@@ -151,10 +153,11 @@ defineExpose({ show: showFunc, hide });
   }
   &.is-selector {
     .el-dialog__header {
-      padding: 12px $space-b2;
+      padding: $space-b1 $space-b1;
       color: $color-black;
       background: $color-primary-100;
-      height: 15px;
+      min-height: 44px;
+      box-sizing: border-box;
     }
     .el-dialog__headerbtn {
       width: 44px;

+ 3 - 0
JLHWEB/src/views/baseinfo/configure/index.vue

@@ -38,6 +38,7 @@
           :table-events="tableEvents_mid"
           :init-param="initParams_mid"
           :auto-load-layout="false"
+          :request-auto="false"
           pagination
         >
           <!-- 表格 header 按钮 -->
@@ -64,6 +65,7 @@
               :table-events="tableEvents_right"
               :init-param="initParams_right"
               :auto-load-layout="false"
+              :request-auto="false"
               pagination
             >
               <!-- 表格 header 按钮 -->
@@ -90,6 +92,7 @@
                   :table-events="tableEvents_bottom"
                   :init-param="initParams_bottom"
                   :auto-load-layout="false"
+                  :request-auto="false"
                   pagination
                 >
                   <!-- 表格 header 按钮 -->

+ 16 - 19
JLHWEB/src/views/quote/mattressQuote/detail.vue

@@ -2111,25 +2111,22 @@ const orderDefaultAction = [
       return !orderStatus.value;
     },
     clickFunc: item => {
-      // if (route.path.indexOf("/new") > -1) {
-      //   tabRemove(route.fullPath);
-      //   router.replace("/mattressQuote");
-      // } else {
-      // router.replace(
-      //   `/mattressQuote/detail?id=${LjDetailRef.value._mainData.mattressid}&code=${LjDetailRef.value._mainData.mattresscode}`
-      // );
-      pageRefresh({
-        name: "mattressQuoteDetail",
-        params: {
-          id: LjDetailRef.value._mainData.mattressid,
-          code: LjDetailRef.value._mainData.mattresscode
-        },
-        query: {
-          id: LjDetailRef.value._mainData.mattressid,
-          code: LjDetailRef.value._mainData.mattresscode
-        }
-      });
-      // }
+      if (route.path.indexOf("/new") > -1) {
+        tabRemove(route.fullPath);
+        router.replace("/mattressQuote");
+      } else {
+        pageRefresh({
+          name: "mattressQuoteDetail",
+          params: {
+            id: LjDetailRef.value._mainData.mattressid,
+            code: LjDetailRef.value._mainData.mattresscode
+          },
+          query: {
+            id: LjDetailRef.value._mainData.mattressid,
+            code: LjDetailRef.value._mainData.mattresscode
+          }
+        });
+      }
     }
   }),
   buttonNew({

+ 981 - 0
JLHWEB/src/views/quote/softbedQuote/components/CustomConfigureDialog.vue

@@ -0,0 +1,981 @@
+<template>
+  <LjDialogNew ref="dialogRef" class="is-selector" width="88%" height="92vh" :closed="handleClosed" ifoverflow>
+    <template #header>
+      <div class="flx-1">
+        <span class="text-h5-b">{{ typeName }}</span>
+      </div>
+    </template>
+
+    <div class="custom-config-dialog">
+      <div class="custom-config-body">
+        <div class="custom-config-pane custom-config-pane--mid">
+          <LjVxeTable
+            ref="midTableRef"
+            row-key="__rowKey"
+            table-cls="h-full"
+            :columns="columnsMid"
+            :data="midList"
+            dwname="web_configure_codelist"
+            :tool-button="[]"
+            :table-props="tableProps"
+            :table-events="midTableEvents"
+            :auto-load-layout="false"
+            :request-auto="false"
+          >
+            <template #tableHeader>
+              <el-space wrap>
+                <template v-if="isTableEditing('mid')">
+                  <el-button type="primary" :loading="tableSaving.mid" @click="saveMidTable">保存</el-button>
+                  <el-button @click="appendMidRow">增行</el-button>
+                  <el-button @click="removeMidEditRow">删行</el-button>
+                  <el-button @click="insertMidRow">插行</el-button>
+                  <el-button @click="cancelTableEdit">取消</el-button>
+                </template>
+                <template v-else>
+                  <el-button type="primary" :disabled="hasActiveEdit" @click="enterTableEdit('mid')">编辑</el-button>
+                  <el-button type="danger" :disabled="!currentMidRow || hasActiveEdit" @click="deleteMidRow">删除</el-button>
+                </template>
+              </el-space>
+            </template>
+          </LjVxeTable>
+        </div>
+
+        <div class="custom-config-pane custom-config-pane--right">
+          <LjVxeTable
+            ref="rightTableRef"
+            row-key="__rowKey"
+            table-cls="h-full"
+            :columns="columnsRight"
+            :data="rightList"
+            dwname="web_configure_codemxlist"
+            :tool-button="[]"
+            :table-props="tableProps"
+            :auto-load-layout="false"
+            :table-events="rightTableEvents"
+            :request-auto="false"
+          >
+            <template #tableHeader>
+              <el-space wrap>
+                <template v-if="isTableEditing('right')">
+                  <el-button type="primary" :loading="tableSaving.right" @click="saveRightTable">保存</el-button>
+                  <el-button @click="appendRightRow">增行</el-button>
+                  <el-button @click="removeRightEditRow">删行</el-button>
+                  <el-button @click="insertRightRow">插行</el-button>
+                  <el-button @click="cancelTableEdit">取消</el-button>
+                </template>
+                <template v-else>
+                  <el-button type="primary" :disabled="!currentMidRow || hasActiveEdit" @click="enterTableEdit('right')"
+                    >编辑</el-button
+                  >
+                  <el-button type="danger" :disabled="!currentRightRow || hasActiveEdit" @click="deleteRightRow">删除</el-button>
+                </template>
+              </el-space>
+            </template>
+          </LjVxeTable>
+        </div>
+
+        <div class="custom-config-pane custom-config-pane--bottom">
+          <LjVxeTable
+            ref="bottomTableRef"
+            row-key="__rowKey"
+            table-cls="h-full"
+            :columns="columnsBottom"
+            :data="bottomList"
+            dwname="web_configure_codemxbomlist"
+            :tool-button="[]"
+            :table-props="tableProps"
+            :table-events="bottomTableEvents"
+            :auto-load-layout="false"
+            :request-auto="false"
+          >
+            <template #tableHeader>
+              <el-space wrap>
+                <template v-if="isTableEditing('bottom')">
+                  <el-button type="primary" :loading="tableSaving.bottom" @click="saveBottomTable">保存</el-button>
+                  <el-button @click="appendBottomRow">增行</el-button>
+                  <el-button @click="removeBottomEditRow">删行</el-button>
+                  <el-button @click="insertBottomRow">插行</el-button>
+                  <el-button @click="cancelTableEdit">取消</el-button>
+                </template>
+                <template v-else>
+                  <el-button type="primary" :disabled="!currentRightRow || hasActiveEdit" @click="enterTableEdit('bottom')"
+                    >编辑</el-button
+                  >
+                  <el-button type="danger" :disabled="!currentBottomRow || hasActiveEdit" @click="deleteBottomRow"
+                    >删除</el-button
+                  >
+                </template>
+              </el-space>
+            </template>
+          </LjVxeTable>
+        </div>
+      </div>
+    </div>
+  </LjDialogNew>
+  <ErpMtrlPriceDialog ref="ErpMtrlPriceDialogRef" v-bind="ErpMtrlPriceDialogProps" />
+</template>
+
+<script setup lang="tsx">
+import { computed, nextTick, ref } from "vue";
+import { cloneDeep } from "lodash-es";
+import { ElMessage, ElMessageBox, ElNotification } from "element-plus";
+import LjDialogNew from "@/components/LjDialog/index-new.vue";
+import ErpMtrlPriceSelect from "@/views/system/selector/erpMtrlPrice/select.vue";
+import ErpMtrlPriceDialog from "@/views/system/selector/erpMtrlPrice/index.vue";
+import {
+  DeleteConfigureBomList,
+  DeleteConfigureCode,
+  DeleteConfigureCodeMx,
+  SaveConfigureBomList,
+  SaveConfigureCode,
+  SaveConfigureCodeMx,
+  SaveConfigureType,
+  getConfigureCodeList,
+  getConfigureCodeMxBomList,
+  getConfigureCodeMxList,
+  getConfigureTypeList
+} from "@/api/modules/basicinfo";
+
+type PartType = 1 | 2 | 4;
+type TableKey = "mid" | "right" | "bottom";
+
+interface DialogPayload {
+  partType: PartType;
+  partKey: "headboard" | "nightstand" | "bedframe";
+  partLabel: string;
+  typeName: string;
+}
+
+interface MidRow {
+  __rowKey: string;
+  pzid: number;
+  typeid: number;
+  pzcode: string;
+  name: string;
+  inputtype: number;
+  ifnum: number;
+  ifcross: number;
+  ifcheck: number;
+  ifuse: number;
+  maxnum: number | string;
+  minnum: number | string;
+  pricestr: string;
+  priceratestr: string;
+}
+
+interface RightRow {
+  __rowKey: string;
+  pzid: number;
+  printid: number;
+  pzcodemx: string;
+  namemx: string;
+  ifuse: number;
+  ifdft: number;
+}
+
+interface BottomRow {
+  __rowKey: string;
+  pzid: number;
+  printid: number;
+  pid: number;
+  mtrlid: number;
+  mtrlname: string;
+  mtrlcode: string;
+  mtrlmode: string;
+  unit: string;
+  default_length: number | string;
+  default_width: number | string;
+  default_qty: number | string;
+  sonscale: number | string;
+  sonscale_formula: string;
+  sonloss: number | string;
+}
+
+const dialogRef = ref();
+const midTableRef = ref();
+const rightTableRef = ref();
+const bottomTableRef = ref();
+const ErpMtrlPriceDialogRef = ref();
+const ErpMtrlPriceDialogProps = ref({});
+
+const typeName = ref("");
+const typeId = ref(0);
+const midList = ref<MidRow[]>([]);
+const rightList = ref<RightRow[]>([]);
+const bottomList = ref<BottomRow[]>([]);
+const currentMidKey = ref("");
+const currentRightKey = ref("");
+const currentBottomKey = ref("");
+const editTable = ref<TableKey | "">("");
+const editSnapshot = ref<any[]>([]);
+const editSelectedKey = ref("");
+const tableSaving = ref({ mid: false, right: false, bottom: false });
+
+let seed = 1;
+const nextKey = (prefix: string) => `${prefix}_${Date.now()}_${seed++}`;
+
+const tableProps = {
+  height: "auto",
+  minHeight: "180px",
+  mouseConfig: { selected: true }
+};
+
+const currentMidRow = computed(() => midList.value.find(item => item.__rowKey === currentMidKey.value) || null);
+const currentRightRow = computed(() => rightList.value.find(item => item.__rowKey === currentRightKey.value) || null);
+const currentBottomRow = computed(() => bottomList.value.find(item => item.__rowKey === currentBottomKey.value) || null);
+const hasActiveEdit = computed(() => Boolean(editTable.value));
+
+const getTableRefByKey = (table: TableKey) => {
+  if (table === "mid") return midTableRef;
+  if (table === "right") return rightTableRef;
+  return bottomTableRef;
+};
+
+function getTableRows<T>(table: TableKey) {
+  const tableData = getTableRefByKey(table).value?.element?.getTableData?.();
+  return (tableData?.fullData || []) as T[];
+}
+
+const isTableEditing = (table: TableKey) => editTable.value === table;
+
+const getCurrentKeyByTable = (table: TableKey) => {
+  if (table === "mid") return currentMidKey.value;
+  if (table === "right") return currentRightKey.value;
+  return currentBottomKey.value;
+};
+
+const setCurrentKeyByTable = (table: TableKey, key: string) => {
+  if (table === "mid") currentMidKey.value = key;
+  if (table === "right") currentRightKey.value = key;
+  if (table === "bottom") currentBottomKey.value = key;
+};
+
+const getRowKey = (row?: any) => row?.__rowKey || "";
+
+const dataCallback = (data: any) => data.datatable || [];
+
+const markMidRow = (row: any): MidRow => ({
+  __rowKey: row.__rowKey || nextKey("mid"),
+  pzid: Number(row.pzid || 0),
+  typeid: Number(row.typeid || 0),
+  pzcode: row.pzcode || "",
+  name: row.name || "",
+  inputtype: Number(row.inputtype || 0),
+  ifnum: Number(row.ifnum || 0),
+  ifcross: Number(row.ifcross || 0),
+  ifcheck: Number(row.ifcheck || 0),
+  ifuse: row.ifuse == null ? 1 : Number(row.ifuse),
+  maxnum: row.maxnum ?? "",
+  minnum: row.minnum ?? "",
+  pricestr: row.pricestr || "",
+  priceratestr: row.priceratestr || ""
+});
+
+const markRightRow = (row: any): RightRow => ({
+  __rowKey: row.__rowKey || nextKey("right"),
+  pzid: Number(row.pzid || 0),
+  printid: Number(row.printid || 0),
+  pzcodemx: row.pzcodemx || "",
+  namemx: row.namemx || "",
+  ifuse: row.ifuse == null ? 1 : Number(row.ifuse),
+  ifdft: Number(row.ifdft || 0)
+});
+
+const markBottomRow = (row: any): BottomRow => ({
+  __rowKey: row.__rowKey || nextKey("bottom"),
+  pzid: Number(row.pzid || 0),
+  printid: Number(row.printid || 0),
+  pid: Number(row.pid || 0),
+  mtrlid: Number(row.mtrlid || 0),
+  mtrlname: row.mtrlname || "",
+  mtrlcode: row.mtrlcode || "",
+  mtrlmode: row.mtrlmode || "",
+  unit: row.unit || "",
+  default_length: row.default_length ?? row.cutting_length ?? "",
+  default_width: row.default_width ?? row.cutting_width ?? "",
+  default_qty: row.default_qty ?? row.cutting_qty ?? "",
+  sonscale: row.sonscale ?? row.useqty ?? "",
+  sonscale_formula: row.sonscale_formula ?? row.use_formula ?? "",
+  sonloss: row.sonloss ?? row.loss_rate ?? ""
+});
+
+const createMidRow = (): MidRow =>
+  markMidRow({
+    pzid: 0,
+    typeid: typeId.value,
+    ifuse: 1
+  });
+
+const createRightRow = (): RightRow =>
+  markRightRow({
+    pzid: Number(currentMidRow.value?.pzid || 0),
+    printid: 0,
+    ifuse: 1,
+    ifdft: 0
+  });
+
+const createBottomRow = (): BottomRow =>
+  markBottomRow({
+    pzid: Number(currentRightRow.value?.pzid || 0),
+    printid: Number(currentRightRow.value?.printid || 0),
+    pid: 0,
+    mtrlid: 0,
+    mtrlname: "",
+    mtrlcode: "",
+    mtrlmode: "",
+    unit: "",
+    default_length: "",
+    default_width: "",
+    default_qty: "",
+    sonscale: "",
+    sonscale_formula: "",
+    sonloss: ""
+  });
+
+const setCurrentTableRow = (table: TableKey, row?: any) => {
+  nextTick(() => {
+    if (row) getTableRefByKey(table).value?.element?.setCurrentRow?.(row);
+  });
+};
+
+const ensureType = async () => {
+  if (typeId.value) return typeId.value;
+
+  await SaveConfigureType({
+    configure: {
+      contfigtypeid: 0,
+      contfigtypename: typeName.value
+    }
+  });
+
+  const result = await getConfigureTypeList({ arg_search: typeName.value, pageSize: 500, pageNum: 1 });
+  const typeRow = (result.datatable || []).find((item: any) => item.contfigtypename === typeName.value);
+  typeId.value = Number(typeRow?.contfigtypeid || 0);
+  if (!typeId.value) throw new Error("无法获取配置类型主键。");
+  return typeId.value;
+};
+
+const loadBottomList = async (preferredPid = 0) => {
+  if (!currentRightRow.value?.pzid || !currentRightRow.value?.printid) {
+    bottomList.value = [];
+    currentBottomKey.value = "";
+    return;
+  }
+
+  const result = await getConfigureCodeMxBomList({
+    pzid: currentRightRow.value.pzid,
+    printid: currentRightRow.value.printid,
+    pageSize: 500,
+    pageNum: 1
+  });
+  const list = dataCallback(result).map((item: any) => markBottomRow(item));
+  bottomList.value = list;
+  const row = list.find((item: BottomRow) => item.pid > 0 && item.pid === preferredPid) || list[0];
+  currentBottomKey.value = row?.__rowKey || "";
+  setCurrentTableRow("bottom", row);
+};
+
+const loadRightList = async (preferredPrintid = 0) => {
+  if (!currentMidRow.value?.pzid) {
+    rightList.value = [];
+    bottomList.value = [];
+    currentRightKey.value = "";
+    currentBottomKey.value = "";
+    return;
+  }
+
+  const result = await getConfigureCodeMxList({
+    pzid: currentMidRow.value.pzid,
+    typeid: currentMidRow.value.typeid || typeId.value
+  });
+  const list = dataCallback(result).map((item: any) => markRightRow(item));
+  rightList.value = list;
+  const row = list.find((item: RightRow) => item.printid > 0 && item.printid === preferredPrintid) || list[0];
+  currentRightKey.value = row?.__rowKey || "";
+  setCurrentTableRow("right", row);
+  await loadBottomList();
+};
+
+const loadMidList = async (preferredPzid = 0) => {
+  if (!typeId.value) {
+    midList.value = [];
+    rightList.value = [];
+    bottomList.value = [];
+    currentMidKey.value = "";
+    currentRightKey.value = "";
+    currentBottomKey.value = "";
+    return;
+  }
+
+  const result = await getConfigureCodeList({ typeid: typeId.value });
+  const list = dataCallback(result).map((item: any) => markMidRow(item));
+  midList.value = list;
+  const row = list.find((item: MidRow) => item.pzid > 0 && item.pzid === preferredPzid) || list[0];
+  currentMidKey.value = row?.__rowKey || "";
+  setCurrentTableRow("mid", row);
+  await loadRightList();
+};
+
+const loadAllData = async () => {
+  const typeResult = await getConfigureTypeList({ arg_search: typeName.value });
+  const typeRow = (typeResult.datatable || []).find((item: any) => item.contfigtypename === typeName.value);
+  typeId.value = Number(typeRow?.contfigtypeid || 0);
+  await loadMidList();
+};
+
+const enterTableEdit = (table: TableKey) => {
+  if (hasActiveEdit.value) {
+    ElMessage.warning("请先保存或取消当前编辑");
+    return;
+  }
+  if (table === "right" && !currentMidRow.value) {
+    ElMessage.warning("请先选择配置项");
+    return;
+  }
+  if (table === "bottom" && !currentRightRow.value) {
+    ElMessage.warning("请先选择配置项明细");
+    return;
+  }
+  editTable.value = table;
+  editSnapshot.value = cloneDeep(getTableRows(table));
+  editSelectedKey.value = getCurrentKeyByTable(table);
+};
+
+const restoreCurrentRowAfterCancel = async (table: TableKey, rows: any[]) => {
+  const row = rows.find(item => getRowKey(item) === editSelectedKey.value) || rows[0] || null;
+  setCurrentKeyByTable(table, getRowKey(row));
+  setCurrentTableRow(table, row);
+
+  if (table === "mid") {
+    await loadRightList();
+    return;
+  }
+  if (table === "right") {
+    await loadBottomList();
+  }
+};
+
+const cancelTableEdit = async () => {
+  if (!editTable.value) return;
+  const table = editTable.value;
+  const rows = cloneDeep(editSnapshot.value);
+  if (table === "mid") midList.value = rows;
+  if (table === "right") rightList.value = rows;
+  if (table === "bottom") bottomList.value = rows;
+  editTable.value = "";
+  editSnapshot.value = [];
+  await restoreCurrentRowAfterCancel(table, rows);
+  editSelectedKey.value = "";
+};
+
+const insertRow = async (table: TableKey, append = true) => {
+  if (!isTableEditing(table)) {
+    ElMessage.warning("请先进入编辑状态");
+    return;
+  }
+  if (table === "right" && !currentMidRow.value) {
+    ElMessage.warning("请先选择配置项");
+    return;
+  }
+  if (table === "bottom" && !currentRightRow.value) {
+    ElMessage.warning("请先选择配置项明细");
+    return;
+  }
+
+  if (table === "mid") {
+    const row = createMidRow();
+    const targetRow = append ? -1 : currentMidRow.value || -1;
+    const result = await midTableRef.value?.element?.insertAt?.(row, targetRow);
+    const newRow = result?.row || row;
+    currentMidKey.value = newRow.__rowKey;
+    setCurrentTableRow("mid", newRow);
+    return;
+  }
+
+  if (table === "right") {
+    const row = createRightRow();
+    const targetRow = append ? -1 : currentRightRow.value || -1;
+    const result = await rightTableRef.value?.element?.insertAt?.(row, targetRow);
+    const newRow = result?.row || row;
+    currentRightKey.value = newRow.__rowKey;
+    setCurrentTableRow("right", newRow);
+    return;
+  }
+
+  const row = createBottomRow();
+  const targetRow = append ? -1 : currentBottomRow.value || -1;
+  const result = await bottomTableRef.value?.element?.insertAt?.(row, targetRow);
+  const newRow = result?.row || row;
+  currentBottomKey.value = newRow.__rowKey;
+  setCurrentTableRow("bottom", newRow);
+};
+
+const removeEditRow = async (table: TableKey) => {
+  if (!isTableEditing(table)) {
+    ElMessage.warning("请先进入编辑状态");
+    return;
+  }
+
+  if (table === "mid") {
+    if (!currentMidKey.value) return ElMessage.warning("请先选择一行数据");
+    const currentRow = currentMidRow.value;
+    if (!currentRow) return;
+    const currentKey = currentMidKey.value;
+    const prevRows = getTableRows<MidRow>("mid");
+    const prevIndex = prevRows.findIndex(item => item.__rowKey === currentKey);
+    await midTableRef.value?.element?.remove?.(currentRow);
+    const rows = getTableRows<MidRow>("mid");
+    const nextRow = rows[Math.max(prevIndex - 1, 0)] || rows[0] || null;
+    currentMidKey.value = nextRow?.__rowKey || "";
+    setCurrentTableRow("mid", nextRow);
+    return;
+  }
+
+  if (table === "right") {
+    if (!currentRightKey.value) return ElMessage.warning("请先选择一行数据");
+    const currentRow = currentRightRow.value;
+    if (!currentRow) return;
+    const currentKey = currentRightKey.value;
+    const prevRows = getTableRows<RightRow>("right");
+    const prevIndex = prevRows.findIndex(item => item.__rowKey === currentKey);
+    await rightTableRef.value?.element?.remove?.(currentRow);
+    const rows = getTableRows<RightRow>("right");
+    const nextRow = rows[Math.max(prevIndex - 1, 0)] || rows[0] || null;
+    currentRightKey.value = nextRow?.__rowKey || "";
+    setCurrentTableRow("right", nextRow);
+    return;
+  }
+
+  if (!currentBottomKey.value) return ElMessage.warning("请先选择一行数据");
+  const currentRow = currentBottomRow.value;
+  if (!currentRow) return;
+  const currentKey = currentBottomKey.value;
+  const prevRows = getTableRows<BottomRow>("bottom");
+  const prevIndex = prevRows.findIndex(item => item.__rowKey === currentKey);
+  await bottomTableRef.value?.element?.remove?.(currentRow);
+  const rows = getTableRows<BottomRow>("bottom");
+  const nextRow = rows[Math.max(prevIndex - 1, 0)] || rows[0] || null;
+  currentBottomKey.value = nextRow?.__rowKey || "";
+  setCurrentTableRow("bottom", nextRow);
+};
+
+const appendMidRow = () => insertRow("mid", true);
+const insertMidRow = () => insertRow("mid", false);
+const removeMidEditRow = () => removeEditRow("mid");
+const appendRightRow = () => insertRow("right", true);
+const insertRightRow = () => insertRow("right", false);
+const removeRightEditRow = () => removeEditRow("right");
+const appendBottomRow = () => insertRow("bottom", true);
+const insertBottomRow = () => insertRow("bottom", false);
+const removeBottomEditRow = () => removeEditRow("bottom");
+
+const setMtrl = (row: BottomRow, item: any) => {
+  row.mtrlid = Number(item.mtrlid || 0);
+  row.mtrlname = item.mtrlname || "";
+  row.mtrlcode = item.mtrlcode || "";
+  row.mtrlmode = item.mtrlmode || "";
+  row.unit = item.unit || "";
+};
+
+const clearMtrl = (row: BottomRow) => {
+  row.mtrlid = 0;
+  row.mtrlname = "";
+  row.mtrlcode = "";
+  row.mtrlmode = "";
+  row.unit = "";
+};
+
+const openMtrlDialog = (row: BottomRow, params: any) => {
+  ErpMtrlPriceDialogProps.value = {
+    onSubmit: (res: any) => {
+      setMtrl(row, res.value[0]);
+    }
+  };
+  ErpMtrlPriceDialogRef.value?.show?.(params);
+};
+
+const deleteMidRow = async () => {
+  if (!currentMidRow.value) return ElMessage.warning("请先选择一行数据");
+  await ElMessageBox.confirm("确认删除当前配置项吗?", "询问", { type: "warning" });
+  if (currentMidRow.value.pzid > 0) {
+    await DeleteConfigureCode({ list: [{ pzid: currentMidRow.value.pzid, typeid: currentMidRow.value.typeid }] });
+  }
+  await loadMidList();
+};
+
+const deleteRightRow = async () => {
+  if (!currentRightRow.value) return ElMessage.warning("请先选择一行数据");
+  await ElMessageBox.confirm("确认删除当前配置项明细吗?", "询问", { type: "warning" });
+  if (currentRightRow.value.printid > 0) {
+    await DeleteConfigureCodeMx({ list: [{ pzid: currentRightRow.value.pzid, printid: currentRightRow.value.printid }] });
+  }
+  await loadRightList();
+};
+
+const deleteBottomRow = async () => {
+  if (!currentBottomRow.value) return ElMessage.warning("请先选择一行数据");
+  await ElMessageBox.confirm("确认删除当前用料明细吗?", "询问", { type: "warning" });
+  if (currentBottomRow.value.pid > 0) {
+    await DeleteConfigureBomList({
+      list: [{ pzid: currentBottomRow.value.pzid, printid: currentBottomRow.value.printid, pid: currentBottomRow.value.pid }]
+    });
+  }
+  await loadBottomList();
+};
+
+const saveMidTable = async () => {
+  if (!isTableEditing("mid")) return;
+  try {
+    tableSaving.value.mid = true;
+    const savedTypeId = await ensureType();
+    const rows = getTableRows<MidRow>("mid");
+    for (const row of rows) {
+      await SaveConfigureCode({
+        configure: {
+          pzid: row.pzid,
+          typeid: savedTypeId,
+          pzcode: row.pzcode,
+          name: row.name,
+          inputtype: row.inputtype,
+          ifnum: row.ifnum,
+          ifcross: row.ifcross,
+          ifcheck: row.ifcheck,
+          ifuse: row.ifuse,
+          maxnum: row.maxnum,
+          minnum: row.minnum,
+          pricestr: row.pricestr,
+          priceratestr: row.priceratestr
+        }
+      });
+    }
+    const preferredPzid = currentMidRow.value?.pzid || 0;
+    editTable.value = "";
+    editSnapshot.value = [];
+    editSelectedKey.value = "";
+    await loadMidList(preferredPzid);
+    ElNotification({ title: "保存成功", type: "success" });
+  } catch (error: any) {
+    ElMessage.error(error?.message || "保存失败");
+  } finally {
+    tableSaving.value.mid = false;
+  }
+};
+
+const saveRightTable = async () => {
+  if (!isTableEditing("right")) return;
+  if (!currentMidRow.value?.pzid) {
+    ElMessage.warning("请先保存配置项");
+    return;
+  }
+  try {
+    tableSaving.value.right = true;
+    const rows = getTableRows<RightRow>("right");
+    for (const row of rows) {
+      await SaveConfigureCodeMx({
+        configure: {
+          pzid: currentMidRow.value.pzid,
+          printid: row.printid,
+          pzcodemx: row.pzcodemx,
+          namemx: row.namemx,
+          ifuse: row.ifuse,
+          ifdft: row.ifdft
+        }
+      });
+    }
+    const preferredPrintid = currentRightRow.value?.printid || 0;
+    editTable.value = "";
+    editSnapshot.value = [];
+    editSelectedKey.value = "";
+    await loadRightList(preferredPrintid);
+    ElNotification({ title: "保存成功", type: "success" });
+  } catch (error: any) {
+    ElMessage.error(error?.message || "保存失败");
+  } finally {
+    tableSaving.value.right = false;
+  }
+};
+
+const saveBottomTable = async () => {
+  if (!isTableEditing("bottom")) return;
+  if (!currentRightRow.value?.pzid || !currentRightRow.value?.printid) {
+    ElMessage.warning("请先保存配置项明细");
+    return;
+  }
+  try {
+    tableSaving.value.bottom = true;
+    const rows = getTableRows<BottomRow>("bottom");
+    await SaveConfigureBomList({
+      bomList: rows.map(row => ({
+        pid: row.pid,
+        pzid: currentRightRow.value!.pzid,
+        printid: currentRightRow.value!.printid,
+        mtrlid: row.mtrlid,
+        mtrlname: row.mtrlname,
+        mtrlcode: row.mtrlcode,
+        mtrlmode: row.mtrlmode,
+        unit: row.unit,
+        default_length: row.default_length ?? 0,
+        default_width: row.default_width ?? 0,
+        default_qty: row.default_qty ?? 0,
+        sonscale: row.sonscale ?? 0,
+        sonscale_formula: row.sonscale_formula || "",
+        sonloss: row.sonloss ?? 0,
+        sondecloss: 0,
+        sonloss_formula: "",
+        sondecloss_formula: "",
+        mng_cost_rate: 0,
+        profit_rate: 0
+      }))
+    });
+    const preferredPid = currentBottomRow.value?.pid || 0;
+    editTable.value = "";
+    editSnapshot.value = [];
+    editSelectedKey.value = "";
+    await loadBottomList(preferredPid);
+    ElNotification({ title: "保存成功", type: "success" });
+  } catch (error: any) {
+    ElMessage.error(error?.message || "保存失败");
+  } finally {
+    tableSaving.value.bottom = false;
+  }
+};
+
+const renderInput = (table: TableKey, getter: (scope: any) => any, editor: (scope: any) => any) => (scope: any) =>
+  isTableEditing(table) ? editor(scope) : getter(scope);
+
+const renderTextInput = (table: TableKey, field: string) =>
+  renderInput(
+    table,
+    (scope: any) => scope.row[field] ?? "",
+    (scope: any) => <el-input v-model={scope.row[field]} />
+  );
+
+const renderCheckbox = (table: TableKey, field: string) =>
+  renderInput(
+    table,
+    (scope: any) => <el-checkbox modelValue={Number(scope.row[field]) === 1} disabled />,
+    (scope: any) => (
+      <el-checkbox
+        modelValue={Number(scope.row[field]) === 1}
+        onChange={(val: boolean) => {
+          scope.row[field] = val ? 1 : 0;
+        }}
+      />
+    )
+  );
+
+const setDefaultRight = (row: RightRow, value: boolean) => {
+  if (!value) {
+    row.ifdft = 0;
+    return;
+  }
+  rightList.value = rightList.value.map(item => ({
+    ...item,
+    ifdft: item.__rowKey === row.__rowKey ? 1 : 0
+  }));
+};
+
+const columnsMid = [
+  { field: "pzcode", title: "配置项编号", width: 120, render: renderTextInput("mid", "pzcode") },
+  { field: "name", title: "配置项名称", minWidth: 140, render: renderTextInput("mid", "name") },
+  {
+    field: "inputtype",
+    title: "录入类型",
+    width: 100,
+    render: renderInput(
+      "mid",
+      (scope: any) => (Number(scope.row.inputtype) === 1 ? "数值" : "文本"),
+      (scope: any) => (
+        <el-select v-model={scope.row.inputtype}>
+          <el-option label="文本" value={0} />
+          <el-option label="数值" value={1} />
+        </el-select>
+      )
+    )
+  },
+  { field: "ifnum", title: "数量", width: 80, render: renderCheckbox("mid", "ifnum") },
+  { field: "ifcross", title: "混搭", width: 80, render: renderCheckbox("mid", "ifcross") },
+  { field: "ifcheck", title: "必填", width: 80, render: renderCheckbox("mid", "ifcheck") },
+  { field: "ifuse", title: "有效", width: 80, render: renderCheckbox("mid", "ifuse") },
+  { field: "maxnum", title: "最大数", width: 100, render: renderTextInput("mid", "maxnum") },
+  { field: "minnum", title: "最小数", width: 100, render: renderTextInput("mid", "minnum") }
+];
+
+const columnsRight = [
+  { field: "pzcodemx", title: "明细编号", width: 120, render: renderTextInput("right", "pzcodemx") },
+  { field: "namemx", title: "配置项明细名", minWidth: 160, render: renderTextInput("right", "namemx") },
+  {
+    field: "ifdft",
+    title: "标准项",
+    width: 90,
+    render: renderInput(
+      "right",
+      (scope: any) => <el-checkbox modelValue={Number(scope.row.ifdft) === 1} disabled />,
+      (scope: any) => (
+        <el-checkbox modelValue={Number(scope.row.ifdft) === 1} onChange={(val: boolean) => setDefaultRight(scope.row, val)} />
+      )
+    )
+  },
+  { field: "ifuse", title: "有效", width: 80, render: renderCheckbox("right", "ifuse") }
+];
+
+const columnsBottom = [
+  {
+    field: "mtrlname",
+    title: "物料名称",
+    minWidth: 220,
+    render: renderInput(
+      "bottom",
+      (scope: any) => `${scope.row.mtrlname || ""}${scope.row.mtrlmode ? ` ${scope.row.mtrlmode}` : ""}`,
+      (scope: any) => {
+        const row = scope.row as BottomRow;
+        const params = { keyword: row.mtrlcode || row.mtrlname || "" };
+        return (
+          <ErpMtrlPriceSelect
+            value={row.mtrlid}
+            {...params}
+            clearable
+            placeholder={row.mtrlname}
+            onOpenModal={() => openMtrlDialog(row, params)}
+            onSelect={(val: any) => setMtrl(row, val)}
+            onClear={() => clearMtrl(row)}
+          >
+            {{
+              label: () => `${row.mtrlname} ${row.mtrlmode || ""}`
+            }}
+          </ErpMtrlPriceSelect>
+        );
+      }
+    )
+  },
+  { field: "unit", title: "单位", width: 70 },
+  { field: "default_length", title: "默认长", width: 100, render: renderTextInput("bottom", "default_length") },
+  { field: "default_width", title: "默认宽", width: 100, render: renderTextInput("bottom", "default_width") },
+  { field: "default_qty", title: "默认数量", width: 100, render: renderTextInput("bottom", "default_qty") },
+  { field: "sonscale", title: "用料量", width: 100, render: renderTextInput("bottom", "sonscale") },
+  { field: "sonscale_formula", title: "用料量公式", minWidth: 180, render: renderTextInput("bottom", "sonscale_formula") },
+  { field: "sonloss", title: "损耗率", width: 100, render: renderTextInput("bottom", "sonloss") }
+];
+
+const midTableEvents = {
+  "cell-click": async ({ row }: any) => {
+    if (editTable.value && editTable.value !== "mid") {
+      ElMessage.warning("请先保存或取消当前编辑");
+      return;
+    }
+    currentMidKey.value = row.__rowKey;
+    setCurrentTableRow("mid", row);
+    if (isTableEditing("mid")) return;
+    await loadRightList();
+  }
+};
+
+const rightTableEvents = {
+  "cell-click": async ({ row }: any) => {
+    if (editTable.value && editTable.value !== "right") {
+      ElMessage.warning("请先保存或取消当前编辑");
+      return;
+    }
+    currentRightKey.value = row.__rowKey;
+    setCurrentTableRow("right", row);
+    if (isTableEditing("right")) return;
+    await loadBottomList();
+  }
+};
+
+const bottomTableEvents = {
+  "cell-click": ({ row }: any) => {
+    if (editTable.value && editTable.value !== "bottom") {
+      ElMessage.warning("请先保存或取消当前编辑");
+      return;
+    }
+    currentBottomKey.value = row.__rowKey;
+    setCurrentTableRow("bottom", row);
+  }
+};
+
+const closeDialog = () => {
+  if (hasActiveEdit.value) {
+    ElMessage.warning("请先保存或取消当前编辑");
+    return;
+  }
+  dialogRef.value?.hide?.();
+};
+
+const handleClosed = () => {
+  editTable.value = "";
+  editSnapshot.value = [];
+  editSelectedKey.value = "";
+  tableSaving.value = { mid: false, right: false, bottom: false };
+};
+
+const open = async (payload: DialogPayload) => {
+  typeName.value = payload.typeName;
+  typeId.value = 0;
+  midList.value = [];
+  rightList.value = [];
+  bottomList.value = [];
+  currentMidKey.value = "";
+  currentRightKey.value = "";
+  currentBottomKey.value = "";
+  editTable.value = "";
+  editSnapshot.value = [];
+  editSelectedKey.value = "";
+  dialogRef.value?.show?.();
+  await loadAllData();
+};
+
+defineExpose({ open });
+</script>
+
+<style scoped lang="scss">
+.custom-config-dialog {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  padding: 0px 8px;
+  box-sizing: border-box;
+}
+
+.dialog-toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+}
+
+.dialog-toolbar__meta {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.dialog-toolbar__type {
+  font-weight: 600;
+}
+
+.custom-config-body {
+  flex: 1;
+  min-height: 0;
+  display: grid;
+  grid-template-columns: 28% 28% 1fr;
+  gap: 12px;
+  overflow: hidden;
+}
+
+.custom-config-pane {
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  overflow: hidden;
+}
+</style>
+<style lang="scss">
+.el-dialog {
+  padding: 0;
+}
+.el-dialog__footer {
+  padding: 8px;
+}
+</style>

+ 52 - 27
JLHWEB/src/views/quote/softbedQuote/detail.vue

@@ -26,9 +26,16 @@
         :request-auto="false"
       >
         <template #tableHeader>
-          <el-space wrap v-if="orderStatus">
-            <el-button type="primary" @click="toAddMx(VxeTableHeadBoardMxRef)">{{ t("common.addText") }}</el-button>
-            <el-button type="danger" @click="toDelMx(VxeTableHeadBoardMxRef)">{{ t("common.delText") }}</el-button>
+          <el-space wrap>
+            <el-button v-if="orderStatus" type="primary" @click="toAddMx(VxeTableHeadBoardMxRef)">{{
+              t("common.addText")
+            }}</el-button>
+            <el-button v-if="orderStatus" type="danger" @click="toDelMx(VxeTableHeadBoardMxRef)">{{
+              t("common.delText")
+            }}</el-button>
+            <el-button v-if="isTemplateQuote()" type="warning" @click="openCustomConfigureDialog(CustomConfigureDialogRef, 1)">
+              自定义配置项
+            </el-button>
           </el-space>
         </template>
       </LjVxeTable>
@@ -47,9 +54,16 @@
         :request-auto="false"
       >
         <template #tableHeader>
-          <el-space wrap v-if="orderStatus">
-            <el-button type="primary" @click="toAddMx(VxeTableNightStandMxRef)">{{ t("common.addText") }}</el-button>
-            <el-button type="danger" @click="toDelMx(VxeTableNightStandMxRef)">{{ t("common.delText") }}</el-button>
+          <el-space wrap>
+            <el-button v-if="orderStatus" type="primary" @click="toAddMx(VxeTableNightStandMxRef)">{{
+              t("common.addText")
+            }}</el-button>
+            <el-button v-if="orderStatus" type="danger" @click="toDelMx(VxeTableNightStandMxRef)">{{
+              t("common.delText")
+            }}</el-button>
+            <el-button v-if="isTemplateQuote()" type="warning" @click="openCustomConfigureDialog(CustomConfigureDialogRef, 2)">
+              自定义配置项
+            </el-button>
           </el-space>
         </template>
       </LjVxeTable>
@@ -68,9 +82,16 @@
         :request-auto="false"
       >
         <template #tableHeader>
-          <el-space wrap v-if="orderStatus">
-            <el-button type="primary" @click="toAddMx(VxeTableBedFrameMxRef)">{{ t("common.addText") }}</el-button>
-            <el-button type="danger" @click="toDelMx(VxeTableBedFrameMxRef)">{{ t("common.delText") }}</el-button>
+          <el-space wrap>
+            <el-button v-if="orderStatus" type="primary" @click="toAddMx(VxeTableBedFrameMxRef)">{{
+              t("common.addText")
+            }}</el-button>
+            <el-button v-if="orderStatus" type="danger" @click="toDelMx(VxeTableBedFrameMxRef)">{{
+              t("common.delText")
+            }}</el-button>
+            <el-button v-if="isTemplateQuote()" type="warning" @click="openCustomConfigureDialog(CustomConfigureDialogRef, 4)">
+              自定义配置项
+            </el-button>
           </el-space>
         </template>
       </LjVxeTable>
@@ -112,6 +133,7 @@
   />
   <ErpMtrlPriceDialog ref="ErpMtrlPriceDialogRef" v-bind="ErpMtrlPriceDialogProps" />
   <SoftBedTemplateDialog ref="SoftBedTemplateDialogRef" v-bind="SoftBedTemplateDialogProps" />
+  <CustomConfigureDialog ref="CustomConfigureDialogRef" @loaded="onCustomConfigureLoaded" @saved="onCustomConfigureSaved" />
   <!-- <FormulaEditorDialog ref="formulaEditorRef" @confirm="handleFormulaConfirm" /> -->
   <FormulaEditor ref="sharedFormulaEditorRef" @confirm="handleFormulaConfirm" />
 </template>
@@ -127,6 +149,7 @@ import { useAuthButtons } from "@/hooks/useAuthButtons";
 import { useHooks } from "./hooks/index";
 import { useUserStore } from "@/stores/modules/user";
 import BedConfigModal from "./components/BedConfigModal.vue";
+import CustomConfigureDialog from "./components/CustomConfigureDialog.vue";
 import ErpMtrlPriceDialog from "@/views/system/selector/erpMtrlPrice/index.vue";
 import SoftBedTemplateDialog from "@/views/system/selector/softbedTemplate/index.vue";
 import FormulaEditorDialog from "@/views/system/formula-editor/index.vue";
@@ -175,6 +198,10 @@ const {
   onCAudit,
   onDelete,
   onConfirmConfigureDialog,
+  isTemplateQuote,
+  openCustomConfigureDialog,
+  onCustomConfigureLoaded,
+  onCustomConfigureSaved,
   GetSoftBedFormulaMapper,
   GetSoftBedAccessoryMapper,
   openSharedFormulaEditor,
@@ -184,6 +211,7 @@ const { CheckPower, CheckOption, buttonNew, buttonDefault } = useAuthButtons(t);
 const { pageRefresh } = usePageRouter();
 
 const initParams = ref({ billid: 0 as Number });
+const CustomConfigureDialogRef = ref();
 
 // const partsConfig = reactive({
 //   headboard: [],
@@ -312,24 +340,21 @@ const orderDefaultAction = [
       return !orderStatus.value;
     },
     clickFunc: item => {
-      // if (route.path.indexOf("/new") > -1) {
-      //   tabRemove(route.fullPath);
-      // } else {
-      // router.replace(
-      //   `/softbedQuote/detail?id=${LjDetailRef.value._mainData.softbed_id}&code=${LjDetailRef.value._mainData.softbed_code}`
-      // );
-      pageRefresh({
-        name: "softbedQuoteDetail",
-        params: {
-          id: LjDetailRef.value._mainData.softbed_id,
-          code: LjDetailRef.value._mainData.softbed_code
-        },
-        query: {
-          id: LjDetailRef.value._mainData.softbed_id,
-          code: LjDetailRef.value._mainData.softbed_code
-        }
-      });
-      // }
+      if (route.path.indexOf("/new") > -1) {
+        tabRemove(route.fullPath);
+      } else {
+        pageRefresh({
+          name: "softbedQuoteDetail",
+          params: {
+            id: LjDetailRef.value._mainData.softbed_id,
+            code: LjDetailRef.value._mainData.softbed_code
+          },
+          query: {
+            id: LjDetailRef.value._mainData.softbed_id,
+            code: LjDetailRef.value._mainData.softbed_code
+          }
+        });
+      }
     }
   }),
   buttonNew({

+ 111 - 8
JLHWEB/src/views/quote/softbedQuote/hooks/index.tsx

@@ -115,6 +115,7 @@ interface defaultState {
   softBedFormulaEnum: any[];
   deptEnum: any[];
   accessoryEnum: any[];
+  customConfigDetailOptionsMap: Record<number, Record<string, Array<{ label: string; value: string }>>>;
   /**
    * @description 按钮loading状态管理
    */
@@ -170,6 +171,11 @@ export const useHooks = (t?: any) => {
     softBedFormulaEnum: [],
     deptEnum: [],
     accessoryEnum: [],
+    customConfigDetailOptionsMap: {
+      1: {},
+      2: {},
+      4: {}
+    },
     loadingStatus: {
       save: false,
       audit: false,
@@ -879,7 +885,6 @@ export const useHooks = (t?: any) => {
     {
       field: "pzname",
       title: "部件选配项",
-      width: 160,
       editRender: {},
       editColRender: (scope: any) => {
         const { row } = scope;
@@ -889,7 +894,7 @@ export const useHooks = (t?: any) => {
         const partTypes = {
           1: "headboardConfigOptions",
           2: "nightstandConfigOptions",
-          8: "bedframeConfigOptions"
+          4: "bedframeConfigOptions"
         };
         const targetArray = partTypes[row.has_type] || "headboardConfigOptions";
         // const options = state[targetArray]?.map(item => <el-option key={item.pzid} label={item.pzname} value={item} />) || [];
@@ -909,10 +914,35 @@ export const useHooks = (t?: any) => {
         );
       }
     },
+    {
+      field: "pznamemx",
+      title: "配置项明细名",
+      editRender: {},
+      editColRender: (scope: any) => {
+        const { row } = scope;
+        const { _mainData } = state.LjDetailRef;
+        const _disabled = !row.allow_edit && !_mainData.is_template;
+        const options = state.customConfigDetailOptionsMap[row.has_type]?.[row.pzname] || [];
+
+        return (
+          <el-autocomplete
+            v-model={row.pznamemx}
+            disabled={_disabled}
+            clearable={true}
+            fetch-suggestions={(queryString, cb) => {
+              const results = options.filter(item => item.label?.toLowerCase().includes(queryString.toLowerCase()));
+              cb(results);
+            }}
+            onSelect={val => {
+              row.pznamemx = val.value;
+            }}
+          />
+        );
+      }
+    },
     {
       field: "formulaname",
       title: "公式名",
-      width: 160,
       enum: async () => {
         const data = (await GetSoftBedFormulaMapper()).datatable.map(t => {
           return { ...t, label: t.formulaname, value: t.formulaid };
@@ -949,7 +979,6 @@ export const useHooks = (t?: any) => {
     {
       field: "mtrlname",
       title: "物料名称规格",
-      width: 300,
       editRender: {},
       editColRender: (scope: any) => {
         const { $table, column, row, status } = scope;
@@ -1157,7 +1186,6 @@ export const useHooks = (t?: any) => {
     {
       field: "parts_type",
       title: "配件",
-      width: 120,
       enum: async () => {
         const data = (await GetSoftBedAccessoryMapper()).datatable;
         state.accessoryEnum = data.map((t: any) => {
@@ -1184,7 +1212,6 @@ export const useHooks = (t?: any) => {
     {
       field: "pzname",
       title: "部件选配项",
-      width: 160,
       editRender: {},
       editColRender: (scope: any) => {
         const { row } = scope;
@@ -1217,7 +1244,6 @@ export const useHooks = (t?: any) => {
     {
       field: "formulaname",
       title: "公式名",
-      width: 160,
       enum: async () => {
         const data = (await GetSoftBedFormulaMapper()).datatable.map(t => {
           return { ...t, label: t.formulaname, value: t.formulaid };
@@ -1254,7 +1280,6 @@ export const useHooks = (t?: any) => {
     {
       field: "mtrlname",
       title: "物料名称规格",
-      width: 300,
       editRender: {},
       editColRender: (scope: any) => {
         const { $table, column, row, status } = scope;
@@ -1475,6 +1500,80 @@ export const useHooks = (t?: any) => {
       state.isModalVisible = true;
     });
   };
+
+  const isTemplateQuote = () => {
+    const { _mainData } = state.LjDetailRef || {};
+    return Boolean(_mainData && Number(_mainData.is_template) === 1);
+  };
+
+  const getPartConfigMeta = (partType: number) => {
+    const { _mainData } = state.LjDetailRef;
+    const labelMap = {
+      1: "床头",
+      2: "床头柜",
+      4: "床架"
+    };
+    const keyMap = {
+      1: "headboard",
+      2: "nightstand",
+      4: "bedframe"
+    };
+
+    return {
+      partType,
+      partLabel: labelMap[partType],
+      partKey: keyMap[partType],
+      typeName: `${_mainData.softbed_code || ""}|${labelMap[partType]}`
+    };
+  };
+
+  const getPartRows = (partType: number) => {
+    if (partType === 1) return cloneDeep(state.headBoardMx);
+    if (partType === 2) return cloneDeep(state.nightStandMx);
+    return cloneDeep(state.bedFrameMx);
+  };
+
+  const updateCustomConfigDetailOptions = (
+    partType: number,
+    detailOptions: Record<string, Array<{ label: string; value: string }>>
+  ) => {
+    state.customConfigDetailOptionsMap[partType] = detailOptions || {};
+  };
+
+  const openCustomConfigureDialog = async (dialogRef: any, partType: number) => {
+    if (!isTemplateQuote()) {
+      ElMessage.warning("仅模板报价支持自定义配置项");
+      return;
+    }
+
+    const { _mainData } = state.LjDetailRef;
+    if (!_mainData?.softbed_code) {
+      ElMessage.warning("请先保存并生成报价编号后再维护自定义配置");
+      return;
+    }
+
+    const dialogInstance = dialogRef?.value || dialogRef;
+    dialogInstance?.open?.({
+      ...getPartConfigMeta(partType),
+      sourceRows: getPartRows(partType)
+    });
+  };
+
+  const onCustomConfigureLoaded = (payload: {
+    partType: number;
+    detailOptions: Record<string, Array<{ label: string; value: string }>>;
+  }) => {
+    updateCustomConfigDetailOptions(payload.partType, payload.detailOptions);
+  };
+
+  const onCustomConfigureSaved = (payload: {
+    partType: number;
+    sourceRows: any[];
+    detailOptions: Record<string, Array<{ label: string; value: string }>>;
+    defaultSelections: Array<{ pzid: number; selectedId: number; selectedValue: string }>;
+  }) => {
+    updateCustomConfigDetailOptions(payload.partType, payload.detailOptions);
+  };
   /**
    *
    * @param item
@@ -2149,6 +2248,10 @@ export const useHooks = (t?: any) => {
     onCopyQuote,
     onShowFormula,
     onConfirmConfigureDialog,
+    isTemplateQuote,
+    openCustomConfigureDialog,
+    onCustomConfigureLoaded,
+    onCustomConfigureSaved,
     GetSoftBedFormulaMapper,
     GetSoftBedAccessoryMapper,
     openSharedFormulaEditor,