Appearance
一些特例汇总
调试本地业务模板
TIP
业务模块 url 地址拼接时,域名默认取登录接口 "/els/account/login" 返回的 userInfo 中的 serivceUrl 值, 支持自定义配置;
ts
// 直接修改系统全局缓存的 userInfo
// 文件路径 "src/stores/user.ts"
// 此方法需要重新登录
// setUserInfo(result.userInfo) 修改为
export const useUserStore = defineStore('user', {
actions: {
setUserInfo<T extends object>(obj: T): void {
this.userInfo = {
...obj,
serivceUrl: 'http://127.0.0.1:8081', // 配置自定义业务模板地址
}
}
}
}
vue
<script setup lang="ts">
// 当前行 currentRow, 当前用户信息 userInfo
import { useGlobalStoreWithDefaultValue } from "@/use/useGlobalStore";
// 全局数据缓存
const { userInfo } = useGlobalStoreWithDefaultValue();
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
// 当前登录租户信息
userInfo: {
...userInfo.value,
serivceUrl: "http://127.0.0.1:8081", // 配置自定义业务模板地址
} as GlobalPageLayoutTypes.UserInfo,
});
</script>
获取模板配置
TIP
某些业务场景下,需要先获取业务模板的所有配置
🙌 🌰 采购协同 > 寻源协同 > 询价管理 成本报价
vue
<script setup lang="tsx">
import { loadJS, getRemoteJsFilePath } from "@qqt-product/ui";
const filePath: string = getRemoteJsFilePath(
{
currentRow: priceRow.value, // 当前行缓存 templateName, templateAccount, templateNumber, templateVersion, elsAccount, busAccount
userInfo: userInfo.value as GlobalPageLayoutTypes.UserInfo,
businessType: "costForm",
role: "purchase",
},
false
);
const config = await loadJS(`${filePath}?t=${+new Date()}`);
</script>
附件操作列判断是否可删除
vue
<script setup lang="tsx">
import type { GlobalPageLayoutTypes } from "@qqt-product/ui";
import type { VxeColumnSlotTypes, VxeTableDataRow } from "vxe-table";
import { useOperationColumnButtonHook } from "@qqt-product/ui";
const { handleDelete, handleDownload, handlePreview } =
useOperationColumnButtonHook();
const operationColumn: GlobalPageLayoutTypes.ColumnItem = {
groupCode: "purchaseAttachmentList",
field: "grid_operation",
title: "操作",
width: 220,
fieldLabelI18nKey: "i18n_title_operation",
align: "center",
fixed: "right",
slots: {
default({
row,
column,
$rowIndex,
}: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>) {
return [
<a-space>
<a-button
type="link"
onClick={() =>
handleDownload({
groupCode: "purchaseAttachmentList",
row,
rowIndex: $rowIndex,
})
}
>
{srmI18n("i18n_title_colunmDownload", "下载")}
</a-button>
<a-button
type="link"
onClick={() =>
handlePreview({
groupCode: "purchaseAttachmentList",
row,
rowIndex: $rowIndex,
})
}
>
{srmI18n("i18n_title_preview", "预览")}
</a-button>
<a-button
type="link"
onClick={() =>
handleDelete({
groupCode: "saleAttachmentList",
row,
rowIndex: $rowIndex,
})
}
disabled={row.uploadElsAccount !== userInfo.value.elsAccount}
>
{srmI18n("i18n_title_delete", "删除")}
</a-button>
</a-space>,
];
},
},
};
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
itemColumns: [operationColumn],
});
</script>
整单确认二次弹窗确认、整单拒绝二次弹窗且拒绝理由必填
TIP
"确认操作时需要二次弹窗确认, 拒绝操作时需要二次弹窗, 且拒绝理由 Textarea 输入框必填", 这类业务逻辑使用函数式方法组装更简洁
vue
<template>
<div class="detail-page">
<q-detail-page-layout
ref="layoutRef"
v-bind="options"
@customAccept="handleCustomAccept"
@customReject="handleCustomReject"
></q-detail-page-layout>
</div>
</template>
<script setup lang="ts">
import { Modal, message as Message, Textarea } from "ant-design-vue";
import type { ComponentPublicInstance } from "vue";
import { useRefInstanceHook } from "@qqt-product/ui";
// 组件 ref
const layoutRef = ref<ComponentPublicInstance | null>(null);
const remark = ref<string>(""); // 意见
const { pageData } = useRefInstanceHook(layoutRef);
const acceptOrRejectAction = (type: string) => {
const isReject = type === "reject";
const url: string = isReject ? "/xxx/reject" : "/xxx/pass";
const data = { ...pageData.value, remark: remark.value };
Request.post({ url, data }).then((res) => {
if (res.success) {
Message.success(res.message);
backToList(true);
} else {
Message.error(res.message);
}
});
};
const showPropsConfirm = (type: "confirm" | "reject") => {
const isReject = type === "reject";
const title = srmI18n("i18n_title_tip", "提示");
const subTitle = isReject
? h(
"span",
{ style: "color:red; margin-top: 10px;" },
srmI18n("i18n_title_isSureReviewNotPassed", "是否确认审查拒绝?")
)
: srmI18n("i18n_title_isSureReviewPassed", "是否确认审查通过?");
const textareaNode = h(Textarea, {
style: {
marginTop: "12px",
},
onChange(e) {
rejectReason.value = e.target.value as string;
},
defaultValue: (pageData.value.remark as string) || "",
placeholder: "请输入备注内容",
maxlength: 400,
autosize: { minRows: 3, maxRows: 6 },
});
const contentNode = h("div", [subTitle, textareaNode]);
const content = isReject
? contentNode
: srmI18n(`i18n_title_isSureReviewPassed`, "是否确认审查通过?");
Modal.confirm({
width: isReject ? 520 : 416,
closable: true,
title,
content,
onOk() {
if (isReject && !rejectReason.value) {
Message.error("请输入拒绝理由");
return Promise.reject("error"); // 阻止弹窗关闭
}
const url: string = isReject
? "/qualification/purchaseQualificationReview/reject"
: "/qualification/purchaseQualificationReview/pass";
const data = { ...pageData.value, remark: remark.value };
Request.post({ url, data })
.then((res) => {
if (res.success) {
Message.success(res.message);
backToList(true);
} else {
Message.error(res.message);
}
})
.finally(() => {
return Promise.resolve("success");
});
},
});
};
const handleCustomAccept = () => {
console.log("handleCustomAccept :>> ");
showPropsConfirm("confirm");
};
const handleCustomReject = () => {
console.log("handleCustomReject :>> ");
showPropsConfirm("reject");
};
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
pageButtons: [
{
...BUTTON_CONFIRM,
emit: true,
emitKey: "customAccept",
args: {
url: "/price/saleInformationRecordsRequest/confirmed",
},
authorityCode:
"informationRecordsRequest#SaleInformationRecordsRequest:confirmed",
disabled(pageData) {
return pageData.status !== "2";
},
},
{
...BUTTON_REJECT,
emit: true,
emitKey: "customReject",
args: {
url: "/price/saleInformationRecordsRequest/rejected",
},
authorityCode:
"informationRecordsRequest#SaleInformationRecordsRequest:rejected",
disabled(pageData) {
return pageData.status !== "2";
},
},
],
});
</script>
表行功能按钮批量操作与操作列渲染单行操作
🙌 🌰 采购协同 > 质量协同 > 样品申请 详情页(收货、关闭、转需求池)
vue
<template>
<div class="detail-page">
<q-detail-page-layout
ref="layoutRef"
v-bind="options"
@batchReceive="handleBatchReceive"
@batchClose="handleBatchClose"
@batchTransform="handleBatchTransform"
></q-detail-page-layout>
</div>
</template>
<script setup lang="tsx">
// ...
// 校验是否已勾选
const checkoutSelectedRows = (
btn: GlobalPageLayoutTypes.PageButtonWithGroupCode
) => {
console.log("click checkoutIsSelected :>> ", btn);
const { groupCode } = btn;
const tableData = pageData.value[
groupCode
] as GlobalPageLayoutTypes.RecordString[];
const selectedRows = tableData.filter((n) => !!n._checked);
if (!selectedRows.length) {
Message.warning(srmI18n(`i18n_title_selectDataMsg`, "请选择数据"));
return [];
}
console.log("selectedRows :>> ", selectedRows);
return selectedRows;
};
// 收货
const handleReceive = (rows: GlobalPageLayoutTypes.RecordString[]) => {
console.log("handleReceive rows :>> ", rows);
for (const [idx, item] of rows.entries()) {
if (item.itemStatus !== "8") {
const errorMsg = ["已选行", `${idx + 1}`, "非送样中状态,无法收货"];
Message.error(errorMsg.join(""));
return;
}
}
const callback = () => {
const url = "/sample/purchaseSampleHead/signSample";
const data = rows.map((n) => n);
Request.post({ url, data }).then((res) => {
if (res.success) {
Message.success(res.message);
handleRefresh();
} else {
Message.error(res.message);
}
});
};
Modal.confirm({
title: srmI18n("i18n_alert_RLlS_38d78a27", "确认收货"),
content: srmI18n("i18n_field_KQRLlSW_ebe6d581", "是否确认收货?"),
onOk: callback,
});
};
// 批量收货操作
const handleBatchReceive: (
btn: GlobalPageLayoutTypes.PageButtonWithGroupCode
) => void = (btn) => {
const selectedRows = checkoutSelectedRows(btn);
if (!selectedRows.length) {
return;
}
handleReceive(selectedRows);
};
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
groups: [
{
groupName: "样品行信息",
groupNameI18nKey: "i18n_field_VNcVH_e5240e50",
groupCode: "purchaseSampleItemList",
groupType: "item",
sortOrder: "2",
buttons: [
{
...BUTTON_ADD_ONE_ROW,
authorityCode: "sample#purchaseSampleHead:signSample",
key: "_receive",
title: "批量收货",
emit: true,
emitKey: "batchReceive",
},
{
...BUTTON_ADD_ONE_ROW,
authorityCode: "sample#purchaseSampleHead:colseByItems",
key: "_close",
title: "批量关闭",
emit: true,
emitKey: "batchClose",
},
{
...BUTTON_DELETE_ROW,
authorityCode: "sample#purchaseSampleHead:transformRequest",
key: "_transform",
title: "批量转需求池",
emit: true,
emitKey: "batchTransform",
},
],
},
],
});
</script>
表行列配置自定义渲染 tsx
TIP
需求:确认项 表行 预制选项 字段,需要根据 填写类型 字段值,动态渲染为 input 或 select(单选或多选)
🙌 🌰 采购协同 > 寻源协同 > 竞价管理 供应方
vue
<script setup lang="tsx">
// ui组件库 types
import type { GlobalPageLayoutTypes } from "@qqt-product/ui";
import type { VxeColumnSlotTypes, VxeTableDataRow } from "vxe-table";
// 确认项 列配置
const purchaseEbiddingConfirmList = [
{
field: "writeType",
title: srmI18n("i18n_field_SMAc_2973057e", "填写类型"),
width: 120,
},
{
field: "content",
title: srmI18n("i18n_field_URid_469b0f42", "预制选项"),
width: 200,
editRender: {
enabled: true,
},
slots: {
default({
row,
column,
}: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>) {
return [<span>{row[column.field]}</span>];
},
edit({
row,
column,
}: VxeColumnSlotTypes.EditSlotParams<VxeTableDataRow>) {
let elem;
let options: GlobalPageLayoutTypes.RecordString[] = [];
// 填写类型 writeType
// 数据字典 inspection_item_write_type (0: 单选, 1: 多选, 2: 手输)
if (row.writeType === "0" || row.writeType === "1") {
options = row.confirmItemList.map(
(n: GlobalPageLayoutTypes.RecordString) => ({
label: n.optionsName,
value: n.optionsCode,
})
);
}
const props = {
options,
transfer: true,
filterable: true,
clearable: true,
};
// 处理单选
if (row.writeType === "0") {
elem = (
<q-vxe-select v-model={row[column.field]} {...props}></q-vxe-select>
);
}
// 处理多选
if (row.writeType === "1") {
elem = (
<q-vxe-select
v-model={row[column.field]}
{...props}
multiple={true}
></q-vxe-select>
);
}
// 处理手输
if (row.writeType === "2") {
elem = <vxe-input v-model={row[column.field]}></vxe-input>;
}
return [elem];
},
},
},
];
const purchaseEbiddingConfirmListColumns = purchaseEbiddingConfirmList.map(
(n) => ({ ...n, groupCode: "purchaseEbiddingConfirmList" })
) as GlobalPageLayoutTypes.ColumnItem[];
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
itemColumns: [...purchaseEbiddingConfirmListColumns],
});
</script>
vue
<script setup lang="tsx">
// ui组件库 types
import type { GlobalPageLayoutTypes } from "@qqt-product/ui";
import type { VxeColumnSlotTypes, VxeTableDataRow } from "vxe-table";
// 确认项 列配置
const purchaseEbiddingConfirmList = [
{
field: "writeType",
title: srmI18n("i18n_field_SMAc_2973057e", "填写类型"),
width: 120,
},
{
field: "content",
title: srmI18n("i18n_field_URid_469b0f42", "预制选项"),
width: 200,
slots: {
default({
row,
column,
}: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>) {
const val = row[column.field];
// 处理单选
if (row.writeType === "0") {
const confirmItemList: GlobalPageLayoutTypes.RecordString[] =
row.confirmItemList || [];
const item: GlobalPageLayoutTypes.RecordString = confirmItemList.find(
(n) => n.optionsCode === val
) || { optionsName: "" };
return [<span>{item.optionsName}</span>];
}
// 处理多选
if (row.writeType === "1") {
const confirmItemList: GlobalPageLayoutTypes.RecordString[] =
row.confirmItemList || [];
let str = "";
if (val) {
let result = val.split(",");
str = confirmItemList.reduce((acc, obj, i) => {
if (result.includes(obj.optionsCode)) {
acc += obj.optionsName;
if (i !== confirmItemList.length - 1) {
acc += ", ";
}
}
return acc;
}, "");
}
return [<span>{str}</span>];
}
// 处理手输
return [<span>{row[column.field]}</span>];
},
},
},
];
const purchaseEbiddingConfirmListColumns = purchaseEbiddingConfirmList.map(
(n) => ({ ...n, groupCode: "purchaseEbiddingConfirmList" })
) as GlobalPageLayoutTypes.ColumnItem[];
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
itemColumns: [...purchaseEbiddingConfirmListColumns],
});
</script>
动态设置表行分组数据
TIP
需求:考察项目分项权重表行数据 需要通过 考察项目 表行动态计算获取
🙌 🌰 采购协同 > 质量协同 > 现场考察 > 考察标准
1.监听模板表行添加、删除事件
2.配合绑定函数联动操作
vue
<template>
<div class="edit-page">
<q-edit-page-layout
ref="layoutRef"
v-bind="options"
@addRow="handleAddRow"
@deleteRow="handleDeleteRow"
></q-edit-page-layout>
</div>
</template>
<script setup lang="ts">
// ...
const { pageData } = useRefInstanceHook(layoutRef);
const handleAddRow = (btn: GlobalPageLayoutTypes.PageButtonWithGroupCode) => {
console.log("btn :>> ", btn);
setItemWeightListData(btn.groupCode);
};
const handleDeleteRow = (
btn: GlobalPageLayoutTypes.PageButtonWithGroupCode
) => {
console.log("btn :>> ", btn);
setItemWeightListData(btn.groupCode);
};
const cacheClassify = ref<string>();
// 过滤 考察项目 表行数据中的 ‘考察项目分类’
// 用于设置考察项目分项权重表行数据
const setItemWeightListData = (groupCode: string) => {
if (groupCode !== "itemList") {
return;
}
// 当前考察项目分类
let itemList = pageData.value[
groupCode
] as GlobalPageLayoutTypes.RecordString[];
let classify = itemList
.filter((n) => !!n.inspectionItemClassify)
.map((n) => {
return n.inspectionItemClassify as string;
});
let currentClassify = classify.join("");
console.log("currentClassify :>> ", currentClassify);
// 如果考察项目未修改,则跳过
if (cacheClassify.value === currentClassify) {
return;
}
cacheClassify.value = currentClassify;
classify = [...new Set(classify)];
let length = classify.length;
let total = 100;
const rows = classify.map((n, idx) => {
// 计算各行项分类权重, 总和为100
let classifyWeight: number;
if (length === 1) {
classifyWeight = 100;
} else if (length > 1 && idx !== length - 1) {
classifyWeight = Math.floor(100 / length);
total -= classifyWeight;
} else {
classifyWeight = total;
}
// classifyWeight = String(classifyWeight)
console.log("classifyWeight :>> ", classifyWeight);
return {
inspectionItemClassify: n,
classifyWeight,
remark: "",
};
});
pageData.value.itemWeightList = rows;
};
</script>
tex
groupCode: 'itemList',
title: '考察项目分类',
fieldLabelI18nKey: 'i18n_dict_BmdIzA_c727a5c6',
fieldType: 'select',
field: 'inspectionItemClassify',
align: '',
headerAlign: 'center',
dataFormat: '',
defaultValue: '',
dictCode: 'inspection_item_classify',
alertMsg: '',
required:'1',
js
/**
* @param {Object} ctx 组件实例
* @param {String} value 当前所选值
* @param {Array} data selectModal, remoteSelect 已选行数据 (如有)
* @param {Object} pageData 页面所有数据
* @param {Object} layoutConfig 模板配置
* @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 自定义设置字段必填/置灰
*/
function callback(
ctx,
{
value,
data,
row,
idx,
pageData,
layoutConfig,
customFormatItem,
setItemRequired,
setItemDisabled,
setItemRequiredOrDisabled,
}
) {
// 当前考察项目分类 inspectionItemClassify
let itemList = pageData["itemList"];
let itemWeightList = pageData["itemWeightList"];
let classify = itemList
.filter((n) => !!n.inspectionItemClassify)
.map((n) => n.inspectionItemClassify);
let currentClassify = classify.join("");
console.log("currentClassify :>> ", currentClassify);
let cacheClassify = itemWeightList
.filter((n) => !!n.inspectionItemClassify)
.map((n) => n.inspectionItemClassify)
.join("");
// 如果考察项目未修改,则跳过
if (cacheClassify === currentClassify) {
return;
}
classify = [...new Set(classify)];
let length = classify.length;
let total = 100;
const rows = classify.map((n, idx) => {
// 计算各行项分类权重, 总和为100
let classifyWeight = 0;
if (length === 1) {
classifyWeight = 100;
} else if (length > 1 && idx !== length - 1) {
classifyWeight = Math.floor(100 / length);
total -= classifyWeight;
} else {
classifyWeight = total;
}
// classifyWeight = String(classifyWeight)
console.log("classifyWeight :>> ", classifyWeight);
return {
inspectionItemClassify: n,
classifyWeight,
remark: "",
};
});
pageData.itemWeightList = rows;
}
表头字段自定义弹窗选择渲染
TIP
业务场景中,表头某字段需要配置为自定义的弹窗,默认传参、接口查询、行勾选并点击确定后需要赋值到字段上
vue
<template>
<div class="edit-page">
<a-spin :spinning="loading || customLoading">
<q-edit-page-layout ref="layoutRef" v-bind="options">
<!-- 关联项目编码 渲染为只读输入框 -->
<template
#vertical_baseForm_customSlot_projectNumber="{ field, pageData }"
>
<a-input
v-model:value="pageData[field.fieldName]"
size="large"
readOnly
allowClear
style="cursor: pointer"
@click="handleFieldClick"
/>
</template>
</q-edit-page-layout>
</a-spin>
<!-- 关联项目编码 弹窗选择 -->
<project-number-modal
v-model="visible"
@success="handleSuccess"
></project-number-modal>
</div>
</template>
<script lang="ts" setup>
import ProjectNumberModal from "./component/projectNumberModal.vue";
import { useRefInstanceHook } from "@qqt-product/ui";
const { pageData, layoutConfig, defaultValues } = useRefInstanceHook(layoutRef);
const visible = ref<boolean>(false);
const handleFieldClick = () => {
visible.value = true;
};
const handleSuccess = ({ data }: Payload) => {
const projectNumber = (data[0].projectNumber as string) || "";
pageData.value.projectNumber = projectNumber;
};
// 过滤业务模板相同字段
const handleAfterRemoteConfig = (config) => {
const formFields = config.formFields || [];
config.formFields = formFields.filter((n) => n.fieldName !== "projectNumber");
return config; // 返回处理后的配置数据, 可以是 promise 对象
};
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
handleAfterRemoteConfig,
// 2. 配置字段为自定义插槽类型
localConfig: {
formFields: [
{
groupCode: "baseForm",
sortOrder: "30",
fieldType: "customSlot",
fieldLabel: "关联项目编码",
fieldLabelI18nKey: "i18n_field_RKdIAo_54458cc1",
fieldName: "projectNumber",
},
],
},
});
</script>
vue
<template>
<div class="checkPrice">
<a-modal
:visible="modelValue"
title="查询关联项目编码"
width="1000px"
@cancel="handleCancel"
@ok="handleOk"
>
<div class="form">
<a-form
style="display: flex; flex-flow: row wrap; margin: 4px"
layout="inline"
>
<a-form-item
style="margin-right: 12px"
:label="srmI18n('i18n_field_availableQty', '可用数量')"
>
<q-select
style="width: 200px"
v-model:value="form.projectType"
:placeholder="
srmI18n('i18n_field_ViFqjWR_6f4361ef', '请选择可用数量')
"
allowClear
dict-code="projectType"
/>
</a-form-item>
<a-form-item
style="margin-right: 12px"
:label="srmI18n('i18n_title_keyword', '关键字')"
>
<a-input
style="width: 200px"
:placeholder="
srmI18n('i18n_title_pleaseInputThekeyword', '请输入关键字')
"
v-model:value="form.keyWord"
allowClear
/>
</a-form-item>
<a-form-item style="margin-right: 12px">
<a-button type="primary" @click="getData">{{
srmI18n("i18n_title_Query", "查询")
}}</a-button>
</a-form-item>
</a-form>
</div>
<div class="grid" style="margin-top: 12px">
<vxe-grid
ref="xGrid"
v-bind="gridConfig"
:pager-config="pagerConfig"
:loading="loading"
:columns="columns"
:data="tableData"
@page-change="handlePageChange"
>
<template #empty>
<q-empty></q-empty>
</template>
</vxe-grid>
</div>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, toRefs, watch } from "vue";
import { srmI18n } from "@/utils/srmI18n";
// ui组件库 types
import type { GlobalPageLayoutTypes } from "@qqt-product/ui";
import type {
VxeGridInstance,
VxeGridProps,
VxeGridPropTypes,
VxePagerEvents,
} from "vxe-table";
import { editVxeTableConfig } from "@qqt-product/ui";
import qqtApi from "@qqt-product/api";
import { message as Message } from "ant-design-vue";
const Request: qqtApi.Request = qqtApi.useHttp();
interface Props {
modelValue: boolean;
}
interface Payload {
data: GlobalPageLayoutTypes.RecordString[];
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
});
const { modelValue } = toRefs(props);
const emit = defineEmits<{
(e: "update:modelValue", modelValue: boolean): void;
(e: "success", payload: Payload): void;
}>();
const xGrid = ref<VxeGridInstance>();
const rawState = {
projectType: "0", // 默认询价管理
keyWord: "",
};
const form = ref<GlobalPageLayoutTypes.RecordString>(rawState);
const loading = ref<boolean>(false);
const tableData = ref<GlobalPageLayoutTypes.RecordString[]>([]);
const pagerConfig = ref({
total: 0,
currentPage: 1,
pageSize: 20,
});
const gridConfig: VxeGridProps = {
...editVxeTableConfig,
rowConfig: {
keyField: "id",
isHover: true,
isCurrent: true,
},
radioConfig: {
trigger: "row",
},
height: 400,
keyboardConfig: {},
menuConfig: {},
mouseConfig: {}, // 鼠标配置项
areaConfig: {}, // 按键配置项
seqConfig: {
startIndex:
((pagerConfig.value.currentPage as number) - 1) *
(pagerConfig.value.pageSize as number),
},
};
const columns: VxeGridPropTypes.Columns = [
{ type: "radio", width: 50, fixed: "left" },
{ type: "seq", width: 60, fixed: "left" },
{ field: "projectNumber", title: "项目编号", minWidth: 150 },
{ field: "projectName", title: "项目名称", minWidth: 150 },
{ field: "projectStatus_dictText", title: "项目状态", minWidth: 150 },
];
const resetData = () => {
pagerConfig.value = {
total: 0,
currentPage: 1,
pageSize: 20,
};
tableData.value = [];
form.value = { ...rawState };
};
watch(modelValue, (bool) => {
if (bool) {
getData();
} else {
resetData();
}
});
const handleCancel = () => {
emit("update:modelValue", false);
};
const handleOk = () => {
const $grid = xGrid.value;
if ($grid) {
let row = $grid.getRadioRecord();
if (!row) {
Message.error(srmI18n(`i18n_title_selectDataMsg`, "请选择数据"));
return;
}
const payload: Payload = {
data: [row],
};
emit("success", payload);
handleCancel();
}
};
const getData = () => {
loading.value = true;
const url = "/enquiry/purchaseEnquiryHead/listRelationProject";
const params = {
pageSize: pagerConfig.value.pageSize,
pageNo: pagerConfig.value.currentPage,
column: "id",
order: "asc",
...form.value,
};
loading.value = true;
Request.get({ url, params })
.then((res) => {
if (!res.success) {
Message.error(res.message);
return;
}
const records = res.result.records || [];
tableData.value = records;
pagerConfig.value.total = res.result.total;
})
.finally(() => {
loading.value = false;
});
};
const handlePageChange: VxePagerEvents.PageChange = ({
currentPage,
pageSize,
}) => {
pagerConfig.value.currentPage = currentPage;
pagerConfig.value.pageSize = pageSize;
getData();
};
</script>
缓存字段值,字段当前值改变后,需要根据字段值请求接口重新赋值表行数据,另一个表行下拉列选项需要根据接口值重新过滤渲染
TIP
需求:表头 考察步骤 和 考察标准名称 需要缓存,任一值改变后需要请求接口,根据接口返回数据重新赋值考察项目内容,同时考察小组表行的“负责的考察分类”列下拉选项内容需要根据接口数据动态过滤生成
🙌 🌰 采购协同 > 质量协同 > 现场考察 > 现场考察
1.handleAfterDetailApiResponse 缓存当前字段值
2.编辑模板监听绑定函数事件,处理请求等复杂逻辑
3.jsx 实现动态列配置需求
vue
<script setup lang="tsx">
// ui组件库 types
import type { GlobalPageLayoutTypes } from "@qqt-product/ui";
// 工具函数及types
import qqtUtils from "@qqt-product/utils";
const { isEqual } = qqtUtils;
// 缓存
let cacheObj: GlobalPageLayoutTypes.RecordString = {};
const handleAfterDetailApiResponse = ({
pageData,
}: GlobalPageLayoutTypes.Expose) => {
console.log("pageData :>> ", pageData);
// 考察步骤 inspectionStep
const inspectionStep = pageData.inspectionStep as string;
// 考察标准名称 inspectionStandardName
const inspectionStandardName = pageData.inspectionStandardName as string;
// 缓存当前考察步骤 + 考察标准名称
cacheObj = {
inspectionStep,
inspectionStandardName,
};
};
</script>
vue
<template>
<div class="edit-page">
<a-spin :spinning="loading">
<q-edit-page-layout
ref="layoutRef"
v-bind="options"
@scoreGroupListAdd="handleScoreGroupListAdd"
@inspectionStep_callback="handleInspectionStepCallback"
@inspectionStandardName_callback="handleInspectionStandardNameCallback"
></q-edit-page-layout>
</a-spin>
</div>
</template>
<script setup lang="tsx">
// 工具函数及types
import qqtUtils from "@qqt-product/utils";
const { isEqual } = qqtUtils;
const isEdit = computed(() => {
// 单据状态 documentsStatus_dictText
// 数据字典 site_inspection_status (0: 新建, 1: 待自评, 2: 待评分, 3: 部分评分, 4: 评分完成, 5: 考察完成, 6: 结案, 7: 作废)
const documentsStatus = pageData.value.documentsStatus as string;
return !documentsStatus || documentsStatus === "0";
});
// 考察步骤 inspectionStep
// 复杂绑定函数
const handleInspectionStepCallback: (
config: GlobalPageLayoutTypes.BindFunctionEvent
) => void = () => {
handleFieldCallback();
};
// 考察标准名称 inspectionStandardName
// 复杂绑定函数
const handleInspectionStandardNameCallback: (
config: GlobalPageLayoutTypes.BindFunctionEvent
) => void = () => {
handleFieldCallback();
};
const handleFieldCallback = () => {
if (!isEdit.value) {
return;
}
// 考察步骤 inspectionStep
const inspectionStep = pageData.value.inspectionStep as string;
// 考察标准名称 inspectionStandardName
const inspectionStandardName = pageData.value
.inspectionStandardName as string;
const inspectionStandardVersion = pageData.value
.inspectionStandardVersion as string;
const cur = {
inspectionStep,
inspectionStandardName,
};
// 缓存数据未改变
if (!inspectionStep && !inspectionStandardName) {
return;
}
if (isEqual(cacheObj, cur)) {
return;
} else {
Modal.confirm({
title: srmI18n("i18n_alert_SMBmdI_9c235a50", "获取考察项目"),
content: srmI18n(
"i18n_alert_BmxsSBmBrRLrASPVVSMBmdIeKVVBmXVWFKQtT_5a374964",
"考察步骤或考察标准名称改变后将重新获取考察项目, 同时清空考察小组数据,是否继续"
),
onOk() {
// 缓存当前考察步骤 + 考察标准名称
cacheObj = {
inspectionStep,
inspectionStandardName,
};
const url = "/other/purchaseInspectionHead/getItems";
const data = {
inspectionStep,
name: inspectionStandardName,
versionNumber: inspectionStandardVersion,
};
loading.value = true;
Request.post({ url, data })
.then((res) => {
if (res.success) {
Message.success(res.message);
pageData.value.scoreGroupList = [];
// 考察项目表行赋值
pageData.value.inspectionItemList = res.result || [];
Notification["warning"]({
message: srmI18n("i18n_alert_DK_c8f6a", "提示"),
description: srmI18n(
"i18n_alert_BmXVWFIVV_68557a98",
"考察小组数据已清空"
),
});
} else {
Message.error(res.message);
}
})
.finally(() => {
loading.value = false;
});
},
onCancel() {
pageData.value.inspectionStep = cacheObj.inspectionStep;
pageData.value.inspectionStandardName = cacheObj.inspectionStandardName;
},
});
}
};
</script>
vue
<script setup lang="tsx">
// ...
let inspectionClassifyColumn: GlobalPageLayoutTypes.ColumnItem = {
groupCode: "scoreGroupList",
field: "inspectionClassify",
title: srmI18n("i18n_field_BFjBmzA_cf3ae791", "负责的考察分类"),
width: 200,
editRender: {
enabled: true, // 允许使用作用域插槽
},
required: "1",
slots: {
default({
row,
column,
}: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>) {
return [
<span>{row[`${column.field}_dictText`] || row[column.field]}</span>,
];
},
edit({ row, column }: VxeColumnSlotTypes.EditSlotParams<VxeTableDataRow>) {
let elem;
const props = {
options: inspectionClassifyOptions.value,
transfer: true,
filterable: true,
clearable: true,
multiple: true,
};
const onVxeSelectChange = ({
value,
label,
}: {
value: string;
label: string;
}) => {
row[`${column.field}`] = value;
row[`${column.field}_dictText`] = label;
};
elem = (
<q-vxe-select
modelValue={row[column.field]}
{...props}
onChange={onVxeSelectChange}
></q-vxe-select>
);
return [elem];
},
},
};
scoreGroupListColumns.splice(3, 1, inspectionClassifyColumn);
</script>
表行数据查询入参为表头某字段, 更新关联关系后弹窗二次确认清空关系表行数据需求
🙌 🌰 采购协同 > 质量协同 > 供应商整改
- 修改业务模板,触发字段绑定函数的 topEmit 方法
- 代码实现
1. 声明缓存变量、连接符、占位符
2. handleAfterDetailApiResponse 缓存关联的字段值
3. 监听字段变化,弹窗二次确认
4. 确认后执行清除表行回调、更新缓存变量值
5. 拒绝后从缓存值中恢复原数据
tex
groupCode: 'baseForm',
sortOrder: '2',
fieldType: 'remoteSelect',
fieldLabel: '对方ELS账号',
fieldLabelI18nKey: 'i18n_field_toElsAccount',
fieldName: 'toElsAccount',
js
/**
* @param {Object} ctx 组件实例
* @param {String} value 当前所选值
* @param {Array} data selectModal, remoteSelect 已选行数据 (如有)
* @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,
data,
pageData,
layoutConfig,
customFormatItem,
setItemRequired,
setItemDisabled,
setItemRequiredOrDisabled,
topEmit,
}
) {
const {
toElsAccount = "",
supplierCode = "",
supplierName = "",
} = data[0] || {};
pageData.toElsAccount = toElsAccount;
pageData.supplierCode = supplierCode;
pageData.supplierName = supplierName;
// 本地页面处理复杂绑定函数需求
topEmit && topEmit();
}
vue
<template>
<q-edit-page-layout
@toElsAccount_callback="handleToElsAccountCallback"
></q-edit-page-layout>
</template>
<script setup lang="ts">
//...
// 缓存
const cacheObj = ref<GlobalPageLayoutTypes.RecordString>({});
const handleAfterDetailApiResponse = ({
pageData,
}: GlobalPageLayoutTypes.Expose) => {
console.log("pageData :>> ", pageData);
// 缓存需要关联的字段值
// 供应商ELS账号 toElsAccount
// 供应商名称 supplierName
// 供应商编码 supplierCode
const { toElsAccount, supplierName, supplierCode } = pageData;
cacheObj.value = { toElsAccount, supplierName, supplierCode };
};
// 供应商ELS账号 toElsAccount
// 复杂绑定函数
const handleToElsAccountCallback: (
config: GlobalPageLayoutTypes.BindFunctionEvent
) => void = () => {
handleFieldCallback();
};
const handleFieldCallback = () => {
const { toElsAccount, supplierName, supplierCode } = pageData.value;
// 缓存数据未改变
if (cacheObj.value.toElsAccount === toElsAccount) {
return;
}
const content = [
srmI18n("i18n_title_supplierELSAccount", "供应商ELS账号"),
srmI18n("i18n_alert_RrAS_25ff6c73", "值改变后"),
",",
srmI18n("i18n_alert_PVG_1675b85", "将清除"),
srmI18n("i18n_title_supplierAllChageLineInfo", "供应商整改行信息"),
srmI18n("i18n_alert_BcWF_400e0fe2", "表行数据"),
",",
srmI18n("i18n_alert_KQtT_2fbef6fd", "是否继续"),
"?",
];
Modal.confirm({
title: "操作确认",
content: content.join(""),
onOk() {
// 清空行数据
pageData.value.purchaseSupplierRectificationReportItemList = [];
const description = [
srmI18n("i18n_title_supplierAllChageLineInfo", "供应商整改行信息"),
"表行数据",
"已清空",
];
Notification["warning"]({
message: srmI18n("i18n_alert_DK_c8f6a", "提示"),
description: description.join(""),
});
// 重置缓存数据
cacheObj.value = { toElsAccount, supplierName, supplierCode };
},
onCancel() {
const { toElsAccount, supplierName, supplierCode } = cacheObj.value;
pageData.value.toElsAccount = toElsAccount;
pageData.value.supplierName = supplierName;
pageData.value.supplierCode = supplierCode;
},
});
};
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
handleAfterDetailApiResponse,
});
</script>
阶梯报价重构-允许输入小于 1 的价格基数
TIP
把阶梯报价拆分为显示、输入两部分,表行插槽动态渲染实现
vue
<template>
<div class="edit-page">
<q-edit-page-layout ref="layoutRef" v-bind="options"></q-edit-page-layout>
<!-- 阶梯报价 -->
<ladder-price-modal
v-model="ladderPriceVisible"
:value="ladderPriceValue"
:cachePayload="ladderPriceCachePayload"
@success="handleLadderPriceSuccess"
></ladder-price-modal>
</div>
</template>
<script setup lang="tsx">
import { ref, reactive, onBeforeMount } from "vue";
import type { ComponentPublicInstance } from "vue";
import LadderPriceDisplay from "./components/ladderPriceDisplay.vue";
import LadderPriceModal from "./components/ladderPriceModal.vue";
// 缓存行信息
const cachePayload =
ref<VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>>();
const ladderPriceVisible = ref<boolean>(false);
const ladderPriceValue = ref<string>("");
const ladderPriceCachePayload =
ref<VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>>();
const handleLadderPriceSuccess = (str) => {
if (ladderPriceCachePayload.value) {
ladderPriceCachePayload.value.row.ladderPriceJson = str;
}
};
const handleAfterRemoteConfig = (config) => {
const itemColumns = config.itemColumns || [];
itemColumns.forEach((n) => {
// 阶梯报价JSON
if (n.field === "ladderPriceJson") {
n.fieldType = "";
n.slots = {
default(
payload: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
) {
const { row, column } = payload;
const val = row[column.field] || "";
const handleLadderPriceClick = () => {
ladderPriceCachePayload.value = payload;
ladderPriceValue.value = val;
ladderPriceVisible.value = true;
};
const handleLadderPriceClear = () => {
row.ladderPriceJson = "";
};
return [
<LadderPriceDisplay
value={val}
onClick={handleLadderPriceClick}
onClear={handleLadderPriceClear}
></LadderPriceDisplay>,
];
},
};
}
});
return config;
};
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
handleAfterRemoteConfig,
});
</script>
vue
<template>
<div class="ladder-price-display">
<a-tooltip placement="topLeft" overlayClassName="tip-overlay-class">
<template #title>
<div class="grid">
<vxe-table
ref="xGrid"
auto-resize
border
row-id="id"
size="mini"
:data="tableData"
>
<vxe-table-column
type="seq"
:title="srmI18n('i18n_alert_seq', '序号')"
width="80"
></vxe-table-column>
<vxe-table-column
field="ladder"
:title="srmI18n('i18n_title_ladderLeve', '阶梯级')"
width="140"
></vxe-table-column>
<vxe-table-column
field="price"
:title="srmI18n('i18n_title_price', '含税价')"
width="140"
></vxe-table-column>
<vxe-table-column
field="netPrice"
:title="srmI18n('i18n_title_netPrice', '不含税价')"
width="140"
></vxe-table-column>
</vxe-table>
</div>
</template>
{{ str }}
</a-tooltip>
<a class="icons">
<StockOutlined @click="handleClick" />
<CloseCircleOutlined v-show="str" @click="handleClear" />
</a>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { StockOutlined, CloseCircleOutlined } from "@ant-design/icons-vue";
// 国际化
import { srmI18n } from "@/utils/srmI18n";
// ui组件库 types
import type { GlobalPageLayoutTypes } from "@qqt-product/ui";
type Props = {
value: string;
};
const props = withDefaults(defineProps<Props>(), {
value: "",
});
const emit = defineEmits<{
(e: "click"): void;
(e: "clear"): void;
}>();
const str = computed<string>(() => {
if (props.value) {
try {
const arr = JSON.parse(props.value);
if (Array.isArray(arr)) {
return arr.map((n) => `${n.ladder}:${n.price || ""}`).join(";");
}
} catch (err) {
console.log("parse ladderPriceJSON error :>> ", err);
}
}
return "";
});
const tableData = computed<GlobalPageLayoutTypes.RecordString[]>(() => {
if (props.value) {
try {
const arr = JSON.parse(props.value);
if (Array.isArray(arr)) {
return arr;
}
} catch (err) {
console.log("parse ladderPriceJSON error :>> ", err);
}
}
return [];
});
const handleClick = () => emit("click");
const handleClear = () => emit("clear");
</script>
<style lang="less">
.ladder-price-display {
position: relative;
min-height: 30px;
padding-right: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.icons {
position: absolute;
// display: inline-block
font-size: 16px;
right: 0px;
top: 3px;
z-index: 10;
}
}
</style>
vue
<template>
<div class="checkPrice">
<a-modal
:visible="modelValue"
width="580px"
:title="srmI18n('i18n_title_ladderSetting', '阶梯设置')"
:maskClosable="false"
:keyboard="false"
@cancel="handleCancel"
@ok="handleOk"
>
<div class="form">
<a-form
style="display: flex; flex-flow: row wrap; margin: 4px"
layout="inline"
>
<a-form-item
style="margin-right: 12px"
:label="srmI18n('i18n_title_ladderCount', '阶梯数量')"
>
<a-input-number
style="width: 200px"
placeholder="请输入大于0的阶梯数量"
v-model:value="form.ladderQuantity"
:min="0"
@pressEnter="setData"
/>
</a-form-item>
<a-form-item style="margin-right: 12px">
<a-button type="primary" @click="setData">{{
srmI18n("i18n_title_add", "添加")
}}</a-button>
</a-form-item>
</a-form>
</div>
<div class="grid" style="margin-top: 12px">
<vxe-grid
ref="xGrid"
v-bind="gridConfig"
:columns="columns"
:data="tableData"
>
<template #empty>
<q-empty></q-empty>
</template>
</vxe-grid>
</div>
</a-modal>
</div>
</template>
<script lang="tsx" setup>
import { ref, toRefs, watch } from "vue";
import { srmI18n } from "@/utils/srmI18n";
// ui组件库 types
import type { GlobalPageLayoutTypes } from "@qqt-product/ui";
import type {
VxeGridInstance,
VxeGridProps,
VxeGridPropTypes,
VxeColumnSlotTypes,
VxeTableDataRow,
} from "vxe-table";
import { editVxeTableConfig } from "@qqt-product/ui";
import { message as Message } from "ant-design-vue";
import qqtUtils from "@qqt-product/utils";
const { cloneDeep } = qqtUtils;
interface Props {
modelValue: boolean;
value: string;
cachePayload:
| VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
| undefined;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
value: "",
});
const { modelValue } = toRefs(props);
const emit = defineEmits<{
(e: "update:modelValue", modelValue: boolean): void;
(e: "success", val: string): void;
}>();
const LT = "< x <";
const ELT = "<= x <";
const GT = "<= x";
const xGrid = ref<VxeGridInstance>();
const rawState = {
ladderQuantity: "",
};
const form = ref<GlobalPageLayoutTypes.RecordString>(rawState);
const tableData = ref<GlobalPageLayoutTypes.RecordString[]>([]);
const gridConfig: VxeGridProps = {
...editVxeTableConfig,
rowConfig: {
keyField: "id",
isHover: true,
isCurrent: true,
},
radioConfig: {
trigger: "row",
},
height: 340,
keyboardConfig: {},
menuConfig: {},
mouseConfig: {}, // 鼠标配置项
areaConfig: {}, // 按键配置项
};
const columns: VxeGridPropTypes.Columns = [
{ type: "seq", width: 60, fixed: "left" },
{
field: "ladder",
title: srmI18n("i18n_title_ladderLeve", "阶梯级"),
width: 150,
},
{
field: "ladderQuantity",
title: srmI18n("i18n_title_ladderCount", "阶梯数量"),
minWidth: 150,
},
{
field: "grid_operation",
// fixed: 'right',
title: "操作",
width: 140,
slots: {
default({
$rowIndex,
}: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>) {
const handleDelete = () => {
tableData.value.splice($rowIndex, 1);
};
return [
<a-button
type="text"
danger
disabled={$rowIndex !== tableData.value.length - 1}
onClick={handleDelete}
>
删除
</a-button>,
];
},
},
},
];
const setNltRow = () => {
if (tableData.value.length <= 1) return;
if (tableData.value.length > 2) {
let last = tableData.value[tableData.value.length - 1];
let prev = tableData.value[tableData.value.length - 2];
prev.ladder = `${prev.ladderQuantity} ${ELT} ${last.ladderQuantity}`;
} else {
let last = tableData.value[tableData.value.length - 1];
last.ladder = `${last.ladderQuantity} ${GT}`;
}
};
const setData = () => {
const val = form.value.ladderQuantity;
if (!val) {
Message.error(srmI18n("title_pleaseEnterladderCount", "请输入阶梯数量!"));
return;
}
let taxCode = "";
let taxRate = "";
if (props.cachePayload) {
taxCode = props.cachePayload.row.taxCode || "";
taxRate = props.cachePayload.row.taxRate || "";
}
if (tableData.value.length === 0) {
let row = {
ladderQuantity: 0,
ladder: `0 ${LT} ${val}`,
price: "",
netPrice: "",
taxCode,
taxRate,
};
tableData.value.push(row);
} else {
let flag = tableData.value.every((n) => n.ladderQuantity < val);
if (!flag) {
Message.error(
srmI18n(
"i18n_title_ladderMostLastLadderCount",
"阶梯数量必须大于上一阶梯数量"
)
);
return;
}
}
let row = {
ladderQuantity: val,
ladder: `${val} ${GT}`,
price: "",
netPrice: "",
taxCode,
taxRate,
};
tableData.value.push(row);
setNltRow();
form.value.ladderQuantity = "";
};
const resetData = () => {
tableData.value = [];
form.value = { ...rawState };
};
watch(modelValue, (bool) => {
if (bool) {
init();
} else {
resetData();
}
});
const handleCancel = () => {
emit("update:modelValue", false);
};
const handleOk = () => {
const arr = cloneDeep(tableData.value);
emit("success", JSON.stringify(arr));
handleCancel();
};
const init = () => {
if (props.value) {
try {
const arr = JSON.parse(props.value);
if (Array.isArray(arr)) {
tableData.value = arr;
}
} catch (err) {
console.log("parse ladderPriceJSON error :>> ", err);
}
}
};
</script>