Skip to content
On this page

模板配置

基本配置

vue
<template>
  <div class="edit-page">
    <q-edit-page-layout
      v-bind="options"
      @pageBack="handlePageBack"
    ></q-edit-page-layout>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from "vue";
// 国际化
import { srmI18n } from "@/utils/srmI18n";
// ui组件库 types
import type { GlobalPageLayoutTypes } from "@qqt-product/ui";
// 按钮常量引用
import { BUTTON_SAVE } from "@qqt-product/ui";
import { message as Message } from "ant-design-vue";
// 全局数据缓存
import { useGlobalStoreWithDefaultValue } from "@/use/useGlobalStore";
import { useRouter } from "vue-router";
const router = useRouter();

// 配置页面缓存
defineOptions({
  name: "srm-enquiry-ebiddingHead-edit",
});

const {
  isCreate, // 布尔值, 是否为新增状态
  getCurrentRow, // 获取父级当前行缓存方法
  userInfo, // 当前登录人信息
} = useGlobalStoreWithDefaultValue();
const currentRow = getCurrentRow(); // 父级当前行缓存

// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  /**
   * 模块业务类型 string
   * 取值根据各模块配置
   * 当定义为"custom"时,使用本地配置
   */
  businessType: "ebidding",
  // 布局模式
  pattern: "tab",
  // 当前行缓存数据
  currentRow: currentRow,
  refreshMethods(row?: GlobalPageLayoutTypes.CurrentRow) {
    if (options.currentRow) {
      options.currentRow =
        row ||
        Object.assign({}, options.currentRow, {
          _t: +new Date(),
        });
    }
  },
  // 当前登录人信息
  userInfo: userInfo.value as GlobalPageLayoutTypes.UserInfo,
  // 明细接口 -- 接口字符串
  detailApi: "/ebidding/purchaseEbiddingHead/queryById",
  // 本地页面数据配置
  localConfig: {
    // 本地分组配置
    groups: [],
    //
    itemColumns: [],
  },
  // 页面按钮
  pageButtons: [
    {
      // 保存按钮
      ...BUTTON_SAVE,
      args: {
        url: "/ebidding/purchaseEbiddingHead/edit",
      },
    },
  ],
});

// 回退操作,如果是新增单据, 列表页需要刷新
const backToList: (flag?: boolean) => void = (flag = false) => {
  const path = "/srm/enquiry/ebiddingHead/list";
  const query = flag ? { t: +new Date() } : {};
  router.push({ path, query });
};

// 回退至列表页
const handlePageBack = () => {
  backToList(isCreate.value);
};
</script>

queryById 接口自定义配置

🙌 🌰 采购协同 > 寻源协同 > 多标包招标管理

vue
<script setup lang="ts">
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  // 方法配置自定义明细接口
  detailApi(row) {
    return {
      url: "/bidding/purchaseBiddingProjectHead/queryById",
      method: "get",
      params: {
        id: row.projectId || '', // 须返回id值
      },
    };
  },
});
</script>

可选自定义属性

vue
<script setup lang="ts">
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  pageTitle: "xxx", // 模板名称
  showPageHeader: false, // 是否隐藏模板头部
  showLayoutAnchor: false, // vertical布局模式,是否隐藏 anchor 锚点
  isUseLocalPattern: false, // 是否只使用本地布局模式

  isUseLocalData: false, // 是否只使用本地同步数据
  localData: {}, // 本地同步数据
});
</script>

本地头分组字段配置、本地行分组字段配置

WARNING

本地的头分组、或行分组字段配置会与业务模板配置所获取的数据混合, 头分组字段按 sortOrder 属性排序, 字段 fieldName 即使相同也不会被覆盖, 可以使用模板预设的钩子方法过滤, 详见后述示例

🙌 🌰 采购协同 > 寻源协同 > 答疑

ts
// 国际化
import { srmI18n } from "@/utils/srmI18n";

const baseForm = [
  {
    fieldLabel: srmI18n("i18n_title_questionNotNo", "答疑编号"),
    fieldName: "mentoringNumber",
  },
  {
    fieldType: "select",
    fieldLabel: srmI18n("i18n_field_businessType", "业务类型"),
    fieldName: "businessType",
    dictCode: "srmClarificationBusinessType",
    disabled: true,
  },
  // ...
];

// 表头分组配置
export const formFields = baseForm.map((n) => ({
  ...n,
  groupCode: "baseForm",
})) as GlobalPageLayoutTypes.FormFieldsItem[];

const purchaseMentoringItemList = [
  {
    field: "mentoringNumber",
    title: srmI18n("i18n_field_mentoringNumber", "答疑单号"),
    minWidth: 180,
  },
  {
    field: "mentoringAnswer",
    title: srmI18n("i18n_title_questionAnswer", "疑问答案"),
    fieldType: "input",
    minWidth: 180,
  },
  // ...
];

// 表行分组配置
export const purchaseMentoringItemListColumns = purchaseMentoringItemList.map(
  (n) => ({ ...n, groupCode: "purchaseMentoringItemList" })
) as GlobalPageLayoutTypes.ColumnItem[];
vue
<script setup lang="ts">
import { formFields, purchaseMentoringItemListColumns } from "./hook/columns";

// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  formFields,
  itemColumns: [...purchaseMentoringItemListColumns, gridOperationColumn],
});
</script>

附件表行列配置

TIP

可以使用本地配置与钩子方法相结合的组装方式

ts
// 附件 列配置
const purchaseAttachmentList = [
  {
    title: srmI18n("i18n_field_fileType", "文件类型"),
    field: "fileType_dictText",
    minWidth: 120,
  },
  {
    title: srmI18n("i18n_title_lineItem", "行项目"),
    field: "itemNumber",
    minWidth: 120,
  },
  {
    title: srmI18n("i18n_field_cdIRL_c7c40624", "行项目名称"),
    field: "materialName",
    width: 120,
  },
];
export const purchaseAttachmentListColumns = purchaseAttachmentList.map(
  (n) => ({ ...n, groupCode: "purchaseAttachmentList" })
) as GlobalPageLayoutTypes.ColumnItem[];
vue
<script setup lang="tsx">
// 附件表行列配置预设方法
import { useFileColumnHook } from "@qqt-product/ui";
// 列配置
import { purchaseAttachmentListColumns } from "./hook/columns";

// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  // 本地页面数据配置
  localConfig: {
    itemColumns: [
      ...purchaseAttachmentListColumns,
      ...useFileColumnHook({ groupCode: "purchaseAttachmentList" }),
    ],
  },
});
</script>

handleAfterRemoteConfig, 自定义处理业务模板数据

TIP

允许自定义修改业务模板返回的数据内容

vue
<script setup lang="ts">
// ...
// 兼容处理业务模板中,原行信息各字段在设置了采购方的 groupCode 后
// 到供应方端字段无法显示bug
const handleAfterRemoteConfig = (config) => {
  const itemColumns = config.itemColumns || [];
  itemColumns.forEach((n) => {
    if (n.groupCode === "purchaseEbiddingItemList") {
      n.groupCode = "saleEbiddingItemList";
    }
  });

  return config;
};

// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  handleAfterRemoteConfig,
});
</script>

handleAfterDetailApiResponse, 自定义组装接口数据

TIP

允许自定义组装接口返回数据 钩子方法会触发两次, 分别为初始化赋值后触发以及 queryById 接口返回赋值后触发,可以加条件判断,通过 pageData.id 来限制是否只在编辑状态下触发

🙌 🌰 采购协同 > 寻源协同 > 物料主数据

vue
<script setup lang="ts">
// 自定义组装详情接口数据
const handleAfterDetailApiResponse = ({
  pageData,
  layoutConfig,
  defaultValues,
}: GlobalPageLayoutTypes.Expose) => {
  // console.log("pageData :>> ", pageData);
  // console.log("layoutConfig :>> ", layoutConfig);
  // console.log("defaultValues :>> ", defaultValues);
  let basicUnit = pageData.basicUnit as string;
  if (!pageData.id) {
    const base = {
      basicUnit,
      basicAmount: 1,
      changeUnit: "=",
      objectAmount: 1,
      objectUnit: "",
      isDefault: "1",
    };

    const tableData = pageData[
      "purchaseMaterialMeterUnitList"
    ] as GlobalPageLayoutTypes.RecordString[];

    if (tableData) {
      tableData.push({ ...base, type: "0", _row_id: uniqueId("_row_id_") });
      tableData.push({ ...base, type: "1", _row_id: uniqueId("_row_id_") });
    }
  }
};

// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  handleAfterDetailApiResponse,
});
</script>

按钮配置

WARNING

V6 产品的按钮规则为: 用户权限配置控制按钮的显示或隐藏;数据只控制按钮是否置灰;与 V5 产品不同,不允许随意设置按钮的显隐; 通常情况下,从 UI 包里获取预设的按钮常量

🙌 🌰 采购协同 > 寻源协同 > 竞价管理

vue
<script setup lang="ts">
// 按钮常量引用
import {
  BUTTON_FILL_CELLS, // 向下填充
  BUTTON_ADD_ONE_ROW, // 添加行
  BUTTON_DELETE_ROW, // 删除行
  BUTTON_SAVE, // 保存
  BUTTON_PUBLISH, // 发布
  BUTTON_SUBMIT, // 提交审批
  BUTTON_CANCELAUDIT, // 撤销审批
  BUTTON_FLOW_VIEW, // 查看流程
  BUTTON_UPLOAD, // 上传附件
  BUTTON_DOWNLOAD_ALL, // 批量下载
  BUTTON_REMOVE_ALL, // 批量删除
  BUTTON_IMPORT_ROW, // 导入Excel
  BUTTON_CONFIRM, // 确认
  BUTTON_REJECT, // 拒绝
  BUTTON_CUSTOM, // 自定义按钮_1
  BUTTON_CUSTOM_PRIMARY, // 自定义按钮_2
  BUTTON_CUSTOM_DANGER, // 自定义按钮_3
} from "@qqt-product/ui";

// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  localConfig: {
    groups: [
      {
        groupName: "确认项",
        groupNameI18nKey: "i18n_field_RLd_1d59643",
        groupCode: "purchaseEbiddingConfirmList",
        groupType: "item",
        sortOrder: "4",
        buttons: [BUTTON_ADD_ONE_ROW, BUTTON_DELETE_ROW], // 普通添加行、删除行, 无需额外配置
      },
    ],
    // 页面按钮
    pageButtons: [
      {
        // 保存
        ...BUTTON_SAVE,
        args: {
          url: "编辑保存api接口地址",
          addUrl: "新增保存 api 接口地址", // 可以不配置, 新增状态模板默认会把 'xxx/edit' 替换为 'xxx/add'
        },
      },
      {
        // 发布
        ...BUTTON_PUBLISH,
        args: {
          url: "发布api接口地址",
        },
        checkBefore: true, // 是否需要校验单据publishAudit字段值状态, 默认为true, 如果需要关闭校验,设置为 false
      },
      {
        // 提交审批
        ...BUTTON_SUBMIT,
        handleBefore: handleBeforeSubmit, // 提交审批前需要格式化审批流的数据, 在 handleBefore 钩子中处理
        args: {
          url: "提交审批api接口地址",
        },
      },
    ],
  },
});
</script>
vue
<script setup lang="ts">
// 1. 引用按钮常量引用
import {
  BUTTON_CUSTOM, // 自定义按钮_1 普通按钮
  BUTTON_CUSTOM_PRIMARY, // 自定义按钮_2 蓝色按钮
  BUTTON_CUSTOM_DANGER, // 自定义按钮_3 红色按钮
} from "@qqt-product/ui";

// 2. 按钮配置及自定义事件
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  localConfig: {
    // 页面按钮
    pageButtons: [
      {
        ...BUTTON_CUSTOM,
        title: "自定义标题_1",
        emit: true, // 触发自定义事件
        emitKey: "customClick", // 自定义事件名
      },
    ],
  },
});

// 4. 自定义事件方法
const handleCustomClick = () => {
  // 处理业务逻辑
};
</script>

<!-- 3 监听自定义事件 -->
<template>
  <q-edit-page-layout @customClick="handleCustomClick"></q-edit-page-layout>
</template>
vue
<script setup lang="ts">
// 按钮常量引用
import {
  BUTTON_SUBMIT, // 提交审批
} from "@qqt-product/ui";

// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  // 页面按钮
  pageButtons: [
    {
      ...BUTTON_SUBMIT,
      disabled(pageData) {
        return !pageData.id || pageData.publishAudit !== "1"; 
      },
      args: {
        url: "/elsUflo/audit/submit",
      },
    },
  ],
});
</script>
vue
<script setup lang="ts">
// 按钮常量引用
import {
  BUTTON_PUBLISH, // 提交审批
} from "@qqt-product/ui";

// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  // 页面按钮
  pageButtons: [
    {
      ...BUTTON_PUBLISH,
      args: {
        url: "/enquiry/purchaseEnquiryHead/publish",
      },
      authorityCode: "enquiry#purchaseEnquiryHead:publish", 
    },
  ],
});
</script>
vue
<template>
  <q-edit-page-layout @customSubmit="handleCustomSubmit"></q-edit-page-layout>
</template>

<script setup lang="ts">
// 按钮常量引用
import {
  BUTTON_PUBLISH, // 提交审批
} from "@qqt-product/ui";

// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  // 页面按钮
  pageButtons: [
    {
      ...BUTTON_SUBMIT,
      emit: true,
      emitKey: "customSubmit",
      args: {
        url: "/elsUflo/audit/submit",
      },
    },
  ],
});

// 自定义处理提交审批业务逻辑
const handleCustomSubmit = () => {};
</script>
vue
<script setup lang="ts">
// 按钮常量引用
import { BUTTON_SAVE } from "@qqt-product/ui";

// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  // 页面按钮
  pageButtons: [
    {
      ...BUTTON_SAVE,
      emit: true,
      emitKey: "customSave",
      // args 可以配置为方法,须返回 PageButtonArgs 类型
      args(pageData) {
        let url: string;
        url = pageData.supplierInfoChangeId
          ? "/supplier/supplierInfoChangeHead/edit"
          : "/supplier/supplierInfoChangeHead/add";
        return {
          url,
        };
      },
      authorityCode: "price#PurchaseInformationRecords:add",
    },
  ],
});
</script>

向下填充功能按钮

TIP

表行可配置"向下填充"按钮,满足用户快速填写单据需求,所有可编辑字段都允许填充,字段配置的绑定函数会在赋值完成后执行,弹窗、远程下拉搜索等类型字段的绑定函数需要做特殊处理,详见示例说明。

vue
<script setup lang="ts">
// 按钮常量引用
import {
  BUTTON_FILL_CELLS, // 向下填充
} from "@qqt-product/ui";

// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  localConfig: {
    groups: [
      {
        groupName: "竞价行信息",
        groupNameI18nKey: "i18n_title_ebiddingBankInfo",
        groupCode: "purchaseEbiddingItemList",
        groupType: "item",
        sortOrder: "2",
        buttons: [BUTTON_FILL_CELLS],
      },
    ],
  },
});
</script>
js
/**
 * @param {Object} ctx 组件实例
 * @param {String} value 当前所选值
 * @param {Array} data selectModal, remoteSelect 已选行数据 (如有)
 * @param {boolean} _isFill 向下填充或粘贴时为 true
 * @param {Object} pageData 页面所有数据
 * @param {Object} layoutConfig 模板配置
 * @param {Object} userInfo 当前登录人信息
 * @param {Object} row 表行数据 (如有)
 * @param {number} idx 表行索引值 (如有)
 * @param {(groupCode: string, fieldName: string, fn: (item: FormFieldsItem | ColumnItem) => void) => void}
 * customFormatItem 遍历模板分组配置,自定义格式化查询到的字段
 * @param {(groupCode: string, fieldName: string, flag: boolean) => void}
 * setItemRequired 自定义设置字段必填
 * @param {(groupCode: string, fieldName: string, flag: boolean) => void}
 * setItemDisabled 自定义设置字段置灰
 * @param {(groupCode: string, fieldName: string, flag: boolean) => void}
 * setItemRequiredOrDisabled 自定义设置字段必填/置灰
 * @param {() => void}
 * topEmit 用于处理复杂绑定函数需求
 */
function callback(
  ctx,
  {
    value,
    _isFill,
    data,
    row,
    idx,
    pageData,
    layoutConfig,
    userInfo,
    customFormatItem,
    setItemRequired,
    setItemDisabled,
    setItemRequiredOrDisabled,
    topEmit,
    Decimal,
  }
) {
  if (data && data.length) {
    // 填充操作时 _isFill 判断标识值为 true
    // 且 data 参数为当前所选行缓存数据
    // 如果赋值操作中字段属性不同, 需要加条件判断区分取值
    if (_isFill) { 
      const { materialId = "" } = data[0] || {};
      row.materialId = materialId;
    } else {
      const { id = "" } = data[0] || {};
      row.materialId = id;
    }
  }
}

发布前或提交审批前查询供应商是否为已冻结

INFO

竞价、询价、招投标、采购订单等模块,发布前需要校验所选供商是否为已冻结,正常状态的供应商才允许发布操作;

🙌 🌰 采购协同 > 寻源协同 > 多标包招标管理

vue
<script setup lang="tsx">
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  pageButtons: [
    {
      ...BUTTON_PUBLISH,
      checkBefore: false,
      isExistFrozenSource: true, // 开关, 是否需要开启冻结校验
      // isExistFrozenSourceUrl: '/supplier/supplierMaster/isExistFrozenStateData', // 使用默认值, 可以不配置
      // 接口参数配置
      isExistFrozenSourceParams: (pageData) => {
        // 设置供应商信息和询价行信息
        const purchaseBiddingItemList =
          pageData.purchaseBiddingItemList as unknown[];
        const biddingSupplierList = pageData.biddingSupplierList as unknown[];

        // 返回
        return Object.assign({}, pageData, {
          purchaseOrgItemList: purchaseBiddingItemList,
          supplierItemList: biddingSupplierList,
        });
      },
      args: {
        url: "/ebidding/purchaseEbiddingHead/publish",
      },
    },
  ],
});
</script>

handleBefore 或 handleAfter(AOP 切片编程, 异步校验)

vue
<script setup lang="ts">
import qqtUtils from "@qqt-product/utils";
const { createPromise } = qqtUtils;
import type { CreatePromise } from "@qqt-product/utils";

// 保存操作前混合当前按钮配置参数
const handleBeforeSave = (
  args: GlobalPageLayoutTypes.ExposeWithPageButtons
) => {
  const fn: CreatePromise<GlobalPageLayoutTypes.ExposeWithPageButtons> = (
    resolve
  ) => {
    let pageData = args.pageData;
    const cacheButton = args.cacheButton as GlobalPageLayoutTypes.PageButton;
    const btnArgs = cacheButton.args as GlobalPageLayoutTypes.RecordString;

    let formatData = {
      ...pageData.value,
      fbk1: btnArgs.fbk1 ?? "", // 混合按钮配置的某些属性
    };

    resolve({ ...args, pageData: ref(formatData) });
  };

  return createPromise(
    fn
  ) as Promise<GlobalPageLayoutTypes.ExposeWithPageButtons>;
};

// 提交审批前格式化入参
const handleBeforeSubmit = (
  args: GlobalPageLayoutTypes.ExposeWithPageButtons
) => {
  const fn: CreatePromise<GlobalPageLayoutTypes.ExposeWithPageButtons> = (
    resolve
  ) => {
    let pageData = args.pageData;

    let formatData = {
      businessId: pageData.value.id || "",
      businessType: "publishEnquiry",
      auditSubject: `询价发布审批,单号:${
        pageData.value.materialNumber || ""
      }`,
      params: JSON.stringify(pageData.value),
    };

    resolve({ ...args, pageData: ref(formatData) });
  };

  return createPromise(
    fn
  ) as Promise<GlobalPageLayoutTypes.ExposeWithPageButtons>;
};

// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  pageButtons: [
    {
      ...BUTTON_SAVE,
      handleBefore: handleBeforeSave,
      args: {
        url: "/enquiry/purchaseEnquiryHead/edit",
      },
    },
    {
      ...BUTTON_SUBMIT,
      emit: true,
      emitKey: "customSubmit",
      handleBefore: handleBeforeSubmit,
      args: {
        url: "/elsUflo/audit/submit",
      },
    },
  ],
});
</script>

模板数据获取

TIP

业务模板暴露的数据都是双向绑定的值,可以读取数据,也可以赋值操作

vue
<!-- 组件ref -->
<q-edit-page-layout ref="layoutRef" v-bind="options"></q-edit-page-layout>

<script setup lang="ts">
// 获取模板 ref 暴露值方法
import { useRefInstanceHook } from "@qqt-product/ui";

const {
  pageData, // 模板所有数据
  layoutConfig, // 模板配置
  defaultValues, // 模板配置默认值
} = useRefInstanceHook(layoutRef);
</script>

获取表行勾选数据、行索引

vue
<q-edit-page-layout
  ref="layoutRef"
  @_checkboxChange="handleCheckboxChange"
></q-edit-page-layout>

<script setup lang="ts">
import {
  BUTTON_CUSTOM, // 自定义按钮_1
} from "@qqt-product/ui";

// 组件 ref
const layoutRef = ref<ComponentPublicInstance | null>(null);

const { pageData } = useRefInstanceHook(layoutRef);


const handleCheckboxChange: (btn: GlobalPageLayoutTypes.PageButtonWithGroupCode) => void) = (btn) => {
  const { groupCode } = btn

  const tableData = pageData[groupCode] as GlobalPageLayoutTypes.RecordString[]
  // 已勾选行数据
  const selectedRows = tableData.filter((n) => !!n._checked)
  console.log('selectedRows :>> ', selectedRows)

  // 已勾行索引值
  const rowIdxs = selectedRows.map(n => n._rowIndex)
  console.log('rowIdxs :>> ', rowIdxs)
}

const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  localConfig: {
    // 页面按钮
    pageButtons: [
      {
        ...BUTTON_CUSTOM,
        emit: true,
        emitKey: "_checkboxChange", // 自定义事件名
      },
    ],
  },
});
</script>

表行数据添加弹窗选择

TIP

许多模块各表行数据需要通过调用接口获取

🙌 🌰 采购协同 > 寻源协同 > 价格申请

vue
<!-- 自定义事件监听 -->
<q-edit-page-layout
  ref="layoutRef"
  @itemListAdd="handleItemListAdd"
  @priceAdd="handlePriceAdd"
></q-edit-page-layout>
<!-- 弹窗组件 -->
<q-field-select-modal ref="fieldSelectModal" @ok="ok"></q-field-select-modal>

<script setup lang="ts">
// 按钮常量引用
import { BUTTON_CUSTOM } from "@qqt-product/ui";
// 组件 ref
const layoutRef = ref<ComponentPublicInstance | null>(null);

// 弹窗选择自定义方法配置 hook
import useFieldSelectModalHook from "./hook/use-field-select-modal-hook";
const { fieldSelectModal, handleItemListAdd, handlePriceAdd, ok } =
  useFieldSelectModalHook(layoutRef);

// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  // ...
  // 本地页面数据配置
  localConfig: {
    groups: [
      {
        groupName: "价格申请行",
        groupCode: "informationRecordsRequestItemList",
        groupType: "item",
        sortOrder: "2",
        buttons: [
          {
            ...BUTTON_CUSTOM,
            title: srmI18n("i18n_alert_iFumdWF_af48dd34", "选择价格主数据"),
            emit: true, // 抛出事件
            emitKey: "priceAdd", // 自定义事件名称
          },
          {
            ...BUTTON_CUSTOM,
            title: srmI18n("i18n_alert_iFSL_42fb1450", "选择物料"),
            emit: true,
            emitKey: "itemListAdd",
          },
        ],
      },
    ],
  },
  // ...
});
</script>
ts
import { ref, watchEffect } from "vue";
import type { Ref, ComponentPublicInstance } from "vue";

import type { GlobalPageLayoutTypes } from "@qqt-product/ui";

import { srmI18n } from "@/utils/srmI18n";
import { message as Message } from "ant-design-vue";

export default function useFieldSelectModal(
  layoutRef: Ref<ComponentPublicInstance | null>
) {
  const fieldSelectModal = ref();
  const handleItemListAdd: Ref<
    ((btn: GlobalPageLayoutTypes.PageButtonWithGroupCode) => void) | null
  > = ref(null);
  const handlePriceAdd: Ref<
    ((btn: GlobalPageLayoutTypes.PageButtonWithGroupCode) => void) | null
  > = ref(null);
  const ok: Ref<((data: GlobalPageLayoutTypes.RecordString[]) => void) | null> =
    ref(null);

  const cache: Ref<GlobalPageLayoutTypes.PageButtonWithGroupCode | null> =
    ref(null);

  watchEffect(() => {
    if (layoutRef.value) {
      const instance = layoutRef.value.instance;
      const { pageData, defaultValues } = instance;
      // 选择物料
      handleItemListAdd.value = (btn) => {
        console.log("click  handleItemListAdd :>> ", btn);

        // 缓存当前按钮配置
        cache.value = btn;

        const url = "/material/purchaseMaterialHead/list-head-or-item";
        const columns = [
          {
            field: "materialNumber",
            title: srmI18n("i18n_title_materialCode", "物料编码"),
            width: 150,
          },
          {
            field: "materialName",
            title: srmI18n("i18n_field_materialName", "物料名称"),
            width: 150,
          },
          {
            field: "materialDesc",
            title: srmI18n("i18n_field_materialDesc", "物料描述"),
            width: 150,
          },
          {
            field: "materialSpec",
            title: srmI18n("i18n_title_materialSpec", "物料规格"),
            width: 150,
          },
          {
            field: "cateCode",
            title: srmI18n("i18n_title_classificationCode", "分类编码"),
            width: 150,
          },
          {
            field: "cateName",
            title: srmI18n("i18n_title_classificationName", "分类名称"),
            width: 150,
          },
          {
            field: "materialGroup",
            title: srmI18n("i18n_field_materialGroup", "物料组"),
            width: 150,
          },
          {
            field: "materialGroup_dictText",
            title: srmI18n("i18n_field_materialGroupName", "物料组名称"),
            width: 150,
          },
          {
            field: "deliveryArrange_dictText",
            title: srmI18n("i18n_field_dSpA_43933cef", "送货安排"),
            width: 150,
          },
          {
            field: "checkQuality_dictText",
            title: srmI18n("i18n_field_qualityTest", "是否质检"),
            width: 150,
          },
          {
            field: "purchaseUnit_dictText",
            title: srmI18n("i18n_field_purchaseUnit", "采购单位"),
            width: 150,
          },
          {
            field: "minOrderQuantity",
            title: srmI18n("i18n_field_minOrderQuantity", "最小交货数量"),
            width: 150,
          },
          {
            field: "factory_dictText",
            title: srmI18n("i18n_field_RH_bb23d", "工厂"),
            width: 150,
          },
        ];
        const params = { blocDel: "0", freeze: "0", filterPurchaseType: "1" };
        const $fieldSelectModal = fieldSelectModal.value;
        if ($fieldSelectModal) {
          $fieldSelectModal.open({ url, params, columns });
        }
      };

      // 价格主数据
      handlePriceAdd.value = (btn) => {
        console.log("click  handleItemListAdd :>> ", btn);
        if (pageData.needSaleConfirm === "1" && !pageData.toElsAccount) {
          Message.error("请选择对方ELS账号");
          return;
        }

        // 缓存当前按钮配置
        cache.value = btn;

        const url = "/price/purchaseInformationRecords/itemList";
        const columns = [
          {
            field: "templateNumber",
            title: srmI18n("i18n_title_templateNumber", "模板编号"),
            width: 150,
            align: "center",
          },
          {
            field: "infoRecordNumber",
            title: srmI18n("i18n_field_infoRecordNumber", "价格记录号"),
            width: 150,
            align: "center",
          },
          {
            field: "recordType_dictText",
            title: srmI18n("i18n_field_recordType", "价格记录类型"),
            width: 150,
            align: "center",
          },
          {
            field: "recordStatus_dictText",
            title: srmI18n("i18n_baseForm01bf_ruleStatus", "状态"),
            width: 150,
            align: "center",
          },
          {
            field: "auditStatus_dictText",
            title: srmI18n("i18n_baseForm925b_auditStatus", "审批状态"),
            width: 150,
            align: "center",
          },
          {
            field: "toElsAccount",
            title: srmI18n("i18n_title_supplierElsNumber", "供应商ELS账号"),
            width: 150,
            align: "center",
          },
          {
            field: "supplierName",
            title: srmI18n("i18n_massProdHead5f0c_supplierName", "供应商名称"),
            width: 150,
            align: "center",
          },
          {
            field: "company_dictText",
            title: srmI18n("i18n_massProdHeadc8f5_companyCode", "公司"),
            width: 150,
            align: "center",
          },
          {
            field: "purchaseOrg_dictText",
            title: srmI18n("i18n_massProdHead1725_purchaseOrgCode", "采购组织"),
            width: 150,
            align: "center",
          },
          {
            field: "purchaseGroup_dictText",
            title: srmI18n("i18n_massProdHead1515_purchaseGroupCode", "采购组"),
            width: 150,
            align: "center",
          },
          {
            field: "factory_dictText",
            title: srmI18n("i18n_massProdHead3e34_factoryCode", "工厂"),
            width: 150,
            align: "center",
          },
          {
            field: "materialNumber",
            title: srmI18n("i18n_title_materialNumber", "物料编码"),
            width: 150,
            align: "center",
          },
          {
            field: "materialName",
            title: srmI18n("i18n_title_materialName", "物料名称"),
            width: 150,
            align: "center",
          },
          {
            field: "materialSpec",
            title: srmI18n("i18n_title_materialSpec", "物料规格"),
            width: 150,
            align: "center",
          },
          {
            field: "price",
            title: srmI18n("i18n_title_unitPriceIncludTax", "含税单价"),
            width: 150,
            align: "center",
          },
          {
            field: "netPrice",
            title: srmI18n("i18n_title_unitPriceExcludingTax", "未税单价"),
            width: 150,
            align: "center",
          },
          {
            field: "ladderPriceJson",
            title: srmI18n("i18n_field_ladderPriceJson", "阶梯价格"),
            width: 150,
            align: "center",
            slots: { default: "ladder_price_json_render" },
          },
          {
            field: "priceType",
            title: srmI18n("i18n_field_priceType", "价格类型"),
            width: 150,
            align: "center",
          },
          {
            field: "taxCode",
            title: srmI18n("i18n_title_enterTaxCode", "税码"),
            width: 150,
            align: "center",
          },
          {
            field: "taxRate",
            title: srmI18n("i18n_title_taxRate", "税率"),
            width: 150,
            align: "center",
          },
          {
            field: "currencyCode_dictText",
            title: srmI18n("i18n_field_currency", "币别"),
            width: 150,
            align: "center",
          },
          {
            field: "effectiveDate",
            title: srmI18n(
              "i18n_field_umtHjXAKBA_f67d10f7",
              "价格记录有效起始日期"
            ),
            width: 150,
            align: "center",
          },
          {
            field: "expiryDate",
            title: srmI18n(
              "i18n_field_umtHjXyRBA_e310233b",
              "价格记录有效截止日期"
            ),
            width: 150,
            align: "center",
          },
          {
            field: "purchaseUnit_dictText",
            title: srmI18n("i18n_field_purchaseUnit", "采购单位"),
            width: 150,
            align: "center",
          },
          {
            field: "sourceType_dictText",
            title: srmI18n("i18n_field_wjAc_30ae977b", "来源类型"),
            width: 150,
            align: "center",
          },
          {
            field: "payTermsCode",
            title: srmI18n("i18n_field_paymentCondition", "付款条件"),
            width: 150,
            align: "center",
          },
        ];
        const params = { toElsAccount: pageData.toElsAccount };
        const $fieldSelectModal = fieldSelectModal.value;
        if ($fieldSelectModal) {
          $fieldSelectModal.open({ url, params, columns });
        }
      };

      ok.value = (data) => {
        if (!data.length) {
          return;
        }

        const emitKey = cache.value?.emitKey;
        if (!emitKey) {
          return;
        }

        // 价格申请行
        // 物料选择弹窗赋值
        if (emitKey === "itemListAdd") {
          const inserData = data.map((n) => {
            const { id, ...others } = n;
            return {
              ...others,
              materialId: id,
              requestOptType: "0",
              changeToPrice: "0",
              toElsAccount: pageData.toElsAccount,
              supplierCode: pageData.supplierCode,
              supplierName: pageData.supplierName,
            };
          });

          const groupCode = cache.value?.groupCode;
          if (!groupCode) {
            return;
          }

          const row = defaultValues[
            groupCode
          ] as GlobalPageLayoutTypes.RecordString;
          const tableData = pageData[
            groupCode
          ] as GlobalPageLayoutTypes.RecordString[];
          inserData.forEach((n) => {
            tableData.push({ ...row, _row_id: uniqueId("_row_id_"), ...n });
          });
        }

        // 价格申请行
        // 选择价格主数据
        if (emitKey === "priceAdd") {
          const inserData = data.map((n) => {
            const { id, ...others } = n;
            return {
              ...others,
              materialId: id,
              infoRecordId: id,
              requestOptType: "1",
              changeToPrice: "0",
            };
          });
          const groupCode = cache.value?.groupCode;
          if (!groupCode) {
            return;
          }

          const row = defaultValues[
            groupCode
          ] as GlobalPageLayoutTypes.RecordString;
          const tableData = pageData[
            groupCode
          ] as GlobalPageLayoutTypes.RecordString[];
          inserData.forEach((n) => {
            tableData.push({ ...row, _row_id: uniqueId("_row_id_"), ...n });
          });
        }
      };
    }
  });

  return {
    fieldSelectModal,
    handleItemListAdd,
    handlePriceAdd,
    ok,
  };
}

表头、表行分组显隐配置

vue
<script setup lang="ts">
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  localConfig: {
    groups: [
      {
        groupName: "价格申请行",
        groupCode: "informationRecordsRequestItemList", // 本地分组与业务模板分组会合并,以groupCode为唯一key
        groupType: "item",
        sortOrder: "2",
        show: false, // 分组显隐
      },
    ],
  },
});
</script>
vue
<template>
  <div class="edit-page">
    <q-edit-page-layout
      ref="layoutRef"
      @toggleGroup="handleToggleGroup"
    ></q-edit-page-layout>
  </div>
</template>

<script setup lang="ts">
import { useRefInstanceHook } from "@qqt-product/ui";
// 组件 ref
const layoutRef = ref<ComponentPublicInstance | null>(null);
const { layoutConfig } = useRefInstanceHook(layoutRef);

// 测试设置分组显隐
const handleToggleGroup = () => {
  layoutConfig.value.groups.forEach((group) => {
    // 供应商信息
    if (group.groupCode === "purchaseEbiddingSupplierList") {
      group.show = !group.show; 
    }
  });
};
</script>

usePromiseStepHook 可解构的步骤方法

TIP

UI 库 提供与模板相配套的步骤方法,满足各种业务场景需求

vue
<script setup lang="ts">
import { usePromiseStepHook, useRefInstanceHook } from "@qqt-product/ui";

// 组件 ref
const layoutRef = ref<ComponentPublicInstance | null>(null);

const { pageData, layoutConfig, defaultValues } = useRefInstanceHook(layoutRef);

const {
  localUid, // 组件唯一标识 uid
  customLoading, // loading 只读状态
  stepTriggerValidate, // 触发模板校验
  composeSave, // 组合式保存
  composePublish, // 组合式发布
  composeSubmit, // 组合式提交审批
  composeConfirm, // 组合式确认
  composeReject, // 组合式拒绝
} = usePromiseStepHook({ pageData, layoutConfig, defaultValues, options });
</script>

撤销审批

INFO

撤销审批功能一般配置在详情页面,且成功后返回至模块的编辑页面, 通常与 查看流程 按钮配套使用

vue
<template>
  <div class="detail-page">
    <q-detail-page-layout @back="back"></q-detail-page-layout>
  </div>
</template>

<script setup lang="ts">
// 按钮常量引用
import { BUTTON_CANCELAUDIT, BUTTON_FLOW_VIEW } from "@qqt-product/ui";

// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  pageButtons: [
    {
      ...BUTTON_CANCELAUDIT,
      args: {
        url: "/a1bpmn/audit/api/cancel",
        auditSubject:
          "评标模板审批:" +
            currentRow.evaluationNumber +
            " " +
            currentRow.evaluationName || "",
      },
    },
    BUTTON_FLOW_VIEW,
  ],
});

const back = (btn: GlobalPageLayoutTypes.PageButton) => {
  // 撤销审批跳转至编辑页面
  if (btn.key === BUTTON_CANCELAUDIT.key) {
    router.replace({
      path: `/srm/enquiry/enquiryHead/edit/${currentRow.id}`,
      query: { t: +new Date() },
    });
  }
};
</script>
vue
<template>
  <div class="detail-page">
    <q-detail-page-layout></q-detail-page-layout>
  </div>
</template>

<script setup lang="ts">
// 按钮常量引用
import { BUTTON_CANCELAUDIT, BUTTON_FLOW_VIEW } from "@qqt-product/ui";

// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  pageButtons: [
    {
      ...BUTTON_CANCELAUDIT,
      args: (pageData) => {
        const flowId =
          pageData.rectificationStatus === "0" ||
          pageData.rectificationStatus === "3"
            ? pageData.preFlowCode
            : pageData.resultFlowCode;

        const businessType =
          pageData.rectificationStatus === "0" ||
          pageData.rectificationStatus === "3"
            ? "supplierRetification"
            : "supplierRetificationResult";
        return {
          url: "/a1bpmn/audit/api/cancel",
          rootProcessInstanceId: flowId,
          businessType,
          auditSubject: "整改单号:" + currentRow.reportCode || "",
        };
      },
    },
    BUTTON_FLOW_VIEW,
  ],
});
</script>

自定义保存、自定义发布、自定义提交审批(同步校验)

TIP

现实情况中,各模块的业务需求可能比较复杂,需要灵活组合使用模板预设的方法,满足业务需求 如:自定义保存,需要在保存前,校验页面数据,校验通过后才允许保存 如:发布、或提交审批前,需要对页面某些字段做特殊校验,同步或异步操作,满足条件后才允许发布或提交审批

vue
<a-spin :spinning="customLoading">
  <q-edit-page-layout
    ref="layoutRef"
    @customSave="handleCustomSave"
    @customPublish="handleCustomPublish"
    @customSubmit="handleCustomSubmit"
    @validate-success="handleValidateSuccess"
  ></q-edit-page-layout>
</a-spin>

<script setup lang="ts">
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  // ...
  pageButtons: [
    {
      ...BUTTON_SAVE,
      title: "保存",
      emit: true,
      emitKey: "customSave",
      args: {
        addUrl: "/archiver/archiverConfigHead/saveConfig",
        url: "/archiver/archiverConfigHead/saveConfig",
      },
    },
    {
      ...BUTTON_PUBLISH,
      title: "custom Publish",
      emit: true,
      emitKey: "customPublish",
      args: {
        url: "/price/purchaseInformationRecordsRequest/publish",
      },
    },
    {
      ...BUTTON_SUBMIT,
      handleBefore: handleBeforeSubmit, // 调用 composeSubmit 方法后, 使有组合式提交审批功能, handleBefore 步骤方法会被调用
      title: "custom Submit",
      emit: true,
      emitKey: "customSubmit",
      args: {
        url: "/a1bpmn/audit/api/submit",
      },
    },
  ],
});

// 组件 ref
const layoutRef = ref<ComponentPublicInstance | null>(null);
const { pageData, layoutConfig, defaultValues } = useRefInstanceHook(layoutRef);
const { customLoading, stepTriggerValidate, composePublish, composeSubmit } =
  usePromiseStepHook({ pageData, layoutConfig, defaultValues, options });

// 自定义保存(页面校验 + 保存)
const handleCustomSave = () => {
  // 页面校验
  stepTriggerValidate();
};

// 页面校验成功后执行下一步回调步骤
const handleValidateSuccess = (button: GlobalPageLayoutTypes.PageButton) => {
  if (button.key === BUTTON_SAVE.key) {
    composeSave();
  }
};

// 执行自定义校验
const customCheck: (pageData: GlobalPageLayoutTypes.RecordString) => boolean = (
  pageData
) => {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const informationRecordsRequestItemList =
    pageData.informationRecordsRequestItemList as any[];
  if (!informationRecordsRequestItemList.length) {
    Message.error(
      srmI18n("i18n_alert_VWSuumUVcWF_1a6ec7eb", "请先添加价格申请行数据")
    );
    return false;
  }

  return true;
};

// 自定义发布
const handleCustomPublish = () => {
  if (!customCheck(pageData.value)) {
    return;
  }
  composePublish();
};

// 自定义提交审批
const handleCustomSubmit = () => {
  if (!customCheck(pageData.value)) {
    return;
  }
  composeSubmit();
};
</script>

back 事件监听组合式方法完成后回调

WARNING

模板提供的保存、发布、提交审批是多个异步方法的组合执行,可以通监听 back 回调事件(参数为当前按钮)执行后继的业务操作; 注:页面回退是监听 pageBack 事件, 两者不一样

vue
<template>
  <div class="edit-page">
    <q-edit-page-layout
      ref="layoutRef"
      @pageBack="handlePageBack"
      @back="back"
    ></q-edit-page-layout>
  </div>
</template>

<script setup lang="ts">
const backToList: (flag?: boolean) => void = (flag = false) => {
  const path = "/srm/trial/trialProductionHead/list";
  const query = flag ? { t: +new Date() } : {};
  router.push({ path, query });
};

const handlePageBack = () => {
  backToList(isCreate.value);
};

// 统一监听 back 事件处理具体业务逻辑
// 退出或自定义路由跳转
const back = (btn: GlobalPageLayoutTypes.PageButton) => {
  beforeRouteLeaveStatus.value = true;
  if (btn.key === BUTTON_SUBMIT.key) {
    // 提交审批完成后执行业务逻辑
    backToList(true);
  }
};
</script>

审批通过、退回(及其他审批功能)操作成功后回调事件监听

WARNING

审批详情页, 通过监听 flowApprovalCallback 事件处理各审批功能成功后的业务操作,e.g 回写 OA 操作。

vue
<template>
  <div class="detail-page">
    <q-detail-page-layout
      ref="layoutRef"
      @flowApprovalCallback="handleFlowApprovalCallback"
    ></q-detail-page-layout>
  </div>
</template>

<script setup lang="ts">
// ui组件库 types
import type { GlobalPageLayoutTypes } from "@qqt-product/ui";

const handleFlowApprovalCallback = ({
  btn,
  params,
}: GlobalPageLayoutTypes.FlowApprovalCallbackPayload) => {
  // 缓存的审批按钮配置
  console.log("btn :>> ", btn);
  // 缓存的审批接口参数
  console.log("params :>> ", params);
};
</script>

多分组表头配置

🙌 🌰 采购协同 > 质量协同 > 8D 改进

vue
<script setup lang="ts">
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
  // 本地页面数据配置
  localConfig: {
    groups: [
      {
        groupName: "D2:问题界定",
        groupNameI18nKey: "i18n_field_WWWQDyI_b3c4cc24",
        groupCode: "eightDisciplinesTwo",
        groupType: "head",
        sortOrder: "3",
        enableTile: "0", // 表头允许嵌套 
      },
      {
        groupName: "D4:原因分析",
        groupNameI18nKey: "i18n_field_WWWjWzH_980f28d5",
        groupCode: "eightDisciplinesFour",
        groupType: "head",
        sortOrder: "5",
        enableTile: "0", // 表头允许嵌套
      },
      {
        groupName: "D7:预防再发生",
        groupNameI18nKey: "i18n_field_WWWUCKhb_f026e026",
        groupCode: "eightDisciplinesSeven",
        groupType: "head",
        sortOrder: "8",
        enableTile: "0", // 表头允许嵌套
      },
      {
        groupName: "D8:结案评价",
        groupNameI18nKey: "i18n_field_WWWypUu_b2ce606e",
        groupCode: "eightDisciplinesEight",
        groupType: "head",
        sortOrder: "9",
        enableTile: "0", // 表头允许嵌套
      },
    ],
  },
});
</script>
js
// ...
// 分组配置 enableTile 为 "0"时,多个表单以嵌套的格式获取和提交
// {
//     "eightDisciplinesTwo": {
//         // ...
//         "id": "1720012953689456642"
//         // ...
//     },
//     "eightDisciplinesFour": {
//         // ...
//         "id": "1720012953865617409",
//         // ...
//     },
//     "eightDisciplinesSeven": {
//         // ...
//         "id": "1720012954029195266",
//         // ...
//     },
//     "eightDisciplinesEight": {
//         // ...
//         "id": "1720012954188578817",
//         // ...
//     },
//     // ...
//     "id": "1720012952280170498",
//     // ...
// }