Ver código fonte

feat(groupPackage): 实现套餐内容表单验证功能

-为套餐内容模块添加独立的表单验证逻辑
- 实现分组类别和菜品信息的动态验证规则
- 添加表单字段验证错误提示样式-优化菜品选择和数量输入的验证交互
- 支持分组展开/收起状态下的验证触发
- 完善表单提交时的验证检查流程
congxuesong 1 mês atrás
pai
commit
963e870d9a
1 arquivos alterados com 451 adições e 164 exclusões
  1. 451 164
      src/views/groupPackageManagement/newGroup.vue

+ 451 - 164
src/views/groupPackageManagement/newGroup.vue

@@ -76,105 +76,143 @@
               <el-input v-model="storeInfoModel.quotaValue" maxlength="15" placeholder="请填写自定义限购数量" clearable />
             </el-form-item>
             <!-- 套餐内容 -->
-            <el-form-item label="套餐内容" prop="lifeGroupBuyThalis" class="package-content-item">
-              <div class="package-content-wrapper">
-                <div class="package-content-header">
-                  <el-button type="primary" @click="addGroup" class="add-group-btn"> 添加分组 </el-button>
-                  <el-button
-                    type="primary"
-                    link
-                    @click="toggleAllGroupsCollapse"
-                    :icon="allGroupsCollapsed ? ArrowDown : ArrowUp"
-                    v-show="lifeGroupBuyThalis.length > 1"
-                  >
-                    {{ allGroupsCollapsed ? "展开全部" : "收起全部" }}
-                  </el-button>
-                </div>
-                <transition-group name="group-fade" tag="div" class="package-content-container">
-                  <div v-for="item in visibleGroups" :key="item.originalIndex" class="package-group">
-                    <div class="package-group-header">
-                      <span class="group-index">{{ item.group.groupName }}</span>
-                      <div class="header-right">
-                        <el-button
-                          type="primary"
-                          link
-                          @click="toggleGroupCollapse(item.originalIndex)"
-                          :icon="isGroupCollapsed(item.originalIndex) ? ArrowDown : ArrowUp"
-                          v-show="item.group.dishes.length > 1"
-                        >
-                          {{ isGroupCollapsed(item.originalIndex) ? "展开" : "收起" }}
-                        </el-button>
-                        <el-button
-                          type="danger"
-                          link
-                          @click="removeGroup(item.originalIndex)"
-                          :icon="Delete"
-                          v-show="lifeGroupBuyThalis.length > 1"
-                        >
-                          删除
-                        </el-button>
-                      </div>
-                    </div>
-                    <div class="package-group-content">
-                      <div class="category-row">
-                        <span class="label">类别</span>
-                        <el-input v-model="item.group.groupName" placeholder="请输入" clearable />
-                      </div>
-                      <!-- 第一行菜品,始终显示 -->
-                      <div v-if="item.group.dishes && item.group.dishes.length > 0" class="dish-row">
-                        <span class="label">菜品</span>
-                        <div class="dish-select-block" @click="openDishDialog(item.originalIndex, 0)">
-                          <span v-if="item.group.dishes[0].dishName" class="dish-selected-name"
-                            >{{ item.group.dishes[0].dishName }},¥{{ item.group.dishes[0].dishPrice }}/{{
-                              item.group.dishes[0].dishesUnit
-                            }}</span
+            <el-form-item label="套餐内容" class="package-content-item">
+              <el-form
+                ref="packageFormRef"
+                :model="packageFormModel"
+                :rules="packageFormRules"
+                label-width="0"
+                class="package-content-form"
+              >
+                <div class="package-content-wrapper">
+                  <div class="package-content-header">
+                    <el-button type="primary" @click="addGroup" class="add-group-btn"> 添加分组 </el-button>
+                    <el-button
+                      type="primary"
+                      link
+                      @click="toggleAllGroupsCollapse"
+                      :icon="allGroupsCollapsed ? ArrowDown : ArrowUp"
+                      v-show="lifeGroupBuyThalis.length > 1"
+                    >
+                      {{ allGroupsCollapsed ? "展开全部" : "收起全部" }}
+                    </el-button>
+                  </div>
+                  <transition-group name="group-fade" tag="div" class="package-content-container">
+                    <div v-for="item in visibleGroups" :key="item.originalIndex" class="package-group">
+                      <div class="package-group-header">
+                        <span class="group-index">{{ item.group.groupName }}</span>
+                        <div class="header-right">
+                          <el-button
+                            type="primary"
+                            link
+                            @click="toggleGroupCollapse(item.originalIndex)"
+                            :icon="isGroupCollapsed(item.originalIndex) ? ArrowDown : ArrowUp"
+                            v-show="item.group.dishes.length > 1"
                           >
-                          <span v-else class="dish-placeholder">请选择</span>
+                            {{ isGroupCollapsed(item.originalIndex) ? "展开" : "收起" }}
+                          </el-button>
+                          <el-button
+                            type="danger"
+                            link
+                            @click="removeGroup(item.originalIndex)"
+                            :icon="Delete"
+                            v-show="lifeGroupBuyThalis.length > 1"
+                          >
+                            删除
+                          </el-button>
                         </div>
-                        <span class="label">数量</span>
-                        <el-input v-model="item.group.dishes[0].qty" placeholder="请输入" clearable class="quantity-input" />
-                        <el-button
-                          :icon="Delete"
-                          link
-                          type="danger"
-                          @click="removeDish(item.originalIndex, 0)"
-                          class="delete-dish-btn"
-                          v-show="item.group.dishes.length > 1"
-                        />
                       </div>
-                      <!-- 第二行及以后的菜品,收起时隐藏 -->
-                      <transition name="slide-fade">
-                        <div v-if="!isGroupCollapsed(item.originalIndex)" class="extra-dishes-container">
-                          <div
-                            v-for="(dish, dishIndex) in item.group.dishes"
-                            :key="dishIndex"
-                            class="dish-row"
-                            v-show="dishIndex > 0"
+                      <div class="package-group-content">
+                        <el-form-item
+                          :prop="`groups.${item.originalIndex}.groupName`"
+                          :rules="packageFormRules[`groups.${item.originalIndex}.groupName`]"
+                          class="category-form-item"
+                        >
+                          <div class="category-row">
+                            <span class="label">类别</span>
+                            <el-input v-model="item.group.groupName" placeholder="请输入" clearable />
+                          </div>
+                        </el-form-item>
+                        <!-- 第一行菜品,始终显示 -->
+                        <div v-if="item.group.dishes && item.group.dishes.length > 0" class="dish-row">
+                          <el-form-item
+                            :prop="`groups.${item.originalIndex}.dishes.0.detailId`"
+                            :rules="packageFormRules[`groups.${item.originalIndex}.dishes.0.detailId`]"
+                            class="dish-form-item"
                           >
                             <span class="label">菜品</span>
-                            <div class="dish-select-block" @click="openDishDialog(item.originalIndex, dishIndex)">
-                              <span v-if="dish.dishName" class="dish-selected-name"
-                                >{{ dish.dishName }},¥{{ dish.dishPrice }}/{{ dish.dishesUnit }}</span
+                            <div class="dish-select-block" @click="openDishDialog(item.originalIndex, 0)">
+                              <span v-if="item.group.dishes[0].dishName" class="dish-selected-name"
+                                >{{ item.group.dishes[0].dishName }},¥{{ item.group.dishes[0].dishPrice }}/{{
+                                  item.group.dishes[0].dishesUnit
+                                }}</span
                               >
                               <span v-else class="dish-placeholder">请选择</span>
                             </div>
+                          </el-form-item>
+                          <el-form-item
+                            :prop="`groups.${item.originalIndex}.dishes.0.qty`"
+                            :rules="packageFormRules[`groups.${item.originalIndex}.dishes.0.qty`]"
+                            class="dish-form-item"
+                          >
                             <span class="label">数量</span>
-                            <el-input v-model="dish.qty" placeholder="请输入" clearable class="quantity-input" />
+                            <el-input v-model="item.group.dishes[0].qty" placeholder="请输入" clearable class="quantity-input" />
                             <el-button
                               :icon="Delete"
                               link
                               type="danger"
-                              @click="removeDish(item.originalIndex, dishIndex)"
+                              @click="removeDish(item.originalIndex, 0)"
                               class="delete-dish-btn"
+                              v-show="item.group.dishes.length > 1"
                             />
-                          </div>
-                          <el-button type="primary" @click="addDish(item.originalIndex)" class="add-dish-btn"> 添加 </el-button>
+                          </el-form-item>
                         </div>
-                      </transition>
+                        <!-- 第二行及以后的菜品,收起时隐藏 -->
+                        <transition name="slide-fade">
+                          <div v-if="!isGroupCollapsed(item.originalIndex)" class="extra-dishes-container">
+                            <div
+                              v-for="(dish, dishIndex) in item.group.dishes"
+                              :key="dishIndex"
+                              class="dish-row"
+                              v-show="dishIndex > 0"
+                            >
+                              <el-form-item
+                                :prop="`groups.${item.originalIndex}.dishes.${dishIndex}.detailId`"
+                                :rules="packageFormRules[`groups.${item.originalIndex}.dishes.${dishIndex}.detailId`]"
+                                class="dish-form-item"
+                              >
+                                <span class="label">菜品</span>
+                                <div class="dish-select-block" @click="openDishDialog(item.originalIndex, dishIndex)">
+                                  <span v-if="dish.dishName" class="dish-selected-name"
+                                    >{{ dish.dishName }},¥{{ dish.dishPrice }}/{{ dish.dishesUnit }}</span
+                                  >
+                                  <span v-else class="dish-placeholder">请选择</span>
+                                </div>
+                              </el-form-item>
+                              <el-form-item
+                                :prop="`groups.${item.originalIndex}.dishes.${dishIndex}.qty`"
+                                :rules="packageFormRules[`groups.${item.originalIndex}.dishes.${dishIndex}.qty`]"
+                                class="dish-form-item"
+                              >
+                                <span class="label">数量</span>
+                                <el-input v-model="dish.qty" placeholder="请输入" clearable class="quantity-input" />
+                                <el-button
+                                  :icon="Delete"
+                                  link
+                                  type="danger"
+                                  @click="removeDish(item.originalIndex, dishIndex)"
+                                  class="delete-dish-btn"
+                                />
+                              </el-form-item>
+                            </div>
+                            <el-button type="primary" @click="addDish(item.originalIndex)" class="add-dish-btn"> 添加 </el-button>
+                          </div>
+                        </transition>
+                      </div>
                     </div>
-                  </div>
-                </transition-group>
-              </div>
+                  </transition-group>
+                </div>
+              </el-form>
             </el-form-item>
           </div>
           <!-- 价格信息模块 -->
@@ -554,47 +592,12 @@ const rules = reactive({
       required: true,
       validator: (rule: any, value: any, callback: any) => {
         try {
-          // 验证独立的 lifeGroupBuyThalis 变量
+          // 只检查是否有至少一个分组,详细验证由独立的套餐内容表单处理
           const thalis = lifeGroupBuyThalis.value;
           if (!thalis || thalis.length === 0) {
             callback(new Error("请至少添加一个套餐分组"));
             return;
           }
-          // 如果设置了跳过最后一个分组验证的标记,则只验证除最后一个分组外的所有分组
-          const groupsToValidate = skipLastGroupValidation && thalis.length > 1 ? thalis.slice(0, thalis.length - 1) : thalis;
-
-          for (let i = 0; i < groupsToValidate.length; i++) {
-            const group = groupsToValidate[i];
-            const groupIndex = i;
-            if (!group.groupName || group.groupName.trim() === "") {
-              callback(new Error(`第${groupIndex + 1}个分组的类别不能为空`));
-              return;
-            }
-            if (!group.dishes || group.dishes.length === 0) {
-              callback(new Error(`第${groupIndex + 1}个分组至少需要添加一个菜品`));
-              return;
-            }
-            for (let j = 0; j < group.dishes.length; j++) {
-              const dish = group.dishes[j];
-              if (!dish.detailId) {
-                callback(new Error(`第${groupIndex + 1}个分组的第${j + 1}个菜品未选择`));
-                return;
-              }
-              if (!dish.qty || dish.qty.toString().trim() === "") {
-                callback(new Error(`第${groupIndex + 1}个分组的第${j + 1}个菜品数量不能为空`));
-                return;
-              }
-              const quantityNum = Number(dish.qty);
-              if (isNaN(quantityNum) || quantityNum <= 0) {
-                callback(new Error(`第${groupIndex + 1}个分组的第${j + 1}个菜品数量必须为正整数`));
-                return;
-              }
-              if (!Number.isInteger(quantityNum)) {
-                callback(new Error(`第${groupIndex + 1}个分组的第${j + 1}个菜品数量必须为正整数`));
-                return;
-              }
-            }
-          }
           callback();
         } catch (error) {
           // 如果验证过程中出错,直接通过验证,避免报错
@@ -1221,6 +1224,77 @@ const addGroup = () => {
     return;
   }
 
+  // 验证所有现有分组是否填写完整
+  for (let i = 0; i < lifeGroupBuyThalis.value.length; i++) {
+    const group = lifeGroupBuyThalis.value[i];
+
+    // 验证类别
+    if (!group.groupName || group.groupName.trim() === "") {
+      ElMessage.warning("请先完成现有分组的填写");
+      // 触发表单验证,显示验证错误
+      nextTick(() => {
+        if (packageFormRef.value) {
+          packageFormRef.value.validate(() => {});
+        }
+      });
+      return;
+    }
+
+    // 验证是否有菜品
+    if (!group.dishes || group.dishes.length === 0) {
+      ElMessage.warning("请先完成现有分组的填写");
+      // 触发表单验证,显示验证错误
+      nextTick(() => {
+        if (packageFormRef.value) {
+          packageFormRef.value.validate(() => {});
+        }
+      });
+      return;
+    }
+
+    // 验证当前分组中每个菜品是否填写完整
+    for (let j = 0; j < group.dishes.length; j++) {
+      const dish = group.dishes[j];
+
+      // 验证菜品是否已选择
+      if (!dish.detailId) {
+        ElMessage.warning("请先完成现有分组的填写");
+        // 触发表单验证,显示验证错误
+        nextTick(() => {
+          if (packageFormRef.value) {
+            packageFormRef.value.validate(() => {});
+          }
+        });
+        return;
+      }
+
+      // 验证数量是否已填写
+      if (!dish.qty || dish.qty.toString().trim() === "") {
+        ElMessage.warning("请先完成现有分组的填写");
+        // 触发表单验证,显示验证错误
+        nextTick(() => {
+          if (packageFormRef.value) {
+            packageFormRef.value.validate(() => {});
+          }
+        });
+        return;
+      }
+
+      // 验证数量格式
+      const quantityNum = Number(dish.qty);
+      if (isNaN(quantityNum) || quantityNum <= 0 || !Number.isInteger(quantityNum)) {
+        ElMessage.warning("请先完成现有分组的填写");
+        // 触发表单验证,显示验证错误
+        nextTick(() => {
+          if (packageFormRef.value) {
+            packageFormRef.value.validate(() => {});
+          }
+        });
+        return;
+      }
+    }
+  }
+
   // 记录旧分组的数量,用于后续只验证旧分组
   const oldGroupsCount = lifeGroupBuyThalis.value.length;
 
@@ -1246,7 +1320,9 @@ const addGroup = () => {
   // 只清除验证状态,不重新触发验证
   // 让用户操作时(如点击添加菜品)再触发验证
   nextTick(() => {
-    ruleFormRef.value?.clearValidate("lifeGroupBuyThalis");
+    if (packageFormRef.value) {
+      packageFormRef.value.clearValidate();
+    }
     skipLastGroupValidation = false;
   });
 };
@@ -1481,6 +1557,13 @@ const confirmDishSelection = () => {
     dish.dishImage = selectedDish.imgUrl;
     dish.dishesUnit = selectedDish.dishesUnit;
     dishDialogVisible.value = false;
+    // 重新验证对应的字段,如果验证通过,错误提示会消失
+    nextTick(() => {
+      if (packageFormRef.value) {
+        const prop = `groups.${currentDishGroupIndex.value}.dishes.${currentDishIndex.value}.detailId`;
+        packageFormRef.value.validateField(prop, () => {});
+      }
+    });
   }
 };
 
@@ -1498,6 +1581,72 @@ const filterDishList = () => {
 
 // ==================== 表单引用 ====================
 const ruleFormRef = ref<FormInstance>(); // 表单引用
+const packageFormRef = ref<FormInstance>(); // 套餐内容表单引用
+
+// 套餐内容表单模型(用于验证)
+const packageFormModel = computed(() => {
+  return {
+    groups: lifeGroupBuyThalis.value
+  };
+});
+
+// 套餐内容验证规则
+const packageFormRules = computed(() => {
+  const rules: any = {};
+  lifeGroupBuyThalis.value.forEach((group, groupIndex) => {
+    // 类别验证规则 - 使用与 prop 相同的路径格式
+    rules[`groups.${groupIndex}.groupName`] = [
+      {
+        required: true,
+        message: `第${groupIndex + 1}个分组的类别不能为空`,
+        trigger: "blur"
+      }
+    ];
+
+    // 每个菜品的验证规则
+    if (group.dishes) {
+      group.dishes.forEach((dish, dishIndex) => {
+        // 菜品选择验证
+        rules[`groups.${groupIndex}.dishes.${dishIndex}.detailId`] = [
+          {
+            required: true,
+            message: `第${groupIndex + 1}个分组的第${dishIndex + 1}个菜品未选择`,
+            trigger: "change"
+          }
+        ];
+
+        // 菜品数量验证
+        rules[`groups.${groupIndex}.dishes.${dishIndex}.qty`] = [
+          {
+            required: true,
+            message: `第${groupIndex + 1}个分组的第${dishIndex + 1}个菜品数量不能为空`,
+            trigger: "blur"
+          },
+          {
+            validator: (rule: any, value: any, callback: any) => {
+              if (!value || value.toString().trim() === "") {
+                callback();
+                return;
+              }
+              const num = Number(value);
+              if (isNaN(num) || num <= 0) {
+                callback(new Error(`第${groupIndex + 1}个分组的第${dishIndex + 1}个菜品数量必须为正整数`));
+                return;
+              }
+              if (!Number.isInteger(num)) {
+                callback(new Error(`第${groupIndex + 1}个分组的第${dishIndex + 1}个菜品数量必须为正整数`));
+                return;
+              }
+              callback();
+            },
+            trigger: "blur"
+          }
+        ];
+      });
+    }
+  });
+  return rules;
+});
 
 /**
  * 提交数据(新增/编辑)
@@ -1534,53 +1683,24 @@ const handleSubmit = (type?) => {
   if (type) {
     return;
   }
-  // 验证套餐内容
-  const validateThalis = () => {
-    const thalis = lifeGroupBuyThalis.value;
-    if (!thalis || thalis.length === 0) {
-      ElMessage.error("请至少添加一个套餐分组");
-      return false;
-    }
-    for (let i = 0; i < thalis.length; i++) {
-      const group = thalis[i];
-      if (!group.groupName || group.groupName.trim() === "") {
-        ElMessage.error(`第${i + 1}个分组的类别不能为空`);
-        return false;
-      }
-      if (!group.dishes || group.dishes.length === 0) {
-        ElMessage.error(`第${i + 1}个分组至少需要添加一个菜品`);
-        return false;
-      }
-      for (let j = 0; j < group.dishes.length; j++) {
-        const dish = group.dishes[j];
-        if (!dish.detailId) {
-          ElMessage.error(`第${i + 1}个分组的第${j + 1}个菜品未选择`);
-          return false;
-        }
-        if (!dish.qty || dish.qty.toString().trim() === "") {
-          ElMessage.error(`第${i + 1}个分组的第${j + 1}个菜品数量不能为空`);
-          return false;
-        }
-        const quantityNum = Number(dish.qty);
-        if (isNaN(quantityNum) || quantityNum <= 0) {
-          ElMessage.error(`第${i + 1}个分组的第${j + 1}个菜品数量必须为正整数`);
-          return false;
-        }
-        if (!Number.isInteger(quantityNum)) {
-          ElMessage.error(`第${i + 1}个分组的第${j + 1}个菜品数量必须为正整数`);
-          return false;
-        }
-      }
-    }
-    return true;
-  };
-
   // 验证表单
   ruleFormRef.value!.validate(async valid => {
     if (!valid) return;
-    // 验证套餐内容
-    if (!validateThalis()) {
-      return;
+    // 验证套餐内容表单
+    if (packageFormRef.value) {
+      let packageValid = false;
+      await new Promise<void>(resolve => {
+        packageFormRef.value!.validate(valid => {
+          packageValid = valid;
+          if (!valid) {
+            ElMessage.error("请完善套餐内容信息");
+          }
+          resolve();
+        });
+      });
+      if (!packageValid) {
+        return;
+      }
     }
     // 组装提交参数
     let params: any = { ...storeInfoModel.value };
@@ -1842,6 +1962,69 @@ const handleImageParam = (list: any[], result: any[]) => {
 .package-content-item {
   width: 100%;
 }
+.package-content-form {
+  width: 100%;
+}
+
+/* 表单项基础样式 */
+.package-content-form :deep(.el-form-item) {
+  margin-bottom: 0;
+}
+.package-content-form :deep(.el-form-item__label) {
+  display: none;
+}
+
+/* 类别表单项样式 */
+.category-form-item {
+  width: 100%;
+  margin-bottom: 16px;
+}
+.category-form-item :deep(.el-form-item__content) {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: flex-start;
+  width: 100%;
+}
+.category-form-item :deep(.el-form-item__error) {
+  position: static;
+  width: 100%;
+  padding-top: 4px;
+  padding-left: 62px;
+  margin-top: 0;
+  font-size: 12px;
+  line-height: 1.5;
+  color: #f56c6c;
+}
+.category-row .el-input {
+  flex: 1;
+  max-width: 400px;
+}
+
+/* 菜品表单项样式 */
+.dish-form-item {
+  position: relative;
+  flex: 1;
+  margin-bottom: 0;
+}
+.dish-form-item :deep(.el-form-item__content) {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+  align-items: center;
+}
+.dish-form-item :deep(.el-form-item__error) {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  z-index: 1;
+  padding-top: 4px;
+  padding-right: 4px;
+  font-size: 12px;
+  line-height: 1.5;
+  color: #f56c6c;
+  white-space: nowrap;
+  background-color: #ffffff;
+}
 .package-content-wrapper {
   display: flex;
   flex-direction: column;
@@ -1878,8 +2061,18 @@ const handleImageParam = (list: any[], result: any[]) => {
   background-color: #f5f7fa;
   border-bottom: 1px solid #e4e7ed;
 }
+.group-index {
+  flex: 1;
+  overflow: hidden;
+  font-size: 14px;
+  font-weight: 500;
+  color: #303133;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
 .header-right {
   display: flex;
+  flex-shrink: 0;
   gap: 12px;
   align-items: center;
 }
@@ -1887,12 +2080,22 @@ const handleImageParam = (list: any[], result: any[]) => {
   display: flex;
   flex-direction: column;
   gap: 16px;
+  min-height: 60px;
   padding: 16px;
 }
 .extra-dishes-container {
   display: flex;
   flex-direction: column;
   gap: 16px;
+  margin-top: 0;
+}
+.extra-dishes-container .dish-row {
+  padding-bottom: 22px;
+  margin-bottom: 4px;
+}
+.extra-dishes-container .dish-row:last-child {
+  padding-bottom: 0;
+  margin-bottom: 0;
 }
 .package-group-collapsed {
   padding: 16px;
@@ -1916,11 +2119,24 @@ const handleImageParam = (list: any[], result: any[]) => {
   display: flex;
   gap: 12px;
   align-items: center;
+  width: 100%;
 }
 .dish-row {
+  position: relative;
   display: flex;
   gap: 12px;
-  align-items: center;
+  align-items: flex-start;
+  width: 100%;
+  margin-bottom: 4px;
+}
+.dish-row:last-child {
+  padding-bottom: 0;
+  margin-bottom: 0;
+}
+
+/* 当表单项有错误时,增加底部间距 */
+.dish-row .dish-form-item.is-error {
+  margin-bottom: 0;
 }
 .label {
   min-width: 50px;
@@ -1929,6 +2145,7 @@ const handleImageParam = (list: any[], result: any[]) => {
   white-space: nowrap;
 }
 .dish-select-block {
+  position: relative;
   display: flex;
   flex: 1;
   align-items: center;
@@ -1947,6 +2164,28 @@ const handleImageParam = (list: any[], result: any[]) => {
 .dish-select-block:focus-within {
   border-color: #409eff;
 }
+
+/* 当菜品选择有验证错误时,显示错误边框 */
+.dish-form-item.is-error .dish-select-block {
+  border-color: #f56c6c;
+}
+.dish-form-item.is-error .dish-select-block:hover {
+  border-color: #f56c6c;
+}
+.dish-form-item.is-error .dish-select-block:focus-within {
+  border-color: #f56c6c;
+}
+
+/* 优化表单项的错误状态 */
+.package-content-form :deep(.el-form-item.is-error .el-input__wrapper) {
+  box-shadow: 0 0 0 1px #f56c6c inset;
+}
+.package-content-form :deep(.el-form-item.is-error .el-input__wrapper:hover) {
+  box-shadow: 0 0 0 1px #f56c6c inset;
+}
+.package-content-form :deep(.el-form-item.is-error .el-input__wrapper.is-focus) {
+  box-shadow: 0 0 0 1px #f56c6c inset;
+}
 .dish-selected-name {
   font-size: 14px;
   color: #606266;
@@ -1956,16 +2195,64 @@ const handleImageParam = (list: any[], result: any[]) => {
   color: #c0c4cc;
 }
 .quantity-input {
+  flex-shrink: 0;
   width: 150px;
 }
 .delete-dish-btn {
   flex-shrink: 0;
+  margin-left: 8px;
 }
 .add-dish-btn {
   width: fit-content;
   margin-top: 8px;
 }
 
+/* 优化菜品行的布局,确保在有验证错误时也能正确显示 */
+.dish-row .dish-form-item {
+  min-width: 0;
+}
+.dish-row .dish-form-item:first-child {
+  flex: 1;
+  min-width: 200px;
+  max-width: 350px;
+}
+.dish-row .dish-form-item:last-child {
+  display: flex;
+  flex: 0 0 auto;
+  gap: 8px;
+  align-items: center;
+}
+
+/* 确保数量输入框和删除按钮在同一行 */
+.dish-form-item:last-child :deep(.el-form-item__content) {
+  display: flex;
+  flex-wrap: nowrap;
+  gap: 8px;
+  align-items: center;
+}
+
+/* 响应式布局优化 */
+@media (width <= 768px) {
+  .dish-row {
+    flex-direction: column;
+    align-items: stretch;
+  }
+  .dish-row .dish-form-item {
+    width: 100%;
+    max-width: 100%;
+  }
+  .dish-row .dish-form-item:first-child {
+    max-width: 100%;
+  }
+  .category-row {
+    flex-direction: column;
+    align-items: stretch;
+  }
+  .category-row .el-input {
+    max-width: 100%;
+  }
+}
+
 /* 菜品选择对话框样式 */
 .dish-dialog-content {
   display: flex;