gaoluyang
13 小时以前 1a67b0ce86c05fdc0a9f6146d0e8cda1c4ad6dd8
src/views/salesManagement/salesLedger/index.vue
@@ -26,6 +26,7 @@
          <el-button type="primary" @click="openForm('add')">
            新增台账
          </el-button>
          <el-button @click="handleImport">导入</el-button>
          <el-button @click="handleOut">导出</el-button>
          <el-button type="danger" plain @click="handleDelete">删除</el-button>
          <el-button type="primary" plain @click="handlePrint">打印</el-button>
@@ -75,8 +76,8 @@
          </template>
        </el-table-column>
        <el-table-column label="发货日期" prop="shippingDate" width="120" show-overflow-tooltip />
        <el-table-column label="录入日期" prop="entryDate" width="120" show-overflow-tooltip />
        <el-table-column label="签订日期" prop="executionDate" width="120" show-overflow-tooltip />
        <el-table-column label="录入日期" prop="entryDate" width="120" show-overflow-tooltip />
        <el-table-column fixed="right" label="操作" min-width="200" align="center">
          <template #default="scope">
            <el-button link type="primary" size="small" @click="openForm('edit', scope.row)">编辑</el-button>
@@ -98,6 +99,11 @@
      @confirm="submitForm"
      @cancel="closeDia">
      <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
        <el-row v-if="operationType !== 'view'">
          <el-col :span="24" style="display:flex; justify-content:flex-end; gap:10px; margin-bottom: 6px;">
            <el-button type="primary" plain @click="openQuotationDialog">从审批通过的报价单导入</el-button>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="销售合同号:" prop="salesContractNo">
@@ -203,6 +209,62 @@
        </el-row>
      </el-form>
    </FormDialog>
    <!-- 从报价单导入(仅审批通过) -->
    <el-dialog
      v-model="quotationDialogVisible"
      title="选择审批通过的销售报价单"
      width="80%"
      :close-on-click-modal="false"
    >
      <div style="margin-bottom: 12px; display:flex; gap: 12px; align-items:center;">
        <el-input
          v-model="quotationSearchForm.quotationNo"
          placeholder="请输入报价单号"
          clearable
          style="max-width: 260px;"
          @change="fetchQuotationList"
        />
        <el-input
          v-model="quotationSearchForm.customer"
          placeholder="请输入客户名称"
          clearable
          style="max-width: 260px;"
          @change="fetchQuotationList"
        />
        <el-button type="primary" @click="fetchQuotationList">搜索</el-button>
        <el-button @click="resetQuotationSearch">重置</el-button>
      </div>
      <el-table
        :data="quotationList"
        border
        stripe
        v-loading="quotationLoading"
        height="420px"
      >
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column prop="quotationNo" label="报价单号" width="180" show-overflow-tooltip />
        <el-table-column prop="customer" label="客户名称" min-width="220" show-overflow-tooltip />
        <el-table-column prop="salesperson" label="业务员" width="120" show-overflow-tooltip />
        <el-table-column prop="quotationDate" label="报价日期" width="140" />
        <el-table-column prop="status" label="审批状态" width="120" align="center" />
        <el-table-column prop="totalAmount" label="报价金额(元)" width="160" align="right">
          <template #default="scope">
            {{ Number(scope.row.totalAmount ?? 0).toFixed(2) }}
          </template>
        </el-table-column>
        <el-table-column fixed="right" label="操作" width="120" align="center">
          <template #default="scope">
            <el-button type="primary" link @click="applyQuotation(scope.row)">选择</el-button>
          </template>
        </el-table-column>
      </el-table>
      <template #footer>
        <el-button @click="quotationDialogVisible = false">关闭</el-button>
      </template>
    </el-dialog>
    <FormDialog 
      v-model="productFormVisible" 
      :title="productOperationType === 'add' ? '新增产品' : '编辑产品'" 
@@ -448,19 +510,58 @@
         </template>
      </el-dialog>
    <FileListDialog ref="fileListRef" v-model="fileListDialogVisible" />
    <!-- 导入对话框 -->
    <el-dialog
      :title="importUpload.title"
      v-model="importUpload.open"
      width="400px"
      append-to-body
    >
      <el-upload
        ref="importUploadRef"
        :limit="1"
        accept=".xlsx, .xls"
        :headers="importUpload.headers"
        :action="importUpload.url"
        :disabled="importUpload.isUploading"
        :before-upload="importUpload.beforeUpload"
        :on-progress="importUpload.onProgress"
        :on-success="importUpload.onSuccess"
        :on-error="importUpload.onError"
        :on-change="importUpload.onChange"
        :auto-upload="false"
        drag
      >
        <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
        <template #tip>
          <div class="el-upload__tip text-center">
            <span>仅允许导入xls、xlsx格式文件。</span>
          </div>
        </template>
      </el-upload>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitImportFile" :loading="importUpload.isUploading">确 定</el-button>
          <el-button @click="importUpload.open = false">取 消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { getToken } from "@/utils/auth";
import pagination from "@/components/PIMTable/Pagination.vue";
import {onMounted, ref} from "vue";
import {onMounted, ref, getCurrentInstance} from "vue";
import { addShippingInfo } from "@/api/salesManagement/deliveryLedger.js";
import { ElMessageBox } from "element-plus";
import { ElMessageBox, ElMessage } from "element-plus";
import { UploadFilled } from "@element-plus/icons-vue";
import useUserStore from "@/store/modules/user";
import { userListNoPage } from "@/api/system/user.js";
import FileListDialog from '@/components/Dialog/FileListDialog.vue';
import FormDialog from '@/components/Dialog/FormDialog.vue';
import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
import {
   ledgerListPage,
   productList,
@@ -576,6 +677,16 @@
const printPreviewVisible = ref(false);
const printData = ref([]);
// 报价单导入相关
const quotationDialogVisible = ref(false);
const quotationLoading = ref(false);
const quotationList = ref([]);
const quotationSearchForm = reactive({
  quotationNo: "",
  customer: "",
});
const selectedQuotation = ref(null);
// 发货相关
const deliveryFormVisible = ref(false);
const currentDeliveryRow = ref(null);
@@ -594,6 +705,54 @@
  },
});
const { deliveryForm, deliveryRules } = toRefs(deliveryFormData);
// 导入相关
const importUploadRef = ref(null);
const importUpload = reactive({
  title: "导入销售台账",
  open: false,
  url: import.meta.env.VITE_APP_BASE_API + "/sales/ledger/import",
  headers: { Authorization: "Bearer " + getToken() },
  isUploading: false,
  beforeUpload: (file) => {
    const isExcel = file.name.endsWith('.xlsx') || file.name.endsWith('.xls');
    const isLt10M = file.size / 1024 / 1024 < 10;
    if (!isExcel) {
      proxy.$modal.msgError("上传文件只能是 xlsx/xls 格式!");
      return false;
    }
    if (!isLt10M) {
      proxy.$modal.msgError("上传文件大小不能超过 10MB!");
      return false;
    }
    return true;
  },
  onChange: (file, fileList) => {
    console.log('文件状态改变', file, fileList);
  },
  onProgress: (event, file, fileList) => {
    console.log('上传中...', event.percent);
  },
  onSuccess: (response, file, fileList) => {
    console.log('上传成功', response, file, fileList);
    importUpload.isUploading = false;
    if (response.code === 200) {
      proxy.$modal.msgSuccess("导入成功");
      importUpload.open = false;
      if (importUploadRef.value) {
        importUploadRef.value.clearFiles();
      }
      getList();
    } else {
      proxy.$modal.msgError(response.msg || "导入失败");
    }
  },
  onError: (error, file, fileList) => {
    console.error('上传失败', error, file, fileList);
    importUpload.isUploading = false;
    proxy.$modal.msgError("导入失败,请重试");
  },
});
const changeDaterange = (value) => {
  if (value) {
@@ -644,8 +803,10 @@
};
// 获取产品大类tree数据
const getProductOptions = () => {
  productTreeList().then((res) => {
  // 返回 Promise,便于在编辑产品时等待加载完成
  return productTreeList().then((res) => {
    productOptions.value = convertIdToValue(res);
    return productOptions.value;
  });
};
const formattedNumber = (row, column, cellValue) => {
@@ -695,6 +856,19 @@
    return newItem;
  });
}
// 根据名称反查产品大类 id,便于仅存名称时的反显
function findNodeIdByLabel(nodes, label) {
  if (!label) return null;
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i];
    if (node.label === label) return node.value;
    if (node.children && node.children.length > 0) {
      const found = findNodeIdByLabel(node.children, label);
      if (found !== null && found !== undefined) return found;
    }
  }
  return null;
}
// 表格选择数据
const handleSelectionChange = (selection) => {
@@ -746,6 +920,7 @@
  operationType.value = type;
  form.value = {};
  productData.value = [];
  selectedQuotation.value = null;
  let userLists = await userListNoPage();
  userList.value = userLists.data;
  customerList().then((res) => {
@@ -774,6 +949,87 @@
  // });
  form.value.entryDate = getCurrentDate(); // 设置默认录入日期为当前日期
  dialogFormVisible.value = true;
};
// 打开报价单选择弹窗(仅审批通过)
const openQuotationDialog = async () => {
  if (operationType.value === "view") return;
  quotationDialogVisible.value = true;
  // 先确保客户列表已加载,便于后续回填 customerId
  if (!customerOption.value || customerOption.value.length === 0) {
    try {
      const res = await customerList();
      customerOption.value = res;
    } catch (e) {
      // ignore,允许用户后续手动选择客户
    }
  }
  await fetchQuotationList();
};
const fetchQuotationList = async () => {
  quotationLoading.value = true;
  try {
    const params = {
      // 兼容后端分页字段:这里沿用报价页面已有可用的字段命名
      currentPage: 1,
      pageSize: 100,
      ...quotationSearchForm,
      status: "通过",
    };
    const res = await getQuotationList(params);
    quotationList.value = res?.data?.records || [];
  } finally {
    quotationLoading.value = false;
  }
};
const resetQuotationSearch = async () => {
  quotationSearchForm.quotationNo = "";
  quotationSearchForm.customer = "";
  await fetchQuotationList();
};
// 选中报价单后回填到台账表单
const applyQuotation = (row) => {
  if (!row) return;
  selectedQuotation.value = row;
  // 业务员
  form.value.salesman = row.salesperson || "";
  // 客户名称 -> customerId
  const customer = (customerOption.value || []).find((c) => c.customerName === row.customer);
  if (customer?.id) {
    form.value.customerId = customer.id;
  } else {
    // 如果找不到,保留原值(允许用户手动选择/不打断已有输入)
    form.value.customerId = form.value.customerId || "";
  }
  // 产品信息映射:报价 products -> 台账 productData
  const products = Array.isArray(row.products) ? row.products : [];
  productData.value = products.map((p) => {
    const quantity = Number(p.quantity ?? 0) || 0;
    const unitPrice = Number(p.unitPrice ?? 0) || 0;
    const taxRate = "13"; // 默认 13%,便于直接提交(如需可在产品中自行修改)
    const taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2);
    const taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(taxInclusiveTotalPrice, taxRate);
    return {
      // 台账字段
      productCategory: p.product || p.productName || "",
      specificationModel: p.specification || "",
      unit: p.unit || "",
      quantity: quantity,
      taxRate: taxRate,
      taxInclusiveUnitPrice: unitPrice.toFixed(2),
      taxInclusiveTotalPrice: taxInclusiveTotalPrice,
      taxExclusiveTotalPrice: taxExclusiveTotalPrice,
      invoiceType: "增普票",
    };
  });
  quotationDialogVisible.value = false;
};
function changs(val) {
  console.log(val);
@@ -847,16 +1103,36 @@
const productIndex = ref(0);
// 打开产品弹框
const openProductForm = (type, row,index) => {
const openProductForm = async (type, row, index) => {
  productOperationType.value = type;
  productForm.value = {};
  proxy.resetForm("productFormRef");
  if (type === "edit") {
    productForm.value = { ...row };
    productIndex.value = index;
    // 编辑时根据产品大类名称反查 tree 节点 id,并加载规格型号列表
    try {
      const options = productOptions.value && productOptions.value.length > 0
        ? productOptions.value
        : await getProductOptions();
      const categoryId = findNodeIdByLabel(options, productForm.value.productCategory);
      if (categoryId) {
        const models = await modelList({ id: categoryId });
        modelOptions.value = models || [];
        // 根据当前规格型号名称反查并设置 productModelId,便于下拉框显示已选值
        const currentModel = (modelOptions.value || []).find(
          (m) => m.model === productForm.value.specificationModel
        );
        if (currentModel) {
          productForm.value.productModelId = currentModel.id;
        }
      }
    } catch (e) {
      // 加载失败时保持可编辑,不中断弹窗
      console.error("加载产品规格型号失败", e);
    }
  }
  productFormVisible.value = true;
  getProductOptions();
};
// 提交产品表单
const submitProduct = () => {
@@ -932,6 +1208,21 @@
  proxy.resetForm("productFormRef");
  productFormVisible.value = false;
};
// 导入
const handleImport = () => {
  importUpload.title = "导入销售台账";
  importUpload.open = true;
  if (importUploadRef.value) {
    importUploadRef.value.clearFiles();
  }
};
// 提交导入文件
const submitImportFile = () => {
  importUpload.isUploading = true;
  proxy.$refs["importUploadRef"].submit();
};
// 导出
const handleOut = () => {
  ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {