yuan
5 天以前 9ea5ff67655241271bdf04bb50ca3a8d60c4e22a
src/views/qualityManagement/metricBinding/index.vue
@@ -1,16 +1,11 @@
<template>
  <div class="app-container metric-binding">
    <el-row :gutter="16"
            class="metric-binding-row">
    <el-row :gutter="16" class="metric-binding-row">
      <!-- 左侧:检测标准列表 -->
      <el-col :xs="24"
              :sm="24"
              :md="12"
              :lg="14"
              :xl="14"
              class="left-col">
      <el-col :xs="24" :sm="24" :md="12" :lg="14" :xl="14" class="left-col">
        <div class="panel left-panel">
          <PIMTable rowKey="id"
      <PIMTable
        rowKey="id"
                    :column="standardColumns"
                    :tableData="standardTableData"
                    :page="page"
@@ -19,149 +14,155 @@
                    :tableLoading="tableLoading"
                    :rowClick="handleTableRowClick"
                    @pagination="handlePagination"
                    :total="page.total">
        :total="page.total"
      >
            <template #standardNoCell="{ row }">
              <span class="clickable-link"
                    @click="handleStandardRowClick(row)">
          <span class="clickable-link" @click="handleStandardRowClick(row)">
                {{ row.standardNo }}
              </span>
            </template>
            <!-- 表头搜索 -->
            <template #standardNoHeader>
              <el-input v-model="searchForm.standardNo"
          <el-input
            v-model="searchForm.standardNo"
                        placeholder="标准编号"
                        clearable
                        size="small"
                        @change="handleQuery"
                        @clear="handleQuery" />
            @clear="handleQuery"
          />
            </template>
            <template #standardNameHeader>
              <el-input v-model="searchForm.standardName"
          <el-input
            v-model="searchForm.standardName"
                        placeholder="标准名称"
                        clearable
                        size="small"
                        @change="handleQuery"
                        @clear="handleQuery" />
            @clear="handleQuery"
          />
            </template>
            <template #inspectTypeHeader>
              <el-select v-model="searchForm.inspectType"
          <el-select
            v-model="searchForm.inspectType"
                         placeholder="类别"
                         clearable
                         size="small"
                         style="width: 120px"
                         @change="handleQuery"
                         @clear="handleQuery">
                <el-option label="原材料检验"
                           value="0" />
                <el-option label="过程检验"
                           value="1" />
                <el-option label="出厂检验"
                           value="2" />
            @clear="handleQuery"
          >
            <el-option label="原材料检验" value="0" />
            <el-option label="过程检验" value="1" />
            <el-option label="出厂检验" value="2" />
              </el-select>
            </template>
            <template #stateHeader>
              <el-select v-model="searchForm.state"
          <el-select
            v-model="searchForm.state"
                         placeholder="状态"
                         clearable
                         size="small"
                         style="width: 110px"
                         @change="handleQuery"
                         @clear="handleQuery">
                <el-option label="草稿"
                           value="0" />
                <el-option label="通过"
                           value="1" />
                <el-option label="撤销"
                           value="2" />
            @clear="handleQuery"
          >
            <el-option label="草稿" value="0" />
            <el-option label="通过" value="1" />
            <el-option label="撤销" value="2" />
              </el-select>
            </template>
          </PIMTable>
        </div>
      </el-col>
      <!-- 右侧:绑定列表 -->
      <el-col :xs="24"
              :sm="24"
              :md="12"
              :lg="10"
              :xl="10"
              class="right-col">
      <el-col :xs="24" :sm="24" :md="12" :lg="10" :xl="10" class="right-col">
        <div class="panel right-panel">
          <div class="right-header">
            <div class="title">绑定关系</div>
            <div class="desc"
                 v-if="currentStandard">
        <div class="desc" v-if="currentStandard">
              当前检测标准编号:<span class="link-text">{{ currentStandard.standardNo }}</span>
            </div>
            <div class="desc"
                 v-else>请选择左侧检测标准</div>
        <div class="desc" v-else>请选择左侧检测标准</div>
          </div>
          <div class="right-toolbar">
            <el-button type="primary"
                       :disabled="!currentStandard"
                       @click="openBindingDialog">添加绑定</el-button>
            <el-button type="danger"
                       plain
                       :disabled="!currentStandard"
                       @click="handleBatchUnbind">删除</el-button>
        <el-button type="primary" :disabled="!currentStandard" @click="openBindingDialog">添加绑定</el-button>
        <el-button type="danger" plain :disabled="!currentStandard" @click="handleBatchUnbind">删除</el-button>
          </div>
          <el-table v-loading="bindingLoading"
      <el-table
        v-loading="bindingLoading"
                    :data="bindingTableData"
                    border
                    :row-class-name="() => 'row-center'"
                    class="center-table"
                    style="width: 100%"
                    height="calc(100vh - 220px)"
                    @selection-change="handleBindingSelectionChange">
            <el-table-column type="selection"
                             width="48"
                             align="center" />
            <el-table-column type="index"
                             label="序号"
                             width="60"
                             align="center" />
            <el-table-column prop="productName"
                             label="产品名称"
                             min-width="140" />
            <el-table-column label="操作"
                             width="120"
                             fixed="right"
                             align="center">
        @selection-change="handleBindingSelectionChange"
      >
        <el-table-column type="selection" width="48" align="center" />
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column prop="productName" label="产品名称" min-width="140" />
        <el-table-column label="操作" width="120" fixed="right" align="center">
              <template #default="{ row }">
                <el-button link
                           type="danger"
                           size="small"
                           @click="handleUnbind(row)">删除</el-button>
            <el-button link type="danger" size="small" @click="handleUnbind(row)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </el-col>
    </el-row>
    <!-- 添加绑定弹框 -->
    <el-dialog v-model="bindingDialogVisible"
    <el-dialog
      v-model="bindingDialogVisible"
               title="添加绑定"
               width="520px"
               @close="closeBindingDialog">
      <el-form label-width="100px">
        <el-form-item label="产品">
          <PIMTreeSelect v-model="selectedProductIds"
                         multiple
                         filterable
                         collapse-tags
                         collapse-tags-tooltip
                         placeholder="请选择产品(可多选)"
      width="640px"
      @close="closeBindingDialog"
    >
      <div class="binding-dialog">
        <el-input
          v-model="productSearchKeyword"
          placeholder="搜索产品"
                         clearable
          prefix-icon="Search"
          class="binding-search"
        />
        <el-tree
          ref="productTreeRef"
          :key="productTreeKey"
          v-loading="productTreeLoading"
          :data="productTreeData"
          show-checkbox
                         check-strictly
                         :data="productOptions"
                         :render-after-expand="false"
                         style="width: 100%" />
        </el-form-item>
      </el-form>
          node-key="id"
          :props="{ label: 'label', children: 'children', disabled: 'disabled' }"
          :filter-node-method="filterProductNode"
          class="product-binding-tree"
          @check="handleProductTreeCheck"
        />
        <div v-if="selectedProductLabels.length" class="selected-products">
          <div class="selected-title">已选择({{ selectedProductLabels.length }})</div>
          <div class="selected-tags">
            <el-tag
              v-for="item in selectedProductLabels"
              :key="item.id"
              closable
              type="info"
              @close="removeSelectedProduct(item.id)"
            >
              {{ item.label }}
            </el-tag>
          </div>
        </div>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="closeBindingDialog">取消</el-button>
          <el-button type="primary"
                     @click="submitBinding">确定</el-button>
          <el-button type="primary" @click="submitBinding">确定</el-button>
        </span>
      </template>
    </el-dialog>
@@ -169,104 +170,90 @@
</template>
<script setup>
  import { Search } from "@element-plus/icons-vue";
  import { ref, reactive, toRefs, onMounted, getCurrentInstance } from "vue";
  import { ElMessageBox } from "element-plus";
  import PIMTable from "@/components/PIMTable/PIMTable.vue";
  import PIMTreeSelect from "@/components/PIMTreeSelect/index.vue";
  import { productTreeList } from "@/api/basicData/product.js";
  import { qualityTestStandardListPage } from "@/api/qualityManagement/metricMaintenance.js";
  import { productProcessListPage } from "@/api/basicData/productProcess.js";
import { ref, reactive, toRefs, computed, watch, nextTick, onMounted, getCurrentInstance } from 'vue'
import { ElMessageBox } from 'element-plus'
import PIMTable from '@/components/PIMTable/PIMTable.vue'
import { productTreeList } from '@/api/basicData/product.js'
import {
  qualityTestStandardListPage
} from '@/api/qualityManagement/metricMaintenance.js'
import { productProcessListPage } from '@/api/basicData/productProcess.js'
  import {
    qualityTestStandardBindingList,
    qualityTestStandardBindingAdd,
    qualityTestStandardBindingDel,
  } from "@/api/qualityManagement/qualityTestStandardBinding.js";
  qualityTestStandardBindingDel
} from '@/api/qualityManagement/qualityTestStandardBinding.js'
  const { proxy } = getCurrentInstance();
const { proxy } = getCurrentInstance()
  // 左侧标准列表:整行内容居中(配合样式)
  const rowClassNameCenter = () => "row-center";
const rowClassNameCenter = () => 'row-center'
  const data = reactive({
    searchForm: {
      standardNo: "",
      standardName: "",
      state: "",
      inspectType: "",
    },
  });
  const { searchForm } = toRefs(data);
    standardNo: '',
    standardName: '',
    state: '',
    inspectType: ''
  }
})
const { searchForm } = toRefs(data)
  // 左侧
  const standardTableData = ref([]);
  const tableLoading = ref(false);
  const page = reactive({ current: 1, size: 10, total: 0 });
const standardTableData = ref([])
const tableLoading = ref(false)
const page = reactive({ current: 1, size: 10, total: 0 })
  // 工序下拉(用于列表回显)
  const processOptions = ref([]);
const processOptions = ref([])
  const getProcessList = async () => {
    try {
      const res = await productProcessListPage({ current: 1, size: 1000 });
    const res = await productProcessListPage({ current: 1, size: 1000 })
      if (res?.code === 200) {
        const records = res?.data?.records || [];
        processOptions.value = records.map(item => ({
      const records = res?.data?.records || []
      processOptions.value = records.map((item) => ({
          label: item.processName || item.name || item.label,
          value: item.id || item.processId || item.value,
        }));
        value: item.id || item.processId || item.value
      }))
      }
    } catch (error) {
      console.error("获取工序列表失败:", error);
    console.error('获取工序列表失败:', error)
    }
  };
}
  const standardColumns = ref([
  { label: '标准编号', prop: 'standardNo', dataType: 'slot', slot: 'standardNoCell', minWidth: 160, align: 'center', headerSlot: 'standardNoHeader' },
  { label: '标准名称', prop: 'standardName', minWidth: 180, align: 'center', headerSlot: 'standardNameHeader' },
    {
      label: "标准编号",
      prop: "standardNo",
      dataType: "slot",
      slot: "standardNoCell",
      minWidth: 160,
      align: "center",
      headerSlot: "standardNoHeader",
    label: '类别',
    prop: 'inspectType',
    headerSlot: 'inspectTypeHeader',
    align: 'center',
    dataType: 'tag',
    formatData: (val) => {
      const map = { 0: '原材料检验', 1: '过程检验', 2: '出厂检验' }
      return map[val] || val
    }
    },
    {
      label: "标准名称",
      prop: "standardName",
      minWidth: 180,
      align: "center",
      headerSlot: "standardNameHeader",
    },
    {
      label: "类别",
      prop: "inspectType",
      headerSlot: "inspectTypeHeader",
      align: "center",
      dataType: "tag",
      formatData: val => {
        const map = { 0: "原材料检验", 1: "过程检验", 2: "出厂检验" };
        return map[val] || val;
      },
    },
    {
      label: "工序",
      prop: "processId",
      align: "center",
      dataType: "tag",
      formatData: val => {
    label: '工序',
    prop: 'processId',
    align: 'center',
    dataType: 'tag',
    formatData: (val) => {
        const target = processOptions.value.find(
          item => String(item.value) === String(val)
        );
        return target?.label || val;
      },
        (item) => String(item.value) === String(val)
      )
      return target?.label || val
    }
    },
    {
      label: "备注",
      prop: "remark",
    label: '备注',
    prop: 'remark',
      minWidth: 160,
      align: "center",
    },
    align: 'center'
  }
    // {
    //   label: '状态',
    //   prop: 'state',
@@ -282,189 +269,240 @@
    //     return 'info'
    //   }
    // }
  ]);
])
  const currentStandard = ref(null);
const currentStandard = ref(null)
  // 右侧绑定
  const bindingTableData = ref([]);
  const bindingLoading = ref(false);
  const bindingSelectedRows = ref([]);
  const bindingDialogVisible = ref(false);
const bindingTableData = ref([])
const bindingLoading = ref(false)
const bindingSelectedRows = ref([])
const bindingDialogVisible = ref(false)
  // 产品树(用于绑定选择)
  const productOptions = ref([]);
  const selectedProductIds = ref([]);
const productTreeData = ref([])
const productTreeLoading = ref(false)
const productTreeRef = ref(null)
const productTreeKey = ref(0)
const productSearchKeyword = ref('')
const selectedProductIds = ref([])
  const getProductOptions = async () => {
    // 避免重复请求
    if (productOptions.value?.length) return;
    const res = await productTreeList();
    productOptions.value = convertIdToValue(Array.isArray(res) ? res : []);
  };
  function convertIdToValue(data) {
    return (data || []).map(item => {
      const { id, children, ...rest } = item;
      const newItem = {
        ...rest,
        value: id,
      };
      if (children && children.length > 0) {
        newItem.children = convertIdToValue(children);
const markParentNodesDisabled = (nodes) => {
  return (nodes || []).map((node) => {
    const children = node.children?.length ? markParentNodesDisabled(node.children) : []
    return {
      ...node,
      children,
      disabled: children.length > 0
      }
      return newItem;
    });
  })
}
const buildProductLabelMap = (nodes, map = {}) => {
  ;(nodes || []).forEach((node) => {
    if (node.id != null) {
      map[node.id] = node.label
    }
    if (node.children?.length) {
      buildProductLabelMap(node.children, map)
    }
  })
  return map
}
const productLabelMap = computed(() => buildProductLabelMap(productTreeData.value))
const selectedProductLabels = computed(() =>
  selectedProductIds.value.map((id) => ({
    id,
    label: productLabelMap.value[id] || id
  }))
)
const filterProductNode = (value, data) => {
  if (!value) return true
  return String(data.label || '').includes(value)
}
watch(productSearchKeyword, (val) => {
  productTreeRef.value?.filter(val)
})
const getProductTreeData = async () => {
  if (productTreeData.value?.length) return
  productTreeLoading.value = true
  try {
    const res = await productTreeList()
    productTreeData.value = markParentNodesDisabled(Array.isArray(res) ? res : [])
  } catch (error) {
    console.error('获取产品树失败:', error)
  } finally {
    productTreeLoading.value = false
  }
}
const handleProductTreeCheck = () => {
  selectedProductIds.value = productTreeRef.value?.getCheckedKeys(true) || []
}
const removeSelectedProduct = (id) => {
  selectedProductIds.value = selectedProductIds.value.filter((item) => item !== id)
  productTreeRef.value?.setChecked(id, false, false)
  }
  const handleQuery = () => {
    page.current = 1;
    getStandardList();
  };
  page.current = 1
  getStandardList()
}
  const handlePagination = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getStandardList();
  };
const handlePagination = (obj) => {
  page.current = obj.page
  page.size = obj.limit
  getStandardList()
}
  const getStandardList = () => {
    tableLoading.value = true;
  tableLoading.value = true
    qualityTestStandardListPage({
      ...searchForm.value,
      current: page.current,
      size: page.size,
      state: 1,
    state: 1
    })
      .then(res => {
        const records = res?.data?.records || [];
        standardTableData.value = records;
        page.total = res?.data?.total || records.length;
    .then((res) => {
      const records = res?.data?.records || []
      standardTableData.value = records
      page.total = res?.data?.total || records.length
      })
      .finally(() => {
        tableLoading.value = false;
      });
  };
      tableLoading.value = false
    })
}
  // 表格行点击,加载右侧绑定列表
  const handleTableRowClick = row => {
    currentStandard.value = row;
    loadBindingList();
  };
const handleTableRowClick = (row) => {
  currentStandard.value = row
  loadBindingList()
}
  // 左侧行点击,加载右侧绑定列表(保留用于标准编号列的点击)
  const handleStandardRowClick = row => {
    currentStandard.value = row;
    loadBindingList();
  };
const handleStandardRowClick = (row) => {
  currentStandard.value = row
  loadBindingList()
}
  const loadBindingList = () => {
    if (!currentStandard.value?.id) {
      bindingTableData.value = [];
      return;
    bindingTableData.value = []
    return
    }
    bindingLoading.value = true;
  bindingLoading.value = true
    qualityTestStandardBindingList({ testStandardId: currentStandard.value.id })
      .then(res => {
        const base = res?.data || [];
    .then((res) => {
      const base = res?.data || []
        // 将当前标准的工序和备注带到绑定列表中展示
        bindingTableData.value = base.map(item => ({
      bindingTableData.value = base.map((item) => ({
          ...item,
          processId: currentStandard.value?.processId,
          remark: currentStandard.value?.remark,
        }));
        remark: currentStandard.value?.remark
      }))
      })
      .finally(() => {
        bindingLoading.value = false;
      });
  };
      bindingLoading.value = false
    })
}
  const handleBindingSelectionChange = selection => {
    bindingSelectedRows.value = selection;
  };
const handleBindingSelectionChange = (selection) => {
  bindingSelectedRows.value = selection
}
  const openBindingDialog = () => {
    if (!currentStandard.value?.id) return;
    selectedProductIds.value = [];
    getProductOptions();
    bindingDialogVisible.value = true;
  };
  if (!currentStandard.value?.id) return
  selectedProductIds.value = []
  productSearchKeyword.value = ''
  productTreeKey.value += 1
  bindingDialogVisible.value = true
  nextTick(() => {
    productTreeRef.value?.setCheckedKeys([])
    productTreeRef.value?.filter('')
  })
}
  const closeBindingDialog = () => {
    bindingDialogVisible.value = false;
  };
  bindingDialogVisible.value = false
  selectedProductIds.value = []
  productSearchKeyword.value = ''
  productTreeRef.value?.setCheckedKeys([])
}
  const submitBinding = async () => {
    const testStandardId = currentStandard.value?.id;
    if (!testStandardId) return;
    const ids = (selectedProductIds.value || []).filter(Boolean);
  const testStandardId = currentStandard.value?.id
  if (!testStandardId) return
  const ids = (selectedProductIds.value || []).filter(Boolean)
    if (!ids.length) {
      proxy.$message.warning("请选择产品");
      return;
    proxy.$message.warning('请选择产品')
    return
    }
    const payload = ids.map(pid => ({
  const payload = ids.map((pid) => ({
      productId: pid,
      testStandardId,
    }));
    await qualityTestStandardBindingAdd(payload);
    proxy.$message.success("添加成功");
    bindingDialogVisible.value = false;
    loadBindingList();
  };
    testStandardId
  }))
  await qualityTestStandardBindingAdd(payload)
  proxy.$message.success('添加成功')
  bindingDialogVisible.value = false
  loadBindingList()
}
  const handleUnbind = async row => {
    const id = row?.id ?? row?.qualityTestStandardBindingId;
    if (id == null || id === "") return;
const handleUnbind = async (row) => {
  const id = row?.id ?? row?.qualityTestStandardBindingId
  if (id == null || id === '') return
    try {
      await ElMessageBox.confirm("确认删除该绑定?", "提示", { type: "warning" });
    await ElMessageBox.confirm('确认删除该绑定?', '提示', { type: 'warning' })
    } catch {
      return;
    return
    }
    try {
      await qualityTestStandardBindingDel([id]);
      proxy.$message.success("删除成功");
      loadBindingList();
    await qualityTestStandardBindingDel([id])
    proxy.$message.success('删除成功')
    loadBindingList()
    } catch (err) {
      console.error("删除绑定失败:", err);
      proxy.$message?.error(err?.message || "删除失败");
    console.error('删除绑定失败:', err)
    proxy.$message?.error(err?.message || '删除失败')
    }
  };
}
  const handleBatchUnbind = async () => {
    if (!bindingSelectedRows.value.length) {
      proxy.$message.warning("请选择数据");
      return;
    proxy.$message.warning('请选择数据')
    return
    }
    const ids = bindingSelectedRows.value
      .map(i => i?.id ?? i?.qualityTestStandardBindingId)
      .filter(id => id != null && id !== "");
    .map((i) => i?.id ?? i?.qualityTestStandardBindingId)
    .filter((id) => id != null && id !== '')
    if (!ids.length) {
      proxy.$message.warning("选中数据缺少有效 id");
      return;
    proxy.$message.warning('选中数据缺少有效 id')
    return
    }
    try {
      await ElMessageBox.confirm(
        "选中的内容将被删除,是否确认删除?",
        "删除提示",
        { type: "warning" }
      );
    await ElMessageBox.confirm('选中的内容将被删除,是否确认删除?', '删除提示', { type: 'warning' })
    } catch {
      return;
    return
    }
    try {
      await qualityTestStandardBindingDel(ids);
      proxy.$message.success("删除成功");
      loadBindingList();
    await qualityTestStandardBindingDel(ids)
    proxy.$message.success('删除成功')
    loadBindingList()
    } catch (err) {
      console.error("批量删除绑定失败:", err);
      proxy.$message?.error(err?.message || "删除失败");
    console.error('批量删除绑定失败:', err)
    proxy.$message?.error(err?.message || '删除失败')
    }
  };
}
  onMounted(() => {
    getStandardList();
    getProcessList();
  });
  getStandardList()
  getProcessList()
  getProductTreeData()
})
</script>
<style scoped>
@@ -569,4 +607,39 @@
    width: 100%;
    margin-top: 4px;
  }
.binding-dialog {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.binding-search {
  width: 100%;
}
.product-binding-tree {
  max-height: 360px;
  overflow-y: auto;
  border: 1px solid #ebeef5;
  border-radius: 4px;
  padding: 8px;
}
.selected-products {
  border-top: 1px solid #ebeef5;
  padding-top: 12px;
}
.selected-title {
  font-size: 13px;
  color: #606266;
  margin-bottom: 8px;
}
.selected-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
</style>