zhangwencui
2026-05-16 88f5470dc4829e2bdde9dc4aeb79be85837c1c84
发货功能重构
已修改2个文件
1080 ■■■■■ 文件已修改
src/api/inventoryManagement/stockInventory.js 104 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesAccount/goOut.vue 976 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockInventory.js
@@ -1,70 +1,78 @@
import request from "@/utils/request";
// 分页查询库存记录列表
export const getStockInventoryListPage = (params) => {
    return request({
        url: "/stockInventory/pagestockInventory",
        method: "get",
        params,
    });
export const getStockInventoryListPage = params => {
  return request({
    url: "/stockInventory/pagestockInventory",
    method: "get",
    params,
  });
};
// 分页查询联合库存记录列表(包含商品信息)
export const getStockInventoryListPageCombined = (params) => {
    return request({
        url: "/stockInventory/pageListCombinedStockInventory",
        method: "get",
        params,
    });
export const getStockInventoryListPageCombined = params => {
  return request({
    url: "/stockInventory/pageListCombinedStockInventory",
    method: "get",
    params,
  });
};
// 创建库存记录
export const createStockInventory = (params) => {
    return request({
        url: "/stockInventory/addstockInventory",
        method: "post",
        data: params,
    });
export const createStockInventory = params => {
  return request({
    url: "/stockInventory/addstockInventory",
    method: "post",
    data: params,
  });
};
// 减少库存记录
export const subtractStockInventory = (params) => {
    return request({
        url: "/stockInventory/subtractStockInventory",
        method: "post",
        data: params,
    });
export const subtractStockInventory = params => {
  return request({
    url: "/stockInventory/subtractStockInventory",
    method: "post",
    data: params,
  });
};
export const getStockInventoryReportList = (params) => {
    return request({
        url: "/stockInventory/stockInventoryPage",
        method: "get",
        params,
    });
export const getStockInventoryReportList = params => {
  return request({
    url: "/stockInventory/stockInventoryPage",
    method: "get",
    params,
  });
};
export const getStockInventoryInAndOutReportList = (params) => {
    return request({
        url: "/stockInventory/stockInAndOutRecord",
        method: "get",
        params,
    });
export const getStockInventoryInAndOutReportList = params => {
  return request({
    url: "/stockInventory/stockInAndOutRecord",
    method: "get",
    params,
  });
};
// 冻结库存记录
export const frozenStockInventory = (params) => {
    return request({
        url: "/stockInventory/frozenStock",
        method: "post",
        data: params,
    });
export const frozenStockInventory = params => {
  return request({
    url: "/stockInventory/frozenStock",
    method: "post",
    data: params,
  });
};
// 解冻库存记录
export const thawStockInventory = (params) => {
    return request({
        url: "/stockInventory/thawStock",
        method: "post",
        data: params,
    });
export const thawStockInventory = params => {
  return request({
    url: "/stockInventory/thawStock",
    method: "post",
    data: params,
  });
};
export const getStockInventoryByModelId = productModelId => {
  return request({
    url: "/stockInventory/getByModelId",
    method: "get",
    params: { productModelId },
  });
};
src/pages/sales/salesAccount/goOut.vue
@@ -1,657 +1,451 @@
<template>
  <view class="account-detail">
  <view class="shipment-page">
    <PageHeader title="发货"
                @back="goBack" />
    <!-- 表单区域 -->
    <u-form ref="formRef"
            @submit="submitForm"
            :rules="rules"
            :model="form"
            label-width="140rpx">
      <u-form-item prop="typeValue"
                   label="发货类型"
                   required>
        <u-input v-model="typeValue"
                 readonly
                 placeholder="请选择发货方式"
                 @click="showPicker = true" />
        <template #right>
          <up-icon name="arrow-right"
                   @click="showPicker = true"></up-icon>
        </template>
      </u-form-item>
    </u-form>
    <!-- 选择器弹窗 -->
    <up-action-sheet :show="showPicker"
                     :actions="productOptions"
                     title="发货方式"
                     @select="onConfirm"
                     @close="showPicker = false" />
    <!-- 审核流程区域 -->
    <view class="approval-process">
      <view class="approval-header">
        <text class="approval-title">审核流程</text>
        <text class="approval-desc">每个步骤只能选择一个审批人</text>
      </view>
      <view class="approval-steps">
        <view v-for="(step, stepIndex) in approverNodes"
              :key="stepIndex"
              class="approval-step">
          <view class="step-dot"></view>
          <view class="step-title">
            <text>审批人</text>
    <view class="form-container">
      <up-form ref="formRef"
               :model="form"
               :rules="rules"
               label-width="100"
               input-align="right"
               error-message-align="right">
        <!-- 基本信息 -->
        <u-cell-group title="基本信息"
                      class="form-section">
          <up-form-item label="发货方式"
                        prop="type"
                        required>
            <up-input v-model="form.type"
                      readonly
                      placeholder="请选择发货方式"
                      @click="showTypePicker = true" />
            <template #right>
              <up-icon name="arrow-right"
                       @click="showTypePicker = true"></up-icon>
            </template>
          </up-form-item>
          <block v-if="form.type === '货车'">
            <up-form-item label="发货车牌"
                          prop="shippingCarNumber"
                          required>
              <up-input v-model="form.shippingCarNumber"
                        placeholder="请输入发货车牌号"
                        clearable />
            </up-form-item>
          </block>
          <block v-if="form.type === '快递'">
            <up-form-item label="快递公司"
                          prop="expressCompany"
                          required>
              <up-input v-model="form.expressCompany"
                        placeholder="请输入快递公司"
                        clearable />
            </up-form-item>
            <up-form-item label="快递单号"
                          prop="expressNumber"
                          required>
              <up-input v-model="form.expressNumber"
                        placeholder="请输入快递单号"
                        clearable />
            </up-form-item>
          </block>
        </u-cell-group>
        <!-- 批次选择 -->
        <u-cell-group title="批次选择"
                      class="form-section">
          <view class="section-header-info">
            <text class="subtitle">待发货数量: {{ goOutData.noQuantity || 0 }}</text>
          </view>
          <view class="approver-container">
            <view v-if="step.nickName"
                  class="approver-item">
              <view class="approver-avatar">
                <text class="avatar-text">{{ step.nickName.charAt(0) }}</text>
                <view class="status-dot"></view>
          <view v-if="batchList.length === 0"
                class="empty-text">
            <text>暂无可用库存批次</text>
          </view>
          <view v-else
                class="batch-list">
            <view v-for="(item, index) in batchList"
                  :key="index"
                  class="batch-card">
              <view class="batch-header">
                <text class="batch-no">批号: {{ item.batchNo }}</text>
                <text class="batch-qty">库存: {{ getAvailableQty(item) }}</text>
              </view>
              <view class="approver-info">
                <text class="approver-name">{{ step.nickName }}</text>
              <up-divider></up-divider>
              <view class="batch-body">
                <up-form-item label="发货数量">
                  <up-input v-model="item.deliveryQuantity"
                            type="digit"
                            placeholder="0.00"
                            input-align="right"
                            @blur="onBatchQtyChange(item)" />
                </up-form-item>
              </view>
              <view class="delete-approver-btn"
                    @click="removeApprover(stepIndex)">×</view>
            </view>
            <view v-else
                  class="add-approver-btn"
                  @click="addApprover(stepIndex)">
              <view class="add-circle">+</view>
              <text class="add-label">选择审批人</text>
            </view>
          </view>
          <view class="step-line"
                v-if="stepIndex < approverNodes.length - 1"></view>
          <view class="delete-step-btn"
                v-if="approverNodes.length > 1"
                @click="removeApprovalStep(stepIndex)">删除节点</view>
        </view>
      </view>
      <view class="add-step-btn">
        <u-button icon="plus"
                  plain
                  type="primary"
                  style="width: 100%"
                  @click="addApprovalStep">新增节点</u-button>
      </view>
        </u-cell-group>
        <!-- 发货图片 -->
        <u-cell-group title="发货图片"
                      class="form-section">
          <view class="upload-container">
            <up-upload :fileList="fileList"
                       @afterRead="afterRead"
                       @delete="deleteFile"
                       multiple
                       :maxCount="9"
                       width="160rpx"
                       height="160rpx" />
          </view>
        </u-cell-group>
      </up-form>
    </view>
    <!-- 底部按钮 -->
    <view class="footer-btns">
      <u-button class="cancel-btn"
                @click="goBack">取消</u-button>
      <u-button class="save-btn"
                @click="submitForm">发货</u-button>
    </view>
    <FooterButtons confirmText="确认发货"
                   @cancel="goBack"
                   @confirm="submitForm" />
    <!-- 发货方式选择器 -->
    <up-action-sheet :show="showTypePicker"
                     :actions="typeActions"
                     title="发货方式"
                     @select="onTypeSelect"
                     @close="showTypePicker = false" />
  </view>
</template>
<script setup>
  import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
  import config from "@/config";
  import { ref, onMounted, reactive } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import FooterButtons from "@/components/FooterButtons.vue";
  import { addShippingInfo } from "@/api/salesManagement/salesLedger";
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  import { userListNoPageByTenantId } from "@/api/system/user";
  import { getStockInventoryByModelId } from "@/api/inventoryManagement/stockInventory";
  import { getToken } from "@/utils/auth";
  const data = reactive({
    form: {
      approveTime: "",
      approveId: "",
      approveUser: "",
      approveUserName: "",
      approveDeptName: "",
      approveDeptId: "",
      approveReason: "",
      checkResult: "",
      tempFileIds: [],
      approverList: [], // 新增字段,存储所有节点的审批人id
      startDate: "",
      endDate: "",
      location: "",
      price: "",
    },
    rules: {
      typeValue: [{ required: false, message: "请选择", trigger: "change" }],
    },
  });
  const { form, rules } = toRefs(data);
  const showPicker = ref(false);
  const productOptions = ref([
    {
      value: "货车",
      name: "货车",
    },
    {
      value: "快递",
      name: "快递",
    },
  ]);
  const operationType = ref("");
  const currentApproveStatus = ref("");
  const approverNodes = ref([]);
  const userList = ref([]);
  const formRef = ref(null);
  const approveType = ref(0);
  const goOutData = ref({});
  const batchList = ref([]);
  const fileList = ref([]);
  const showTypePicker = ref(false);
  const typeActions = [
    { name: "货车", value: "货车" },
    { name: "快递", value: "快递" },
  ];
  const form = reactive({
    type: "货车",
    shippingCarNumber: "",
    expressCompany: "",
    expressNumber: "",
  });
  const rules = {
    type: [{ required: true, message: "请选择发货方式", trigger: "change" }],
    shippingCarNumber: [
      {
        required: true,
        validator: (rule, value, callback) => {
          if (form.type === "货车" && !value) {
            return false;
          }
          return true;
        },
        message: "请输入车牌号",
        trigger: "blur",
      },
    ],
    expressCompany: [
      {
        required: true,
        validator: (rule, value, callback) => {
          if (form.type === "快递" && !value) {
            return false;
          }
          return true;
        },
        message: "请输入快递公司",
        trigger: "blur",
      },
    ],
    expressNumber: [
      {
        required: true,
        validator: (rule, value, callback) => {
          if (form.type === "快递" && !value) {
            return false;
          }
          return true;
        },
        message: "请输入快递单号",
        trigger: "blur",
      },
    ],
  };
  const formRef = ref(null);
  onMounted(async () => {
    try {
      userListNoPageByTenantId().then(res => {
        userList.value = res.data;
      });
      // 从本地存储获取发货详情
      goOutData.value = JSON.parse(uni.getStorageSync("goOutData"));
      console.log(goOutData.value, "goOutData.value");
      // 初始化审批流程节点,默认一个节点
      approverNodes.value = [{ id: 1, userId: null }];
      // 监听联系人选择事件
      uni.$on("selectContact", handleSelectContact);
    } catch (error) {
      console.error("获取失败:", error);
    const storedData = uni.getStorageSync("goOutData");
    goOutData.value = JSON.parse(storedData || "{}");
    if (goOutData.value.productModelId) {
      loadBatches(goOutData.value.productModelId);
    }
  });
  onUnmounted(() => {
    // 移除事件监听
    uni.$off("selectContact", handleSelectContact);
  });
  const typeValue = ref("货车");
  const onConfirm = item => {
    // 设置选中的部门
    typeValue.value = item.name;
    showPicker.value = false;
  const loadBatches = async modelId => {
    if (!modelId) return;
    try {
      const res = await getStockInventoryByModelId(modelId);
      const rawList = Array.isArray(res?.data)
        ? res.data
        : res?.data?.records || res?.data?.rows || res || [];
      const seenIds = new Set();
      batchList.value = rawList
        .filter(item => {
          if (!item?.id || !item?.batchNo || seenIds.has(item.id)) {
            return false;
          }
          seenIds.add(item.id);
          return true;
        })
        .map(item => ({
          ...item,
          deliveryQuantity: "",
        }));
    } catch (e) {
      console.error("加载批次失败", e);
    }
  };
  const goBack = () => {
    // 清除本地存储的数据
    uni.removeStorageSync("operationType");
    uni.removeStorageSync("invoiceLedgerEditRow");
    uni.removeStorageSync("approveType");
    uni.navigateBack();
  const getAvailableQty = item => {
    const quantity =
      item?.qualitity ??
      item?.quantity ??
      item?.unLockedQuantity ??
      item?.qualifiedUnLockedQuantity ??
      item?.qualifiedQuantity ??
      item?.stockQuantity;
    return quantity ?? 0;
  };
  const submitForm = () => {
    // 检查每个审批步骤是否都有审批人
    const hasEmptyStep = approverNodes.value.some(step => !step.nickName);
    if (hasEmptyStep) {
      showToast("请为每个审批步骤选择审批人");
  const onBatchQtyChange = item => {
    const val = parseFloat(item.deliveryQuantity);
    if (isNaN(val) || val <= 0) {
      item.deliveryQuantity = "";
      return;
    }
    formRef.value
      .validate()
      .then(valid => {
        if (valid) {
          // 表单校验通过,可以提交数据
          // 收集所有节点的审批人id
          console.log("approverNodes---", approverNodes.value);
          const approveUserIds = approverNodes.value
            .map(node => node.userId)
            .join(",");
          const params = {
            salesLedgerId: goOutData.value.salesLedgerId,
            salesLedgerProductId: goOutData.value.id,
            type: typeValue.value,
            approveUserIds,
          };
          console.log(params, "params");
          addShippingInfo(params).then(res => {
            showToast("发货成功");
            setTimeout(() => {
              goBack();
            }, 500);
          });
        }
      })
      .catch(error => {
        console.error("表单校验失败:", error);
        // 尝试获取具体的错误字段
        if (error && error.errors) {
          const firstError = error.errors[0];
          if (firstError) {
            uni.showToast({
              title: firstError.message || "表单校验失败,请检查必填项",
              icon: "none",
            });
            return;
    const available = getAvailableQty(item);
    if (val > available) {
      uni.showToast({ title: "不能超过库存数量", icon: "none" });
      item.deliveryQuantity = available.toString();
    }
    const totalToShip = Number(goOutData.value.noQuantity || 0);
    const otherBatchesTotal = batchList.value.reduce((sum, b) => {
      if (b.id === item.id) return sum;
      return sum + Number(b.deliveryQuantity || 0);
    }, 0);
    if (val + otherBatchesTotal > totalToShip) {
      uni.showToast({ title: "总数不能超过待发货数量", icon: "none" });
      item.deliveryQuantity = (totalToShip - otherBatchesTotal).toString();
    }
  };
  const onTypeSelect = item => {
    form.type = item.name;
    showTypePicker.value = false;
  };
  const afterRead = async event => {
    const { file } = event;
    const lists = [].concat(file);
    const token = getToken();
    for (let i = 0; i < lists.length; i++) {
      const item = lists[i];
      const uploadIndex = fileList.value.length;
      fileList.value.push({
        ...item,
        status: "uploading",
        message: "上传中",
      });
      uni.uploadFile({
        url: config.baseUrl + "/common/upload",
        filePath: item.url,
        name: "files",
        header: {
          Authorization: "Bearer " + token,
        },
        success: res => {
          try {
            const data = JSON.parse(res.data);
            if (data.code === 200) {
              const fileData = Array.isArray(data.data)
                ? data.data[0]
                : data.data || data;
              fileList.value[uploadIndex].status = "success";
              fileList.value[uploadIndex].message = "";
              fileList.value[uploadIndex].url = fileData.url;
              fileList.value[uploadIndex].storageBlobDTO = fileData;
            } else {
              fileList.value[uploadIndex].status = "failed";
              fileList.value[uploadIndex].message = data.msg || "上传失败";
            }
          } catch (e) {
            fileList.value[uploadIndex].status = "failed";
            fileList.value[uploadIndex].message = "解析失败";
          }
        }
        // 显示通用错误信息
        uni.showToast({
          title: "表单校验失败,请检查必填项",
          icon: "none",
        });
        },
        fail: () => {
          fileList.value[uploadIndex].status = "failed";
          fileList.value[uploadIndex].message = "网络异常";
        },
      });
    }
  };
  // 处理联系人选择结果
  const handleSelectContact = data => {
    const { stepIndex, contact } = data;
    // 将选中的联系人设置为对应审批步骤的审批人
    approverNodes.value[stepIndex].userId = contact.userId;
    approverNodes.value[stepIndex].nickName = contact.nickName;
  const deleteFile = event => {
    fileList.value.splice(event.index, 1);
  };
  const addApprover = stepIndex => {
    // 跳转到联系人选择页面
    uni.setStorageSync("stepIndex", stepIndex);
    uni.navigateTo({
      url: "/pages/cooperativeOffice/collaborativeApproval/contactSelect",
    });
  };
  const goBack = () => uni.navigateBack();
  const addApprovalStep = () => {
    // 添加新的审批步骤
    approverNodes.value.push({ userId: null, nickName: null });
  };
  const submitForm = async () => {
    const valid = await formRef.value.validate().catch(() => false);
    if (!valid) return;
  const removeApprover = stepIndex => {
    // 移除审批人
    approverNodes.value[stepIndex].userId = null;
    approverNodes.value[stepIndex].nickName = null;
  };
    const selectedBatches = batchList.value.filter(
      b => parseFloat(b.deliveryQuantity) > 0
    );
    if (selectedBatches.length === 0) {
      uni.showToast({ title: "请至少填写一个批次的发货数量", icon: "none" });
      return;
    }
  const removeApprovalStep = stepIndex => {
    // 确保至少保留一个审批步骤
    if (approverNodes.value.length > 1) {
      approverNodes.value.splice(stepIndex, 1);
    } else {
      uni.showToast({
        title: "至少需要一个审批步骤",
        icon: "none",
      });
    // Check if any file is still uploading
    if (fileList.value.some(f => f.status === "uploading")) {
      uni.showToast({ title: "请等待图片上传完成", icon: "none" });
      return;
    }
    const payload = {
      salesLedgerId: goOutData.value.salesLedgerId,
      salesLedgerProductId: goOutData.value.id,
      type: form.type,
      shippingCarNumber: form.type === "货车" ? form.shippingCarNumber : "",
      expressCompany: form.type === "快递" ? form.expressCompany : "",
      expressNumber: form.type === "快递" ? form.expressNumber : "",
      storageBlobDTOs: fileList.value
        .filter(f => f.status === "success")
        .map(f => f.storageBlobDTO),
      batchNo: selectedBatches.map(b => b.id),
      batchNoDetailList: selectedBatches.map(b => ({
        stockInventoryId: b.id,
        batchNo: b.batchNo,
        quantity: parseFloat(b.deliveryQuantity),
        productModelId: goOutData.value.productModelId,
      })),
    };
    try {
      uni.showLoading({ title: "提交中..." });
      const res = await addShippingInfo(payload);
      uni.hideLoading();
      uni.showToast({ title: "发货成功" });
      setTimeout(() => goBack(), 500);
    } catch (e) {
      uni.hideLoading();
      uni.showToast({ title: "发货失败", icon: "none" });
    }
  };
</script>
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
  .approval-process {
    background: #fff;
    margin: 16px;
    border-radius: 16px;
    padding: 16px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
  .shipment-page {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 100px;
  }
  .approval-header {
    margin-bottom: 16px;
  .form-container {
    padding: 12px 12px 0;
  }
  .approval-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
    display: block;
    margin-bottom: 4px;
  }
  .approval-desc {
    font-size: 12px;
    color: #999;
  }
  /* 样式增强为“简洁小圆圈风格” */
  .approval-steps {
    padding-left: 22px;
    position: relative;
    &::before {
      content: "";
      position: absolute;
      left: 11px;
      top: 40px;
      bottom: 40px;
      width: 2px;
      background: linear-gradient(
        to bottom,
        #e6f7ff 0%,
        #bae7ff 50%,
        #91d5ff 100%
      );
      border-radius: 1px;
    }
  }
  .approval-step {
    position: relative;
    margin-bottom: 24px;
    &::before {
      content: "";
      position: absolute;
      left: -18px;
      top: 14px; // 从 8px 调整为 14px,与文字中心对齐
      width: 12px;
      height: 12px;
      background: #fff;
      border: 3px solid #006cfb;
      border-radius: 50%;
      z-index: 2;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }
  }
  .step-title {
    top: 12px;
  .form-section {
    margin-bottom: 12px;
    position: relative;
    margin-left: 6px;
  }
  .step-title text {
    font-size: 14px;
    color: #666;
    background: #f0f0f0;
    padding: 4px 12px;
    border-radius: 12px;
    position: relative;
    line-height: 1.4; // 确保文字行高一致
    overflow: hidden;
    box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
  }
  .approver-item {
  .section-header-info {
    padding: 10px 18px;
    background: #f8fbff;
    display: flex;
    align-items: center;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
    border-radius: 16px;
    padding: 16px;
    justify-content: flex-end;
    .subtitle {
      font-size: 13px;
      color: #7a8599;
    }
  }
  .batch-list {
    padding: 12px;
    display: flex;
    flex-direction: column;
    gap: 12px;
    position: relative;
    border: 1px solid #e6f7ff;
    box-shadow: 0 4px 12px rgba(0, 108, 251, 0.08);
    transition: all 0.3s ease;
  }
  .approver-avatar {
    width: 48px;
    height: 48px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
  }
  .avatar-text {
    color: #fff;
    font-size: 18px;
    font-weight: 600;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
  }
  .approver-info {
    flex: 1;
    position: relative;
  }
  .approver-name {
    display: block;
    font-size: 16px;
    color: #333;
    font-weight: 500;
    position: relative;
  }
  .approver-dept {
    font-size: 12px;
    color: #999;
    background: rgba(0, 108, 251, 0.05);
    padding: 2px 8px;
    border-radius: 8px;
    display: inline-block;
    position: relative;
    &::before {
      content: "";
      position: absolute;
      left: 4px;
      top: 50%;
      transform: translateY(-50%);
      width: 2px;
      height: 2px;
      background: #006cfb;
      border-radius: 50%;
    }
  }
  .delete-approver-btn {
    font-size: 16px;
    color: #ff4d4f;
    background: linear-gradient(
      135deg,
      rgba(255, 77, 79, 0.1) 0%,
      rgba(255, 77, 79, 0.05) 100%
    );
    width: 28px;
    height: 28px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 0.3s ease;
    position: relative;
  }
  .add-approver-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(135deg, #f0f8ff 0%, #e6f7ff 100%);
    border: 2px dashed #006cfb;
    border-radius: 16px;
    padding: 20px;
    color: #006cfb;
    font-size: 14px;
    position: relative;
    transition: all 0.3s ease;
    &::before {
      content: "";
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      width: 32px;
      height: 32px;
      border: 2px solid #006cfb;
      border-radius: 50%;
      opacity: 0;
      transition: all 0.3s ease;
    }
  }
  .delete-step-btn {
    color: #ff4d4f;
    font-size: 12px;
    background: linear-gradient(
      135deg,
      rgba(255, 77, 79, 0.1) 0%,
      rgba(255, 77, 79, 0.05) 100%
    );
    padding: 6px 12px;
    border-radius: 12px;
    display: inline-block;
    position: relative;
    transition: all 0.3s ease;
    &::before {
      content: "";
      position: absolute;
      left: 6px;
      top: 50%;
      transform: translateY(-50%);
      width: 4px;
      height: 4px;
      background: #ff4d4f;
      border-radius: 50%;
    }
  }
  .step-line {
    display: none; // 隐藏原来的线条,使用伪元素代替
  }
  .add-step-btn {
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .footer-btns {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
  .batch-card {
    background: #fff;
    border-radius: 12px;
    padding: 0 12px 12px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
    border: 1px solid #f0f3f7;
  }
  .batch-header {
    padding: 12px 0;
    display: flex;
    justify-content: space-around;
    justify-content: space-between;
    align-items: center;
    padding: 0.75rem 0;
    box-shadow: 0 -0.125rem 0.5rem rgba(0, 0, 0, 0.05);
    z-index: 1000;
  }
  .cancel-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #ffffff;
    width: 6.375rem;
    background: #c7c9cc;
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3, 88, 185, 0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
  }
  .save-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #ffffff;
    width: 14rem;
    background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3, 88, 185, 0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
  }
  // 动画定义
  @keyframes pulse {
    0% {
      transform: scale(1);
      opacity: 1;
    .batch-no {
      font-size: 14px;
      font-weight: 600;
      color: #22324d;
    }
    50% {
      transform: scale(1.2);
      opacity: 0.7;
    }
    100% {
      transform: scale(1);
      opacity: 1;
    .batch-qty {
      font-size: 13px;
      color: #2979ff;
      background: #eef6ff;
      padding: 2px 8px;
      border-radius: 4px;
    }
  }
  @keyframes rotate {
    0% {
      transform: rotate(0deg);
    }
    100% {
      transform: rotate(360deg);
    }
  }
  @keyframes ripple {
    0% {
      transform: translate(-50%, -50%) scale(0.8);
      opacity: 1;
    }
    100% {
      transform: translate(-50%, -50%) scale(1.6);
      opacity: 0;
    }
  }
  /* 如果已有 .step-line,这里更精准定位到左侧与小圆点对齐 */
  .step-line {
    position: absolute;
    left: 4px;
    top: 48px;
    width: 2px;
    height: calc(100% - 48px);
    background: #e5e7eb;
  }
  .approver-container {
    display: flex;
    align-items: center;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
    border-radius: 16px;
    gap: 12px;
    padding: 10px 0;
    background: transparent;
    border: none;
    box-shadow: none;
  }
  .approver-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 10px;
    background: transparent;
    border: none;
    box-shadow: none;
    border-radius: 0;
  }
  .approver-avatar {
    position: relative;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: #f3f4f6;
    border: 2px solid #e5e7eb;
    display: flex;
    align-items: center;
    justify-content: center;
    animation: none; /* 禁用旋转等动画,回归简洁 */
  }
  .avatar-text {
    font-size: 14px;
    color: #374151;
    font-weight: 600;
  }
  .add-approver-btn {
    display: flex;
    align-items: center;
    gap: 8px;
    background: transparent;
    border: none;
    box-shadow: none;
    padding: 0;
  }
  .add-approver-btn .add-circle {
    width: 40px;
    height: 40px;
    border: 2px dashed #a0aec0;
    border-radius: 50%;
    color: #6b7280;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 22px;
    line-height: 1;
  }
  .add-approver-btn .add-label {
    color: #3b82f6;
  .empty-text {
    padding: 30px 12px;
    text-align: center;
    color: #999;
    font-size: 14px;
  }
</style>
  .upload-container {
    padding: 12px 18px;
  }
  :deep(.u-cell-group__title) {
    padding: 14px 18px 10px !important;
    font-size: 15px !important;
    font-weight: 600 !important;
    color: #22324d !important;
    background: #f8fbff !important;
  }
  :deep(.u-form-item) {
    padding: 0 18px !important;
  }
</style>