yyb
5 天以前 23ff9ba58505814008984ffd4b8125cc67b9cc12
销售台账,其他金额维护,工艺路线维护,客户地区维护
已添加4个文件
已修改2个文件
1711 ■■■■ 文件已修改
src/api/salesManagement/salesProcessFlowConfig.js 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFile/index.vue 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/components/OtherAmountMaintenanceButton.vue 278 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/components/ProcessFlowConfigSelectDialog.vue 234 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/components/ProcessFlowMaintenanceButton.vue 525 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 539 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/salesProcessFlowConfig.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,89 @@
// å·¥è‰ºæµç¨‹é…ç½®ï¼ˆæ¨¡æ¿ï¼‰ä¸Žç»‘定到销售台账产品的接口
import request from "@/utils/request";
// å·¥è‰ºè·¯çº¿åˆ†é¡µåˆ—表
export function salesProcessFlowConfigList(query = {}) {
  return request({
    url: "/processRoute/page",
    method: "get",
    params: query,
  });
}
// å·¥è‰ºè·¯çº¿è¯¦æƒ…(兼容旧调用)
export function salesProcessFlowConfigGetById(configId) {
  return request({
    url: `/processRoute/${configId}`,
    method: "get",
  });
}
// æ–°å¢ž/编辑工艺路线
export function salesProcessFlowConfigUpsert(data) {
  return request({
    url: "/processRoute",
    method: "post",
    data: data,
  });
}
// åˆ é™¤å·¥è‰ºè·¯çº¿
export function salesProcessFlowConfigDelete(configId) {
  return request({
    url: `/processRoute/${configId}`,
    method: "delete",
  });
}
// è®¾ç½®é»˜è®¤å·¥è‰ºè·¯çº¿
export function salesProcessFlowConfigSetDefault(configId) {
  return request({
    url: `/processRoute/default/${configId}`,
    method: "put",
  });
}
// æŸ¥è¯¢å·¥è‰ºè·¯çº¿ä¸‹çš„工序
export function salesProcessFlowConfigItemList(routeId) {
  return request({
    url: "/processRouteItem/list",
    method: "get",
    params: { routeId },
  });
}
// æ–°å¢ž/修改工序
export function salesProcessFlowConfigItemUpsert(data) {
  return request({
    url: "/processRouteItem/",
    method: "post",
    data,
  });
}
// å·¥åºæŽ’序接口
export function salesProcessFlowConfigItemSort(data) {
  return request({
    url: "/processRouteItem/sort",
    method: "post",
    data,
  });
}
// åˆ é™¤å·¥åº
export function salesProcessFlowConfigItemDelete(itemId) {
  return request({
    url: `/processRouteItem/batchDelete/${itemId}`,
    method: "delete",
  });
}
// å°†æŸå¥—配置应用到具体产品(同步更新)
export function salesLedgerProductSetProcessFlowConfig(data) {
  return request({
    url: "/salesLedgerProduct/setProcessFlowConfig",
    method: "post",
    data: data,
  });
}
src/views/basicData/customerFile/index.vue
@@ -76,7 +76,7 @@
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="公司地址:"
            <el-form-item label="客户地址:"
                          prop="companyAddress">
              <el-input v-model="form.companyAddress"
                        placeholder="请输入"
@@ -84,11 +84,35 @@
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="公司电话:"
            <el-form-item label="客户地区:"
                          prop="regions">
              <el-input v-model="form.regions"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="客户电话:"
                          prop="companyPhone">
              <el-input v-model="form.companyPhone"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="客户分类:"
                          prop="customerType">
              <el-select v-model="form.customerType"
                         placeholder="请选择"
                         clearable>
                <el-option label="零售客户"
                           value="零售客户" />
                <el-option label="进销商客户"
                           value="进销商客户" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
@@ -117,19 +141,6 @@
              <el-input v-model="form.bankCode"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="客户分类:"
                          prop="customerType">
              <el-select v-model="form.customerType"
                         placeholder="请选择"
                         clearable>
                <el-option label="零售客户"
                           value="零售客户" />
                <el-option label="进销商客户"
                           value="进销商客户" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
@@ -720,6 +731,11 @@
      width: 220,
    },
    {
      label: "客户地区",
      prop: "regions",
      width: 120,
    },
    {
      label: "纳税人识别码",
      prop: "taxpayerIdentificationNumber",
      width: 220,
src/views/salesManagement/salesLedger/components/OtherAmountMaintenanceButton.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,278 @@
<template>
  <div style="display: inline-block;">
    <el-button type="primary" plain @click="openDialog">
      å…¶ä»–金额维护
    </el-button>
    <el-dialog
      v-model="visible"
      title="其他金额维护"
      width="80%"
      :close-on-click-modal="false"
      @close="closeDialog"
    >
      <el-row :gutter="20">
        <el-col :span="14">
          <el-table
            :data="records"
            border
            v-loading="loading"
            height="55vh"
          >
            <el-table-column label="编码" prop="code" min-width="120" show-overflow-tooltip />
            <el-table-column label="项目" prop="processName" min-width="180" show-overflow-tooltip />
            <el-table-column label="数量" prop="quantity" min-width="110" :formatter="formattedNumber" />
            <el-table-column label="单价(元)" prop="unitPrice" min-width="130" :formatter="formattedNumber" />
            <el-table-column label="金额(元)" prop="amount" min-width="160" :formatter="formattedNumber" />
            <el-table-column fixed="right" label="操作" width="160" align="center">
              <template #default="scope">
                <el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
                <el-button link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
          <pagination
            v-show="total > 0"
            :total="total"
            layout="total, sizes, prev, pager, next, jumper"
            :page="page.current"
            :limit="page.size"
            @pagination="paginationChange"
          />
        </el-col>
        <el-col :span="10">
          <div style="padding: 8px 0;">
            <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 10px;">
              <div style="font-weight:600;">
                {{ operationType === "add" ? "新增其他金额" : "编辑其他金额" }}
              </div>
              <el-button
                type="primary"
                plain
                size="small"
                @click="handleAdd"
                :disabled="operationType === 'add'"
              >
                æ–°å¢ž
              </el-button>
            </div>
            <el-form
              :model="form"
              label-width="120px"
              label-position="top"
              :rules="rules"
              ref="formRef"
            >
              <el-form-item label="编码">
                <el-input v-model="form.code" placeholder="请输入编码(可选)" clearable />
              </el-form-item>
              <el-form-item label="项目" prop="processName">
                <el-input v-model="form.processName" placeholder="请输入项目名称" clearable />
              </el-form-item>
              <el-form-item label="数量" prop="quantity">
                <el-input-number
                  v-model="form.quantity"
                  :min="0"
                  :precision="2"
                  style="width:100%"
                  placeholder="请输入数量"
                  clearable
                  @change="recalcAmount"
                />
              </el-form-item>
              <el-form-item label="单价(元)" prop="unitPrice">
                <el-input-number
                  v-model="form.unitPrice"
                  :min="0"
                  :precision="2"
                  style="width:100%"
                  placeholder="请输入单价"
                  clearable
                  @change="recalcAmount"
                />
              </el-form-item>
              <el-form-item label="金额(元)">
                <el-input v-model="form.amount" disabled />
              </el-form-item>
              <div style="display:flex; justify-content:flex-end; gap: 10px; margin-top: 8px;">
                <el-button @click="closeDialog">取消</el-button>
                <el-button type="primary" @click="submitForm">保存</el-button>
              </div>
            </el-form>
          </div>
        </el-col>
      </el-row>
    </el-dialog>
  </div>
</template>
<script setup>
import { getCurrentInstance, reactive, ref } from "vue";
import { ElMessageBox } from "element-plus";
import pagination from "@/components/PIMTable/Pagination.vue";
import {
  salesLedgerProductProcessList,
  salesLedgerProductProcessAdd,
  salesLedgerProductProcessUpdate,
  salesLedgerProductProcessDelete,
} from "@/api/salesManagement/salesLedger.js";
const { proxy } = getCurrentInstance();
const visible = ref(false);
const loading = ref(false);
const records = ref([]);
const total = ref(0);
const page = reactive({
  current: 1,
  size: 10,
});
const operationType = ref("add");
const formRef = ref(null);
const form = reactive({
  id: null,
  code: "",
  processName: "",
  quantity: 0,
  unitPrice: 0,
  amount: "0.00",
});
const rules = reactive({
  processName: [{ required: true, message: "请输入项目名称", trigger: "change" }],
  quantity: [{ required: true, message: "请输入数量", trigger: "blur" }],
  unitPrice: [{ required: true, message: "请输入单价", trigger: "blur" }],
});
const formattedNumber = (row, column, cellValue) => {
  return Number(cellValue || 0).toFixed(2);
};
const recalcAmount = () => {
  const quantity = Number(form.quantity ?? 0) || 0;
  const unitPrice = Number(form.unitPrice ?? 0) || 0;
  form.amount = (quantity * unitPrice).toFixed(2);
};
const resetForm = (type = "add") => {
  operationType.value = type;
  form.id = null;
  form.code = "";
  form.processName = "";
  form.quantity = 0;
  form.unitPrice = 0;
  form.amount = "0.00";
};
const fetchList = async () => {
  loading.value = true;
  try {
    const res = await salesLedgerProductProcessList({
      current: page.current,
      size: page.size,
    });
    const list = res?.records ?? res?.data?.records ?? [];
    const sum = res?.total ?? res?.data?.total ?? 0;
    records.value = list.map((item) => {
      const quantity = Number(item.quantity ?? 0) || 0;
      const unitPrice = Number(item.unitPrice ?? 0) || 0;
      const amount = Number(item.amount ?? quantity * unitPrice) || 0;
      return {
        id: item.id,
        code: item.code ?? item.remark ?? "",
        processName: item.processName ?? "",
        quantity,
        unitPrice,
        amount: amount.toFixed(2),
      };
    });
    total.value = sum;
  } finally {
    loading.value = false;
  }
};
const openDialog = () => {
  visible.value = true;
  resetForm("add");
  fetchList();
};
const closeDialog = () => {
  visible.value = false;
  resetForm("add");
};
const paginationChange = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  fetchList();
};
const handleAdd = () => {
  resetForm("add");
};
const handleEdit = (row) => {
  if (!row) return;
  operationType.value = "edit";
  form.id = row.id ?? null;
  form.code = row.code ?? "";
  form.processName = row.processName ?? "";
  form.quantity = Number(row.quantity ?? 0) || 0;
  form.unitPrice = Number(row.unitPrice ?? 0) || 0;
  recalcAmount();
};
const submitForm = () => {
  formRef.value?.validate((valid) => {
    if (!valid) return;
    const payload = {
      processName: form.processName,
      quantity: Number(form.quantity) || 0,
      unitPrice: Number(form.unitPrice) || 0,
      amount: Number(form.amount) || 0,
      remark: form.code,
      code: form.code,
    };
    const req =
      operationType.value === "edit"
        ? salesLedgerProductProcessUpdate({ ...payload, id: form.id })
        : salesLedgerProductProcessAdd(payload);
    req.then(() => {
      proxy.$modal.msgSuccess("保存成功");
      fetchList();
      resetForm("add");
    });
  });
};
const handleDelete = (row) => {
  if (!row?.id) return;
  ElMessageBox.confirm("确认删除该记录?", "删除", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      return salesLedgerProductProcessDelete(row.id).then(() => {
        proxy.$modal.msgSuccess("删除成功");
        fetchList();
        if (operationType.value === "edit" && form.id === row.id) {
          resetForm("add");
        }
      });
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
};
</script>
src/views/salesManagement/salesLedger/components/ProcessFlowConfigSelectDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,234 @@
<template>
  <el-dialog
    v-model="visible"
    title="选择工艺路线配置"
    width="1000px"
    :close-on-click-modal="false"
    @close="handleClose"
  >
    <el-row :gutter="20">
      <el-col :span="10">
        <div style="font-weight: 600; margin-bottom: 8px;">配置</div>
        <el-select
          v-model="selectedConfigId"
          filterable
          clearable
          placeholder="请选择工艺路线配置"
          style="width: 100%;"
          @change="handleConfigChange"
        >
          <el-option
            v-for="cfg in configList"
            :key="cfg.configId"
            :label="cfg.configName"
            :value="cfg.configId"
          />
        </el-select>
        <el-divider style="margin: 16px 0;" />
        <div style="font-weight: 600; margin-bottom: 8px;">步骤预览</div>
        <div style="font-size: 12px; color: #909399; margin-bottom: 6px;">
          æ ¹æ®æ‰€é€‰é…ç½®å±•示流程图
        </div>
      </el-col>
      <el-col :span="14">
        <div class="process-diagram">
          <div v-if="steps.length === 0" class="process-diagram-empty">暂无步骤</div>
          <div
            v-for="(step, idx) in steps"
            :key="String(step.processId) + '_' + idx"
            class="process-diagram-segment"
          >
            <div class="process-diagram-node">
              <div class="process-diagram-index">{{ idx + 1 }}</div>
              <div class="process-diagram-name">{{ step.processName }}</div>
            </div>
            <div v-if="idx < steps.length - 1" class="process-diagram-arrow">→</div>
          </div>
        </div>
        <div v-if="selectedConfigId === null" style="margin-top: 10px; font-size: 12px; color: #909399;">
          è¯·å…ˆé€‰æ‹©ä¸€æ¡å·²ç»´æŠ¤å¥½çš„工艺路线
        </div>
      </el-col>
    </el-row>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="handleClose">取消</el-button>
        <el-button type="primary" :loading="saving" @click="confirmSelect">
          ç¡®å®š
        </el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script setup>
import { computed, getCurrentInstance, ref, watch } from "vue";
import { salesProcessFlowConfigList, salesProcessFlowConfigGetById } from "@/api/salesManagement/salesProcessFlowConfig.js";
const emit = defineEmits(["update:visible", "confirm"]);
const props = defineProps({
  visible: { type: Boolean, default: false },
  defaultConfigId: { type: [Number, String, null], default: null },
});
const { proxy } = getCurrentInstance();
const visible = computed({
  get() {
    return props.visible;
  },
  set(v) {
    emit("update:visible", v);
  },
});
const configList = ref([]);
const selectedConfigId = ref(null);
const steps = ref([]);
const saving = ref(false);
const normalizeStepsFromApi = (list) => {
  if (!Array.isArray(list)) return [];
  return list.map((s, idx) => ({
    stepId: s.stepId ?? s.id ?? null,
    processId: s.processId ?? s.process_id ?? s.id ?? null,
    processName: s.processName ?? s.process_name ?? s.name ?? "",
    sortNo: s.sortNo ?? idx + 1,
  }));
};
const fetchConfigList = async () => {
  const res = await salesProcessFlowConfigList();
  const list = res?.data ?? res?.records ?? res ?? [];
  configList.value = Array.isArray(list) ? list : [];
};
const fetchConfigDetail = async (id) => {
  if (!id) {
    steps.value = [];
    return;
  }
  const res = await salesProcessFlowConfigGetById(id);
  const detail = res?.data ?? res ?? {};
  steps.value = normalizeStepsFromApi(detail?.steps ?? []);
};
const initDefault = async () => {
  await fetchConfigList();
  selectedConfigId.value = props.defaultConfigId ?? null;
  await fetchConfigDetail(selectedConfigId.value);
};
watch(
  () => props.visible,
  async (v) => {
    if (v) {
      try {
        await initDefault();
      } catch {
        proxy?.$modal?.msgError?.("获取工艺路线配置失败");
      }
    }
  }
);
const handleConfigChange = async () => {
  await fetchConfigDetail(selectedConfigId.value);
};
const handleClose = () => {
  emit("update:visible", false);
  saving.value = false;
};
const confirmSelect = async () => {
  if (saving.value) return;
  if (selectedConfigId.value === null || selectedConfigId.value === undefined || selectedConfigId.value === "") {
    proxy?.$modal?.msgWarning?.("请选择工艺路线配置");
    return;
  }
  saving.value = true;
  try {
    emit("confirm", selectedConfigId.value);
    handleClose();
  } catch (e) {
    proxy?.$modal?.msgError?.("确认失败,请稍后重试");
  } finally {
    saving.value = false;
  }
};
</script>
<style scoped>
.process-diagram {
  display: flex;
  align-items: center;
  gap: 0;
  flex-wrap: nowrap;
  overflow-x: auto;
  padding: 10px 0;
}
.process-diagram-segment {
  display: flex;
  align-items: center;
}
.process-diagram-node {
  width: 160px;
  min-width: 160px;
  height: 78px;
  border: 1px solid #ebeef5;
  border-radius: 10px;
  background: #fff;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 10px 12px;
  margin-right: 10px;
  box-sizing: border-box;
}
.process-diagram-index {
  font-size: 12px;
  color: #909399;
  margin-bottom: 4px;
}
.process-diagram-name {
  font-size: 14px;
  font-weight: 600;
  color: #303133;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.process-diagram-arrow {
  font-size: 18px;
  color: #909399;
  margin-right: 14px;
  margin-left: -6px;
}
.process-diagram-empty {
  width: 100%;
  text-align: center;
  padding: 40px 0;
  color: #909399;
  border: 1px dashed #ebeef5;
  border-radius: 8px;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
</style>
src/views/salesManagement/salesLedger/components/ProcessFlowMaintenanceButton.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,525 @@
<template>
  <div style="display: inline-block; margin-left: 8px;">
    <el-button type="primary" plain @click="openDialog">工艺流程</el-button>
    <el-dialog
      v-model="visible"
      title="工艺路线与工序维护"
      width="1280px"
      class="process-route-dialog"
      :close-on-click-modal="false"
      @close="closeDialog"
    >
      <el-row :gutter="16" class="dialog-main">
        <el-col :span="10">
          <div class="left-panel">
            <div style="font-weight: 600; margin-bottom: 10px;">工艺路线</div>
            <div class="route-toolbar route-toolbar-left">
              <el-input
                v-model="routeKeyword"
                placeholder="按工艺路线名称查询"
                clearable
                @keyup.enter="handleRouteQuery"
              />
              <el-button type="primary" @click="handleRouteQuery">查询</el-button>
              <el-input
                v-model="routeNameDraft"
                placeholder="输入名称后新增"
                clearable
              />
              <el-button type="primary" plain @click="createRoute">新增</el-button>
            </div>
            <div class="left-table-wrap">
              <el-table
                :data="routeList"
                border
                row-key="routeId"
                highlight-current-row
                height="100%"
                table-layout="fixed"
                @current-change="handleRouteSelect"
              >
                <el-table-column label="序号" width="56" align="center">
                  <template #default="scope">{{ scope.$index + 1 }}</template>
                </el-table-column>
                <el-table-column label="工艺路线名称" min-width="150" prop="processRouteName" show-overflow-tooltip />
                <el-table-column label="默认" width="56" align="center">
                  <template #default="scope">
                    <el-tag v-if="scope.row.isDefault" type="success" size="small">是</el-tag>
                    <span v-else>-</span>
                  </template>
                </el-table-column>
                <el-table-column label="操作" width="160" align="center">
                  <template #default="scope">
                    <el-button link type="primary" size="small" @click="editRoute(scope.row)">改名</el-button>
                    <el-button link type="primary" size="small" @click="setDefaultRoute(scope.row)">默认</el-button>
                    <el-button link type="danger" size="small" @click="deleteRoute(scope.row)">删除</el-button>
                  </template>
                </el-table-column>
              </el-table>
            </div>
            <pagination
              v-show="routeTotal > 0"
              :total="routeTotal"
              layout="total, sizes, prev, pager, next, jumper"
              :page="routePage.current"
              :limit="routePage.size"
              @pagination="handleRoutePaginationChange"
            />
          </div>
        </el-col>
        <el-col :span="14">
          <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
            <div style="font-weight: 600;">工序维护</div>
            <el-tag v-if="selectedRouteId" type="success" size="small">当前路线:{{ selectedRouteName }}</el-tag>
            <el-tag v-else type="info" size="small">请先选择工艺路线</el-tag>
          </div>
          <div class="route-toolbar">
            <el-input
              v-model="processNameDraft"
              placeholder="输入工序名称后新增"
              style="max-width: 260px;"
              clearable
              :disabled="!selectedRouteId"
            />
            <el-button type="primary" plain :disabled="!selectedRouteId" @click="createProcessItem">新增工序</el-button>
          </div>
          <div class="process-diagram">
            <div v-if="processItems.length === 0" class="process-diagram-empty">暂无工序</div>
            <div
              v-for="(step, idx) in processItems"
              :key="String(step.itemId) + '_' + idx"
              class="process-diagram-segment"
            >
              <div class="process-diagram-node">
                <div class="process-diagram-index">{{ idx + 1 }}</div>
                <div class="process-diagram-name">{{ step.processName }}</div>
              </div>
              <div v-if="idx < processItems.length - 1" class="process-diagram-arrow">→</div>
            </div>
          </div>
          <el-table ref="processTableRef" :data="processItems" border row-key="itemId" height="420px" size="small">
            <el-table-column label="序号" width="80" align="center">
              <template #default="scope">{{ scope.$index + 1 }}</template>
            </el-table-column>
            <el-table-column label="工序名称" min-width="220" prop="processName" show-overflow-tooltip />
            <el-table-column label="操作" width="300" align="center">
              <template #default="scope">
                <el-button link type="primary" size="small" @click="editProcessItem(scope.row)">编辑</el-button>
                <el-button link type="danger" size="small" @click="deleteProcessItem(scope.row)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
        </el-col>
      </el-row>
    </el-dialog>
  </div>
</template>
<script setup>
import { getCurrentInstance, ref, watch, onBeforeUnmount, nextTick } from "vue";
import { ElMessageBox } from "element-plus";
import Sortable from "sortablejs";
import {
  salesProcessFlowConfigList,
  salesProcessFlowConfigUpsert,
  salesProcessFlowConfigDelete,
  salesProcessFlowConfigSetDefault,
  salesProcessFlowConfigItemList,
  salesProcessFlowConfigItemUpsert,
  salesProcessFlowConfigItemSort,
  salesProcessFlowConfigItemDelete,
} from "@/api/salesManagement/salesProcessFlowConfig.js";
const { proxy } = getCurrentInstance();
const visible = ref(false);
let prevBodyOverflow = "";
let prevBodyOverflowY = "";
const lockBodyScroll = () => {
  // å…œåº•处理:有些场景下 Element Plus ä¸ä¼šå®Œå…¨ç¦æ­¢èƒŒæ™¯é¡µé¢æ»šåЍ
  prevBodyOverflow = document.body.style.overflow || "";
  prevBodyOverflowY = document.body.style.overflowY || "";
  document.body.style.overflow = "hidden";
  document.body.style.overflowY = "hidden";
};
const unlockBodyScroll = () => {
  document.body.style.overflow = prevBodyOverflow;
  document.body.style.overflowY = prevBodyOverflowY;
};
watch(visible, (v) => {
  if (v) lockBodyScroll();
  else unlockBodyScroll();
});
onBeforeUnmount(() => {
  unlockBodyScroll();
});
const routeKeyword = ref("");
const routeNameDraft = ref("");
const processNameDraft = ref("");
const routeList = ref([]);
const routeTotal = ref(0);
const routePage = ref({
  current: 1,
  size: 10,
});
const selectedRouteId = ref(null);
const selectedRouteName = ref("");
const processItems = ref([]);
const processTableRef = ref(null);
let processStepsSortable = null;
let isProcessingDrag = false;
const destroyProcessSortable = () => {
  if (processStepsSortable) {
    processStepsSortable.destroy();
    processStepsSortable = null;
  }
};
const initProcessSortable = () => {
  destroyProcessSortable();
  if (!processTableRef.value) return;
  const tbody = processTableRef.value?.$el?.querySelector(".el-table__body tbody")
    || processTableRef.value?.$el?.querySelector(".el-table__body-wrapper > table > tbody");
  if (!tbody) return;
  processStepsSortable = new Sortable(tbody, {
    animation: 150,
    ghostClass: "sortable-ghost",
    draggable: ".el-table__row",
    handle: ".el-table__row",
    filter: ".el-button, .el-input, .el-select",
    preventOnFilter: true,
    onEnd: async (evt) => {
      if (isProcessingDrag) return;
      const { oldIndex, newIndex } = evt;
      if (oldIndex === newIndex) return;
      if (!selectedRouteId.value) return;
      if (!processItems.value[oldIndex]) return;
      isProcessingDrag = true;
      try {
        const arr = [...processItems.value];
        const moving = arr.splice(oldIndex, 1)[0];
        arr.splice(newIndex, 0, moving);
        processItems.value = arr;
        ensureSortNo();
        if (!moving?.itemId) {
          proxy?.$modal?.msgError?.("当前工序缺少ID,无法排序");
          await fetchProcessItems(selectedRouteId.value);
          return;
        }
          // ä½¿ç”¨ä¸“用排序接口,避免 upsert é€ æˆé‡å¤æ–°å¢ž
          await salesProcessFlowConfigItemSort({
            id: moving.itemId,
            dragSort: newIndex + 1,
          });
        proxy?.$modal?.msgSuccess?.("顺序调整成功");
        await fetchProcessItems(selectedRouteId.value);
      } finally {
        isProcessingDrag = false;
      }
    },
  });
};
const normalizeRouteList = (list) => {
  if (!Array.isArray(list)) return [];
  return list.map((r) => ({
    routeId: r.routeId ?? r.id ?? null,
    processRouteName: r.processRouteName ?? r.routeName ?? r.name ?? "",
    isDefault: Boolean(r.isDefault),
  }));
};
const normalizeItemList = (list) => {
  if (!Array.isArray(list)) return [];
  return list.map((i, idx) => ({
    itemId: i.itemId ?? i.id ?? null,
    routeId: i.routeId ?? i.processRouteId ?? selectedRouteId.value,
    processName: i.processName ?? i.name ?? "",
    sortNo: i.sortNo ?? idx + 1,
  }));
};
const fetchRouteList = async () => {
  const res = await salesProcessFlowConfigList({
    current: routePage.value.current,
    size: routePage.value.size,
    processRouteName: routeKeyword.value || undefined,
  });
  const records = res?.records ?? res?.data?.records ?? [];
  const total = res?.total ?? res?.data?.total ?? 0;
  routeTotal.value = Number(total) || 0;
  routeList.value = normalizeRouteList(records);
};
const handleRouteQuery = async () => {
  routePage.value.current = 1;
  await fetchRouteList();
};
const handleRoutePaginationChange = async (obj) => {
  routePage.value.current = obj.page;
  routePage.value.size = obj.limit;
  await fetchRouteList();
};
const fetchProcessItems = async (routeId) => {
  if (!routeId) {
    processItems.value = [];
    destroyProcessSortable();
    return;
  }
  const res = await salesProcessFlowConfigItemList(routeId);
  const raw = res?.data ?? res ?? [];
  processItems.value = normalizeItemList(raw);
  ensureSortNo();
  await nextTick();
  initProcessSortable();
};
const openDialog = async () => {
  visible.value = true;
  selectedRouteId.value = null;
  selectedRouteName.value = "";
  processItems.value = [];
  routePage.value.current = 1;
  await fetchRouteList();
};
const closeDialog = () => {
  visible.value = false;
  selectedRouteId.value = null;
  selectedRouteName.value = "";
  processItems.value = [];
  routeNameDraft.value = "";
  processNameDraft.value = "";
  routeTotal.value = 0;
  destroyProcessSortable();
};
const handleRouteSelect = async (row) => {
  if (!row?.routeId) return;
  selectedRouteId.value = row.routeId;
  selectedRouteName.value = row.processRouteName;
  await fetchProcessItems(selectedRouteId.value);
};
const createRoute = async () => {
  if (!routeNameDraft.value) {
    proxy?.$modal?.msgWarning("请先输入工艺路线名称");
    return;
  }
  const payload = { processRouteName: routeNameDraft.value };
  await salesProcessFlowConfigUpsert(payload);
  proxy?.$modal?.msgSuccess("工艺路线新增成功");
  routeNameDraft.value = "";
  await handleRouteQuery();
};
const editRoute = async (row) => {
  const oldName = row?.processRouteName ?? "";
  const { value } = await ElMessageBox.prompt("请输入新的工艺路线名称", "修改工艺路线", {
    inputValue: oldName,
    confirmButtonText: "确认",
    cancelButtonText: "取消",
  });
  await salesProcessFlowConfigUpsert({
    routeId: row.routeId,
    processRouteName: value,
  });
  proxy?.$modal?.msgSuccess("工艺路线修改成功");
  await fetchRouteList();
  if (selectedRouteId.value === row.routeId) selectedRouteName.value = value;
};
const deleteRoute = async (row) => {
  await ElMessageBox.confirm("确认删除该工艺路线?", "删除", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  });
  await salesProcessFlowConfigDelete(row.routeId);
  proxy?.$modal?.msgSuccess("删除成功");
  if (selectedRouteId.value === row.routeId) {
    selectedRouteId.value = null;
    selectedRouteName.value = "";
    processItems.value = [];
  }
  // åˆ é™¤åŽè‹¥å½“前页被清空,回退一页
  if (routeList.value.length <= 1 && routePage.value.current > 1) {
    routePage.value.current = routePage.value.current - 1;
  }
  await fetchRouteList();
};
const setDefaultRoute = async (row) => {
  await salesProcessFlowConfigSetDefault(row.routeId);
  proxy?.$modal?.msgSuccess("默认工艺路线设置成功");
  await fetchRouteList();
};
const ensureSortNo = () => {
  processItems.value = processItems.value.map((i, idx) => ({ ...i, sortNo: idx + 1 }));
};
const createProcessItem = async () => {
  if (!selectedRouteId.value) {
    proxy?.$modal?.msgWarning("请先选择工艺路线");
    return;
  }
  if (!processNameDraft.value) {
    proxy?.$modal?.msgWarning("请先输入工序名称");
    return;
  }
  await salesProcessFlowConfigItemUpsert({
    routeId: selectedRouteId.value,
    processName: processNameDraft.value,
    sortNo: processItems.value.length + 1,
  });
  proxy?.$modal?.msgSuccess("工序新增成功");
  processNameDraft.value = "";
  await fetchProcessItems(selectedRouteId.value);
};
const editProcessItem = async (row) => {
  const { value } = await ElMessageBox.prompt("请输入新的工序名称", "修改工序", {
    inputValue: row.processName,
    confirmButtonText: "确认",
    cancelButtonText: "取消",
  });
  await salesProcessFlowConfigItemUpsert({
    id: row.itemId,
    routeId: selectedRouteId.value,
    processName: value,
    sortNo: row.sortNo,
  });
  proxy?.$modal?.msgSuccess("工序修改成功");
  await fetchProcessItems(selectedRouteId.value);
};
const deleteProcessItem = async (row) => {
  await ElMessageBox.confirm("确认删除该工序?", "删除", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  });
  await salesProcessFlowConfigItemDelete(row.itemId);
  proxy?.$modal?.msgSuccess("工序删除成功");
  await fetchProcessItems(selectedRouteId.value);
};
</script>
<style scoped>
.route-toolbar {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
}
.route-toolbar-left {
  flex-wrap: nowrap;
}
.route-toolbar-left :deep(.el-input) {
  width: 190px;
}
.process-route-dialog :deep(.el-dialog__body) {
  height: 760px;
  overflow: hidden;
  overflow-y: hidden;
}
.dialog-main {
  height: 100%;
}
.left-panel {
  height: 100%;
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.left-table-wrap {
  flex: 1;
  min-height: 0;
  overflow: hidden;
}
.process-diagram {
  display: flex;
  align-items: center;
  gap: 0;
  flex-wrap: nowrap;
  overflow-x: auto;
  padding: 10px 0;
}
.process-diagram-segment {
  display: flex;
  align-items: center;
}
.process-diagram-node {
  width: 160px;
  min-width: 160px;
  height: 78px;
  border: 1px solid #ebeef5;
  border-radius: 10px;
  background: #fff;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 10px 12px;
  margin-right: 10px;
  box-sizing: border-box;
}
.process-diagram-index {
  font-size: 12px;
  color: #909399;
  margin-bottom: 4px;
}
.process-diagram-name {
  font-size: 14px;
  font-weight: 600;
  color: #303133;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.process-diagram-arrow {
  font-size: 18px;
  color: #909399;
  margin-right: 14px;
  margin-left: -6px;
}
.process-diagram-empty {
  width: 100%;
  text-align: center;
  padding: 24px 0;
  color: #909399;
  border: 1px dashed #ebeef5;
  border-radius: 8px;
}
</style>
src/views/salesManagement/salesLedger/index.vue
@@ -25,13 +25,20 @@
    </div>
    <div class="table_list">
      <div class="actions">
        <div></div>
        <div>
          <OtherAmountMaintenanceButton />
          <ProcessFlowMaintenanceButton />
        </div>
        <ProcessFlowConfigSelectDialog
          v-model:visible="processFlowSelectDialogVisible"
          @confirm="handleProcessFlowSelectConfirm"
        />
        <div>
          <el-button type="primary" @click="openForm('add')">
            æ–°å¢žå°è´¦
          </el-button>
          <el-button type="primary" plain @click="openOtherAmountDialog">
            å…¶ä»–金额维护
          <el-button type="primary"  @click="handleBulkDelivery">
            å‘è´§
          </el-button>
          <el-button type="primary" plain @click="handleImport">导入</el-button>
          <el-button @click="handleOut">导出</el-button>
@@ -49,7 +56,11 @@
              <el-table-column align="center" label="序号" type="index"/>
              <el-table-column label="产品大类" prop="productCategory" />
              <el-table-column label="规格型号" prop="specificationModel" />
              <el-table-column label="单位" prop="unit" />
              <el-table-column label="厚度" prop="thickness" min-width="90">
                <template #default="scope">
                  {{ scope.row.thickness ?? "" }}
                </template>
              </el-table-column>
                            <el-table-column label="产品状态"
                                                             width="100px"
                                                             align="center">
@@ -96,7 +107,7 @@
              <el-table-column label="含税总价(元)" prop="taxInclusiveTotalPrice" :formatter="formattedNumber" />
              <el-table-column label="不含税总价(元)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" />
            <!--操作-->
              <el-table-column Width="60px" label="操作" align="center">
              <!-- <el-table-column Width="60px" label="操作" align="center">
                <template #default="scope">
                  <el-button 
                    link 
@@ -106,7 +117,7 @@
                    å‘è´§
                  </el-button>
                </template>
              </el-table-column>
              </el-table-column> -->
            </el-table>
          </template>
        </el-table-column>
@@ -123,9 +134,10 @@
        <el-table-column label="签订日期" prop="executionDate" width="120" show-overflow-tooltip />
        <el-table-column label="交付日期" prop="deliveryDate" width="120" show-overflow-tooltip />
        <el-table-column label="备注" prop="remarks" width="200" show-overflow-tooltip />
        <el-table-column fixed="right" label="操作" width="130" align="center">
        <el-table-column fixed="right" label="操作" width="200" align="center">
          <template #default="scope">
            <el-button link type="primary" @click="openForm('edit', scope.row)" :disabled="!scope.row.isEdit">编辑</el-button>
            <el-button link type="primary" @click="openProcessFlowSelect(scope.row)" :disabled="!scope.row.isEdit">工艺路线</el-button>
            <el-button link type="primary" @click="downLoadFile(scope.row)">附件</el-button>
          </template>
        </el-table-column>
@@ -229,7 +241,11 @@
                    <el-table-column align="center" label="序号" type="index" width="60" />
                    <el-table-column label="产品大类" prop="productCategory" />
                    <el-table-column label="规格型号" prop="specificationModel" />
                    <el-table-column label="单位" prop="unit" />
                    <el-table-column label="厚度" prop="thickness" min-width="90">
                        <template #default="scope">
                            {{ scope.row.thickness ?? "" }}
                        </template>
                    </el-table-column>
                    <el-table-column label="数量" prop="quantity" />
                    <el-table-column label="税率(%)" prop="taxRate" />
                    <el-table-column label="含税单价(元)" prop="taxInclusiveUnitPrice" :formatter="formattedNumber" />
@@ -247,6 +263,20 @@
                    <el-col :span="24">
                        <el-form-item label="备注:" prop="remarks">
                            <el-input v-model="form.remarks" placeholder="请输入" clearable type="textarea" :rows="2" :disabled="operationType === 'view'" />
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="24">
                        <el-form-item label="客户备注:" prop="customerRemarks">
                            <el-input
                                v-model="form.customerRemarks"
                                placeholder="请输入"
                                clearable
                                type="textarea"
                                :rows="2"
                                :disabled="operationType === 'view'"
                            />
                        </el-form-item>
                    </el-col>
                </el-row>
@@ -374,8 +404,16 @@
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                        <el-form-item label="单位:" prop="unit">
                            <el-input v-model="productForm.unit" placeholder="请输入" clearable />
                        <el-form-item label="厚度:" prop="thickness">
                            <el-input-number
                                v-model="productForm.thickness"
                                :min="0"
                                :step="0.000000000000001"
                                :precision="15"
                                style="width: 100%;"
                                placeholder="请输入"
                                clearable
                            />
                        </el-form-item>
                    </el-col>
                </el-row>
@@ -564,6 +602,13 @@
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="24">
                        <el-form-item label="楼层编号:" prop="floorCode">
                            <el-input v-model="productForm.floorCode" placeholder="请输入楼层编号" clearable type="textarea" :rows="2" :disabled="operationType === 'view'" />
                        </el-form-item>
                    </el-col>
                </el-row>
                <!-- å…¶ä»–金额(占满一行:等同于 3 åˆ—网格的整行) -->
                <el-row :gutter="30">
                    <el-col :span="12">
@@ -684,6 +729,7 @@
                </el-button>
            </template>
        </el-dialog>
        <!-- å¯¼å…¥å¼¹çª— -->
        <FormDialog
            v-model="importUpload.open"
@@ -778,7 +824,7 @@
                                    <tr>
                                        <th>产品名称</th>
                                        <th>规格型号</th>
                                        <th>单位</th>
                                        <th>厚度</th>
                                        <th>单价</th>
                                        <th>零售数量</th>
                                        <th>零售金额</th>
@@ -788,7 +834,7 @@
                                    <tr v-for="product in item.products" :key="product.id">
                                        <td>{{ product.productCategory || '' }}</td>
                                        <td>{{ product.specificationModel || '' }}</td>
                                        <td>{{ product.unit || '' }}</td>
                                        <td>{{ product.thickness ?? '' }}</td>
                                        <td>{{ product.taxInclusiveUnitPrice || '0' }}</td>
                                        <td>{{ product.quantity || '0' }}</td>
                                        <td>{{ product.taxInclusiveTotalPrice || '0' }}</td>
@@ -916,114 +962,6 @@
            </template>
        </el-dialog>
        <!-- å…¶ä»–金额维护(新增/编辑/删除) -->
        <el-dialog
            v-model="otherAmountDialogVisible"
            title="其他金额维护"
            width="80%"
            :close-on-click-modal="false"
            @close="closeOtherAmountDialog"
        >
            <el-row :gutter="20">
                <el-col :span="14">
                    <el-table
                        :data="otherAmountRecords"
                        border
                        v-loading="otherAmountLoading"
                        height="55vh"
                    >
                        <el-table-column label="编码" prop="code" min-width="120" show-overflow-tooltip />
                        <el-table-column label="项目" prop="processName" min-width="180" show-overflow-tooltip />
                        <el-table-column label="数量" prop="quantity" min-width="110" :formatter="formattedNumber" />
                        <el-table-column label="单价(元)" prop="unitPrice" min-width="130" :formatter="formattedNumber" />
                        <el-table-column label="金额(元)" prop="amount" min-width="160" :formatter="formattedNumber" />
                        <el-table-column fixed="right" label="操作" width="160" align="center">
                            <template #default="scope">
                                <el-button link type="primary" size="small" @click="handleOtherEdit(scope.row)">编辑</el-button>
                                <el-button link type="danger" size="small" @click="handleOtherDelete(scope.row)">删除</el-button>
                            </template>
                        </el-table-column>
                    </el-table>
                    <pagination
                        v-show="otherAmountTotal > 0"
                        :total="otherAmountTotal"
                        layout="total, sizes, prev, pager, next, jumper"
                        :page="otherAmountPage.current"
                        :limit="otherAmountPage.size"
                        @pagination="otherAmountPaginationChange"
                    />
                </el-col>
                <el-col :span="10">
                    <div style="padding: 8px 0;">
                        <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 10px;">
                            <div style="font-weight:600;">
                                {{ otherAmountOperationType === 'add' ? '新增其他金额' : '编辑其他金额' }}
                            </div>
                            <el-button
                                type="primary"
                                plain
                                size="small"
                                @click="handleOtherAdd"
                                :disabled="otherAmountOperationType === 'add'"
                            >
                                æ–°å¢ž
                            </el-button>
                        </div>
                        <el-form
                            :model="otherAmountForm"
                            label-width="120px"
                            label-position="top"
                            :rules="otherAmountRules"
                            ref="otherAmountFormRef"
                        >
                            <el-form-item label="编码">
                                <el-input v-model="otherAmountForm.code" placeholder="请输入编码(可选)" clearable />
                            </el-form-item>
                            <el-form-item label="项目" prop="processName">
                                <el-input v-model="otherAmountForm.processName" placeholder="请输入项目名称" clearable />
                            </el-form-item>
                            <el-form-item label="数量" prop="quantity">
                                <el-input-number
                                    v-model="otherAmountForm.quantity"
                                    :min="0"
                                    :precision="2"
                                    style="width:100%"
                                    placeholder="请输入数量"
                                    clearable
                                    @change="recalcOtherAmount"
                                />
                            </el-form-item>
                            <el-form-item label="单价(元)" prop="unitPrice">
                                <el-input-number
                                    v-model="otherAmountForm.unitPrice"
                                    :min="0"
                                    :precision="2"
                                    style="width:100%"
                                    placeholder="请输入单价"
                                    clearable
                                    @change="recalcOtherAmount"
                                />
                            </el-form-item>
                            <el-form-item label="金额(元)">
                                <el-input v-model="otherAmountForm.amount" disabled />
                            </el-form-item>
                            <div style="display:flex; justify-content:flex-end; gap: 10px; margin-top: 8px;">
                                <el-button @click="closeOtherAmountDialog">取消</el-button>
                                <el-button type="primary" @click="submitOtherAmountForm">保存</el-button>
                            </div>
                        </el-form>
                    </div>
                </el-col>
            </el-row>
        </el-dialog>
    </div>
</template>
@@ -1038,6 +976,9 @@
import { userListNoPage } from "@/api/system/user.js";
import FileListDialog from '@/components/Dialog/FileListDialog.vue';
import FormDialog from '@/components/Dialog/FormDialog.vue';
import OtherAmountMaintenanceButton from "./components/OtherAmountMaintenanceButton.vue";
import ProcessFlowMaintenanceButton from "./components/ProcessFlowMaintenanceButton.vue";
import ProcessFlowConfigSelectDialog from "./components/ProcessFlowConfigSelectDialog.vue";
import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
import {
    ledgerListPage,
@@ -1051,14 +992,12 @@
    delLedgerFile,
    getProductInventory,
    salesLedgerProductProcessList,
    salesLedgerProductProcessAdd,
    salesLedgerProductProcessUpdate,
    salesLedgerProductProcessDelete,
} from "@/api/salesManagement/salesLedger.js";
import { modelList, productTreeList } from "@/api/basicData/product.js";
import useFormData from "@/hooks/useFormData.js";
import dayjs from "dayjs";
import { getCurrentDate } from "@/utils/index.js";
import { salesLedgerProductSetProcessFlowConfig } from "@/api/salesManagement/salesProcessFlowConfig.js";
const userStore = useUserStore();
const { proxy } = getCurrentInstance();
@@ -1077,6 +1016,10 @@
});
const total = ref(0);
const fileList = ref([]);
// å·¥è‰ºè·¯çº¿é…ç½®é€‰æ‹©å¼¹çª—(绑定到台账产品)
const processFlowSelectDialogVisible = ref(false);
const processFlowSelectLedgerRow = ref(null);
// ç”¨æˆ·ä¿¡æ¯è¡¨å•弹框数据
const operationType = ref("");
@@ -1119,7 +1062,7 @@
    productForm: {
        productCategory: "",
        specificationModel: "",
        unit: "",
        thickness:null,
        quantity: "",
        taxInclusiveUnitPrice: "",
        taxRate: "",
@@ -1138,6 +1081,8 @@
        processRequirement: "", // åŠ å·¥è¦æ±‚
        remark: "", // å¤‡æ³¨
        salesProductProcessList: [], // å…¶ä»–金额:[{id, processName, quantity}]
        processFlowConfigId: null, // å·¥è‰ºæµç¨‹é…ç½®ç»‘定
        floorCode: "", // æ¥¼å±‚编号
    },
    productRules: {
        productCategory: [{ required: true, message: "请选择", trigger: "change" }],
@@ -1145,7 +1090,7 @@
        specificationModel: [
            { required: true, message: "请选择", trigger: "change" },
        ],
        unit: [{ required: true, message: "请输入", trigger: "blur" }],
        thickness: [{ required: true, message: "请输入", trigger: "blur" }],
        quantity: [{ required: true, message: "请输入", trigger: "blur" }],
        taxInclusiveUnitPrice: [
            { required: true, message: "请输入", trigger: "blur" },
@@ -1191,7 +1136,7 @@
// å‘货相关
const deliveryFormVisible = ref(false);
const currentDeliveryRow = ref(null);
const currentDeliveryRows = ref([]);
const deliveryFormData = reactive({
  deliveryForm: {
    type: "货车", // è´§è½¦, å¿«é€’
@@ -1203,169 +1148,6 @@
  },
});
const { deliveryForm, deliveryRules } = toRefs(deliveryFormData);
// å…¶ä»–金额维护(工序/流程金额维护)
const otherAmountDialogVisible = ref(false);
const otherAmountLoading = ref(false);
const otherAmountRecords = ref([]);
const otherAmountTotal = ref(0);
const otherAmountPage = reactive({
    current: 1,
    size: 10,
});
const otherAmountOperationType = ref("add"); // add/edit
const otherAmountFormRef = ref(null);
const otherAmountForm = reactive({
    id: null,
    code: "", // å‰ç«¯å­—段名:code;后端接口列表返回 remark,此处进行映射
    processName: "",
    quantity: 0,
    unitPrice: 0,
    amount: "0.00",
});
const otherAmountRules = reactive({
    processName: [{ required: true, message: "请输入项目名称", trigger: "change" }],
    quantity: [{ required: true, message: "请输入数量", trigger: "blur" }],
    unitPrice: [{ required: true, message: "请输入单价", trigger: "blur" }],
});
const recalcOtherAmount = () => {
    const quantity = Number(otherAmountForm.quantity ?? 0) || 0;
    const unitPrice = Number(otherAmountForm.unitPrice ?? 0) || 0;
    otherAmountForm.amount = (quantity * unitPrice).toFixed(2);
};
const resetOtherAmountForm = (type = "add") => {
    otherAmountOperationType.value = type;
    otherAmountForm.id = null;
    otherAmountForm.code = "";
    otherAmountForm.processName = "";
    otherAmountForm.quantity = 0;
    otherAmountForm.unitPrice = 0;
    otherAmountForm.amount = "0.00";
};
const openOtherAmountDialog = () => {
    otherAmountDialogVisible.value = true;
    resetOtherAmountForm("add");
    // æ‰“开弹框时刷新数据,避免长时间停留导致数据过期
    otherAmountPage.current = otherAmountPage.current || 1;
    fetchOtherAmountList();
};
const closeOtherAmountDialog = () => {
    otherAmountDialogVisible.value = false;
    resetOtherAmountForm("add");
};
const fetchOtherAmountList = async () => {
    otherAmountLoading.value = true;
    try {
        const params = {
            current: otherAmountPage.current,
            size: otherAmountPage.size,
        };
        const res = await salesLedgerProductProcessList(params);
        // å…¼å®¹ä¸åŒæŽ¥å£å“åº”结构:可能是 res.records / res.total æˆ– res.data.records / res.data.total
        const records = res?.records ?? res?.data?.records ?? [];
        const total = res?.total ?? res?.data?.total ?? 0;
        otherAmountRecords.value = records.map((item) => {
            const quantity = Number(item.quantity ?? 0) || 0;
            const unitPrice = Number(item.unitPrice ?? 0) || 0;
            const amount = Number(item.amount ?? quantity * unitPrice) || 0;
            return {
                id: item.id,
                code: item.code ?? item.remark ?? "",
                processName: item.processName ?? "",
                quantity,
                unitPrice,
                amount: amount.toFixed(2),
            };
        });
        otherAmountTotal.value = total;
    } finally {
        otherAmountLoading.value = false;
    }
};
const otherAmountPaginationChange = (obj) => {
    otherAmountPage.current = obj.page;
    otherAmountPage.size = obj.limit;
    fetchOtherAmountList();
};
const handleOtherAdd = () => {
    resetOtherAmountForm("add");
};
const handleOtherEdit = (row) => {
    if (!row) return;
    otherAmountOperationType.value = "edit";
    otherAmountForm.id = row.id ?? null;
    otherAmountForm.code = row.code ?? "";
    otherAmountForm.processName = row.processName ?? "";
    otherAmountForm.quantity = Number(row.quantity ?? 0) || 0;
    otherAmountForm.unitPrice = Number(row.unitPrice ?? 0) || 0;
    recalcOtherAmount();
};
const submitOtherAmountForm = () => {
    otherAmountFormRef.value?.validate((valid) => {
        if (!valid) return;
        const payload = {
            processName: otherAmountForm.processName,
            quantity: Number(otherAmountForm.quantity) || 0,
            unitPrice: Number(otherAmountForm.unitPrice) || 0,
            amount: Number(otherAmountForm.amount) || 0,
            // åˆ—表返回字段是 remark,这里按“code=remark”做映射
            remark: otherAmountForm.code,
            // å…¼å®¹åŽç«¯å¯èƒ½ç›´æŽ¥ä½¿ç”¨ code å­—段
            code: otherAmountForm.code,
        };
        if (otherAmountOperationType.value === "edit") {
            payload.id = otherAmountForm.id;
            salesLedgerProductProcessUpdate(payload).then(() => {
                proxy.$modal.msgSuccess("保存成功");
                fetchOtherAmountList();
                resetOtherAmountForm("add");
            });
        } else {
            salesLedgerProductProcessAdd(payload).then(() => {
                proxy.$modal.msgSuccess("保存成功");
                fetchOtherAmountList();
                resetOtherAmountForm("add");
            });
        }
    });
};
const handleOtherDelete = (row) => {
    if (!row?.id) return;
    ElMessageBox.confirm("确认删除该记录?", "删除", {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
    })
        .then(() => {
            return salesLedgerProductProcessDelete(row.id).then(() => {
                proxy.$modal.msgSuccess("删除成功");
                fetchOtherAmountList();
                if (otherAmountOperationType.value === "edit" && otherAmountForm.id === row.id) {
                    resetOtherAmountForm("add");
                }
            });
        })
        .catch(() => {
            proxy.$modal.msg("已取消");
        });
};
// äº§å“å¼¹æ¡†ï¼šå…¶ä»–金额多选下拉(基于“其他金额维护”查询接口)
const otherAmountSelectOptions = ref([]); // [{id, processName}]
@@ -1636,6 +1418,59 @@
            tableLoading.value = false;
        });
};
// æ‰“开“工艺路线配置”选择弹窗(必须显式选择)
const openProcessFlowSelect = (ledgerRow) => {
    if (!ledgerRow) return;
    if (!ledgerRow.isEdit) return;
    processFlowSelectLedgerRow.value = ledgerRow;
    processFlowSelectDialogVisible.value = true;
};
// å°†é…ç½®åº”用到台账下的所有未发货产品
const handleProcessFlowSelectConfirm = async (configId) => {
    const ledgerRow = processFlowSelectLedgerRow.value;
    if (!ledgerRow?.id) return;
    const finalConfigId = configId ?? null;
    if (!finalConfigId) return;
    proxy?.$modal?.loading?.("正在应用工艺路线配置,请稍候...");
    try {
        const res = await productList({ salesLedgerId: ledgerRow.id, type: 1 });
        const products = res?.data ?? res?.records ?? res ?? [];
        if (!Array.isArray(products) || products.length === 0) {
            proxy?.$modal?.msgWarning?.("该台账下没有产品数据");
            return;
        }
        for (const product of products) {
            // å·²å‘è´§/审核通过不可编辑,跳过
            if (isProductShipped(product)) continue;
            await salesLedgerProductSetProcessFlowConfig({
                salesLedgerProductId: product.id,
                processFlowConfigId: finalConfigId,
            });
        }
        proxy?.$modal?.msgSuccess?.("工艺路线配置应用成功");
        // è‹¥å½“前行已展开,刷新其子产品列表
        if (expandedRowKeys.value.includes(ledgerRow.id)) {
            const childRes = await productList({ salesLedgerId: ledgerRow.id, type: 1 });
            const children = childRes?.data ?? childRes?.records ?? childRes ?? [];
            const index = tableData.value.findIndex((item) => item.id === ledgerRow.id);
            if (index > -1) {
                tableData.value[index].children = children;
            }
        }
    } catch (e) {
        proxy?.$modal?.msgError?.("应用失败,请稍后重试");
    } finally {
        proxy?.$modal?.closeLoading?.();
    }
};
// èŽ·å–äº§å“å¤§ç±»tree数据
const getProductOptions = () => {
    // è¿”回 Promise,便于在编辑产品时等待加载完成
@@ -1658,10 +1493,8 @@
    const index = modelOptions.value.findIndex((item) => item.id === value);
    if (index !== -1) {
        productForm.value.specificationModel = modelOptions.value[index].model;
        productForm.value.unit = modelOptions.value[index].unit;
    } else {
        productForm.value.specificationModel = null;
        productForm.value.unit = null;
    }
};
const findNodeById = (nodes, productId) => {
@@ -1784,11 +1617,14 @@
        form.value.entryDate = getCurrentDate();
        // ç­¾è®¢æ—¥æœŸé»˜è®¤ä¸ºå½“天
        form.value.executionDate = getCurrentDate();
        form.value.customerRemarks = "";
    } else {
        currentId.value = row.id;
        getSalesLedgerWithProducts({ id: row.id, type: 1 }).then((res) => {
            form.value = { ...res };
            form.value.entryPerson = Number(res.entryPerson);
            // å­—段名兼容:后端可能返回 customer_remarks
            form.value.customerRemarks = res?.customerRemarks ?? res?.customer_remarks ?? "";
            productData.value = form.value.productData;
            fileList.value = form.value.salesLedgerFiles;
        });
@@ -1886,7 +1722,7 @@
            // å°è´¦å­—段
            productCategory: p.product || p.productName || "",
            specificationModel: p.specification || "",
            unit: p.unit || "",
            thickness: p.thickness,
            quantity: quantity,
            taxRate: taxRate,
            taxInclusiveUnitPrice: unitPrice.toFixed(2),
@@ -1901,6 +1737,7 @@
            settlePieceArea: 0,
            settleTotalArea: 0,
            processRequirement: "",
            floorCode: "",
            remark: "",
            salesProductProcessList: [],
        };
@@ -2007,10 +1844,17 @@
        productForm.value.processRequirement =
            row?.processRequirement ?? row?.process_requirement ?? "";
        productForm.value.remark = row?.remark ?? row?.remarks ?? "";
        productForm.value.floorCode = row?.floorCode ?? row?.floor_code ?? "";
        // å·¥è‰ºæµç¨‹é…ç½®ç»‘定字段(后续由后端确认字段名)
        productForm.value.processFlowConfigId =
            row?.processFlowConfigId ?? row?.process_flow_config_id ?? null;
        // å‘¨é•¿å›žæ˜¾ï¼ˆå¦‚后端返回;最终仍以公式计算为准)
        productForm.value.perimeter =
            row?.perimeter ?? row?.heavyBoxPerimeter ?? row?.heavyboxPerimeter ?? 0;
        // åŽç«¯ç›´æŽ¥è¿”回 thickness
        productForm.value.thickness = row?.thickness;
        productForm.value.salesProductProcessList = normalizeOtherAmountsFromRow(row);
        productIndex.value = index;
@@ -2055,6 +1899,11 @@
const submitProduct = () => {
    proxy.$refs["productFormRef"].validate((valid) => {
        if (valid) {
            // åŽšåº¦ä¿ç•™ 15 ä½å°æ•°ï¼Œé¿å…ç”±äºŽæµ®ç‚¹è®¡ç®—/输入导致精度偏差
            if (productForm.value.thickness !== null && productForm.value.thickness !== undefined) {
                productForm.value.thickness = Number(Number(productForm.value.thickness).toFixed(15));
            }
            // é¢ç§¯/总计字段在提交前兜底计算一次
            recalcAreaTotals();
            // å…¶ä»–金额只提交 {id, processName, quantity}(后端字段:salesProductProcessList)
@@ -2471,7 +2320,7 @@
                <tr>
                  <th>产品名称</th>
                  <th>规格型号</th>
                  <th>单位</th>
                  <th>厚度</th>
                  <th>单价</th>
                  <th>零售数量</th>
                  <th>零售金额</th>
@@ -2483,7 +2332,7 @@
                    <tr>
                      <td>${product.productCategory || ''}</td>
                      <td>${product.specificationModel || ''}</td>
                      <td>${product.unit || ''}</td>
                      <td>${product.thickness ?? ''}</td>
                      <td>${product.taxInclusiveUnitPrice || '0'}</td>
                      <td>${product.quantity || '0'}</td>
                      <td>${product.taxInclusiveTotalPrice || '0'}</td>
@@ -2926,6 +2775,72 @@
    return statusStr === '待发货' || statusStr === '审核拒绝';
};
const handleBulkDelivery = async () => {
    if (selectedRows.value.length === 0) {
        proxy.$modal.msgWarning("请选择数据");
        return;
    }
    const customerNames = selectedRows.value.map((r) => String(r.customerName || "").trim());
    const uniqueCustomers = Array.from(new Set(customerNames));
    // å®¢æˆ·åç§°ä¸ä¸€è‡´ä¸å…è®¸å‘è´§
    if (uniqueCustomers.length > 1) {
        proxy.$modal.msgWarning("客户名称不一致,不允许发货");
        return;
    }
    // å¤šæ¡ä¸”客户一致:二次确认
    if (selectedRows.value.length > 1) {
        try {
            await ElMessageBox.confirm("是否确认合并发货?", "合并发货", {
                confirmButtonText: "确认",
                cancelButtonText: "取消",
                type: "warning",
            });
        } catch (e) {
            proxy.$modal.msg("已取消");
            return;
        }
    }
    proxy.$modal.loading("正在获取产品数据,请稍候...");
    try {
        const targets = [];
        for (const ledger of selectedRows.value) {
            let products = [];
            try {
                const res = await productList({ salesLedgerId: ledger.id, type: 1 });
                products = res?.data || [];
            } catch {
                products = [];
            }
            for (const product of products) {
                if (!canShip(product)) continue;
                targets.push({
                    ...product,
                    salesLedgerId: product.salesLedgerId || ledger.id,
                });
            }
        }
        if (targets.length === 0) {
            proxy.$modal.msgWarning("没有可发货的数据");
            return;
        }
        currentDeliveryRows.value = targets;
        deliveryForm.value = { type: "货车" };
        // é‡ç½®å®¡æ‰¹äººèŠ‚ç‚¹ï¼ˆé»˜è®¤ä¸€ä¸ªç©ºèŠ‚ç‚¹ï¼‰
        approverNodes.value = [{ id: 1, userId: null }];
        nextApproverId = 2;
        deliveryFormVisible.value = true;
    } finally {
        proxy.$modal.closeLoading();
    }
};
/**
 * ä¸‹è½½æ–‡ä»¶
 *
@@ -2949,7 +2864,7 @@
        return;
    }
    
    currentDeliveryRow.value = row;
    currentDeliveryRows.value = [row];
  deliveryForm.value = {
    type: "货车",
  };
@@ -2972,13 +2887,28 @@
      const approveUserIds = approverNodes.value.map(node => node.userId).join(",");
      // ä¿å­˜å½“前展开的行ID,以便发货后重新加载子表格数据
      const currentExpandedKeys = [...expandedRowKeys.value];
      const salesLedgerId = currentDeliveryRow.value.salesLedgerId;
      addShippingInfo({
        salesLedgerId: salesLedgerId,
        salesLedgerProductId: currentDeliveryRow.value.id,
      const targets = currentDeliveryRows.value || [];
      if (targets.length === 0) {
        proxy.$modal.msgWarning("未选择可发货的数据");
        return;
      }
      // ä¾æ¬¡å‘货(避免并发下库存扣减/状态更新互相影响)
      const run = async () => {
        for (const item of targets) {
          const salesLedgerId = item.salesLedgerId;
          if (!salesLedgerId) continue;
          await addShippingInfo({
            salesLedgerId,
            salesLedgerProductId: item.id,
        type: deliveryForm.value.type,
                approveUserIds,
      })
          });
        }
      };
      run()
        .then(() => {
          proxy.$modal.msgSuccess("发货成功");
          closeDeliveryDia();
@@ -2986,8 +2916,7 @@
          getList().then(() => {
            // å¦‚果之前有展开的行,重新加载这些行的子表格数据
            if (currentExpandedKeys.length > 0) {
              // ä½¿ç”¨ Promise.all å¹¶è¡ŒåŠ è½½æ‰€æœ‰å±•å¼€è¡Œçš„å­è¡¨æ ¼æ•°æ®
              const loadPromises = currentExpandedKeys.map(ledgerId => {
              const loadPromises = currentExpandedKeys.map((ledgerId) => {
                return productList({ salesLedgerId: ledgerId, type: 1 }).then((res) => {
                  const index = tableData.value.findIndex((item) => item.id === ledgerId);
                  if (index > -1) {
@@ -2996,12 +2925,14 @@
                });
              });
              Promise.all(loadPromises).then(() => {
                // æ¢å¤å±•开状态
                expandedRowKeys.value = currentExpandedKeys;
              });
            }
          });
        })
        .catch(() => {
          proxy.$modal.msgError("发货失败,请稍后重试");
        });
    }
  });
};
@@ -3010,7 +2941,7 @@
const closeDeliveryDia = () => {
  proxy.resetForm("deliveryFormRef");
  deliveryFormVisible.value = false;
  currentDeliveryRow.value = null;
  currentDeliveryRows.value = [];
};
const currentFactoryName = ref("");
const getCurrentFactoryName = async () => {