tuqin
14 小时以前 77431c1b49946528520d92323f5ef2d383eb2136
feat(生产核算): 生产核算页面支持成品/白盒部门产量工时折算

- 重写 productionCosting 页面:部门类型切换、月份/生产人筛选、工作日信息栏、汇总台账+明细双表(列随部门类型变化)
- 新增配置弹窗:部门类型 / 标准数量 / 折算率 / 月度工作日
- 更新 API:productionCosting.js(核算查询、配置 CRUD、导出)
已添加1个文件
已修改2个文件
1283 ■■■■■ 文件已修改
src/api/productionManagement/productionCosting.js 147 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionCosting/components/CostConfigDialog.vue 414 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionCosting/index.vue 722 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionCosting.js
@@ -1,49 +1,154 @@
// ç”Ÿäº§æ ¸ç®—页面接口
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢
export function productionAccountingListPage(query) {
/* ===================== æ ¸ç®—查询 ===================== */
// æŒ‰å‘˜å·¥æ±‡æ€»ï¼ˆäº§é‡ã€æœˆæ—¶é—´åˆè®¡ã€æŠ˜åˆå€æ•°ï¼‰
export function costingSummary(query) {
  return request({
    url: "/salesLedger/productionAccounting/listPage",
    url: "/productionCosting/summary",
    method: "get",
    params: query,
  });
}
// å·¦è¾¹è¡¨æ ¼çš„æŽ¥å£
// salesLedger/productionAccounting/page
export function salesLedgerProductionAccountingList(query) {
// æ˜Žç»†ï¼ˆç™½ç›’:工序+型号;成品:工序)
export function costingDetail(query) {
  return request({
    url: "/productionAccount/listPage",
    url: "/productionCosting/detail",
    method: "get",
    params: query,
  });
}
// å³è¾¹è¡¨æ ¼çš„æŽ¥å£
//
export function salesLedgerProductionAccountingListProductionDetails(query) {
// æœˆä»½å·¥ä½œæ—¥ä¿¡æ¯
export function costingWorkday(yearMonth) {
  return request({
    url: "/productionAccount/listProductionDetails",
    url: "/productionCosting/workday",
    method: "get",
    params: query,
    params: { yearMonth },
  });
}
// æ‰¹é‡æ ¸ç®—
export function productionAccountingBatchAccounting(data) {
// å¯¼å‡ºæ˜Žç»†
export function costingExport(query) {
  return request({
    url: "/productionAccount/batchAccounting",
    url: "/productionCosting/export",
    method: "post",
    data,
    params: query,
    responseType: "blob",
  });
}
// å•个修正
export function productionAccountingSingleAdjust(data) {
/* ===================== éƒ¨é—¨ç±»åž‹é…ç½® ===================== */
export function costingDeptList() {
  return request({ url: "/productionCosting/dept/list", method: "get" });
}
export function costingDeptAdd(data) {
  return request({ url: "/productionCosting/dept", method: "post", data });
}
export function costingDeptUpdate(data) {
  return request({ url: "/productionCosting/dept", method: "put", data });
}
export function costingDeptDel(id) {
  return request({ url: "/productionCosting/dept/" + id, method: "delete" });
}
/* ===================== æ ‡å‡†æ•°é‡é…ç½® ===================== */
export function costingStandardList(query) {
  return request({
    url: "/productionAccount/singleAdjust",
    method: "post",
    data,
    url: "/productionCosting/standard/list",
    method: "get",
    params: query,
  });
}
export function costingStandardAdd(data) {
  return request({ url: "/productionCosting/standard", method: "post", data });
}
export function costingStandardUpdate(data) {
  return request({ url: "/productionCosting/standard", method: "put", data });
}
export function costingStandardDel(id) {
  return request({ url: "/productionCosting/standard/" + id, method: "delete" });
}
/* ===================== æŠ˜ç®—率配置 ===================== */
export function costingFactorList(query) {
  return request({
    url: "/productionCosting/factor/list",
    method: "get",
    params: query,
  });
}
export function costingFactorAdd(data) {
  return request({ url: "/productionCosting/factor", method: "post", data });
}
export function costingFactorUpdate(data) {
  return request({ url: "/productionCosting/factor", method: "put", data });
}
export function costingFactorDel(id) {
  return request({ url: "/productionCosting/factor/" + id, method: "delete" });
}
/* ===================== æœˆåº¦å·¥ä½œæ—¥é…ç½® ===================== */
export function costingWorkdayList(query) {
  return request({
    url: "/productionCosting/workdayConfig/list",
    method: "get",
    params: query,
  });
}
export function costingWorkdayAdd(data) {
  return request({ url: "/productionCosting/workdayConfig", method: "post", data });
}
export function costingWorkdayUpdate(data) {
  return request({ url: "/productionCosting/workdayConfig", method: "put", data });
}
export function costingWorkdayDel(id) {
  return request({ url: "/productionCosting/workdayConfig/" + id, method: "delete" });
}
/* ===================== ä¸‹æ‹‰æ•°æ®æº ===================== */
// å·¥åºä¸‹æ‹‰
export function costingProcessOptions(query) {
  return request({
    url: "/technologyOperation/listPage",
    method: "get",
    params: { current: 1, size: 1000, ...query },
  });
}
// åž‹å·ä¸‹æ‹‰
export function costingModelOptions(query) {
  return request({
    url: "/basic/product/pageModel",
    method: "get",
    params: { current: 1, size: 1000, ...query },
  });
}
// éƒ¨é—¨ä¸‹æ‹‰
export function costingDeptOptions(query) {
  return request({
    url: "/system/dept/list",
    method: "get",
    params: query,
  });
}
src/views/productionManagement/productionCosting/components/CostConfigDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,414 @@
<template>
  <el-dialog
    v-model="visible"
    title="生产核算配置"
    width="900px"
    append-to-body
    @close="handleClose"
  >
    <el-tabs v-model="activeTab">
      <!-- éƒ¨é—¨ç±»åž‹é…ç½® -->
      <el-tab-pane label="部门类型" name="dept">
        <div class="tab-toolbar">
          <el-button type="primary" size="small" @click="openDeptForm()">新增</el-button>
        </div>
        <el-table :data="deptList" border size="small" max-height="420">
          <el-table-column label="部门" prop="deptName" min-width="140" />
          <el-table-column label="类型" min-width="110">
            <template #default="{ row }">{{ deptTypeText(row.deptType) }}</template>
          </el-table-column>
          <el-table-column label="备注" prop="remark" min-width="160" />
          <el-table-column label="操作" width="140" fixed="right">
            <template #default="{ row }">
              <el-button link type="primary" @click="openDeptForm(row)">编辑</el-button>
              <el-button link type="danger" @click="removeDept(row)">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
      </el-tab-pane>
      <!-- æ ‡å‡†æ•°é‡é…ç½® -->
      <el-tab-pane label="标准数量" name="standard">
        <div class="tab-toolbar">
          <el-select v-model="standardQuery.deptType" placeholder="部门类型" clearable size="small" style="width: 140px" @change="loadStandard">
            <el-option label="成品部门" :value="1" />
            <el-option label="白盒部门" :value="2" />
          </el-select>
          <el-button type="primary" size="small" @click="openStandardForm()">新增</el-button>
        </div>
        <el-table :data="standardList" border size="small" max-height="400">
          <el-table-column label="部门类型" min-width="100">
            <template #default="{ row }">{{ deptTypeText(row.deptType) }}</template>
          </el-table-column>
          <el-table-column label="工序" prop="processName" min-width="110" />
          <el-table-column label="型号" prop="modelName" min-width="140" />
          <el-table-column label="标准数量" prop="standardQuantity" min-width="100" />
          <el-table-column label="单位" prop="unit" min-width="80" />
          <el-table-column label="操作" width="140" fixed="right">
            <template #default="{ row }">
              <el-button link type="primary" @click="openStandardForm(row)">编辑</el-button>
              <el-button link type="danger" @click="removeStandard(row)">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
      </el-tab-pane>
      <!-- æŠ˜ç®—率配置 -->
      <el-tab-pane label="折算率" name="factor">
        <div class="tab-toolbar">
          <el-select v-model="factorQuery.processId" placeholder="工序" clearable filterable size="small" style="width: 180px" @change="loadFactor">
            <el-option v-for="p in processOptions" :key="p.id" :label="p.name" :value="p.id" />
          </el-select>
          <el-button type="primary" size="small" @click="openFactorForm()">新增</el-button>
        </div>
        <el-table :data="factorList" border size="small" max-height="400">
          <el-table-column label="工序" prop="processName" min-width="110" />
          <el-table-column label="员工" prop="staffName" min-width="110" />
          <el-table-column label="小提琴折算率" prop="violinFactor" min-width="110" />
          <el-table-column label="大提琴折算率" prop="violaFactor" min-width="110" />
          <el-table-column label="贝斯折算率" prop="bassFactor" min-width="100" />
          <el-table-column label="操作" width="140" fixed="right">
            <template #default="{ row }">
              <el-button link type="primary" @click="openFactorForm(row)">编辑</el-button>
              <el-button link type="danger" @click="removeFactor(row)">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
      </el-tab-pane>
      <!-- æœˆåº¦å·¥ä½œæ—¥é…ç½® -->
      <el-tab-pane label="月度工作日" name="workday">
        <div class="tab-toolbar">
          <el-button type="primary" size="small" @click="openWorkdayForm()">新增</el-button>
          <span class="tip">未配置的月份默认按“周一~周五”计算</span>
        </div>
        <el-table :data="workdayList" border size="small" max-height="400">
          <el-table-column label="年月" prop="yearMonth" min-width="110" />
          <el-table-column label="工作日(天)" prop="workDays" min-width="100" />
          <el-table-column label="满勤工时(小时)" min-width="120">
            <template #default="{ row }">{{ (row.workDays || 0) * 8 }}</template>
          </el-table-column>
          <el-table-column label="备注" prop="remark" min-width="160" />
          <el-table-column label="操作" width="140" fixed="right">
            <template #default="{ row }">
              <el-button link type="primary" @click="openWorkdayForm(row)">编辑</el-button>
              <el-button link type="danger" @click="removeWorkday(row)">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
      </el-tab-pane>
    </el-tabs>
    <!-- éƒ¨é—¨ç±»åž‹è¡¨å• -->
    <el-dialog v-model="deptFormVisible" :title="deptForm.id ? '编辑部门类型' : '新增部门类型'" width="420px" append-to-body>
      <el-form :model="deptForm" label-width="90px">
        <el-form-item label="部门" required>
          <el-select v-model="deptForm.deptId" placeholder="请选择部门" filterable style="width: 100%">
            <el-option v-for="d in deptOptions" :key="d.deptId" :label="d.deptName" :value="d.deptId" />
          </el-select>
        </el-form-item>
        <el-form-item label="类型" required>
          <el-select v-model="deptForm.deptType" placeholder="请选择类型" style="width: 100%">
            <el-option label="成品部门" :value="1" />
            <el-option label="白盒部门" :value="2" />
          </el-select>
        </el-form-item>
        <el-form-item label="备注">
          <el-input v-model="deptForm.remark" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="deptFormVisible = false">取消</el-button>
        <el-button type="primary" @click="submitDept">确认</el-button>
      </template>
    </el-dialog>
    <!-- æ ‡å‡†æ•°é‡è¡¨å• -->
    <el-dialog v-model="standardFormVisible" :title="standardForm.id ? '编辑标准数量' : '新增标准数量'" width="460px" append-to-body>
      <el-form :model="standardForm" label-width="90px">
        <el-form-item label="部门类型" required>
          <el-select v-model="standardForm.deptType" placeholder="请选择" style="width: 100%">
            <el-option label="成品部门" :value="1" />
            <el-option label="白盒部门" :value="2" />
          </el-select>
        </el-form-item>
        <el-form-item label="工序" required>
          <el-select v-model="standardForm.processId" placeholder="请选择工序" filterable style="width: 100%">
            <el-option v-for="p in processOptions" :key="p.id" :label="p.name" :value="p.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="型号" :required="standardForm.deptType === 2">
          <el-select v-model="standardForm.productModelId" placeholder="白盒必填" filterable clearable style="width: 100%">
            <el-option v-for="m in modelOptions" :key="m.id" :label="m.model + '(' + (m.productName || '') + ')'" :value="m.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="标准数量" required>
          <el-input-number v-model="standardForm.standardQuantity" :min="0" :precision="4" style="width: 100%" />
        </el-form-item>
        <el-form-item label="单位">
          <el-input v-model="standardForm.unit" />
        </el-form-item>
        <el-form-item label="备注">
          <el-input v-model="standardForm.remark" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="standardFormVisible = false">取消</el-button>
        <el-button type="primary" @click="submitStandard">确认</el-button>
      </template>
    </el-dialog>
    <!-- æŠ˜ç®—率表单 -->
    <el-dialog v-model="factorFormVisible" :title="factorForm.id ? '编辑折算率' : '新增折算率'" width="460px" append-to-body>
      <el-form :model="factorForm" label-width="110px">
        <el-form-item label="工序" required>
          <el-select v-model="factorForm.processId" placeholder="请选择工序" filterable style="width: 100%">
            <el-option v-for="p in processOptions" :key="p.id" :label="p.name" :value="p.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="员工" required>
          <el-select v-model="factorForm.staffId" placeholder="请选择员工" filterable style="width: 100%" @change="onStaffChange">
            <el-option v-for="u in staffOptions" :key="u.userId" :label="u.nickName || u.userName" :value="u.userId" />
          </el-select>
        </el-form-item>
        <el-form-item label="小提琴折算率">
          <el-input-number v-model="factorForm.violinFactor" :min="0" :precision="4" style="width: 100%" />
        </el-form-item>
        <el-form-item label="大提琴折算率">
          <el-input-number v-model="factorForm.violaFactor" :min="0" :precision="4" style="width: 100%" />
        </el-form-item>
        <el-form-item label="贝斯折算率">
          <el-input-number v-model="factorForm.bassFactor" :min="0" :precision="4" style="width: 100%" />
        </el-form-item>
        <el-form-item label="备注">
          <el-input v-model="factorForm.remark" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="factorFormVisible = false">取消</el-button>
        <el-button type="primary" @click="submitFactor">确认</el-button>
      </template>
    </el-dialog>
    <!-- æœˆåº¦å·¥ä½œæ—¥è¡¨å• -->
    <el-dialog v-model="workdayFormVisible" :title="workdayForm.id ? '编辑月度工作日' : '新增月度工作日'" width="420px" append-to-body>
      <el-form :model="workdayForm" label-width="110px">
        <el-form-item label="年月" required>
          <el-date-picker v-model="workdayForm.yearMonth" type="month" value-format="YYYY-MM" placeholder="请选择年月" style="width: 100%" />
        </el-form-item>
        <el-form-item label="工作日(天)" required>
          <el-input-number v-model="workdayForm.workDays" :min="0" :max="31" :precision="0" style="width: 100%" />
        </el-form-item>
        <el-form-item label="备注">
          <el-input v-model="workdayForm.remark" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="workdayFormVisible = false">取消</el-button>
        <el-button type="primary" @click="submitWorkday">确认</el-button>
      </template>
    </el-dialog>
  </el-dialog>
</template>
<script setup>
  import { ref, reactive, getCurrentInstance } from "vue";
  import request from "@/utils/request";
  import {
    costingDeptList,
    costingDeptAdd,
    costingDeptUpdate,
    costingDeptDel,
    costingStandardList,
    costingStandardAdd,
    costingStandardUpdate,
    costingStandardDel,
    costingFactorList,
    costingFactorAdd,
    costingFactorUpdate,
    costingFactorDel,
    costingWorkdayList,
    costingWorkdayAdd,
    costingWorkdayUpdate,
    costingWorkdayDel,
    costingProcessOptions,
    costingModelOptions,
    costingDeptOptions,
  } from "@/api/productionManagement/productionCosting.js";
  const { proxy } = getCurrentInstance();
  const emit = defineEmits(["close"]);
  const visible = ref(false);
  const activeTab = ref("dept");
  const deptList = ref([]);
  const standardList = ref([]);
  const factorList = ref([]);
  const workdayList = ref([]);
  const processOptions = ref([]);
  const modelOptions = ref([]);
  const deptOptions = ref([]);
  const staffOptions = ref([]);
  const standardQuery = reactive({ deptType: undefined });
  const factorQuery = reactive({ processId: undefined });
  const deptTypeText = v => (v === 1 ? "成品部门" : v === 2 ? "白盒部门" : "-");
  /* ---------- åŠ è½½ ---------- */
  const loadDept = () => costingDeptList().then(res => (deptList.value = res.data || []));
  const loadStandard = () =>
    costingStandardList({ ...standardQuery }).then(res => (standardList.value = res.data || []));
  const loadFactor = () =>
    costingFactorList({ ...factorQuery }).then(res => (factorList.value = res.data || []));
  const loadWorkday = () => costingWorkdayList().then(res => (workdayList.value = res.data || []));
  const loadOptions = () => {
    costingProcessOptions().then(res => {
      processOptions.value = res?.data?.records || [];
    });
    costingModelOptions().then(res => {
      modelOptions.value = res?.records || [];
    });
    costingDeptOptions().then(res => {
      deptOptions.value = res?.data || [];
    });
    request({ url: "/system/user/list", method: "get", params: { pageNum: 1, pageSize: 1000 } }).then(res => {
      staffOptions.value = res?.rows || [];
    });
  };
  const openDialog = () => {
    visible.value = true;
    loadOptions();
    loadDept();
    loadStandard();
    loadFactor();
    loadWorkday();
  };
  const handleClose = () => emit("close");
  /* ---------- éƒ¨é—¨ç±»åž‹ ---------- */
  const deptFormVisible = ref(false);
  const deptForm = reactive({ id: null, deptId: null, deptType: 1, remark: "" });
  const openDeptForm = row => {
    Object.assign(deptForm, { id: null, deptId: null, deptType: 1, remark: "" }, row || {});
    deptFormVisible.value = true;
  };
  const submitDept = () => {
    if (!deptForm.deptId || !deptForm.deptType) return proxy.$modal.msgWarning("请填写部门与类型");
    const fn = deptForm.id ? costingDeptUpdate : costingDeptAdd;
    fn({ ...deptForm }).then(() => {
      proxy.$modal.msgSuccess("保存成功");
      deptFormVisible.value = false;
      loadDept();
    });
  };
  const removeDept = row => {
    proxy.$modal.confirm("确认删除该配置吗?").then(() => costingDeptDel(row.id)).then(() => {
      proxy.$modal.msgSuccess("删除成功");
      loadDept();
    }).catch(() => {});
  };
  /* ---------- æ ‡å‡†æ•°é‡ ---------- */
  const standardFormVisible = ref(false);
  const standardForm = reactive({
    id: null, deptType: 1, processId: null, productModelId: null, standardQuantity: 0, unit: "", remark: "",
  });
  const openStandardForm = row => {
    Object.assign(standardForm, {
      id: null, deptType: standardQuery.deptType || 1, processId: null, productModelId: null,
      standardQuantity: 0, unit: "", remark: "",
    }, row || {});
    standardFormVisible.value = true;
  };
  const submitStandard = () => {
    if (!standardForm.deptType || !standardForm.processId) return proxy.$modal.msgWarning("请填写部门类型与工序");
    if (standardForm.deptType === 2 && !standardForm.productModelId) return proxy.$modal.msgWarning("白盒部门必须选择型号");
    const fn = standardForm.id ? costingStandardUpdate : costingStandardAdd;
    fn({ ...standardForm }).then(() => {
      proxy.$modal.msgSuccess("保存成功");
      standardFormVisible.value = false;
      loadStandard();
    });
  };
  const removeStandard = row => {
    proxy.$modal.confirm("确认删除该配置吗?").then(() => costingStandardDel(row.id)).then(() => {
      proxy.$modal.msgSuccess("删除成功");
      loadStandard();
    }).catch(() => {});
  };
  /* ---------- æŠ˜ç®—率 ---------- */
  const factorFormVisible = ref(false);
  const factorForm = reactive({
    id: null, processId: null, staffId: null, staffName: "", violinFactor: 1, violaFactor: 1, bassFactor: 1, remark: "",
  });
  const openFactorForm = row => {
    Object.assign(factorForm, {
      id: null, processId: factorQuery.processId || null, staffId: null, staffName: "",
      violinFactor: 1, violaFactor: 1, bassFactor: 1, remark: "",
    }, row || {});
    factorFormVisible.value = true;
  };
  const onStaffChange = id => {
    const u = staffOptions.value.find(s => s.userId === id);
    factorForm.staffName = u ? u.nickName || u.userName : "";
  };
  const submitFactor = () => {
    if (!factorForm.processId || !factorForm.staffId) return proxy.$modal.msgWarning("请填写工序与员工");
    const fn = factorForm.id ? costingFactorUpdate : costingFactorAdd;
    fn({ ...factorForm }).then(() => {
      proxy.$modal.msgSuccess("保存成功");
      factorFormVisible.value = false;
      loadFactor();
    });
  };
  const removeFactor = row => {
    proxy.$modal.confirm("确认删除该配置吗?").then(() => costingFactorDel(row.id)).then(() => {
      proxy.$modal.msgSuccess("删除成功");
      loadFactor();
    }).catch(() => {});
  };
  /* ---------- æœˆåº¦å·¥ä½œæ—¥ ---------- */
  const workdayFormVisible = ref(false);
  const workdayForm = reactive({ id: null, yearMonth: "", workDays: 22, remark: "" });
  const openWorkdayForm = row => {
    Object.assign(workdayForm, { id: null, yearMonth: "", workDays: 22, remark: "" }, row || {});
    workdayFormVisible.value = true;
  };
  const submitWorkday = () => {
    if (!workdayForm.yearMonth) return proxy.$modal.msgWarning("请选择年月");
    if (workdayForm.workDays == null) return proxy.$modal.msgWarning("请填写工作日天数");
    const fn = workdayForm.id ? costingWorkdayUpdate : costingWorkdayAdd;
    fn({ ...workdayForm }).then(() => {
      proxy.$modal.msgSuccess("保存成功");
      workdayFormVisible.value = false;
      loadWorkday();
    });
  };
  const removeWorkday = row => {
    proxy.$modal.confirm("确认删除该配置吗?").then(() => costingWorkdayDel(row.id)).then(() => {
      proxy.$modal.msgSuccess("删除成功");
      loadWorkday();
    }).catch(() => {});
  };
  defineExpose({ openDialog });
</script>
<style scoped>
  .tab-toolbar {
    display: flex;
    gap: 10px;
    margin-bottom: 10px;
  }
  .tip {
    color: #909399;
    font-size: 12px;
    line-height: 32px;
  }
</style>
src/views/productionManagement/productionCosting/index.vue
@@ -1,579 +1,247 @@
<template>
  <div class="app-container">
    <div class="table_list">
      <el-row :gutter="16"
              class="content-row">
        <!-- å·¦ä¾§å°è´¦ + é¡¶éƒ¨ç­›é€‰ -->
        <el-col :xs="24"
                :sm="24"
                :md="24"
                :lg="8"
                :xl="8"
                class="left-col">
          <div class="left-panel">
            <el-alert
              title="工资核算口径:仅统计审核通过的报工数据。"
              type="info"
              :closable="false"
              show-icon
            />
            <div class="left-header">
              <el-form :model="searchForm"
                       inline>
                <el-form-item label="报工人员:">
                  <el-input v-model="searchForm.schedulingUserName"
                            placeholder="请输入"
                            clearable
                            style="width: 160px"
                            @change="reloadData" />
                </el-form-item>
                <el-form-item label="工单编号:">
                  <el-input v-model="searchForm.workOrderNo"
                            placeholder="请输入"
                            clearable
                            style="width: 160px"
                            @change="reloadData" />
                </el-form-item>
                <el-form-item prop="dateType">
                  <el-radio-group v-model="searchForm.dateType"
                                  size="small"
                                  @change="handleDateTypeChange">
                    <el-radio-button label="day">日</el-radio-button>
                    <el-radio-button label="month">月</el-radio-button>
                  </el-radio-group>
                </el-form-item>
                <el-form-item label="日期:"
                              prop="dateRange">
                  <el-date-picker v-model="searchForm.dateRange"
                                  :type="searchForm.dateType === 'day' ? 'date' : 'daterange'"
                                  range-separator="至"
                                  start-placeholder="开始日期"
                                  end-placeholder="结束日期"
                                  format="YYYY-MM-DD"
                                  value-format="YYYY-MM-DD"
                                  style="width: 200px"
                                  @change="handleDateRangeChange" />
                </el-form-item>
              </el-form>
            </div>
            <PIMTable rowKey="id"
                      :column="leftTableColumn"
                      :tableData="leftTableData"
                      :tableLoading="tableLoading"
                      :page="page"
                      @row-click="handleLeftRowClick"
                      @pagination="pagination"></PIMTable>
          </div>
        </el-col>
        <!-- å³ä¾§æ˜Žç»† -->
        <el-col :xs="24"
                :sm="24"
                :md="24"
                :lg="16"
                :xl="16"
                class="right-col">
          <div class="right-panel">
            <div class="right-toolbar">
              <el-button type="primary" @click="handleBatchAccounting">批量核算</el-button>
            </div>
            <PIMTable rowKey="id"
                      :column="tableColumn"
                      :tableData="tableData"
                      :page="page1"
                      :isSelection="true"
                      @selection-change="handleSelectionChange"
                      :tableLoading="tableLoading1"
                      style="margin-right: 20px;"
                      @pagination="pagination1"></PIMTable>
          </div>
        </el-col>
      </el-row>
    </div>
    <el-dialog v-model="adjustDialogVisible"
               title="单个修正"
               width="520px">
      <el-form :model="adjustForm" label-width="120px">
        <el-form-item label="报工单号">
          <el-input v-model="adjustForm.reportNo" disabled />
    <!-- é¡¶éƒ¨ç­›é€‰ + ä¿¡æ¯ -->
    <el-card shadow="never" class="filter-card">
      <el-form :model="searchForm" inline>
        <el-form-item label="部门:">
          <el-radio-group v-model="searchForm.deptType" @change="handleQuery">
            <el-radio-button :label="1">成品部门</el-radio-button>
            <el-radio-button :label="2">白盒部门</el-radio-button>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="报工人员">
          <el-input v-model="adjustForm.schedulingUserName" disabled />
        <el-form-item label="月份:">
          <el-date-picker
            v-model="searchForm.yearMonth"
            type="month"
            value-format="YYYY-MM"
            placeholder="请选择月份"
            style="width: 160px"
            @change="handleQuery"
          />
        </el-form-item>
        <el-form-item label="修正金额">
          <el-input-number v-model="adjustForm.wages" :min="0" :precision="2" style="width: 100%" />
        <el-form-item label="生产人:">
          <el-input
            v-model="searchForm.staffName"
            placeholder="请输入"
            clearable
            style="width: 160px"
            @keyup.enter="handleQuery"
            @clear="handleQuery"
          />
        </el-form-item>
        <el-form-item label="修正说明">
          <el-input v-model="adjustForm.remark" type="textarea" :rows="3" />
        <el-form-item>
          <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
          <el-button icon="Setting" @click="openConfig">配置</el-button>
          <el-button icon="Download" @click="handleExport">导出</el-button>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="adjustDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="confirmAdjust">确认</el-button>
      </template>
    </el-dialog>
      <el-alert
        :title="infoText"
        type="info"
        :closable="false"
        show-icon
      />
    </el-card>
    <el-row :gutter="16" class="content-row">
      <!-- å·¦ï¼šæŒ‰å‘˜å·¥æ±‡æ€» -->
      <el-col :xs="24" :sm="24" :md="24" :lg="9" :xl="9">
        <div class="panel-title">汇总台账({{ searchForm.deptType === 1 ? '成品部门' : '白盒部门' }})</div>
        <PIMTable
          rowKey="staffId"
          :column="summaryColumns"
          :tableData="summaryData"
          :tableLoading="loading"
          :isShowPagination="false"
          :highlightCurrentRow="true"
          @row-click="handleSummaryRowClick"
        />
      </el-col>
      <!-- å³ï¼šæ˜Žç»† -->
      <el-col :xs="24" :sm="24" :md="24" :lg="15" :xl="15">
        <div class="panel-title">
          æ˜Žç»†{{ detailStaffName ? '(' + detailStaffName + ')' : '' }}
        </div>
        <PIMTable
          rowKey="rowKey"
          :column="detailColumns"
          :tableData="detailData"
          :tableLoading="loadingDetail"
          :isShowPagination="false"
        />
      </el-col>
    </el-row>
    <!-- é…ç½®å¼¹çª— -->
    <CostConfigDialog ref="configDialog" @close="handleQuery" />
  </div>
</template>
<script setup>
  import { onMounted, ref, reactive, toRefs, getCurrentInstance } from "vue";
  import { ElMessageBox } from "element-plus";
  import dayjs from "dayjs";
  import { onMounted, ref, reactive, computed, getCurrentInstance } from "vue";
  import CostConfigDialog from "./components/CostConfigDialog.vue";
  import {
    salesLedgerProductionAccountingListProductionDetails,
    salesLedgerProductionAccountingList,
    productionAccountingBatchAccounting,
    productionAccountingSingleAdjust,
    costingSummary,
    costingDetail,
    costingWorkday,
  } from "@/api/productionManagement/productionCosting.js";
  const { proxy } = getCurrentInstance();
  const formatTag = (val, map) => {
    const item = map[String(val)];
    if (!item) return "-";
    return item.label;
  };
  const tableColumn = ref([
    {
      label: "报工单号",
      prop: "reportNo",
      minWidth: 140,
    },
    {
      label: "生产日期",
      prop: "schedulingDate",
      minWidth: 100,
    },
    {
      label: "生产人",
      prop: "schedulingUserName",
      minWidth: 100,
    },
    {
      label: "审核状态",
      prop: "auditStatus",
      minWidth: 100,
      dataType: "tag",
      formatData: val => formatTag(val, {
        0: { label: "待审核" },
        1: { label: "审核通过" },
        2: { label: "审核不通过" },
      }),
      formatType: val => {
        const typeMap = {
          0: "warning",
          1: "success",
          2: "danger",
        };
        return typeMap[String(val)] || "info";
      },
    },
    {
      label: "报工类型",
      prop: "reportType",
      minWidth: 100,
      dataType: "tag",
      formatData: val => formatTag(val, {
        0: { label: "合格" },
        1: { label: "轻微返工" },
        2: { label: "严重返工" },
        3: { label: "报废" },
      }),
      formatType: val => {
        const typeMap = {
          0: "success",
          1: "warning",
          2: "danger",
          3: "info",
        };
        return typeMap[String(val)] || "info";
      },
    },
    {
      label: "销售合同号",
      prop: "salesContractNo",
      minWidth: 140,
    },
    {
      label: "产品大类",
      prop: "productName",
      minWidth: 100,
    },
    {
      label: "产品标准数量",
      prop: "productStandardQuantity",
      minWidth: 120,
    },
    {
      label: "规格型号",
      prop: "productModelName",
      minWidth: 100,
    },
    {
      label: "单位",
      prop: "unit",
      minWidth: 100,
    },
    {
      label: "工序",
      prop: "process",
      minWidth: 100,
    },
    {
      label: "标准数量",
      prop: "standardQuantity",
      minWidth: 100,
    },
    {
      label: "工单编号",
      prop: "workOrderNo",
      minWidth: 140,
    },
    {
      label: "补修理工时",
      prop: "repairWorkHour",
      minWidth: 120,
    },
    {
      label: "生产数量",
      prop: "quantity",
      minWidth: 100,
    },
    {
      label: "折算工时",
      prop: "convertedWorkHours",
      minWidth: 100,
    },
  ]);
  // å·¦ä¾§æ±‡æ€»å°è´¦åˆ—(生产人、产量、工资、合格率)
  const leftTableColumn = ref([
    {
      label: "生产人",
      prop: "schedulingUserName",
      minWidth: 100,
    },
    {
      label: "产量",
      prop: "finishedNum",
      minWidth: 100,
    },
    {
      label: "折算工时合计",
      prop: "workHours",
      minWidth: 100,
    },
    {
      label: "合格率",
      prop: "outputRate",
      minWidth: 100,
      formatData: val => {
        if (val == null || val === "") return "-";
        return parseFloat(val).toFixed(2) + "%";
      },
    },
  ]);
  const tableData = ref([]);
  const tableLoading = ref(false);
  const tableLoading1 = ref(false);
  const leftTableData = ref([]);
  const selectedRows = ref([]);
  const adjustDialogVisible = ref(false);
  const adjustForm = reactive({
    id: null,
    reportNo: "",
    schedulingUserName: "",
    wages: 0,
    remark: "",
  });
  // æ—¥ / æœˆ åˆ‡æ¢ï¼ˆé»˜è®¤æŒ‰æ—¥ï¼‰
  const page = reactive({
    current: 1,
    size: 100,
    total: 0,
  });
  const page1 = reactive({
    current: 1,
    size: 100,
    total: 0,
  });
  const data = reactive({
    searchForm: {
      schedulingUserName: "",
      workOrderNo: "",
      salesContractNo: "",
      dateType: "day",
      dateRange: dayjs().format("YYYY-MM-DD"),
      entryDate: dayjs().format("YYYY-MM-DD"),
      entryDateStart: undefined,
      entryDateEnd: undefined,
      deptType: 1,
      yearMonth: "",
      staffName: "",
    },
  });
  const { searchForm } = toRefs(data);
  const { searchForm } = data;
  const pagination = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  const summaryData = ref([]);
  const detailData = ref([]);
  const loading = ref(false);
  const loadingDetail = ref(false);
  const detailStaffName = ref("");
  const workInfo = reactive({ workDays: 0, dayHours: 8, monthHours: 0 });
  const pagination1 = obj => {
    page1.current = obj.page;
    page1.size = obj.limit;
    getList1();
  };
  const configDialog = ref();
  const handleSelectionChange = selection => {
    selectedRows.value = selection || [];
  };
  const infoText = computed(
    () =>
      `当月工作日 ${workInfo.workDays} å¤© Â· æ¯æ—¥ ${workInfo.dayHours} å°æ—¶ Â· æ»¡å‹¤å·¥æ—¶ ${workInfo.monthHours} å°æ—¶ï¼ˆæŠ˜åˆå€æ•° = æœˆæ—¶é—´åˆè®¡ Ã· (工作日 Ã— 8))`
  );
  const handleDateRangeChange = value => {
    if (value) {
      if (searchForm.value.dateType === "day") {
        searchForm.value.entryDate = value;
      } else {
        searchForm.value.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
        searchForm.value.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
      }
    } else {
      searchForm.value.entryDate = undefined;
      searchForm.value.entryDateStart = undefined;
      searchForm.value.entryDateEnd = undefined;
    }
    reloadData();
  };
  const summaryColumns = ref([
    { label: "生产人", prop: "staffName", minWidth: 100 },
    { label: "产量", prop: "output", minWidth: 90 },
    { label: "月时间合计", prop: "monthTimeTotal", minWidth: 110 },
    {
      label: "折合倍数",
      prop: "conversionRatio",
      minWidth: 90,
      formatData: v => (v == null ? "-" : Number(v).toFixed(2)),
    },
  ]);
  const getList = () => {
    tableLoading.value = true;
    const params = { ...searchForm.value, ...page };
    salesLedgerProductionAccountingList(params)
      .then(res => {
        const records = res.data.records || [];
        leftTableData.value = records;
        page.total = res.data.total || 0;
      })
      .finally(() => {
        tableLoading.value = false;
      });
  };
  const getList1 = () => {
    tableLoading1.value = true;
    const params = { ...page1, ...searchForm.value };
    salesLedgerProductionAccountingListProductionDetails(params)
      .then(res => {
        tableData.value = res.data.records || [];
        page1.total = res.data.total || 0;
      })
      .finally(() => {
        tableLoading1.value = false;
      });
  };
  // æž„建左侧汇总台账(按生产人汇总产量、工资等)
  const buildLeftTableData = records => {
    const map = {};
    records.forEach(item => {
      const key = item.schedulingUserName || "未知";
      if (!map[key]) {
        map[key] = {
          id: key,
          schedulingUserName: key,
          finishedNum: 0,
          wages: 0,
          qualifiedRate: item.qualifiedRate ?? null,
        };
      }
      map[key].finishedNum += Number(item.finishedNum || 0);
      map[key].wages += Number(item.wages || 0);
      if (item.qualifiedRate != null) {
        map[key].qualifiedRate = item.qualifiedRate;
      }
    });
    leftTableData.value = Object.values(map);
  };
  // å·¦ä¾§æ—¥/月切换
  const handleDateTypeChange = value => {
    // è¿™é‡Œåªä½œä¸ºç­›é€‰æ¡ä»¶çš„一部分,直接重新查询列表
    if (value === "day") {
      searchForm.value.entryDate = dayjs().format("YYYY-MM-DD");
      searchForm.value.dateRange = searchForm.value.entryDate;
    } else {
      searchForm.value.entryDateStart = dayjs()
        .startOf("month")
        .format("YYYY-MM-DD");
      searchForm.value.entryDateEnd = dayjs().endOf("month").format("YYYY-MM-DD");
      searchForm.value.dateRange = [
        searchForm.value.entryDateStart,
        searchForm.value.entryDateEnd,
  const detailColumns = computed(() => {
    if (searchForm.deptType === 2) {
      return [
        { label: "生产人", prop: "staffName", minWidth: 90 },
        { label: "工序", prop: "processName", minWidth: 90 },
        { label: "产品大类", prop: "productName", minWidth: 100 },
        { label: "型号/零件", prop: "modelName", minWidth: 140 },
        { label: "月产量", prop: "quantity", minWidth: 90 },
        { label: "标准数量", prop: "standardQuantity", minWidth: 90 },
        { label: "折算时长", prop: "convertedHours", minWidth: 90 },
      ];
    }
    return [
      { label: "生产人", prop: "staffName", minWidth: 90 },
      { label: "工序", prop: "processName", minWidth: 80 },
      { label: "小提琴数量", prop: "violinQty", minWidth: 90 },
      { label: "大提琴数量", prop: "celloQty", minWidth: 90 },
      { label: "贝斯数量", prop: "bassQty", minWidth: 80 },
      { label: "大提折算率", prop: "celloFactor", minWidth: 90 },
      { label: "贝斯折算率", prop: "bassFactor", minWidth: 90 },
      { label: "月产折合小提琴", prop: "foldedQuantity", minWidth: 120 },
      { label: "标准数量", prop: "standardQuantity", minWidth: 90 },
      { label: "折算时长", prop: "convertedHours", minWidth: 90 },
    ];
  });
    reloadData();
  const defaultMonth = () => {
    const d = new Date();
    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
  };
  const reloadData = () => {
    page.current = 1;
    page1.current = 1;
    getList();
    tableData.value = [];
  const buildParams = () => ({
    deptType: searchForm.deptType,
    yearMonth: searchForm.yearMonth,
    staffName: detailStaffName.value || undefined,
  });
  const fetchWorkday = () => {
    costingWorkday(searchForm.yearMonth).then(res => {
      const d = res.data || {};
      workInfo.workDays = d.workDays || 0;
      workInfo.dayHours = d.dayHours || 8;
      workInfo.monthHours = d.monthHours || 0;
    });
  };
  // ç‚¹å‡»å·¦ä¾§è¡Œï¼Œåˆ·å³ä¾§æ˜Žç»†ï¼ˆæŒ‰ç”Ÿäº§äººè¿‡æ»¤ï¼‰
  const handleLeftRowClick = row => {
    searchForm.value.schedulingUserName = row.schedulingUserName || "";
    handleQuery();
  const getSummary = () => {
    loading.value = true;
    costingSummary({
      deptType: searchForm.deptType,
      yearMonth: searchForm.yearMonth,
      staffName: searchForm.staffName || undefined,
    })
      .then(res => {
        summaryData.value = res.data || [];
      })
      .finally(() => {
        loading.value = false;
      });
  };
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
  const getDetail = () => {
    loadingDetail.value = true;
    costingDetail(buildParams())
      .then(res => {
        detailData.value = (res.data || []).map((item, idx) => ({
          ...item,
          rowKey: `${item.staffId}-${item.processId}-${item.productModelId ?? ""}-${idx}`,
        }));
      })
      .finally(() => {
        loadingDetail.value = false;
      });
  };
  const handleQuery = () => {
    page1.current = 1;
    getList1();
    detailStaffName.value = "";
    getSummary();
    getDetail();
    fetchWorkday();
  };
  // å¯¼å‡º
  const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        proxy.download(
          "/productionAccount/exportSalarySlip",
          { ...searchForm.value },
          "生产工资条.xlsx"
        );
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  const handleSummaryRowClick = row => {
    detailStaffName.value = row.staffName || "";
    getDetail();
  };
  const handleBatchAccounting = () => {
    ElMessageBox.confirm("确定要按当前筛选条件执行批量核算吗?", "批量核算", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        const { dateRange, ...batchPayload } = searchForm.value;
        return productionAccountingBatchAccounting(batchPayload);
      })
      .then(() => {
        proxy.$modal.msgSuccess("批量核算成功");
        reloadData();
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  const openConfig = () => {
    configDialog.value?.openDialog();
  };
  const handleSingleAdjust = () => {
    if (!selectedRows.value.length) {
      proxy.$modal.msgWarning("请先勾选一条记录");
      return;
    }
    if (selectedRows.value.length > 1) {
      proxy.$modal.msgWarning("单个修正只能选择一条记录");
      return;
    }
    const row = selectedRows.value[0];
    adjustForm.id = row.id;
    adjustForm.reportNo = row.reportNo || "";
    adjustForm.schedulingUserName = row.schedulingUserName || "";
    adjustForm.wages = Number(row.wages || 0);
    adjustForm.remark = "";
    adjustDialogVisible.value = true;
  };
  const confirmAdjust = () => {
    const selectedRow = selectedRows.value[0];
    if (!selectedRow || !selectedRow.userId) {
      proxy.$modal.msgWarning("当前记录缺少报工人员信息,无法修正");
      return;
    }
    if (!adjustForm.wages && adjustForm.wages !== 0) {
      proxy.$modal.msgWarning("请输入修正金额");
      return;
    }
    productionAccountingSingleAdjust({
      productionProductMainId: adjustForm.id,
      schedulingUserId: selectedRow.userId,
      schedulingUserName: adjustForm.schedulingUserName,
      adjustAmount: adjustForm.wages,
      adjustRemark: adjustForm.remark,
      adjustUser: proxy.$store?.state?.user?.userId || undefined,
    }).then(() => {
      proxy.$modal.msgSuccess("修正成功");
      adjustDialogVisible.value = false;
      reloadData();
    }).catch(() => {});
  };
  const handleRowAdjust = row => {
    selectedRows.value = [row];
    handleSingleAdjust();
  const handleExport = () => {
    proxy.download(
      "/productionCosting/export",
      {
        deptType: searchForm.deptType,
        yearMonth: searchForm.yearMonth,
        staffName: detailStaffName.value || undefined,
      },
      "生产核算明细.xlsx"
    );
  };
  onMounted(() => {
    getList();
    searchForm.yearMonth = defaultMonth();
    handleQuery();
  });
</script>
<style scoped lang="scss">
  .filter-card {
    margin-bottom: 12px;
  }
  .content-row {
    width: 100%;
  }
  .content-row .left-col,
  .content-row .right-col {
    margin-bottom: 16px;
  }
  .left-panel,
  .right-panel {
    display: flex;
    flex-direction: column;
    gap: 10px;
    min-width: 0;
  }
  .right-toolbar {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
  }
  .left-header {
    display: flex;
    align-items: center;
    gap: 12px;
  }
  .left-title {
    font-size: 16px;
    color: #ffffff;
  }
  .header-filters {
    display: flex;
    align-items: center;
    flex: 1;
    justify-content: flex-end;
    gap: 8px;
  }
  .search_title {
    color: #ffffff;
  }
  .ml10 {
    margin-left: 10px;
  .panel-title {
    font-weight: 600;
    margin-bottom: 8px;
    color: #303133;
  }
</style>