新疆-新聚
1.销售台账中的销售合同按框架合同处理
2.设备台账需按车间/区域汇总设备,车间/位置可自行新增,且支持子父级递归
3.计量器具台账支持通过 Excel 文件批量新增数据
4.设备保养定时任务新增“年度保养”任务频率
已修改10个文件
1120 ■■■■ 文件已修改
src/api/equipmentManagement/ledger.js 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/salesLedger.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/formDia.vue 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/ledger/Form.vue 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/ledger/index.vue 460 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/index.vue 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/formDia.vue 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/index.vue 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementReport/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 284 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/ledger.js
@@ -36,9 +36,10 @@
  });
};
export const getDeviceLedger = () => {
export const getDeviceLedger = (params) => {
  return request({
    url: "/device/ledger/getDeviceLedger",
    method: "get",
    params,
  });
};
src/api/salesManagement/salesLedger.js
@@ -117,3 +117,30 @@
        params: query,
    });
}
// 创建框架订单
export function createFrameworkOrder(query) {
  return request({
    url: "/sales/ledger/createFrameworkOrder",
    method: "post",
    data: query,
  });
}
// 查询框架订单列表
export function listFrameworkOrders(query) {
  return request({
    url: "/sales/ledger/listFrameworkOrders",
    method: "get",
    params: query,
  });
}
// 查询框架合同累计发货数量
export function frameworkContractShippedQuantity(query) {
  return request({
    url: "/sales/ledger/frameworkContractShippedQuantity",
    method: "get",
    params: query,
  });
}
src/views/equipmentManagement/inspectionManagement/components/formDia.vue
@@ -5,15 +5,11 @@
      <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
        <el-row>
          <el-col :span="12">
            <el-form-item label="设备名称" prop="taskId">
              <el-select v-model="form.taskId" @change="setDeviceModel">
                <el-option
                  v-for="(item, index) in deviceOptions"
                  :key="index"
                  :label="item.deviceName"
                  :value="item.id"
                ></el-option>
              </el-select>
            <el-form-item label="设备名称" prop="taskName">
              <div style="display: flex; gap: 8px; width: 100%">
                <el-input v-model="form.taskName" placeholder="请选择设备" readonly style="flex: 1" />
                <el-button @click="openDeviceDialog">选择设备</el-button>
              </div>
            </el-form-item>
          </el-col>
          <el-col :span="12">
@@ -111,6 +107,50 @@
        </div>
      </template>
    </el-dialog>
    <!-- 选择巡检设备弹窗 -->
    <el-dialog v-model="deviceDialogVisible" title="选择巡检设备" width="760px" draggable append-to-body>
      <div class="device-select-wrap">
        <div class="device-select-tree">
          <div class="all-device-node"
               :class="{ active: deviceAreaId === null }"
               @click="selectAllDevice">
            全部设备
          </div>
          <el-tree
            ref="deviceAreaTreeRef"
            v-loading="deviceAreaLoading"
            :data="areaTree"
            node-key="id"
            :props="{ label: 'label', children: 'children' }"
            default-expand-all
            :expand-on-click-node="false"
            highlight-current
            :current-node-key="deviceAreaId"
            @node-click="onDeviceAreaClick"
          />
        </div>
        <div class="device-select-list">
          <el-table
            :data="deviceList"
            v-loading="deviceListLoading"
            border
            height="420"
            style="cursor: pointer"
            @row-click="selectDevice"
          >
            <el-table-column prop="deviceName" label="设备名称" min-width="140" />
            <el-table-column prop="deviceModel" label="规格型号" min-width="120" />
            <el-table-column prop="areaName" label="所属区域" min-width="120" />
            <el-table-column label="操作" width="80" align="center">
              <template #default="scope">
                <el-button link type="primary" @click.stop="selectDevice(scope.row)">选择</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </div>
    </el-dialog>
  </div>
</template>
@@ -120,6 +160,7 @@
import {addOrEditTimingTask} from "@/api/inspectionManagement/index.js";
import {userListNoPageByTenantId} from "@/api/system/user.js";
import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
import { getDeviceAreaTree } from "@/api/equipmentManagement/area";
const { proxy } = getCurrentInstance()
const emit = defineEmits()
@@ -127,6 +168,16 @@
const dialogVisitable = ref(false);
const operationType = ref('add');
const deviceOptions = ref([]);
// 设备选择弹窗相关
const deviceDialogVisible = ref(false);
const areaTree = ref([]);
const deviceAreaId = ref(null);
const deviceAreaLoading = ref(false);
const deviceAreaTreeRef = ref();
const deviceList = ref([]);
const deviceListLoading = ref(false);
const data = reactive({
  form: {
    taskId: undefined,
@@ -142,7 +193,7 @@
    time: ''
  },
    rules: {
        taskId: [{ required: true, message: "请选择设备", trigger: "change" },],
        taskName: [{ required: true, message: "请选择设备", trigger: "change" },],
        inspector: [{ required: true, message: "请输入巡检人", trigger: "blur" },],
        dateStr: [{ required: true, message: "请选择登记时间", trigger: "change" }],
        frequencyType: [{ required: true, message: "请选择任务频率", trigger: "change" }],
@@ -207,7 +258,7 @@
const loadDeviceName = async () => {
  const { data } = await getDeviceLedger();
  deviceOptions.value = data;
  deviceOptions.value = data || [];
};
const setDeviceModel = (id) => {
@@ -216,6 +267,57 @@
    form.value.taskName = option.deviceName;
  }
}
// 加载区域树
const loadDeviceAreaTree = () => {
  deviceAreaLoading.value = true;
  getDeviceAreaTree()
    .then((res) => {
      areaTree.value = res.data || [];
    })
    .finally(() => {
      deviceAreaLoading.value = false;
    });
};
// 加载设备列表
const loadDevices = (areaId) => {
  deviceListLoading.value = true;
  getDeviceLedger(areaId ? { areaId } : undefined)
    .then((res) => {
      deviceList.value = res.data || [];
    })
    .finally(() => {
      deviceListLoading.value = false;
    });
};
// 打开设备选择弹窗
const openDeviceDialog = () => {
  deviceDialogVisible.value = true;
  deviceAreaId.value = null;
  loadDeviceAreaTree();
  loadDevices();
};
// 选择全部设备
const selectAllDevice = () => {
  deviceAreaId.value = null;
  loadDevices();
};
// 点击区域节点
const onDeviceAreaClick = (data) => {
  deviceAreaId.value = data.id;
  loadDevices(data.id);
};
// 选中设备回填
const selectDevice = (row) => {
  form.value.taskId = row.id;
  form.value.taskName = row.deviceName;
  deviceDialogVisible.value = false;
};
// 打开弹框
const openDialog = async (type, row) => {
@@ -230,7 +332,7 @@
    userList.value = res.data;
  });
  
  // 加载设备列表
  // 加载设备列表(编辑回显用)
  await loadDeviceName();
  
  if (type === 'edit' && row) {
@@ -239,7 +341,9 @@
    
    // 如果有设备ID,自动设置设备信息
    if (form.value.taskId) {
      if (!form.value.taskName) {
      setDeviceModel(form.value.taskId);
      }
    }
  }
}
@@ -302,5 +406,34 @@
</script>
<style scoped>
.device-select-wrap {
  display: flex;
}
.device-select-tree {
  width: 260px;
  flex-shrink: 0;
  padding-right: 12px;
  border-right: 1px solid #ebeef5;
  max-height: 460px;
  overflow-y: auto;
}
.device-select-list {
  flex: 1;
  min-width: 0;
  padding-left: 12px;
}
.all-device-node {
  padding: 8px 12px;
  cursor: pointer;
  font-size: 14px;
  border-radius: 4px;
  margin-bottom: 8px;
}
.all-device-node:hover {
  background: #f5f7fa;
}
.all-device-node.active {
  background: #ecf5ff;
  color: #409eff;
}
</style>
src/views/equipmentManagement/ledger/Form.vue
@@ -38,6 +38,20 @@
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="所属区域" prop="areaId">
          <el-tree-select
            v-model="form.areaId"
            :data="areaTree"
            node-key="id"
            :props="{ label: 'label', children: 'children' }"
            check-strictly
            clearable
            placeholder="请选择所属区域"
            style="width: 100%"
          />
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="供应商" prop="supplierName">
          <el-input v-model="form.supplierName" placeholder="请输入供应商" />
        </el-form-item>
@@ -174,6 +188,7 @@
import useFormData from "@/hooks/useFormData";
// import useUserStore from "@/store/modules/user";
import { getLedgerById } from "@/api/equipmentManagement/ledger";
import { getDeviceAreaTree } from "@/api/equipmentManagement/area";
import dayjs from "dayjs";
import {
  calculateTaxIncludeTotalPrice,
@@ -190,6 +205,7 @@
  name: "设备台账表单",
});
const formRef = ref(null);
const areaTree = ref([]);
const operationType = ref('');
// 设备类型固定选项
const deviceTypeOptions = ref([
@@ -228,6 +244,7 @@
  deviceModel: undefined, // 规格型号
  deviceBrand: undefined, // 设备品牌
  type: undefined, // 设备类型
  areaId: undefined, // 所属区域
  supplierName: undefined, // 供应商
  storageLocation: undefined, // 存放位置
  isDepr: 2, // 是否启用折旧 1-是 2-否
@@ -255,7 +272,15 @@
  }
});
const loadAreaTree = () => {
  getDeviceAreaTree().then((res) => {
    areaTree.value = res.data || [];
  });
};
loadAreaTree();
const loadForm = async (id) => {
  loadAreaTree();
    if (id) {
        operationType.value = 'edit'
    }
@@ -266,6 +291,7 @@
    form.deviceBrand = data.deviceBrand;
    form.type = data.type;
    form.supplierName = data.supplierName;
    form.areaId = data.areaId;
    form.storageLocation = data.storageLocation;
    form.isDepr = data.isDepr;
    form.annualDepreciationAmount = data.annualDepreciationAmount;
src/views/equipmentManagement/ledger/index.vue
@@ -1,5 +1,71 @@
<template>
  <div class="app-container">
    <div class="area-view">
      <!-- 左侧区域树 -->
      <div class="area-left">
        <div class="area-left-header">
          <el-button type="primary" @click="openAreaDialog('add')" icon="Plus">
            新增区域
          </el-button>
        </div>
        <div
          class="all-device-node"
          :class="{ active: currentAreaId === null }"
          @click="handleAllDeviceClick"
        >
          全部设备
        </div>
        <el-tree
          ref="areaTreeRef"
          v-loading="areaTreeLoading"
          :data="areaTree"
          node-key="id"
          :props="{ label: 'label', children: 'children' }"
          :expand-on-click-node="false"
          default-expand-all
          draggable
          highlight-current
          :current-node-key="currentAreaId"
          @node-click="handleAreaNodeClick"
          @node-drop="handleAreaNodeDrop"
        >
          <template #default="{ node, data }">
            <div class="area-tree-node">
              <span class="area-node-label">{{ data.label }}</span>
              <span class="area-node-count">{{ data.deviceCount ?? 0 }}</span>
              <span class="area-node-actions">
                <el-button
                  link
                  type="primary"
                  size="small"
                  @click.stop="openAreaDialog('addChild', data)"
                >
                  子级
                </el-button>
                <el-button
                  link
                  type="primary"
                  size="small"
                  @click.stop="openAreaDialog('edit', data)"
                >
                  编辑
                </el-button>
                <el-button
                  link
                  type="danger"
                  size="small"
                  @click.stop="handleAreaDelete(node, data)"
                >
                  删除
                </el-button>
              </span>
            </div>
          </template>
        </el-tree>
      </div>
      <!-- 右侧设备列表 -->
      <div class="area-right">
    <el-form :model="filters" :inline="true">
      <el-form-item label="设备名称">
        <el-input
@@ -29,12 +95,19 @@
        />
      </el-form-item>
      <el-form-item label="录入日期:">
        <el-date-picker v-model="filters.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
                        placeholder="请选择" clearable @change="changeDaterange" />
            <el-date-picker
              v-model="filters.entryDate"
              value-format="YYYY-MM-DD"
              format="YYYY-MM-DD"
              type="daterange"
              placeholder="请选择"
              clearable
              @change="changeDaterange"
            />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getTableData">搜索</el-button>
        <el-button @click="resetFilters">重置</el-button>
            <el-button @click="handleReset">重置</el-button>
      </el-form-item>
    </el-form>
    <div class="table_list">
@@ -69,11 +142,14 @@
      >
      </PIMTable>
    </div>
      </div>
    </div>
    <Modal ref="modalRef" @success="getTableData"></Modal>
    <el-dialog v-model="qrDialogVisible" title="二维码" width="300px" draggable>
      <div style="text-align:center;">
        <img :src="qrCodeUrl" alt="二维码" style="width:200px;height:200px;" />
        <div style="margin:10px 0;">
      <div style="text-align: center">
        <img :src="qrCodeUrl" alt="二维码" style="width: 200px; height: 200px" />
        <div style="margin: 10px 0">
          <el-button type="primary" @click="downloadQRCode">下载二维码图片</el-button>
        </div>
      </div>
@@ -98,7 +174,14 @@
        <template #tip>
          <div class="el-upload__tip text-center">
            <span>仅允许导入xls、xlsx格式文件。</span>
            <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline; margin-left: 5px;" @click="importTemplate">下载模板</el-link>
            <el-link
              type="primary"
              :underline="false"
              style="font-size: 12px; vertical-align: baseline; margin-left: 5px"
              @click="importTemplate"
            >
              下载模板
            </el-link>
          </div>
        </template>
      </el-upload>
@@ -118,6 +201,7 @@
        <el-descriptions-item label="设备品牌">{{ detailData.deviceBrand }}</el-descriptions-item>
        <el-descriptions-item label="设备类型">{{ detailData.type }}</el-descriptions-item>
        <el-descriptions-item label="供应商">{{ detailData.supplierName }}</el-descriptions-item>
        <el-descriptions-item label="所属区域">{{ detailData.areaName }}</el-descriptions-item>
        <el-descriptions-item label="存放位置">{{ detailData.storageLocation }}</el-descriptions-item>
        <el-descriptions-item label="单位">{{ detailData.unit }}</el-descriptions-item>
        <el-descriptions-item label="数量">{{ detailData.number }}</el-descriptions-item>
@@ -128,14 +212,19 @@
        <el-descriptions-item label="税率(%)">{{ detailData.taxRate }}</el-descriptions-item>
        <el-descriptions-item label="不含税总价">{{ detailData.unTaxIncludingPriceTotal }}</el-descriptions-item>
        <el-descriptions-item label="录入日期">{{ detailData.createTime }}</el-descriptions-item>
        <el-descriptions-item label="预计运行时间">{{ detailData.planRuntimeTime ? dayjs(detailData.planRuntimeTime).format('YYYY-MM-DD') : '' }}</el-descriptions-item>
        <el-descriptions-item label="预计运行时间">{{
          detailData.planRuntimeTime ? dayjs(detailData.planRuntimeTime).format('YYYY-MM-DD') : ''
        }}</el-descriptions-item>
        <el-descriptions-item label="设备图片" :span="2">
          <div v-if="detailData.storageBlobVOs && detailData.storageBlobVOs.length > 0" style="display: flex; gap: 10px; flex-wrap: wrap;">
          <div
            v-if="detailData.storageBlobVOs && detailData.storageBlobVOs.length > 0"
            style="display: flex; gap: 10px; flex-wrap: wrap"
          >
            <el-image 
              v-for="(file, index) in detailData.storageBlobVOs" 
              :key="index" 
              :src="file.previewURL || file.url" 
              :preview-src-list="detailData.storageBlobVOs.map(u => u.previewURL || u.url)"
              :preview-src-list="detailData.storageBlobVOs.map((u) => u.previewURL || u.url)"
              :initial-index="index"
              style="width: 100px; height: 100px" 
              fit="cover" 
@@ -148,14 +237,57 @@
        <el-button @click="detailDialogVisible = false">关闭</el-button>
      </template>
    </el-dialog>
    <!-- 区域新增/编辑弹窗 -->
    <el-dialog
      v-model="areaDialogVisible"
      :title="areaDialogTitle"
      width="420px"
      draggable
      @closed="areaFormRef && areaFormRef.resetFields()"
    >
      <el-form :model="areaForm" :rules="areaRules" ref="areaFormRef" label-width="90px">
        <el-form-item label="上级区域" prop="parentId">
          <el-tree-select
            v-model="areaForm.parentId"
            :data="areaTree"
            node-key="id"
            :props="{ label: 'label', children: 'children' }"
            check-strictly
            clearable
            placeholder="不选则为根区域"
            style="width: 100%"
          />
        </el-form-item>
        <el-form-item label="区域名称" prop="name">
          <el-input v-model="areaForm.name" placeholder="请输入区域/车间/位置名称" clearable />
        </el-form-item>
        <el-form-item label="排序" prop="sort">
          <el-input-number v-model="areaForm.sort" :min="0" style="width: 100%" placeholder="可选" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button type="primary" @click="submitAreaForm">确定</el-button>
        <el-button @click="areaDialogVisible = false">取消</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { usePaginationApi } from "@/hooks/usePaginationApi";
// import { Search } from "@element-plus/icons-vue";
import { getLedgerPage, delLedger, getLedgerById } from "@/api/equipmentManagement/ledger";
import { onMounted, getCurrentInstance, ref, reactive } from "vue";
import {
  getLedgerPage,
  delLedger,
  getLedgerById,
} from "@/api/equipmentManagement/ledger";
import {
  getDeviceAreaTree,
  addDeviceArea,
  editDeviceArea,
  delDeviceArea,
} from "@/api/equipmentManagement/area";
import { onMounted, getCurrentInstance, ref, reactive, computed } from "vue";
import Modal from "./Modal.vue";
import { ElMessageBox, ElMessage } from "element-plus";
import { UploadFilled } from "@element-plus/icons-vue";
@@ -178,20 +310,38 @@
const detailDialogVisible = ref(false);
const detailData = ref({});
// 区域相关
const areaTree = ref([]);
const areaTreeLoading = ref(false);
const areaTreeRef = ref();
const currentAreaId = ref(null);
const areaDialogVisible = ref(false);
const areaDialogType = ref("add");
const areaFormRef = ref();
const areaForm = reactive({
  id: undefined,
  name: "",
  parentId: null,
  sort: 0,
});
const areaRules = {
  name: [{ required: true, message: "请输入区域名称", trigger: "blur" }],
};
const areaDialogTitle = computed(() => {
  if (areaDialogType.value === "edit") return "编辑区域";
  if (areaDialogType.value === "addChild") return "新增子区域";
  return "新增区域";
});
// 导入相关
const uploadRef = ref(null)
const uploadRef = ref(null);
const upload = reactive({
  // 是否显示弹出层
  open: false,
  // 弹出层标题
  title: "",
  // 是否禁用上传
  isUploading: false,
  // 设置上传的请求头部
  headers: { Authorization: "Bearer " + getToken() },
  // 上传的地址
  url: import.meta.env.VITE_APP_BASE_API + "/device/ledger/import"
})
  url: import.meta.env.VITE_APP_BASE_API + "/device/ledger/import",
});
const {
  filters,
@@ -209,6 +359,7 @@
    supplierName: undefined,
    entryDateStart: undefined,
    entryDateEnd: undefined,
    areaId: undefined,
  },
  [
    {
@@ -226,6 +377,10 @@
    {
      label: "设备类型",
      prop: "type",
    },
    {
      label: "所属区域",
      prop: "areaName",
    },
    {
      label: "供应商",
@@ -247,10 +402,9 @@
      label: "录入日期",
      prop: "createTime",
      formatData: (v) => {
        if (!v) return '';
        // 如果包含时分秒,只取日期部分
        if (v.includes(' ')) {
          return v.split(' ')[0];
        if (!v) return "";
        if (v.includes(" ")) {
          return v.split(" ")[0];
        }
        return v;
      },
@@ -259,7 +413,7 @@
            dataType: "action",
            label: "操作",
            align: "center",
            fixed: 'right',
      fixed: "right",
            width: 180,
            operation: [
        {
@@ -271,19 +425,144 @@
                {
                    name: "编辑",
                    clickFun: (row) => {
                        edit(row.id)
            edit(row.id);
                    },
                },
                {
                    name: "二维码",
                    clickFun: (row) => {
                        showQRCode(row)
            showQRCode(row);
                    },
                },
            ],
        },
  ]
);
// 加载区域树
const getAreaTree = () => {
  areaTreeLoading.value = true;
  getDeviceAreaTree()
    .then((res) => {
      areaTree.value = res.data || [];
    })
    .finally(() => {
      areaTreeLoading.value = false;
    });
};
// 重置搜索与区域筛选
const handleReset = () => {
  currentAreaId.value = null;
  resetFilters();
};
// 点击"全部设备"
const handleAllDeviceClick = () => {
  currentAreaId.value = null;
  filters.areaId = undefined;
  getTableData();
};
// 点击区域节点
const handleAreaNodeClick = (data) => {
  currentAreaId.value = data.id;
  filters.areaId = data.id;
  getTableData();
};
// 拖拽调整父子关系
const handleAreaNodeDrop = (draggingNode, dropNode, dropType) => {
  const node = draggingNode.data;
  let parentId = null;
  if (dropNode && dropType === "inner") {
    parentId = dropNode.data.id;
  } else if (dropNode) {
    parentId = dropNode.data.parentId ?? null;
  } else {
    parentId = null;
  }
  if (parentId === node.id) {
    proxy.$modal.msgWarning("不能将区域挂载到自身");
    getAreaTree();
    return;
  }
  editDeviceArea({
    id: node.id,
    name: node.label,
    parentId,
    sort: node.sort,
  })
    .then(() => {
      proxy.$modal.msgSuccess("调整成功");
      getAreaTree();
    })
    .catch(() => {
      getAreaTree();
    });
};
// 打开区域弹窗
const openAreaDialog = (type, data) => {
  areaDialogType.value = type;
  areaForm.id = undefined;
  areaForm.name = "";
  areaForm.parentId = null;
  areaForm.sort = 0;
  if (type === "addChild" && data) {
    areaForm.parentId = data.id;
  } else if (type === "edit" && data) {
    areaForm.id = data.id;
    areaForm.name = data.label;
    areaForm.parentId = data.parentId ?? null;
    areaForm.sort = data.sort ?? 0;
  }
  areaDialogVisible.value = true;
};
// 提交区域表单
const submitAreaForm = () => {
  if (!areaFormRef.value) return;
  areaFormRef.value.validate((valid) => {
    if (!valid) return;
    const payload = {
      name: areaForm.name,
      parentId: areaForm.parentId ?? null,
      sort: areaForm.sort ?? 0,
    };
    const api =
      areaDialogType.value === "edit"
        ? editDeviceArea({ ...payload, id: areaForm.id })
        : addDeviceArea(payload);
    api
      .then(() => {
        proxy.$modal.msgSuccess("操作成功");
        areaDialogVisible.value = false;
        getAreaTree();
      })
      .catch(() => {});
  });
};
// 删除区域
const handleAreaDelete = (node, data) => {
  ElMessageBox.confirm("确认删除该区域吗?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => delDeviceArea(data.id))
    .then(() => {
      proxy.$modal.msgSuccess("删除成功");
      if (currentAreaId.value === data.id) {
        currentAreaId.value = null;
        filters.areaId = undefined;
      }
      getAreaTree();
      getTableData();
    })
    .catch(() => {});
};
// 多选后做什么
const handleSelectionChange = (selectionList) => {
@@ -351,36 +630,33 @@
};
const showQRCode = async (row) => {
  // 直接使用URL,不要用JSON.stringify包装
  const qrContent = proxy.javaApi + '/device-info?deviceId=' + row.id;
  const qrContent = proxy.javaApi + "/device-info?deviceId=" + row.id;
  const qrDataUrl = await QRCode.toDataURL(qrContent, { width: 200, margin: 2 });
  
  // 创建canvas合成带名称的二维码图片
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d");
  const qrSize = 200;
  const textHeight = 30;
  const padding = 10;
  canvas.width = qrSize + padding * 2;
  canvas.height = qrSize + textHeight + padding * 2;
  
  // 填充白色背景
  ctx.fillStyle = '#ffffff';
  ctx.fillStyle = "#ffffff";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  
  // 绘制二维码
  const qrImg = new Image();
  qrImg.src = qrDataUrl;
  await new Promise((resolve) => { qrImg.onload = resolve; });
  await new Promise((resolve) => {
    qrImg.onload = resolve;
  });
  ctx.drawImage(qrImg, padding, padding, qrSize, qrSize);
  
  // 绘制设备名称
  ctx.fillStyle = '#333333';
  ctx.font = 'bold 14px Arial';
  ctx.textAlign = 'center';
  ctx.fillText(row.deviceName || '', canvas.width / 2, qrSize + padding + 20);
  ctx.fillStyle = "#333333";
  ctx.font = "bold 14px Arial";
  ctx.textAlign = "center";
  ctx.fillText(row.deviceName || "", canvas.width / 2, qrSize + padding + 20);
  
  qrCodeUrl.value = canvas.toDataURL('image/png');
  qrCodeUrl.value = canvas.toDataURL("image/png");
  qrRowData.value = row;
  qrDialogVisible.value = true;
};
@@ -392,42 +668,100 @@
  a.click();
};
// 导入按钮操作
const handleImport = () => {
  upload.title = "设备台账导入"
  upload.open = true
}
  upload.title = "设备台账导入";
  upload.open = true;
};
// 下载模板操作
const importTemplate = () => {
  proxy.download("/device/ledger/downloadTemplate", {}, `设备台账导入模板_${new Date().getTime()}.xlsx`)
}
  proxy.download(
    "/device/ledger/downloadTemplate",
    {},
    `设备台账导入模板_${new Date().getTime()}.xlsx`
  );
};
// 文件上传中处理
const handleFileUploadProgress = (event, file, fileList) => {
  upload.isUploading = true
}
  upload.isUploading = true;
};
// 文件上传成功处理
const handleFileSuccess = (response, file, fileList) => {
  upload.open = false
  upload.isUploading = false
  proxy.$refs["uploadRef"].handleRemove(file)
  proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true })
  getTableData()
}
  upload.open = false;
  upload.isUploading = false;
  proxy.$refs["uploadRef"].handleRemove(file);
  proxy.$alert(
    "<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" +
      response.msg +
      "</div>",
    "导入结果",
    { dangerouslyUseHTMLString: true }
  );
  getTableData();
};
// 提交上传文件
const submitFileForm = () => {
  proxy.$refs["uploadRef"].submit()
}
  proxy.$refs["uploadRef"].submit();
};
onMounted(() => {
  getAreaTree();
  getTableData();
});
</script>
<style lang="scss" scoped>
.area-view {
  display: flex;
}
.area-left {
  width: 320px;
  flex-shrink: 0;
  padding: 16px;
  background: #ffffff;
  margin-right: 16px;
}
.area-left-header {
  margin-bottom: 10px;
}
.all-device-node {
  padding: 8px 12px;
  cursor: pointer;
  font-size: 14px;
  border-radius: 4px;
  margin-bottom: 8px;
}
.all-device-node:hover {
  background: #f5f7fa;
}
.all-device-node.active {
  background: #ecf5ff;
  color: #409eff;
}
.area-tree-node {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 14px;
  padding-right: 4px;
}
.area-node-count {
  color: #909399;
  font-size: 12px;
  margin-left: 6px;
}
.area-node-actions {
  display: none;
}
.area-tree-node:hover .area-node-actions {
  display: inline-flex;
}
.area-right {
  flex: 1;
  min-width: 0;
  padding: 16px;
  background: #ffffff;
}
.table_list {
  margin-top: unset;
}
src/views/equipmentManagement/measurementEquipment/index.vue
@@ -37,6 +37,10 @@
      <div>
        <el-button type="primary"
                   @click="openForm('add')">新增计量器具</el-button>
        <el-button type="primary"
                   plain
                   @click="handleImport">批量导入</el-button>
        <el-button @click="handleDownloadTemplate">下载模板</el-button>
        <el-button type="danger"
                   plain
                   @click="handleDelete">删除</el-button>
@@ -61,6 +65,17 @@
                     @close="handleQuery"></calibration-dia>
    <files-dia ref="filesDia"></files-dia>
    <rowClickDataForm ref="rowClickData"></rowClickDataForm>
    <ImportDialog ref="importDialogRef"
                  v-model="importDialogVisible"
                  title="批量导入计量器具"
                  :action="importAction"
                  :headers="importHeaders"
                  :auto-upload="false"
                  :on-success="handleImportSuccess"
                  :on-error="handleImportError"
                  @confirm="handleImportConfirm"
                  @download-template="handleDownloadTemplate"
                  @close="handleImportClose" />
  </div>
</template>
@@ -76,6 +91,8 @@
  import FormDia from "@/views/equipmentManagement/measurementEquipment/components/formDia.vue";
  import { ElMessageBox } from "element-plus";
  import useUserStore from "@/store/modules/user.js";
  import { getToken } from "@/utils/auth";
  import ImportDialog from "@/components/Dialog/ImportDialog.vue";
  import CalibrationDia from "@/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue";
  import {
    measuringInstrumentDelete,
@@ -336,6 +353,54 @@
        proxy.$modal.msg("已取消");
      });
  };
  // 批量导入
  const importDialogRef = ref();
  const importDialogVisible = ref(false);
  const importAction =
    import.meta.env.VITE_APP_BASE_API + "/measuringInstrumentLedger/import";
  const importHeaders = ref({
    Authorization: `Bearer ${getToken()}`,
  });
  const handleImport = () => {
    importDialogVisible.value = true;
  };
  const handleImportSuccess = response => {
    if (response.code === 200) {
      proxy.$modal.msgSuccess(response.msg || "导入成功");
      importDialogVisible.value = false;
      if (importDialogRef.value) {
        importDialogRef.value.clearFiles();
      }
      getList();
    } else {
      proxy.$modal.msgError(response.msg || "导入失败");
    }
  };
  const handleImportError = error => {
    proxy.$modal.msgError("导入失败,请检查文件格式是否正确");
  };
  const handleImportConfirm = () => {
    if (importDialogRef.value) {
      importDialogRef.value.submit();
    }
  };
  const handleDownloadTemplate = () => {
    proxy.download(
      "/measuringInstrumentLedger/downloadTemplate",
      {},
      "计量器具台账导入模板.xlsx"
    );
  };
  const handleImportClose = () => {
    importDialogVisible.value = false;
  };
  onMounted(() => {
    getList();
  });
src/views/equipmentManagement/upkeep/Form/formDia.vue
@@ -95,6 +95,7 @@
                            <el-option label="每周" value="WEEKLY"/>
                            <el-option label="每月" value="MONTHLY"/>
                            <el-option label="季度" value="QUARTERLY"/>
                            <el-option label="年度保养" value="YEARLY"/>
                        </el-select>
                    </el-form-item>
                </el-col>
@@ -143,6 +144,20 @@
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12" v-if="form.frequencyType === 'YEARLY'">
                    <el-form-item label="执行日期" prop="frequencyDetail">
                        <div style="display: flex; gap: 4px; width: 100%">
                            <el-select v-model="form.yearMonth" placeholder="月" clearable style="flex: 1">
                                <el-option v-for="m in 12" :key="m" :label="m + '月'" :value="m" />
                            </el-select>
                            <el-select v-model="form.yearDay" placeholder="日" clearable style="flex: 1">
                                <el-option v-for="d in 31" :key="d" :label="d + '日'" :value="d" />
                            </el-select>
                            <el-time-picker v-model="form.yearTime" placeholder="时间" format="HH:mm" value-format="HH:mm" style="flex: 1.2" />
                        </div>
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row>
                <el-col :span="12">
@@ -182,6 +197,9 @@
        frequencyDetail: '',
        week: '',
        time: '',
        yearMonth: undefined,
        yearDay: undefined,
        yearTime: '',
        deviceModel: undefined, // 规格型号
        registrationDate: '',
        maintenancePerson: '' // 保养人
@@ -238,6 +256,13 @@
        if (form.value.taskId) {
            setDeviceModel(form.value.taskId);
        }
        // YEARLY 频率回显:解析 月份,日期,时间
        if (form.value.frequencyType === 'YEARLY' && form.value.frequencyDetail) {
            const parts = String(form.value.frequencyDetail).split(',');
            form.value.yearMonth = Number(parts[0]);
            form.value.yearDay = Number(parts[1]);
            form.value.yearTime = parts[2] || '';
        }
    } else if (type === 'add') {
        // 新增时设置登记日期为当天
        form.value.registrationDate = getCurrentDate();
@@ -269,6 +294,9 @@
        frequencyDetail: '',
        week: '',
        time: '',
        yearMonth: undefined,
        yearDay: undefined,
        yearTime: '',
        deviceModel: undefined,
        registrationDate: '',
        maintenancePerson: ''
@@ -301,6 +329,25 @@
                    payload.frequencyDetail = frequencyDetail
                }
                
                if (payload.frequencyType === 'YEARLY') {
                    const month = Number(payload.yearMonth);
                    const day = Number(payload.yearDay);
                    const time = payload.yearTime;
                    if (!month || month < 1 || month > 12) {
                        proxy.$modal.msgError('月份必须在1-12之间');
                        return;
                    }
                    if (!day || day < 1 || day > 31) {
                        proxy.$modal.msgError('日期必须在1-31之间');
                        return;
                    }
                    if (!time || !/^\d{2}:\d{2}$/.test(time)) {
                        proxy.$modal.msgError('时间格式必须为 HH:mm');
                        return;
                    }
                    payload.frequencyDetail = month + ',' + day + ',' + time;
                }
                // 录入日期:直接使用表单里的 registrationDate 字段
                // 一些默认状态字段
                if (payload.status === undefined || payload.status === null || payload.status === '') {
src/views/equipmentManagement/upkeep/index.vue
@@ -63,6 +63,9 @@
            }"
                    @selection-change="handleScheduledSelectionChange"
                    @pagination="changeScheduledPage">
            <template #frequencyDetailRef="{ row }">
              {{ formatFrequencyDetail(row) }}
            </template>
            <template #statusRef="{ row }">
              <el-tag v-if="row.status === 1"
                      type="success">启用</el-tag>
@@ -316,31 +319,15 @@
          WEEKLY: "每周",
          MONTHLY: "每月",
          QUARTERLY: "季度",
          YEARLY: "年度保养",
        }[cell] || ""),
    },
    {
      prop: "frequencyDetail",
      label: "开始日期与时间",
      minWidth: 150,
      // 同样改用 formatData,PIMTable 内部会把单元格值传进来
      formatData: cell => {
        if (typeof cell !== "string") return "";
        let val = cell;
        const replacements = {
          MON: "周一",
          TUE: "周二",
          WED: "周三",
          THU: "周四",
          FRI: "周五",
          SAT: "周六",
          SUN: "周日",
        };
        // 使用正则一次性替换所有匹配项
        return val.replace(
          /MON|TUE|WED|THU|FRI|SAT|SUN/g,
          match => replacements[match]
        );
      },
      dataType: "slot",
      slot: "frequencyDetailRef",
    },
    { prop: "maintenancePerson", label: "保养人", minWidth: 100 },
    { prop: "registrant", label: "登记人", minWidth: 100 },
@@ -362,6 +349,28 @@
  ]);
  // 保养记录表格列配置(原设备保养表格列)
  const formatFrequencyDetail = (row) => {
    if (!row) return "";
    const val = String(row.frequencyDetail || "");
    if (row.frequencyType === "YEARLY") {
      const parts = val.split(",");
      if (parts.length === 3) {
        return `每年${parts[0]}月${parts[1]}日 ${parts[2]}`;
      }
      return val;
    }
    const replacements = {
      MON: "周一",
      TUE: "周二",
      WED: "周三",
      THU: "周四",
      FRI: "周五",
      SAT: "周六",
      SUN: "周日",
    };
    return val.replace(/MON|TUE|WED|THU|FRI|SAT|SUN/g, (m) => replacements[m]);
  };
  const columns = ref([
    {
      label: "设备名称",
src/views/procurementManagement/procurementReport/index.vue
@@ -139,7 +139,7 @@
    }
  },
  {
    label: '退款储量',
    label: '采购金额',
    prop: 'purchaseAmount',
    formatData: (val) => {
      return val ? `¥${parseFloat(val).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : '¥0.00'
src/views/salesManagement/salesLedger/index.vue
@@ -177,7 +177,7 @@
                  <el-button
                    link
                    type="primary"
                    :disabled="!canShip(scope.row)"
                    :disabled="!canShip(scope.row) || props.row.contractType === 2"
                    @click="openDeliveryForm(scope.row)"
                  >
                    发货
@@ -261,7 +261,7 @@
          width="200"
          show-overflow-tooltip
        />
        <el-table-column fixed="right" label="操作" width="220" align="center">
        <el-table-column fixed="right" label="操作" width="320" align="center">
          <template #default="scope">
            <el-button link type="primary" @click="openForm('view', scope.row)"
              >详情
@@ -276,6 +276,20 @@
                !canEditLedger(scope.row)
              "
              >编辑
            </el-button>
            <el-button
              v-if="scope.row.contractType === 2"
              link
              type="success"
              @click="openFrameworkOrderDialog(scope.row)"
              >创建框架订单
            </el-button>
            <el-button
              v-if="scope.row.contractType === 2"
              link
              type="primary"
              @click="openFrameworkOrderList(scope.row)"
              >框架订单
            </el-button>
            <el-button link type="primary" @click="openFileDialog(scope.row)"
              >附件
@@ -320,6 +334,21 @@
            <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="contractType">
              <el-select
                v-model="form.contractType"
                placeholder="请选择"
                clearable
                :disabled="operationType === 'view' || operationType === 'edit'"
              >
                <el-option label="普通销售合同" :value="1" />
                <el-option label="框架合同" :value="2" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
@@ -467,7 +496,7 @@
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
        <el-row :gutter="30" v-if="!isFrameworkContract">
          <el-col :span="12">
            <el-form-item label="交货日期:" prop="entryDate">
              <el-date-picker
@@ -750,7 +779,7 @@
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
          <el-col :span="12" v-if="!isFrameworkContract">
            <el-form-item label="税率(%):" prop="taxRate">
              <el-select
                v-model="productForm.taxRate"
@@ -783,7 +812,7 @@
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
          <el-col :span="12" v-if="!isFrameworkContract">
            <el-form-item label="数量:" prop="quantity">
              <el-input-number
                :step="0.1"
@@ -799,7 +828,7 @@
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
          <el-col :span="12" v-if="!isFrameworkContract">
            <el-form-item label="含税总价(元):" prop="taxInclusiveTotalPrice">
              <el-input
                v-model="productForm.taxInclusiveTotalPrice"
@@ -809,7 +838,7 @@
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
          <el-col :span="12" v-if="!isFrameworkContract">
            <el-form-item
              label="不含税总价(元):"
              prop="taxExclusiveTotalPrice"
@@ -824,7 +853,7 @@
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
          <el-col :span="12" v-if="!isFrameworkContract">
            <el-form-item label="发票类型:" prop="invoiceType">
              <el-select
                v-model="productForm.invoiceType"
@@ -836,7 +865,7 @@
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
          <el-col :span="12" v-if="!isFrameworkContract">
            <el-form-item label="是否生产:" prop="isProduction">
              <el-radio-group v-model="productForm.isProduction">
                <el-radio label="是" :value="true" />
@@ -1177,6 +1206,104 @@
        </div>
      </template>
    </el-dialog>
    <!-- 创建框架订单弹窗 -->
    <el-dialog
      v-model="frameworkOrderDialogVisible"
      title="创建框架订单"
      width="70%"
      :close-on-click-modal="false"
    >
      <el-form label-width="120px" label-position="top">
        <el-form-item label="父框架合同:">
          <el-input
            :model-value="currentFrameworkContract?.salesContractNo"
            disabled
          />
        </el-form-item>
        <el-form-item label="备注:">
          <el-input
            v-model="frameworkOrderForm.remarks"
            type="textarea"
            :rows="2"
            placeholder="请输入备注"
          />
        </el-form-item>
        <el-form-item label="产品(填写交货日期):">
          <el-table :data="frameworkOrderProducts" border>
            <el-table-column label="产品大类" prop="productCategory" />
            <el-table-column label="规格型号" prop="specificationModel" />
            <el-table-column label="单位" prop="unit" />
            <el-table-column label="交货日期" align="center">
              <template #default="scope">
                <el-date-picker
                  v-model="scope.row.deliveryDate"
                  value-format="YYYY-MM-DD"
                  format="YYYY-MM-DD"
                  type="date"
                  placeholder="请选择"
                  clearable
                />
              </template>
            </el-table-column>
          </el-table>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button type="primary" @click="submitFrameworkOrder"
          >确认创建
        </el-button>
        <el-button @click="frameworkOrderDialogVisible = false">取消</el-button>
      </template>
    </el-dialog>
    <!-- 框架订单列表弹窗 -->
    <el-dialog
      v-model="frameworkOrderListVisible"
      title="框架订单列表"
      width="80%"
      :close-on-click-modal="false"
    >
      <el-alert
        v-if="frameworkContractShippedQty !== null"
        type="info"
        :closable="false"
        style="margin-bottom: 12px"
        :title="'父框架合同累计已发货数量:' + frameworkContractShippedQty"
      />
      <el-table :data="frameworkOrderList" border :row-key="(row) => row.id">
        <el-table-column type="expand">
          <template #default="props">
            <el-table :data="props.row.products" border size="small">
              <el-table-column label="产品大类" prop="productCategory" />
              <el-table-column label="规格型号" prop="specificationModel" />
              <el-table-column label="单位" prop="unit" />
              <el-table-column label="操作" width="80" align="center">
                <template #default="scope">
                  <el-button
                    link
                    type="primary"
                    @click="shipFrameworkOrderProduct(scope.row, props.row)"
                    >发货
                  </el-button>
                </template>
              </el-table-column>
            </el-table>
          </template>
        </el-table-column>
        <el-table-column
          label="订单编号"
          prop="salesContractNo"
          min-width="180"
        />
        <el-table-column label="项目名称" prop="projectName" min-width="160" />
        <el-table-column label="备注" prop="remarks" min-width="160" />
        <el-table-column
          label="累计发货数量"
          prop="shippedQuantity"
          width="140"
          align="center"
        />
      </el-table>
    </el-dialog>
  </div>
</template>
@@ -1201,6 +1328,9 @@
  delProduct,
  delLedgerFile,
  getProductInventory,
  createFrameworkOrder,
  listFrameworkOrders,
  frameworkContractShippedQuantity,
} from "@/api/salesManagement/salesLedger.js";
import { getStockInventoryByModelId } from "@/api/inventoryManagement/stockInventory.js";
import { modelList, productTreeList } from "@/api/basicData/product.js";
@@ -1250,6 +1380,7 @@
  },
  form: {
    salesContractNo: "",
    contractType: 2,
    autoGenerateContractNo: true,
    salesman: "",
    customerId: "",
@@ -1267,11 +1398,31 @@
    customerId: [{ required: true, message: "请选择", trigger: "change" }],
    entryPerson: [{ required: true, message: "请选择", trigger: "change" }],
    entryDate: [{ required: true, message: "请选择", trigger: "change" }],
    deliveryDate: [{ required: true, message: "请选择", trigger: "change" }],
    deliveryDate: [
      {
        validator: (rule, value, callback) => {
          if (form.value.contractType === 2) return callback();
          if (!value) return callback(new Error("请选择"));
          callback();
        },
        trigger: "change",
      },
    ],
    executionDate: [{ required: true, message: "请选择", trigger: "change" }],
  },
});
const { form, rules } = toRefs(data);
const isFrameworkContract = computed(() => form.value.contractType === 2);
const frameworkOptional = (message, trigger = "blur") => ({
  validator: (_rule, value, callback) => {
    if (isFrameworkContract.value) return callback();
    if (value === undefined || value === null || value === "") {
      return callback(new Error(message));
    }
    callback();
  },
  trigger,
});
const { form: searchForm } = useFormData(data.searchForm);
// 产品表单弹框数据
const productFormVisible = ref(false);
@@ -1297,18 +1448,12 @@
      { required: true, message: "请选择", trigger: "change" },
    ],
    unit: [{ required: true, message: "请输入", trigger: "blur" }],
    quantity: [{ required: true, message: "请输入", trigger: "blur" }],
    taxInclusiveUnitPrice: [
      { required: true, message: "请输入", trigger: "blur" },
    ],
    taxRate: [{ required: true, message: "请选择", trigger: "change" }],
    taxInclusiveTotalPrice: [
      { required: true, message: "请输入", trigger: "blur" },
    ],
    taxExclusiveTotalPrice: [
      { required: true, message: "请输入", trigger: "blur" },
    ],
    invoiceType: [{ required: true, message: "请选择", trigger: "change" }],
    quantity: [frameworkOptional("请输入")],
    taxInclusiveUnitPrice: [{ required: true, message: "请输入", trigger: "blur" }],
    taxRate: [frameworkOptional("请选择", "change")],
    taxInclusiveTotalPrice: [frameworkOptional("请输入")],
    taxExclusiveTotalPrice: [frameworkOptional("请输入")],
    invoiceType: [frameworkOptional("请选择", "change")],
    isProduction: [{ required: true, message: "请选择", trigger: "change" }],
  },
});
@@ -1740,6 +1885,7 @@
  form.value.entryPerson = userStore.id;
  if (type === "add") {
    // 新增时设置录入日期为当天
    form.value.contractType = 2;
    form.value.entryDate = getCurrentDate();
    // 签订日期默认为当天
    form.value.executionDate = getCurrentDate();
@@ -1892,6 +2038,14 @@
      }
      form.value.storageBlobDTOs = fileList;
      form.value.type = 1;
      if (isFrameworkContract.value) {
        form.value.productData = form.value.productData.map((item) => ({
          ...item,
          quantity: null,
          deliveryDate: null,
          isProduction: false,
        }));
      }
      if (form.value.autoGenerateContractNo) {
        form.value.salesContractNo = "";
      }
@@ -2933,6 +3087,92 @@
  deliveryFormVisible.value = false;
  currentDeliveryRow.value = null;
};
// 框架合同 / 框架订单相关
const frameworkOrderDialogVisible = ref(false);
const frameworkOrderListVisible = ref(false);
const currentFrameworkContract = ref(null);
const frameworkOrderForm = reactive({
  remarks: "",
});
const frameworkOrderProducts = ref([]);
const frameworkOrderList = ref([]);
const frameworkContractShippedQty = ref(null);
const openFrameworkOrderDialog = async (row) => {
  currentFrameworkContract.value = row;
  frameworkOrderForm.remarks = "";
  frameworkOrderProducts.value = [];
  try {
    const res = await productList({ salesLedgerId: row.id, type: 1 });
    frameworkOrderProducts.value = (res.data || []).map((p) => ({
      parentProductId: p.id,
      productCategory: p.productCategory,
      specificationModel: p.specificationModel,
      unit: p.unit,
      deliveryDate: "",
    }));
  } catch (e) {
    frameworkOrderProducts.value = [];
  }
  frameworkOrderDialogVisible.value = true;
};
const submitFrameworkOrder = async () => {
  const selected = frameworkOrderProducts.value.filter((p) => p.deliveryDate);
  if (selected.length === 0) {
    proxy.$modal.msgWarning("请至少为一个产品填写交货日期");
    return;
  }
  try {
    await createFrameworkOrder({
      id: currentFrameworkContract.value.id,
      remarks: frameworkOrderForm.remarks,
      productData: selected.map((p) => ({
        parentProductId: p.parentProductId,
        deliveryDate: p.deliveryDate,
      })),
    });
    proxy.$modal.msgSuccess("框架订单创建成功");
    frameworkOrderDialogVisible.value = false;
  } catch (e) {
    // 错误已由请求拦截器统一提示
  }
};
const openFrameworkOrderList = async (row) => {
  currentFrameworkContract.value = row;
  frameworkContractShippedQty.value = null;
  frameworkOrderList.value = [];
  frameworkOrderListVisible.value = true;
  listFrameworkOrders({ parentContractId: row.id }).then(async (res) => {
    const orders = res.data || [];
    const withProducts = [];
    for (const order of orders) {
      let products = [];
      try {
        const productRes = await productList({ salesLedgerId: order.id, type: 1 });
        products = productRes.data || [];
      } catch (e) {
        products = [];
      }
      withProducts.push({ ...order, products });
    }
    frameworkOrderList.value = withProducts;
  });
  frameworkContractShippedQuantity({ parentContractId: row.id }).then((res) => {
    frameworkContractShippedQty.value = res.data ?? 0;
  });
};
const shipFrameworkOrderProduct = (productRow, orderRow) => {
  if (!orderRow || !orderRow.id) return;
  frameworkOrderListVisible.value = false;
  openDeliveryForm({
    ...productRow,
    salesLedgerId: orderRow.id,
  });
};
const currentFactoryName = ref("");
const getCurrentFactoryName = async () => {
  let res = await userStore.getInfo();