wujianwei
6 天以前 89fd2cf7202c321512c2ea699a3a220a7138ed44
ui/admin-ui3/src/views/cwgl/voucherSubjectSetting/index.vue
@@ -10,30 +10,39 @@
      @search-reset="searchReset" @selection-change="selectionChange" @current-change="currentChange"
      @size-change="sizeChange" @on-load="onLoad">
      <template #menu-left>
        <el-button type="success" icon="Edit" :disabled="pageF.single" v-hasPermi="['cwgl:voucherSubjectSetting:edit']"
        <!-- <el-button type="success" icon="Edit" :disabled="pageF.single" v-hasPermi="['cwgl:voucherSubjectSetting:edit']"
          @click="handleUpdate">修改
        </el-button>
        <el-button type="danger" icon="Delete" :disabled="pageF.multiple" @click="handleDelete"
          v-hasPermi="['cwgl:voucherSubjectSetting:remove']">删除
        </el-button>
        </el-button> -->
        <el-button type="warning" plain icon="Download" @click="handleExport"
          v-hasPermi="['cwgl:voucherSubjectSetting:export']">导出
        </el-button>
      </template>
      <template #menu="{ row, index, size }">
        <el-button type="primary" text icon="Plus" @click="handleRowAdd(row)">新增子项</el-button>
        <el-button type="primary" text icon="Plus" v-hasPermi="['cwgl:voucherSubjectSetting:addz']"
          @click="handleRowAdd(row)">新增子项</el-button>
        <el-button type="text" icon="View" @click="handleFlow(row)"
          v-hasPermi="['cwgl:voucherSubjectSetting:flow']">日志</el-button>
      </template>
    </avue-crud>
  </basicContainer>
  <OperationLogModal ref="logModalRef" />
</template>
<script setup name="voucherSubjectSetting" lang="ts">
import { listVoucherSubjectSettingLog, } from "@/api/cwgl/voucherSubjectSettingLog";
import { VoucherSubjectSettingI, addVoucherSubjectSetting, delVoucherSubjectSetting, exportVoucherSubjectSetting, getVoucherSubjectSetting, listVoucherSubjectSetting, updateVoucherSubjectSetting } from "@/api/cwgl/voucherSubjectSetting";
import useCurrentInstance from "@/utils/useCurrentInstance";
import { computed, reactive, ref, toRefs } from "vue";
import { PagesInterface, PageQueryInterface } from "@/utils/globalInterface";
import { usePagePlus } from "@/hooks/usePagePlus";
import { hasPermission } from "@/utils/permissionUtils";
import OperationLogModal from '@/components/OperationLogModal/index.vue';
const { proxy } = useCurrentInstance();
const crudRef = ref();
@@ -53,6 +62,7 @@
  form: <VoucherSubjectSettingI>{},
  queryParams: <VoucherSubjectSettingI & PageQueryInterface>{
    type: "0",
    accountingItemsContains: [], // 必须初始化为数组
  },
  page: <PagesInterface>{
    pageSize: 10,
@@ -65,6 +75,10 @@
const option = ref({
  pageKey: 'VoucherSubjectSetting',
  rowKey: 'id',
  tree: true,           // 开启树形表格模式
  childrenHandler: 'children', // 指定子节点字段名,默认为 children
  expandAll: true,     // 是否默认展开所有行
  viewBtn: false,
  searchSpan: 5,
  labelWidth: 150,
  searchLabelWidth: 120,
@@ -91,10 +105,10 @@
    //     }
    //   ],
    // },
    accountSet: {
    type: {
      label: '账套',
      search: true,
      minWidth: 120,
      minWidth: 220,
      addDisabled: true,
      editDisabled: true,
      type: 'select', dataType: 'string', dicUrl: '/system/dict/data/type/sys_accounting_type',
@@ -106,62 +120,49 @@
      ],
    },
    // parentSubjectName: {
    //   label: '上级科目名称',
    //   prop: 'parentSubjectCode',
    //   type: 'cascader',      // 级联选择器
    //   dataType: 'string',
    //   // dicUrl: '/api/cwgl/subject/tree',
    //   placeholder: '请选择上级科目名称',
    //   // --- 控制显示逻辑 ---
    //   hide: true,            // 在表格列中隐藏
    //   search: false,         // 在搜索栏中隐藏
    //   display: true,         // 在新增/修改弹窗中显示(默认为true)
    //   // --------------------
    //   filterable: true,      // 开启搜索过滤
    //   checkStrictly: true,   // 允许选择任意一级(可选父级)
    //   emitPath: false,       // 提交时只保留最后一级的值
    //   props: {
    //     label: 'name',
    //     value: 'code',
    //     children: 'children'
    //   },
    //   label: '上级科目名称2',
    //   minWidth: 150,
    //   // rules: [
    //   //   {
    //   //     required: true,
    //   //     message: "上级科目名称不能为空", trigger: "blur"
    //   //   }
    //   // ],
    //   /*  */
    //   search: true,
    //   rules: [
    //     {
    //       required: true,
    //       message: "科目名称不能为空", trigger: "blur"
    //     }
    //   ],
    // },
    parentSubjectName: {
    parentId: {
      label: '上级科目名称',
      minWidth: 150,
      search: true,
      prop: 'parentId',
      type: 'tree',
      hide: true,
      dicData: [],
      props: {
        label: 'fullName', // 关键点:绑定我们预处理好的全路径字段
        value: 'id'
      },
      addDisabled: true,
      editDisabled: true,
      rules: [
        {
          required: true,
          message: "科目名称不能为空", trigger: "blur"
        }
      ],
        { required: true, message: "上级科目不能为空", trigger: "change" }
      ]
    },
    parentSubjectCode: {
      label: '上级科目代码',
      minWidth: 150,
      search: true,
      rules: [
        {
          required: true,
          message: "科目名称不能为空", trigger: "blur"
        }
      ],
      disabled: true, // 设置为禁用,仅供查看
      // 如果希望在表格也显示,这里不要写 hide: true
    },
    parentSubjectCode: {
      label: '上级科目代码',
      minWidth: 150,
      hide: true,
      // rules: [
      //   {
      //     required: true,
      //     message: "科目名称不能为空", trigger: "blur"
      //   }
      // ],
    },
    // parentSubjectCode: {
    //   label: '上级科目代码',
@@ -180,16 +181,21 @@
    //   //   }
    //   // ],
    // },
    // 在 option.value.column 中找到 subjectName
    // 找到 option 里的 subjectName 列
    subjectName: {
      label: '科目名称',
      minWidth: 150,
      prop: 'subjectName', // 保持原始 prop,这样弹窗里的输入框才是干净的名称
      minWidth: 250,       // 拼接后会很长,建议调大宽度
      search: true,
      overHidden: true,
      // --- 使用 formatter 来控制表格界面的显示 ---
      formatter: (row: any) => {
        // 优先显示我们递归生成的拼接字段,如果没有(比如刚新增还没刷新的数据),显示原始名称
        return row.subjectNameWithParent || row.subjectName;
      },
      rules: [
        {
          required: true,
          message: "科目名称不能为空", trigger: "blur"
        }
        { required: true, message: "科目名称不能为空", trigger: "blur" }
      ],
    },
    subjectCode: {
@@ -235,7 +241,7 @@
      editDisplay: false,
    },
    accountingItemsContains: {
      label: '核算项目1',
      label: '核算项目',
      minWidth: 150,
      search: true,
      type: 'select', // 确保类型为 select
@@ -252,11 +258,10 @@
      }
    },
    accountingItems: {
      label: '核算项目2',
      label: '核算项目',
      minWidth: 150,
      type: 'select',
      multiple: true,
      dataType: 'string',
      dicUrl: '/system/dict/data/type/sys_accounting_item_name',
      placeholder: '请选择核算项目(可多选)',
      props: {
@@ -390,7 +395,7 @@
  }
})
// sys_accounting_type
// listVoucherSubjectSetting
const { tableData, pageF, rowSave, rowUpdate, rowDel, beforeOpen, searchChange,
  searchReset, selectionChange, onLoad, currentChange, sizeChange, handleDelete, handleExport, handleUpdate, refreshChange } = usePagePlus({
    form: form,
@@ -400,10 +405,77 @@
    page: page.value,
    getListApi: listVoucherSubjectSetting,
    getDetailApi: getVoucherSubjectSetting,
    // 【关键修复点】:拦截详情接口,在数据进入 form 之前完成格式化
    getDetailApi: (id: any) => {
      return getVoucherSubjectSetting(id).then((res: any) => {
        console.log(res);
        if (res.code === 200 && res.data) {
          // 1. 处理 accountingItems 数字转数组
          // 1. 如果 accountSet 是数字,转为字符串
          res.data.accountSet = res.data.accountSet ? String(res.data.accountSet) : "0";
          if (typeof res.data.accountingItems === 'number') {
            res.data.accountingItems = decomposeAccountingItems(res.data.accountingItems);
          } else if (!res.data.accountingItems) {
            res.data.accountingItems = [];
          }
          // 2. 处理 accountingItemsContains 确保是数组
          if (!Array.isArray(res.data.accountingItemsContains)) {
            res.data.accountingItemsContains = [];
          }
        }
        return res;
      });
    },
    exportApi: exportVoucherSubjectSetting,
    deleteApi: delVoucherSubjectSetting,
    addApi: addVoucherSubjectSetting,
    updateApi: updateVoucherSubjectSetting,
    getListFunc: (res: any) => {
      // 1. 转换树结构
      const tree = proxy.handleTree(res.rows, "id");
      // 2. 递归拼接名称函数 (核心修改)
      const recursionFormatName = (list: any[], pName = '') => {
        list.forEach(item => {
          // 这里的逻辑:如果有父级名称,拼在一起;否则只显示自己
          item.subjectNameWithParent = pName ? `${pName} / ${item.subjectName}` : item.subjectName;
          // 如果有子节点,继续递归,把当前的拼接结果传给子节点作为 pName
          if (item.children && item.children.length > 0) {
            recursionFormatName(item.children, item.subjectNameWithParent);
          }
        });
      };
      // 执行递归
      recursionFormatName(tree);
      // 3. 赋值给表格数据
      tableData.value = tree;
      // 4. 递归展开所有层级 (你原有的逻辑)
      nextTick(() => {
        if (crudRef.value) {
          const tableMethods = crudRef.value.$refs.table;
          if (tableMethods && tableData.value) {
            const expandAllNodes = (list: any[]) => {
              list.forEach(row => {
                if (row.children && row.children.length > 0) {
                  tableMethods.toggleRowExpansion(row, true);
                  expandAllNodes(row.children);
                }
              });
            };
            expandAllNodes(tableData.value);
          }
        }
      });
    },
    handleUpdateFunc: () => {
      isFromRow.value = true; // 修改操作也视为禁用上级科目
      crudRef.value.rowEdit(selectionList.value[0]);
@@ -440,75 +512,132 @@
      queryParams.value = newParams;
      return newParams;
    },
   handleBeforeOpenFunc: (type: string) => {
  if (!option.value?.column) return;
  const nameCol = option.value.column.parentSubjectName;
  const codeCol = option.value.column.parentSubjectCode;
    handleBeforeOpenFunc: (type: string) => {
      if (!option.value?.column) return;
  // --- 1. 新增逻辑修改 ---
  if (type === 'add') {
    if (isFromRow.value) {
      // 行内“新增子项”点击
      nameCol.disabled = true;
      codeCol.disabled = true;
      option.value.saveBtnText = '新增';
      // 赋值当前页签的账套(虽然行内新增通常会带入父级账套,但这里显式同步一次)
      form.value.accountSet = activeAccountSet.value;
    } else {
      // 顶部“新增”按钮点击
      nameCol.disabled = false;
      codeCol.disabled = false;
      option.value.saveBtnText = '保 存';
      // 【关键修复】先清空可能残留的旧数据(如 ID、科目代码等)
      Object.keys(form.value).forEach(key => delete form.value[key]);
      // 【核心要求】将页签的值赋值给表单账套字段
      form.value.accountSet = activeAccountSet.value;
      form.value.type = activeAccountSet.value;
      const codeCol = option.value.column.parentSubjectCode;
      const parentIdCol = option.value.column.parentId;
      // 初始化其他必要字段
      form.value.accountingItems = [];
      form.value.enabled = "1"; // 默认启用
    }
  }
      // --- 1. 定义名称递归处理器 ---
      const formatTreeData = (list: any[], parentName = '') => {
        return list.map(item => {
          // 拼接全路径:父级名称 > 当前名称
          const currentFullName = parentName ? `${parentName} > ${item.subjectName}` : item.subjectName;
          const newItem = {
            ...item,
            fullName: currentFullName
          };
          // 如果有子节点,继续递归
          if (newItem.children && newItem.children.length > 0) {
            newItem.children = formatTreeData(newItem.children, currentFullName);
          }
          return newItem;
        });
      };
  // --- 2. 修改/查看逻辑(保持原样) ---
  else if (type === 'edit' || type === 'view') {
    nameCol.disabled = true;
    codeCol.disabled = true;
    option.value.updateBtnText = '修 改';
      // --- 2. 加载并处理字典数据 ---
      listVoucherSubjectSetting({ type: activeAccountSet.value }).then(res => {
        // 将扁平数据转为树结构 (proxy.handleTree 是 Ruoyi 常用工具)
        const treeData = proxy.handleTree(res.rows, "id", "parentId");
        // 执行全路径拼接
        const fullPathTree = formatTreeData(treeData);
    if (typeof form.value.accountingItems === 'number') {
      form.value.accountingItems = decomposeAccountingItems(form.value.accountingItems);
    } else if (!form.value.accountingItems) {
      form.value.accountingItems = [];
    }
  }
        // 组合最终字典,加入“顶级”选项
        const fullDic = [
          {
            id: 0,
            subjectName: '顶级科目',
            fullName: '顶级科目',
            children: fullPathTree
          }
        ];
        if (parentIdCol) parentIdCol.dicData = fullDic;
  nextTick(() => {
    isFromRow.value = false;
  });
},
        // 异步回调中再次确保代码字段显示
        if (String(form.value.parentId) === "0" || !form.value.parentId) {
          form.value.parentSubjectCode = "顶级科目代码";
        }
      });
      // --- 3. 处理不同模式下的显示逻辑 ---
      if (type !== 'add') {
        // 【修改/查看】
        if (String(form.value.parentId) === "0" || !form.value.parentId) {
          form.value.parentSubjectCode = "顶级科目代码";
        }
        if (parentIdCol) { parentIdCol.display = true; parentIdCol.disabled = true; }
        if (codeCol) { codeCol.display = true; codeCol.disabled = true; }
      } else {
        // 【新增】
        if (isFromRow.value) {
          // 行内新增子项
          if (parentIdCol) { parentIdCol.display = true; parentIdCol.disabled = true; }
          if (codeCol) { codeCol.display = true; codeCol.disabled = true; }
        } else {
          // 顶部新增 (默认是顶级科目)
          if (parentIdCol) parentIdCol.display = false;
          if (codeCol) codeCol.display = false;
          const currentSet = activeAccountSet.value;
          Object.assign(form.value, {
            id: undefined,
            parentId: 0,
            parentSubjectCode: "无",
            accountSet: currentSet,
            type: currentSet,
            accountingItems: [],
            enabled: "1"
          });
        }
      }
      // --- 4. 格式化多选字段 ---
      const fieldsToFix = ['accountingItems', 'accountingItemsContains'];
      fieldsToFix.forEach(field => {
        const val = form.value[field];
        if (typeof val === 'number') {
          form.value[field] = decomposeAccountingItems(val);
        } else if (!Array.isArray(val)) {
          form.value[field] = [];
        }
      });
      nextTick(() => {
        isFromRow.value = false;
      });
    },
    // 新增保存前的逻辑
    rowSaveBegin: (row: any, done: any, loading: any) => {
      processAccountingItems(row);
      delete row.accountingItemsContains;
      // 如果 row 对象里带了 id,新增接口可能会报错或变成修改,如果是顶部新增,确保没有 id
      // --- 核心逻辑开始 ---
      if (!isFromRow.value) {
        // 1. 确保没有 ID(防止误操作)
        delete row.id;
        // 2. 如果是顶级科目(parentId 为 0 或 无)
        if (row.parentId === 0 || !row.parentId) {
          // 将上级科目代码设置为当前填写的科目代码
          row.parentSubjectCode = row.subjectCode;
        }
      } else {
        // 如果是行内新增,parentId 已经是父节点的 ID
      }
      done(row); // 必须调用 done 并传入处理后的 row 才会继续请求接口
      // --- 核心逻辑结束 ---
      done(row);
    },
    // 修改保存前的逻辑
    rowUpdateBegin: (row: any, done: any, loading: any) => {
      processAccountingItems(row);
      delete row.accountingItemsContains;
      if (row.parentSubjectCode == "顶级科目代码") {
        row.parentSubjectCode = row.subjectCode;
      }
      done(row); // 同理
    },
  })
@@ -539,17 +668,14 @@
};
const handleTabClick = (tab: any) => {
  const selectedTabName = tab.props.name;
  console.log("Tab点击的值:", selectedTabName);
  activeAccountSet.value = selectedTabName;
  queryParams.value.type = selectedTabName;
  const nextParams = {
    ...queryParams.value,
    type: selectedTabName
  };
  // 4. 重置分页并加载
  // 先清空当前数据,防止旧数据的树结构影响新数据的渲染
  tableData.value = [];
  queryParams.value.type = selectedTabName;
  page.value.currentPage = 1;
  onLoad(page.value, nextParams);
  onLoad(page.value, { type: selectedTabName });
}
/**
@@ -567,22 +693,37 @@
  return result;
}
/**
 * 处理行内“新增”点击
 * 处理行内“新增子项”点击
 */
const handleRowAdd = (row: any) => {
  isFromRow.value = true;
  getVoucherSubjectSetting(row.id).then((res: any) => {
  // 1. 先触发新增动作,这会弹出窗口并初始化 form
  crudRef.value.rowAdd();
  // 2. 在 DOM 更新后的下一次循环中强制赋值
  nextTick(() => {
    // 确保赋值给 data 响应式对象中的 form
    form.value.parentId = row.id;
    form.value.parentSubjectCode = row.subjectCode;
    form.value.accountSet = row.accountSet;
    form.value.type = row.accountSet;
    form.value.enabled = "1";
    form.value.accountingItems = [];
  });
};
const logModalRef = ref(null);
const handleFlow = (row: any) => {
  // 这里可以从 row 中直接获取日志,或者调用后端接口查询
  // 示例模拟数据
  listVoucherSubjectSettingLog({ subjectId: row.id }).then((res) => {
    if (res.code == 200) {
      form.value = res.data;
      form.value.parentId = form.value.id;
      const num = form.value.accountingItems;
      form.value.accountingItems = decomposeAccountingItems(num);
      // 触发 Avue 的内置新增弹窗
      crudRef.value.rowAdd();
      logModalRef.value.open(res.rows, 'payable');
    }
  });
};
}
</script>