Appearance
阶梯报价、成本报价
TIP
阶梯报价或成本报价本质上是特定格式拼接成的 JSON 字符串数据,实现模板选择、增删改查、实时动态计算等需求的业务组件。
询价管理模块阶梯报价、成本报价
vue
<template>
<div class="edit-page">
<q-edit-page-layout ref="layoutRef" v-bind="options"></q-edit-page-layout>
<!-- 模板选择 -->
<q-select-template
v-model:visible="templateVisible"
businessType="costForm"
:elsAccount="currentRow.elsAccount"
:autoVisible="false"
@ok="handleSetTemplate"
></q-select-template>
<!-- 阶梯报价 -->
<ladder-price-modal
v-model="ladderPriceVisible"
:value="ladderPriceValue"
:cachePayload="ladderPriceCachePayload"
@success="handleLadderPriceSuccess"
></ladder-price-modal>
<!-- 成本模板 编辑 -->
<cost-price-edit
v-model="priceEditShow"
:currentRow="priceRow"
:priceData="priceData"
:groups="priceGroups"
:priceMap="priceMap"
:cachePageData="cachePageData"
:cachePayload="cachePayload"
@success="handleCostSuccess"
></cost-price-edit>
</div>
</template>
<script setup lang="tsx">
import { ref, reactive } from "vue";
import type { ComponentPublicInstance } from "vue";
// 当前行 currentRow, 当前用户信息 userInfo
import { useGlobalStoreWithDefaultValue } from "@/use/useGlobalStore";
// 国际化
import { srmI18n } from "@/utils/srmI18n";
// ui组件库 types
import type { GlobalPageLayoutTypes } from "@qqt-product/ui";
import type { VxeColumnSlotTypes, VxeTableDataRow } from "vxe-table";
import { useRefInstanceHook } from "@qqt-product/ui";
import { loadJS, getRemoteJsFilePath } from "@qqt-product/ui";
import { Decimal } from "decimal.js";
// 工具函数及types
import qqtUtils from "@qqt-product/utils";
import CostPriceEdit from "./components/costPriceEdit.vue";
import LadderPriceDisplay from "./components/ladderPriceDisplay.vue";
import LadderPriceModal from "./components/ladderPriceModal.vue";
import type { SuccessPayload } from "./components/costPriceEdit.vue";
import { QIcon } from "@qqt-product/icons";
defineOptions({
name: "srm-enquiry-enquiryHead-edit",
});
const DIGIT = 4;
const ladderPriceVisible = ref<boolean>(false);
const ladderPriceValue = ref<string>("");
// 缓存行信息
const ladderPriceCachePayload =
ref<VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>>();
// 全局数据缓存
const { getCurrentRow, userInfo, token } = useGlobalStoreWithDefaultValue();
const currentRow = getCurrentRow();
const { cloneDeep } = qqtUtils;
const templateVisible = ref<boolean>(false);
const cachePageData = ref<GlobalPageLayoutTypes.RecordString>({});
const priceEditShow = ref<boolean>(false);
const priceRow = ref<GlobalPageLayoutTypes.CurrentRow>({
templateNumber: "",
templateVersion: 0,
templateAccount: "",
busAccount: "",
elsAccount: "",
});
const priceGroups = ref<GlobalPageLayoutTypes.Group[]>([]);
const priceData = ref<GlobalPageLayoutTypes.RecordString>({});
const priceMap = ref<GlobalPageLayoutTypes.RecordString>({});
// 缓存行信息
const cachePayload =
ref<VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>>();
// 组件 ref
const layoutRef = ref<ComponentPublicInstance | null>(null);
const { pageData } = useRefInstanceHook(layoutRef);
const chooseTemplate = (
payload: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
) => {
cachePayload.value = payload;
templateVisible.value = true;
};
// 模板选择后赋值成本报价 costFormJson
const handleSetTemplate = async ({ row }) => {
const {
templateName = "",
templateAccount = "",
templateNumber = "",
templateVersion = 0,
elsAccount = "",
} = row;
priceRow.value = {
templateName,
templateAccount,
templateNumber,
templateVersion,
elsAccount,
busAccount: elsAccount,
};
if (cachePayload.value) {
priceRow.value.id = cachePayload.value.row.id || "";
const costFormJson: GlobalPageLayoutTypes.RecordString = {
...priceRow.value,
data: {},
groups: [],
};
const filePath: string = getRemoteJsFilePath({
currentRow: priceRow.value,
userInfo: userInfo.value as GlobalPageLayoutTypes.UserInfo,
businessType: "costForm",
role: "purchase",
token: token.value,
});
const config = (await loadJS(
`${filePath}?t=${+new Date()}`
)) as GlobalPageLayoutTypes.RemoteConfig;
const groups = (config.groups as GlobalPageLayoutTypes.Group[]) || [];
const itemColumns = config.itemColumns || [];
if (groups && groups.length) {
priceGroups.value = cloneDeep(groups);
// 获取 group 分组的 sum 合计列的 field
priceMap.value = groups.reduce((acc, group) => {
if (acc[group.groupCode]) {
acc[group.groupCode] = "";
}
const { field = "" } = itemColumns.find(
(n) => n.groupCode === group.groupCode && n.sum === "1"
) || { field: "" };
acc[group.groupCode] = field;
return acc;
}, {});
costFormJson.groups = groups.map((group) => {
return {
groupName: group.groupName || "",
groupNameI18nKey: group.groupNameI18nKey || "",
groupCode: group.groupCode || "",
groupType: group.groupType || "",
totalValue: 0,
price: 0,
netPrice: 0,
};
});
}
cachePayload.value.row.costFormJson = JSON.stringify(costFormJson);
}
templateVisible.value = false;
};
const openCostPrice = async (
payload: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
) => {
const row = payload.row;
cachePayload.value = payload;
const costFormJson = row.costFormJson || "";
if (costFormJson) {
try {
const obj = JSON.parse(costFormJson);
const {
templateName = "",
templateAccount = "",
templateNumber = "",
templateVersion = 0,
busAccount = "",
elsAccount = "",
id = "",
} = obj;
priceRow.value = {
templateName,
templateAccount,
templateNumber,
templateVersion,
busAccount,
elsAccount,
id,
};
priceData.value = obj.data
? (obj.data as GlobalPageLayoutTypes.RecordString)
: {};
const filePath: string = getRemoteJsFilePath({
currentRow: priceRow.value,
userInfo: userInfo.value as GlobalPageLayoutTypes.UserInfo,
businessType: "costForm",
role: "purchase",
token: token.value,
});
const config = (await loadJS(
`${filePath}?t=${+new Date()}`
)) as GlobalPageLayoutTypes.RemoteConfig;
const groups = (config.groups as GlobalPageLayoutTypes.Group[]) || [];
const itemColumns = config.itemColumns || [];
if (groups && groups.length) {
priceGroups.value = cloneDeep(groups);
// 获取 group 分组的 sum 合计列的 field
priceMap.value = groups.reduce((acc, group) => {
if (acc[group.groupCode]) {
acc[group.groupCode] = "";
}
const { field = "" } = itemColumns.find(
(n) => n.groupCode === group.groupCode && n.sum === "1"
) || { field: "" };
acc[group.groupCode] = field;
return acc;
}, {});
}
cachePageData.value = cloneDeep(pageData.value);
priceEditShow.value = true;
} catch (err) {
console.log(err);
}
}
};
// 行赋值 赋值成本报价模型 及 计算 含税价、未税单价、含税总额、未税总额
const handleCostSuccess = ({ costFormJson, pageTotalVal }: SuccessPayload) => {
const purchaseEnquiryItemList = pageData.value[
"purchaseEnquiryItemList"
] as GlobalPageLayoutTypes.RecordString[];
// 报价项 quoteType;
// 数据字典 srmQuoteType (0: 含税价, 1: 不含税价);
const quoteType = (pageData.value.quoteType as string) || "0";
const { row } = cachePayload.value || { row: { taxRate: 0 } };
const taxRate = row["taxRate"] || 0;
const formula = Decimal.add(1, Decimal.div(taxRate, 100));
if (cachePayload.value) {
const { rowIndex } = cachePayload.value;
if (purchaseEnquiryItemList[rowIndex]) {
purchaseEnquiryItemList[rowIndex]["costFormJson"] = costFormJson;
let requireQuantity =
purchaseEnquiryItemList[rowIndex]["requireQuantity"];
// 根据含税价 price, 实时计算未税单价
if (quoteType === "0") {
purchaseEnquiryItemList[rowIndex]["price"] = pageTotalVal;
let price = pageTotalVal;
if (price && taxRate) {
let netPrice = Decimal.div(price, formula);
// 4位小数
purchaseEnquiryItemList[rowIndex]["netPrice"] = new Decimal(
netPrice
).toFixed(DIGIT);
if (requireQuantity) {
let taxAmount = Decimal.mul(price, requireQuantity);
let netAmount = Decimal.div(taxAmount, formula);
purchaseEnquiryItemList[rowIndex]["taxAmount"] = new Decimal(
taxAmount
).toFixed(DIGIT);
purchaseEnquiryItemList[rowIndex]["netAmount"] = new Decimal(
netAmount
).toFixed(DIGIT);
}
}
} else {
// 根据未税单价价 netPrice, 实时计算含税价
purchaseEnquiryItemList[rowIndex]["netPrice"] = pageTotalVal;
let netPrice = pageTotalVal;
if (netPrice && taxRate) {
let price = Decimal.mul(netPrice, formula);
// 4位小数
purchaseEnquiryItemList[rowIndex]["price"] = new Decimal(
price
).toFixed(DIGIT);
if (requireQuantity) {
let netAmount = Decimal.mul(netPrice, requireQuantity);
let taxAmount = Decimal.mul(netAmount, formula);
purchaseEnquiryItemList[rowIndex]["netAmount"] = new Decimal(
netAmount
).toFixed(DIGIT);
purchaseEnquiryItemList[rowIndex]["taxAmount"] = new Decimal(
taxAmount
).toFixed(DIGIT);
}
}
}
}
}
};
const handleLadderPriceSuccess = (str: string) => {
if (ladderPriceCachePayload.value) {
const { row, column } = ladderPriceCachePayload.value;
row[column.field] = str;
}
};
const handleAfterRemoteConfig = (config) => {
const itemColumns = config.itemColumns || [];
itemColumns.forEach((n) => {
if (n.field == "costFormJson") {
n.slots = {
default(
payload: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
) {
const row = payload.row;
let name = srmI18n("i18n_title_pleaseSelect", "请选择");
let flag = false;
try {
const costFormJson = row.costFormJson || "";
if (costFormJson) {
const obj = JSON.parse(costFormJson);
if (obj.templateName) {
name = obj.templateName;
flag = true;
}
}
} catch (err) {
flag = false;
console.log(err);
}
const elem = flag ? (
<div style="display: flex; justify-content: center;">
<a style="color: #1677ff;" onClick={() => openCostPrice(payload)}>
{name}
</a>
<span onClick={() => (row.costFormJson = "")}>
<QIcon
type="icon-Q-remove"
size="14"
style="margin-left: 4px; cursor: pointer; color: #ee1d1d;"
></QIcon>
</span>
</div>
) : (
<a style="color: #1677ff;" onClick={() => chooseTemplate(payload)}>
{name}
</a>
);
// 报价方式 quotePriceWay
// 数据字典 srmQuotePriceWay (0: 常规报价, 1: 阶梯报价, 2: 成本报价)
return [row.quotePriceWay === "2" ? elem : null];
},
};
}
// 阶梯报价JSON
if (n.field === "ladderPriceJson") {
n.fieldType = "";
n.width = 250;
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[column.field] = "";
};
// 报价方式 quotePriceWay
// 数据字典 srmQuotePriceWay (0: 常规报价, 1: 阶梯报价, 2: 成本报价)
const elem =
row.quotePriceWay === "1" ? (
<LadderPriceDisplay
value={val}
onClick={handleLadderPriceClick}
onClear={handleLadderPriceClear}
></LadderPriceDisplay>
) : null;
return [elem];
},
};
}
});
return config;
};
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
// ...
handleAfterRemoteConfig,
});
</script>vue
<template>
<div class="detail-page">
<q-detail-page-layout
ref="layoutRef"
v-bind="options"
></q-detail-page-layout>
<!-- 阶梯报价 -->
<ladder-price-modal
v-model="ladderPriceVisible"
:value="ladderPriceValue"
:cachePayload="ladderPriceCachePayload"
disabled
></ladder-price-modal>
<!-- 成本模板 详情 -->
<cost-price-detail
v-model="priceDetailShow"
:currentRow="priceRow"
:priceData="priceData"
:groups="priceGroups"
:priceMap="priceMap"
:cachePageData="cachePageData"
:cachePayload="cachePayload"
></cost-price-detail>
</div>
</template>
<script setup lang="tsx">
import { ref, reactive } from "vue";
import type { ComponentPublicInstance } from "vue";
import type { VxeColumnSlotTypes, VxeTableDataRow } from "vxe-table";
import type { GlobalPageLayoutTypes } from "@qqt-product/ui";
import { useRefInstanceHook } from "@qqt-product/ui";
import { loadJS, getRemoteJsFilePath } from "@qqt-product/ui";
// 全局数据缓存
// 当前行 currentRow, 当前用户信息 userInfo
import { useGlobalStoreWithDefaultValue } from "@/use/useGlobalStore";
import CostPriceDetail from "./components/costPriceDetail.vue";
import LadderPriceDisplay from "./components/ladderPriceDisplay.vue";
import LadderPriceModal from "./components/ladderPriceModal.vue";
// 工具函数及types
import qqtUtils from "@qqt-product/utils";
const { cloneDeep } = qqtUtils;
const ladderPriceVisible = ref<boolean>(false);
const ladderPriceValue = ref<string>("");
// 缓存行信息
const ladderPriceCachePayload =
ref<VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>>();
const cachePageData = ref<GlobalPageLayoutTypes.RecordString>({});
const priceDetailShow = ref<boolean>(false);
const priceRow = ref<GlobalPageLayoutTypes.CurrentRow>({
templateNumber: "",
templateVersion: 0,
templateAccount: "",
busAccount: "",
elsAccount: "",
});
const priceGroups = ref<GlobalPageLayoutTypes.Group[]>([]);
const priceData = ref<GlobalPageLayoutTypes.RecordString>({});
const priceMap = ref<GlobalPageLayoutTypes.RecordString>({});
// 缓存行信息
const cachePayload =
ref<VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>>();
const { token, userInfo } = useGlobalStoreWithDefaultValue();
// 组件 ref
const layoutRef = ref<ComponentPublicInstance | null>(null);
const { pageData } = useRefInstanceHook(layoutRef);
const openCostPrice = async (
payload: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
) => {
const row = payload.row;
cachePayload.value = payload;
const costFormJson = row.costFormJson || "";
if (costFormJson) {
try {
const obj = JSON.parse(costFormJson);
const {
templateName = "",
templateAccount = "",
templateNumber = "",
templateVersion = 0,
busAccount = "",
elsAccount = "",
id = "",
} = obj;
priceRow.value = {
templateName,
templateAccount,
templateNumber,
templateVersion,
busAccount,
elsAccount,
id,
};
priceData.value = obj.data
? (obj.data as GlobalPageLayoutTypes.RecordString)
: {};
const filePath: string = getRemoteJsFilePath({
currentRow: priceRow.value,
userInfo: userInfo.value as GlobalPageLayoutTypes.UserInfo,
businessType: "costForm",
role: "purchase",
token: token.value,
});
const config = (await loadJS(
`${filePath}?t=${+new Date()}`
)) as GlobalPageLayoutTypes.RemoteConfig;
const groups = (config.groups as GlobalPageLayoutTypes.Group[]) || [];
const itemColumns = config.itemColumns || [];
if (groups && groups.length) {
priceGroups.value = cloneDeep(groups);
// 获取 group 分组的 sum 合计列的 field
priceMap.value = groups.reduce((acc, group) => {
if (acc[group.groupCode]) {
acc[group.groupCode] = "";
}
const { field = "" } = itemColumns.find(
(n) => n.groupCode === group.groupCode && n.sum === "1"
) || { field: "" };
acc[group.groupCode] = field;
return acc;
}, {});
}
cachePageData.value = cloneDeep(pageData.value);
priceDetailShow.value = true;
} catch (err) {
console.log(err);
}
}
};
const handleAfterRemoteConfig = (config) => {
const itemColumns = config.itemColumns || [];
itemColumns.forEach((n) => {
if (n.field == "costFormJson") {
n.slots = {
default(
payload: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
) {
const row = payload.row;
let name = "";
let flag = false;
try {
const costFormJson = row.costFormJson || "";
if (costFormJson) {
const obj = JSON.parse(costFormJson);
if (obj.templateName) {
name = obj.templateName;
flag = true;
}
}
} catch (err) {
flag = false;
console.log(err);
}
const elem = flag ? (
<a style="color: #1677ff;" onClick={() => openCostPrice(payload)}>
{name}
</a>
) : null;
// 报价方式 quotePriceWay
// 数据字典 srmQuotePriceWay (0: 常规报价, 1: 阶梯报价, 2: 成本报价)
return [row.quotePriceWay === "2" ? elem : null];
},
};
}
// 阶梯报价JSON
if (n.field === "ladderPriceJson") {
n.fieldType = "";
n.width = 250;
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;
};
// 报价方式 quotePriceWay
// 数据字典 srmQuotePriceWay (0: 常规报价, 1: 阶梯报价, 2: 成本报价)
const elem =
row.quotePriceWay === "1" ? (
<LadderPriceDisplay
value={val}
onClick={handleLadderPriceClick}
disabled={true}
></LadderPriceDisplay>
) : null;
return [elem];
},
};
}
});
return config;
};
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
// ...
handleAfterRemoteConfig,
});
</script>vue
<template>
<div :class="['ladder-price-display', str && !props.disabled ? 'set' : '']">
<a-tooltip 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 && !props.disabled"
class="clear"
@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;
disabled?: boolean;
};
const props = withDefaults(defineProps<Props>(), {
value: "",
disabled: false,
});
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" scoped>
.ladder-price-display {
position: relative;
height: 36px;
line-height: 36px;
padding-right: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
a:hover {
color: #1677ff;
}
&.set {
padding-right: 42px;
}
.icons {
position: absolute;
// display: inline-block
font-size: 16px;
right: 0px;
top: 3px;
z-index: 10;
}
.clear {
color: red;
margin-left: 8px;
&.unDelete {
color: rgba(0, 0, 0, 0.25);
cursor: not-allowed;
}
}
}
</style>vue
<template>
<div class="checkPrice">
<q-basic-modal
:visible="modelValue"
:width="1000"
:title="srmI18n('i18n_title_ladderSetting', '阶梯设置')"
:maskClosable="false"
:keyboard="false"
:bodyStyle="{ padding: '0 12px' }"
@cancel="handleCancel"
@ok="handleOk"
>
<div class="form" v-if="!disabled">
<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>
</q-basic-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;
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
value: "",
disabled: false,
});
const { modelValue, disabled } = 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: "price",
title: srmI18n("i18n_title_price", "含税价"),
minWidth: 150,
},
{
field: "netPrice",
title: srmI18n("i18n_title_netPrice", "不含税价"),
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={
disabled.value || $rowIndex !== tableData.value.length - 1
}
onClick={handleDelete}
>
{srmI18n(`i18n_title_delete`, "删除")}
</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;
}
if (val <= 1) {
Message.error(srmI18n("i18n_title_ladderMostOne", "梯数量必须大于1"));
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>vue
<template>
<div class="cost-price">
<a-modal
:open="modelValue"
width="100%"
wrap-class-name="full-modal"
:title="srmI18n(`i18n_title_costTemplate`, '成本模板')"
@cancel="handleCancel"
@ok="setCostOk"
>
<a-spin :spinning="loading">
<div class="page-container">
<q-edit-page-layout
v-if="show"
class="cost-price-layout"
ref="layoutRef"
v-bind="options"
>
<template
v-for="el in namedSlots"
:key="el.append"
#[el.append]="{ groupCode }"
>
<div class="desc" v-show="getTotal(groupCode)">
<span class="item">
<span class="tit">{{
srmI18n(`i18n_field_quoteType`, "报价项")
}}</span>
<span class="ant-typography">
<strong>{{
isPriceQuoteType
? srmI18n("i18n_title_price", "含税价")
: srmI18n("i18n_title_netPrice", "不含税价")
}}</strong>
</span>
</span>
<span class="item">
<span class="tit">{{
srmI18n(`i18n_field_taxRate`, "税率")
}}</span>
<span class="ant-typography">
<strong>{{ curTaxRate }}</strong>
</span>
</span>
</div>
<div class="total" v-show="getTotal(groupCode)">
<span class="item">
<span class="tit">{{
`${srmI18n("i18n_title_price", "含税价")}${srmI18n(
`i18n_field_kMk_176ba4f`,
"总汇总"
)}`
}}</span>
<span class="ant-typography">
<strong>{{ getTotalPrice(groupCode) }}</strong>
</span>
</span>
<span class="item">
<span class="tit">{{
`${srmI18n("i18n_title_netPrice", "不含税价")}${srmI18n(
`i18n_field_kMk_176ba4f`,
"总汇总"
)}`
}}</span>
<span class="ant-typography">
<strong>{{ getTotalNetPrice(groupCode) }}</strong>
</span>
</span>
</div>
</template>
</q-edit-page-layout>
</div>
</a-spin>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, toRefs, watch, nextTick } from "vue";
import type { ComponentPublicInstance } from "vue";
// 国际化
import { srmI18n } from "@/utils/srmI18n";
// ui组件库 types
import type { GlobalPageLayoutTypes } from "@qqt-product/ui";
import type { VxeColumnSlotTypes, VxeTableDataRow } from "vxe-table";
import { BUTTON_ADD_ONE_ROW, BUTTON_DELETE_ROW } from "@qqt-product/ui";
import { useRefInstanceHook } from "@qqt-product/ui";
// 当前行 currentRow, 当前用户信息 userInfo
import { useGlobalStoreWithDefaultValue } from "@/use/useGlobalStore";
import REGEXP from "@/utils/regexp";
import { Decimal } from "decimal.js";
import qqtUtils from "@qqt-product/utils";
const { cloneDeep } = qqtUtils;
const DIGIT = 4;
const loading = ref<boolean>(false);
// 组件 ref
const layoutRef = ref<ComponentPublicInstance | null>(null);
const { pageData } = useRefInstanceHook(layoutRef);
interface Props {
modelValue: boolean;
currentRow: GlobalPageLayoutTypes.CurrentRow;
priceData: GlobalPageLayoutTypes.RecordString;
groups: GlobalPageLayoutTypes.Group[];
priceMap: GlobalPageLayoutTypes.RecordString;
cachePageData: GlobalPageLayoutTypes.RecordString;
cachePayload:
| VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
| undefined;
}
export interface SuccessPayload {
costFormJson: string;
pageTotalVal: string;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
currentRow: () => ({
templateNumber: "",
templateVersion: 0,
templateAccount: "",
busAccount: "",
elsAccount: "",
}),
priceData: () => ({}),
groups: () => [],
priceMap: () => ({}),
});
const { modelValue, priceMap } = toRefs(props);
const namedSlots = computed(() => {
return props.groups.map((group) => ({
append: `vertical_${group.groupCode}_append`,
}));
});
const emit = defineEmits<{
(e: "update:modelValue", modelValue: boolean): void;
(e: "success", payload: SuccessPayload): void;
}>();
// 全局数据缓存
const { userInfo, token, manuallyExit } = useGlobalStoreWithDefaultValue();
const show = ref<boolean>(false);
// 是否为“含税价”报价项
const isPriceQuoteType = computed<boolean>(() => {
// 报价项 quoteType;
// 数据字典 srmQuoteType (0: 含税价, 1: 不含税价);
const quoteType = (props.cachePageData.quoteType as string) || "0";
return quoteType === "0";
});
const curTaxRate = computed(() => {
if (props.cachePayload) {
return props.cachePayload.row.taxRate || 0;
}
return 0;
});
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
showPageHeader: false,
showLayoutAnchor: false,
/**
* 模块业务类型 string
* 取值根据各模块配置
* 当定义为"custom"时,使用本地配置
*/
businessType: "costForm",
// 布局模式: 平铺布局 vertical (默认), Tab布局 tab, 主从布局 master
pattern: "vertical",
// 仅使用本地布局模式
isUseLocalPattern: true,
// 当前已选行数据
currentRow: {
templateNumber: "",
templateVersion: 0,
templateAccount: "",
busAccount: "",
elsAccount: "",
},
refreshMethods(row?: GlobalPageLayoutTypes.CurrentRow) {
if (options.currentRow) {
options.currentRow =
row ||
Object.assign({}, options.currentRow, {
_t: +new Date(),
});
}
},
// 当前登录租户信息
userInfo: userInfo.value as GlobalPageLayoutTypes.UserInfo,
// 有导入excel 操作的必填
token: token.value,
// 本地页面数据配置
localConfig: {
groups: [],
},
isUseLocalData: true,
localData: {},
});
watch(modelValue, (bool) => {
if (bool) {
if (options.localConfig) {
options.localConfig.groups = props.groups.map((n) => {
return {
...n,
buttons: [BUTTON_ADD_ONE_ROW, BUTTON_DELETE_ROW],
};
});
}
const copyData = cloneDeep(props.priceData);
options.currentRow = { ...props.currentRow, ...copyData };
nextTick(() => {
show.value = true;
});
}
});
const getTotal = (groupCode: string) => {
if (!groupCode || !Decimal) {
return 0;
}
const tableData = pageData.value[
groupCode
] as GlobalPageLayoutTypes.RecordString[];
if (!tableData || !tableData.length) {
return 0;
}
// 合计列配置 property
// 没有配置, 默认取 total
const key = priceMap.value[groupCode] || "total";
const flag = tableData.some((n) => !!n[key]);
if (flag) {
let totalAmount = tableData.reduce((acc: Decimal, row) => {
let count = REGEXP.interger.test(row[key]) ? row[key] : 0;
return Decimal.add(acc, count);
}, new Decimal(0));
const num = new Decimal(totalAmount).toFixed(DIGIT);
return Number(num) > 0 ? num : 0;
}
return 0;
};
const getTotalPrice = (groupCode: string) => {
if (Decimal && getTotal(groupCode)) {
const taxRate = curTaxRate.value;
const formula = Decimal.add(1, Decimal.div(taxRate, 100));
if (isPriceQuoteType.value) {
return getTotal(groupCode);
} else {
let netPrice = getTotal(groupCode);
let price = Decimal.mul(netPrice, formula);
return new Decimal(price).toFixed(DIGIT);
}
}
return 0;
};
const getTotalNetPrice = (groupCode: string) => {
if (Decimal && getTotal(groupCode)) {
const taxRate = curTaxRate.value;
const formula = Decimal.add(1, Decimal.div(taxRate, 100));
if (isPriceQuoteType.value) {
let price = getTotal(groupCode);
let netPrice = Decimal.div(price, formula);
return new Decimal(netPrice).toFixed(DIGIT);
} else {
return getTotal(groupCode);
}
}
return 0;
};
const cancelFunc = () => {
show.value = false;
emit("update:modelValue", false);
};
// 生成成本报价JSON 及 页面价格汇总
const generateCostFormJsonData = () => {
let copyCachePageData = cloneDeep(props.cachePageData);
// 赋值表行成本报价JSON
let copyPageData = cloneDeep(pageData.value);
const costFormJson: GlobalPageLayoutTypes.RecordString = {
...props.currentRow,
data: { ...copyPageData },
groups: [],
};
// 报价项 quoteType;
// 数据字典 srmQuoteType (0: 含税价, 1: 不含税价);
const quoteType = (copyCachePageData.quoteType as string) || "0";
const taxRate = curTaxRate.value;
const formula = Decimal.add(1, Decimal.div(taxRate, 100));
// 获取页面所有价格汇总
const pageTotal = props.groups.reduce((acc: Decimal, group) => {
// 返回 groups 分组数据供给后端比价接口使用
const obj: GlobalPageLayoutTypes.RecordString = {
groupName: group.groupName || "",
groupNameI18nKey: group.groupNameI18nKey || "",
groupCode: group.groupCode || "",
groupType: group.groupType || "",
totalValue: "",
price: 0,
netPrice: 0,
};
// 分组合计
let groupTotal = getTotal(group.groupCode);
obj.totalValue = groupTotal;
if (quoteType === "0") {
obj.price = groupTotal;
let netPrice = Decimal.div(groupTotal, formula);
obj.netPrice = new Decimal(netPrice).toFixed(DIGIT);
} else {
obj.netPrice = groupTotal;
let price = Decimal.mul(groupTotal, formula);
obj.price = new Decimal(price).toFixed(DIGIT);
}
costFormJson.groups.push(obj);
return Decimal.add(acc, groupTotal);
}, new Decimal(0));
const pageTotalVal = new Decimal(pageTotal).toFixed(DIGIT);
emit("success", {
costFormJson: JSON.stringify(costFormJson),
pageTotalVal,
});
};
const handleCancel = () => {
cancelFunc();
};
const setCostOk = () => {
generateCostFormJsonData();
cancelFunc();
};
</script>
<style lang="less">
.cost-price {
height: 100%;
}
.full-modal {
.ant-modal {
max-width: 100%;
top: 0;
padding-bottom: 0;
margin: 0;
}
.ant-modal-content {
display: flex;
flex-direction: column;
height: calc(100vh);
max-height: calc(100vh);
overflow-x: hidden;
.page-container {
min-height: calc(100vh - 55px - 53px);
}
}
.ant-modal-body {
flex: 1;
padding: 0;
}
}
.total,
.desc {
margin-top: 6px;
.tit {
&::after {
margin-right: 6px;
color: #171832;
content: ":";
}
}
.item {
& + .item {
margin-left: 10px;
}
}
}
</style>vue
<template>
<div class="cost-price">
<a-modal
:open="modelValue"
width="100%"
wrap-class-name="full-modal"
:title="srmI18n(`i18n_title_costTemplate`, '成本模板')"
:footer="null"
@cancel="handleCancel"
>
<a-spin :spinning="loading">
<div class="page-container">
<q-detail-page-layout
v-if="show"
class="cost-price-layout"
ref="layoutRef"
v-bind="options"
>
<template
v-for="el in namedSlots"
:key="el.append"
#[el.append]="{ groupCode }"
>
<div class="desc" v-show="getTotal(groupCode)">
<span class="item">
<span class="tit">{{
srmI18n(`i18n_field_quoteType`, "报价项")
}}</span>
<span class="ant-typography">
<strong>{{
isPriceQuoteType
? srmI18n("i18n_title_price", "含税价")
: srmI18n("i18n_title_netPrice", "不含税价")
}}</strong>
</span>
</span>
<span class="item">
<span class="tit">{{
srmI18n(`i18n_field_taxRate`, "税率")
}}</span>
<span class="ant-typography">
<strong>{{ curTaxRate }}</strong>
</span>
</span>
</div>
<div class="total" v-show="getTotal(groupCode)">
<span class="item">
<span class="tit">{{
`${srmI18n("i18n_title_price", "含税价")}${srmI18n(
`i18n_field_kMk_176ba4f`,
"总汇总"
)}`
}}</span>
<span class="ant-typography">
<strong>{{ getTotalPrice(groupCode) }}</strong>
</span>
</span>
<span class="item">
<span class="tit">{{
`${srmI18n("i18n_title_netPrice", "不含税价")}${srmI18n(
`i18n_field_kMk_176ba4f`,
"总汇总"
)}`
}}</span>
<span class="ant-typography">
<strong>{{ getTotalNetPrice(groupCode) }}</strong>
</span>
</span>
</div>
</template>
</q-detail-page-layout>
</div>
</a-spin>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, toRefs, watch, nextTick } from "vue";
import type { ComponentPublicInstance } from "vue";
// 国际化
import { srmI18n } from "@/utils/srmI18n";
// ui组件库 types
import type { GlobalPageLayoutTypes } from "@qqt-product/ui";
import type { VxeColumnSlotTypes, VxeTableDataRow } from "vxe-table";
import { useRefInstanceHook } from "@qqt-product/ui";
// 当前行 currentRow, 当前用户信息 userInfo
import { useGlobalStoreWithDefaultValue } from "@/use/useGlobalStore";
import REGEXP from "@/utils/regexp";
import { Decimal } from "decimal.js";
import qqtUtils from "@qqt-product/utils";
const { cloneDeep } = qqtUtils;
const DIGIT = 4;
const loading = ref<boolean>(false);
// 组件 ref
const layoutRef = ref<ComponentPublicInstance | null>(null);
const { pageData } = useRefInstanceHook(layoutRef);
interface Props {
modelValue: boolean;
currentRow: GlobalPageLayoutTypes.CurrentRow;
priceData: GlobalPageLayoutTypes.RecordString;
groups: GlobalPageLayoutTypes.Group[];
priceMap: GlobalPageLayoutTypes.RecordString;
cachePageData: GlobalPageLayoutTypes.RecordString;
cachePayload:
| VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
| undefined;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
currentRow: () => ({
templateNumber: "",
templateVersion: 0,
templateAccount: "",
busAccount: "",
elsAccount: "",
}),
priceData: () => ({}),
groups: () => [],
priceMap: () => ({}),
});
const { modelValue, priceMap } = toRefs(props);
const namedSlots = computed(() => {
return props.groups.map((group) => ({
append: `vertical_${group.groupCode}_append`,
}));
});
const emit = defineEmits<{
(e: "update:modelValue", modelValue: boolean): void;
}>();
// 全局数据缓存
const { userInfo, token, manuallyExit } = useGlobalStoreWithDefaultValue();
const show = ref<boolean>(false);
// 是否为“含税价”报价项
const isPriceQuoteType = computed<boolean>(() => {
// 报价项 quoteType;
// 数据字典 srmQuoteType (0: 含税价, 1: 不含税价);
const quoteType = (props.cachePageData.quoteType as string) || "0";
return quoteType === "0";
});
const curTaxRate = computed(() => {
if (props.cachePayload) {
return props.cachePayload.row.taxRate || 0;
}
return 0;
});
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
showPageHeader: false,
showLayoutAnchor: false,
/**
* 模块业务类型 string
* 取值根据各模块配置
* 当定义为"custom"时,使用本地配置
*/
businessType: "costForm",
// 布局模式: 平铺布局 vertical (默认), Tab布局 tab, 主从布局 master
pattern: "vertical",
// 仅使用本地布局模式
isUseLocalPattern: true,
// 当前已选行数据
currentRow: {
templateNumber: "",
templateVersion: 0,
templateAccount: "",
busAccount: "",
elsAccount: "",
},
refreshMethods(row?: GlobalPageLayoutTypes.CurrentRow) {
if (options.currentRow) {
options.currentRow =
row ||
Object.assign({}, options.currentRow, {
_t: +new Date(),
});
}
},
// 当前登录租户信息
userInfo: userInfo.value as GlobalPageLayoutTypes.UserInfo,
// 有导入excel 操作的必填
token: token.value,
// 本地页面数据配置
localConfig: {
groups: [],
},
isUseLocalData: true,
localData: {},
});
watch(modelValue, (bool) => {
if (bool) {
if (options.localConfig) {
options.localConfig.groups = cloneDeep(props.groups);
}
const copyData = cloneDeep(props.priceData);
options.currentRow = { ...props.currentRow, ...copyData };
nextTick(() => {
show.value = true;
});
}
});
const getTotal = (groupCode: string) => {
if (!groupCode || !Decimal) {
return 0;
}
const tableData = pageData.value[
groupCode
] as GlobalPageLayoutTypes.RecordString[];
if (!tableData || !tableData.length) {
return 0;
}
// 合计列配置 property
// 没有配置, 默认取 total
const key = priceMap.value[groupCode] || "total";
const flag = tableData.some((n) => !!n[key]);
if (flag) {
let totalAmount = tableData.reduce((acc: Decimal, row) => {
let count = REGEXP.interger.test(row[key]) ? row[key] : 0;
return Decimal.add(acc, count);
}, new Decimal(0));
const num = new Decimal(totalAmount).toFixed(DIGIT);
return Number(num) > 0 ? num : 0;
}
return 0;
};
const getTotalPrice = (groupCode: string) => {
if (Decimal && getTotal(groupCode)) {
const taxRate = curTaxRate.value;
const formula = Decimal.add(1, Decimal.div(taxRate, 100));
if (isPriceQuoteType.value) {
return getTotal(groupCode);
} else {
let netPrice = getTotal(groupCode);
let price = Decimal.mul(netPrice, formula);
return new Decimal(price).toFixed(DIGIT);
}
}
return 0;
};
const getTotalNetPrice = (groupCode: string) => {
if (Decimal && getTotal(groupCode)) {
const taxRate = curTaxRate.value;
const formula = Decimal.add(1, Decimal.div(taxRate, 100));
if (isPriceQuoteType.value) {
let price = getTotal(groupCode);
let netPrice = Decimal.div(price, formula);
return new Decimal(netPrice).toFixed(DIGIT);
} else {
return getTotal(groupCode);
}
}
return 0;
};
const cancelFunc = () => {
show.value = false;
emit("update:modelValue", false);
};
const handleCancel = () => {
cancelFunc();
};
</script>
<style lang="less">
.cost-price {
height: 100%;
}
.full-modal {
.ant-modal {
max-width: 100%;
top: 0;
padding-bottom: 0;
margin: 0;
}
.ant-modal-content {
display: flex;
flex-direction: column;
height: calc(100vh);
max-height: calc(100vh);
overflow-x: hidden;
.page-container {
min-height: calc(100vh - 55px);
}
}
.ant-modal-body {
flex: 1;
padding: 0;
}
}
.total,
.desc {
margin-top: 6px;
.tit {
&::after {
margin-right: 6px;
color: #171832;
content: ":";
}
}
.item {
& + .item {
margin-left: 10px;
}
}
}
</style>示例


报价管理模块阶梯报价、成本报价
vue
<template>
<div class="edit-page">
<q-edit-page-layout ref="layoutRef" v-bind="options"></q-edit-page-layout>
<!-- 阶梯报价 -->
<sale-ladder-price-modal
v-model="ladderPriceVisible"
:quoteType="quoteType"
:value="ladderPriceValue"
:cachePayload="ladderPriceCachePayload"
@success="handleLadderPriceSuccess"
></sale-ladder-price-modal>
<!-- 成本模板 编辑 -->
<cost-price-edit
v-model="priceEditShow"
:currentRow="priceRow"
:priceData="priceData"
:groups="priceGroups"
:priceMap="priceMap"
:cachePageData="cachePageData"
:cachePayload="cachePayload"
@success="handleCostSuccess"
></cost-price-edit>
<!-- 成本模板 详情 -->
<cost-price-detail
v-model="priceDetailShow"
:currentRow="priceRow"
:priceData="priceData"
:groups="priceGroups"
:priceMap="priceMap"
:cachePageData="cachePageData"
:cachePayload="cachePayload"
></cost-price-detail>
</div>
</template>
<script setup lang="tsx">
import { ref, reactive } from "vue";
// 国际化
import { srmI18n } from "@/utils/srmI18n";
import type { ComponentPublicInstance } from "vue";
// ui组件库 types
import type { GlobalPageLayoutTypes } from "@qqt-product/ui";
import type { VxeColumnSlotTypes, VxeTableDataRow } from "vxe-table";
// 当前行 currentRow, 当前用户信息 userInfo
import { useGlobalStoreWithDefaultValue } from "@/use/useGlobalStore";
import { useRefInstanceHook } from "@qqt-product/ui";
import { loadJS, getRemoteJsFilePath } from "@qqt-product/ui";
import qqtApi from "@qqt-product/api";
import { message as Message } from "ant-design-vue";
// 工具函数及types
import qqtUtils from "@qqt-product/utils";
import SaleLadderPriceDisplay from "./components/saleLadderPriceDisplay.vue";
import SaleLadderPriceModal from "./components/saleLadderPriceModal.vue";
import CostPriceEdit from "./components/costPriceEdit.vue";
import CostPriceDetail from "./components/costPriceDetail.vue";
import type { SuccessPayload } from "./components/costPriceEdit.vue";
import { Decimal } from "decimal.js";
const quoteType = ref<string>("0");
const ladderPriceVisible = ref<boolean>(false);
const ladderPriceValue = ref<string>("");
// 缓存行信息
const ladderPriceCachePayload =
ref<VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>>();
const DIGIT = 4;
const { cloneDeep } = qqtUtils;
const URL = "/base/heartBeat/noToken/check";
const getTimeStamp = () => Request.get({ url: URL });
const cachePageData = ref<GlobalPageLayoutTypes.RecordString>({});
const priceEditShow = ref<boolean>(false);
const priceDetailShow = ref<boolean>(false);
const priceRow = ref<GlobalPageLayoutTypes.CurrentRow>({
templateNumber: "",
templateVersion: 0,
templateAccount: "",
busAccount: "",
elsAccount: "",
});
const priceGroups = ref<GlobalPageLayoutTypes.Group[]>([]);
const priceData = ref<GlobalPageLayoutTypes.RecordString>({});
const priceMap = ref<GlobalPageLayoutTypes.RecordString>({});
const Request: qqtApi.Request = qqtApi.useHttp();
// 全局数据缓存
const { userInfo, token } = useGlobalStoreWithDefaultValue();
// 缓存行信息
const cachePayload =
ref<VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>>();
const openCostPrice = async (
payload: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
) => {
const row = payload.row;
if (!row.taxCode) {
Message.error(srmI18n(`i18n_title_pleaseEnterTaxCode`, "请先选择税码"));
return;
}
cachePayload.value = payload;
const costFormJson = row.costFormJson || "";
if (costFormJson) {
try {
const obj = JSON.parse(costFormJson);
const {
templateName = "",
templateAccount = "",
templateNumber = "",
templateVersion = 0,
busAccount = "",
elsAccount = "",
} = obj;
priceRow.value = {
templateName,
templateAccount,
templateNumber,
templateVersion,
busAccount,
elsAccount,
id: row.id,
};
priceData.value = obj.data
? (obj.data as GlobalPageLayoutTypes.RecordString)
: {};
const filePath: string = getRemoteJsFilePath({
currentRow: priceRow.value,
userInfo: userInfo.value as GlobalPageLayoutTypes.UserInfo,
businessType: "costForm",
role: "sale",
token: token.value,
});
const config = (await loadJS(
`${filePath}?t=${+new Date()}`
)) as GlobalPageLayoutTypes.RemoteConfig;
const groups = (config.groups as GlobalPageLayoutTypes.Group[]) || [];
const itemColumns = config.itemColumns || [];
if (groups && groups.length) {
priceGroups.value = cloneDeep(groups);
// 获取 group 分组的 sum 合计列的 field
priceMap.value = groups.reduce((acc, group) => {
if (acc[group.groupCode]) {
acc[group.groupCode] = "";
}
const { field = "" } = itemColumns.find(
(n) => n.groupCode === group.groupCode && n.sum === "1"
) || { field: "" };
acc[group.groupCode] = field;
return acc;
}, {});
}
// 询价单状态 enquiryStatus_dictText
// 数据字典 srmEnquiryStatus (0: 新建, 1: 报价中, 2: 已报价, 3: 未报价, 4: 接受, 5: 拒绝, 6: 不能报价, 7: 议价中, 8: 重报价, 9: 已定价, 10: 已作废, 11: 已悔标, 12: 发布中, 13: 发布失败, 14: 已转单)
const props = ["1", "2", "8"];
let status =
props.includes(pageData.value.enquiryStatus) ||
row.itemStatus === "1" ||
row.itemStatus === "8"
? "edit"
: "detail";
const res = await getTimeStamp();
const timestamp = (res?.timestamp as number) || Date.now();
// 报价截止日期 expiryDate
const expiryDate_timestamp = row.expiryDate_timestamp as number;
if (timestamp >= expiryDate_timestamp) {
status = "detail";
}
cachePageData.value = cloneDeep(pageData.value);
status === "edit"
? (priceEditShow.value = true)
: (priceDetailShow.value = true);
} catch (err) {
console.log(err);
}
}
};
// 行赋值 赋值成本报价模型 及 计算 含税价、未税单价、含税总额、未税总额
const handleCostSuccess = ({ costFormJson, pageTotalVal }: SuccessPayload) => {
const saleEnquiryItemList = pageData.value[
"saleEnquiryItemList"
] as GlobalPageLayoutTypes.RecordString[];
// 报价项 quoteType;
// 数据字典 srmQuoteType (0: 含税价, 1: 不含税价);
const quoteType = (pageData.value.quoteType as string) || "0";
const { row } = cachePayload.value || { row: { taxRate: 0 } };
const taxRate = row["taxRate"] || 0;
const formula = Decimal.add(1, Decimal.div(taxRate, 100));
if (cachePayload.value) {
const { rowIndex } = cachePayload.value;
if (saleEnquiryItemList[rowIndex]) {
saleEnquiryItemList[rowIndex]["costFormJson"] = costFormJson;
let requireQuantity = saleEnquiryItemList[rowIndex]["requireQuantity"];
// 根据含税价 price, 实时计算未税单价
if (quoteType === "0") {
saleEnquiryItemList[rowIndex]["price"] = pageTotalVal;
let price = pageTotalVal;
if (price && taxRate) {
let netPrice = Decimal.div(price, formula);
// 4位小数
saleEnquiryItemList[rowIndex]["netPrice"] = new Decimal(
netPrice
).toFixed(DIGIT);
if (requireQuantity) {
let taxAmount = Decimal.mul(price, requireQuantity);
let netAmount = Decimal.div(taxAmount, formula);
saleEnquiryItemList[rowIndex]["taxAmount"] = new Decimal(
taxAmount
).toFixed(DIGIT);
saleEnquiryItemList[rowIndex]["netAmount"] = new Decimal(
netAmount
).toFixed(DIGIT);
}
}
} else {
// 根据未税单价价 netPrice, 实时计算含税价
saleEnquiryItemList[rowIndex]["netPrice"] = pageTotalVal;
let netPrice = pageTotalVal;
if (netPrice && taxRate) {
let price = Decimal.mul(netPrice, formula);
// 4位小数
saleEnquiryItemList[rowIndex]["price"] = new Decimal(price).toFixed(
DIGIT
);
if (requireQuantity) {
let netAmount = Decimal.mul(netPrice, requireQuantity);
let taxAmount = Decimal.mul(netAmount, formula);
saleEnquiryItemList[rowIndex]["netAmount"] = new Decimal(
netAmount
).toFixed(DIGIT);
saleEnquiryItemList[rowIndex]["taxAmount"] = new Decimal(
taxAmount
).toFixed(DIGIT);
}
}
}
}
}
};
// 获取行对应阶梯价格
const handleLadderPriceSuccess = (
arr: GlobalPageLayoutTypes.RecordString[]
) => {
if (!arr.length || !ladderPriceCachePayload.value) {
return;
}
const { row, column } = ladderPriceCachePayload.value;
row[column.field] = JSON.stringify(arr);
let requireQuantity = row.requireQuantity || 1;
let idx = arr.findIndex((n) => {
return Number(requireQuantity) < n.ladderQuantity;
});
// 取最后一个阶梯的报价
if (idx === -1) {
idx = arr.length - 1;
} else {
idx = idx - 1;
}
const { price, netPrice } = arr[idx];
row.price = price; // 含税价
row.netPrice = netPrice; // 未税价
row.taxAmount = Decimal.mul(price, requireQuantity).toFixed(DIGIT); // 含税总额
row.netAmount = Decimal.mul(netPrice, requireQuantity).toFixed(DIGIT); // 未税总额
};
// 自定义业务模板数据配置
// 兼容方法设置供应方分组groupCode
const handleAfterRemoteConfig = (config) => {
// 过滤采购方表行编辑规则
const groups = config.groups || [];
config.groups = groups.filter(
(n) => n.groupCode !== "purchaseEnquiryItemList"
);
const formFields = config.formFields || [];
// 去除采购方callback
formFields.forEach((n) => {
if (n.fieldName === "enquiryScope") {
if (n.callback) {
delete n.callback;
}
}
});
const itemColumns = config.itemColumns || [];
itemColumns.forEach((n) => {
if (n.groupCode === "purchaseEnquiryItemList") {
n.groupCode = "saleEnquiryItemList";
}
// 成本组成JSON
if (n.field === "costFormJson") {
n.slots = {
default(
payload: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
) {
const row = payload.row;
const price = row.price;
const netPrice = row.netPrice;
const flag =
!!(price && Number(price) > 0) ||
!!(netPrice && Number(netPrice) > 0);
// debugger
const elem = (
<a onClick={() => openCostPrice(payload)}>
{flag
? srmI18n("i18n_title_quotationPrice", "已报价")
: srmI18n("i18n_title_noQuotationPrice", "未报价")}
</a>
);
// 报价方式 quotePriceWay
// 数据字典 srmQuotePriceWay (0: 常规报价, 1: 阶梯报价, 2: 成本报价)
return [
row.quotePriceWay === "2" && row.quotePrice == "1" ? elem : null,
];
},
};
}
// 是否报价
if (n.field === "quotePrice") {
n.fieldType = "";
n.slots = {
default({
row,
column,
}: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>) {
function quotePriceChange(val) {
if (!val) {
column.disabled = false;
} else {
column.disabled = true;
}
}
return [
<q-switch
v-model:checked={row[column.field]}
onChange={quotePriceChange}
></q-switch>,
];
},
};
}
// 阶梯报价JSON
if (n.field === "ladderPriceJson") {
n.fieldType = "";
n.width = 250;
n.slots = {
default(
payload: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
) {
const { row, column } = payload;
const val = row[column.field] || "";
const handleLadderPriceClick = () => {
if (!payload.row.taxCode) {
Message.error(
srmI18n(`i18n_title_pleaseEnterTaxCode`, "请先选择税码")
);
return;
}
ladderPriceCachePayload.value = payload;
ladderPriceValue.value = val;
quoteType.value = pageData.value.quoteType || "0";
ladderPriceVisible.value = true;
};
const handleLadderPriceClear = () => {
row[column.field] = "";
};
// 报价方式 quotePriceWay
// 数据字典 srmQuotePriceWay (0: 常规报价, 1: 阶梯报价, 2: 成本报价)
const elem =
row.quotePriceWay === "1" ? (
<SaleLadderPriceDisplay
value={val}
onClick={handleLadderPriceClick}
onClear={handleLadderPriceClear}
></SaleLadderPriceDisplay>
) : null;
return [elem];
},
};
}
//供应商税率为是时,税码可弹框
// if (n.field === 'taxCode' && currentRow['supplierTaxRate'] == '1') {
// n.disabled = false
// }
// if (n.field === 'taxCode' && currentRow['supplierTaxRate'] != '1') {
// n.disabled = true
// }
});
return config; // 返回处理后的配置数据, 可以是 promise 对象
};
// 组件 ref
const layoutRef = ref<ComponentPublicInstance | null>(null);
const { pageData } = useRefInstanceHook(layoutRef);
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
// ...
// 角色: 供应方
role: "sale",
handleAfterRemoteConfig,
});
</script>vue
<template>
<div class="detail-page">
<q-detail-page-layout
ref="layoutRef"
v-bind="options"
></q-detail-page-layout>
<!-- 阶梯报价 -->
<sale-ladder-price-modal
v-model="ladderPriceVisible"
:quoteType="quoteType"
:value="ladderPriceValue"
:cachePayload="ladderPriceCachePayload"
disabled
></sale-ladder-price-modal>
<!-- 成本模板 编辑 -->
<cost-price-edit
v-model="priceEditShow"
:currentRow="priceRow"
:priceData="priceData"
:groups="priceGroups"
:priceMap="priceMap"
:cachePageData="cachePageData"
:cachePayload="cachePayload"
@success="handleCostSuccess"
></cost-price-edit>
<!-- 成本模板 详情 -->
<cost-price-detail
v-model="priceDetailShow"
:currentRow="priceRow"
:priceData="priceData"
:groups="priceGroups"
:priceMap="priceMap"
:cachePageData="cachePageData"
:cachePayload="cachePayload"
></cost-price-detail>
</div>
</template>
<script setup lang="tsx">
import { ref, reactive } from "vue";
// 国际化
import { srmI18n } from "@/utils/srmI18n";
import type { ComponentPublicInstance } from "vue";
// ui组件库 types
import type { GlobalPageLayoutTypes } from "@qqt-product/ui";
import type { VxeColumnSlotTypes, VxeTableDataRow } from "vxe-table";
// 当前行 currentRow, 当前用户信息 userInfo
import { useGlobalStoreWithDefaultValue } from "@/use/useGlobalStore";
import { useRefInstanceHook } from "@qqt-product/ui";
import { loadJS, getRemoteJsFilePath } from "@qqt-product/ui";
import { message as Message } from "ant-design-vue";
// 工具函数及types
import qqtUtils from "@qqt-product/utils";
import SaleLadderPriceDisplay from "./components/saleLadderPriceDisplay.vue";
import SaleLadderPriceModal from "./components/saleLadderPriceModal.vue";
import CostPriceEdit from "./components/costPriceEdit.vue";
import CostPriceDetail from "./components/costPriceDetail.vue";
import type { SuccessPayload } from "./components/costPriceEdit.vue";
import { Decimal } from "decimal.js";
const quoteType = ref<string>("0");
const ladderPriceVisible = ref<boolean>(false);
const ladderPriceValue = ref<string>("");
// 缓存行信息
const ladderPriceCachePayload =
ref<VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>>();
const DIGIT = 4;
const { cloneDeep } = qqtUtils;
const cachePageData = ref<GlobalPageLayoutTypes.RecordString>({});
const priceEditShow = ref<boolean>(false);
const priceDetailShow = ref<boolean>(false);
const priceRow = ref<GlobalPageLayoutTypes.CurrentRow>({
templateNumber: "",
templateVersion: 0,
templateAccount: "",
busAccount: "",
elsAccount: "",
});
const priceGroups = ref<GlobalPageLayoutTypes.Group[]>([]);
const priceData = ref<GlobalPageLayoutTypes.RecordString>({});
const priceMap = ref<GlobalPageLayoutTypes.RecordString>({});
// 全局数据缓存
const { userInfo, token } = useGlobalStoreWithDefaultValue();
// 缓存行信息
const cachePayload =
ref<VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>>();
const openCostPrice = async (
payload: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
) => {
const row = payload.row;
if (!row.taxCode) {
Message.error(srmI18n(`i18n_title_pleaseEnterTaxCode`, "请先选择税码"));
return;
}
cachePayload.value = payload;
const costFormJson = row.costFormJson || "";
if (costFormJson) {
try {
const obj = JSON.parse(costFormJson);
const {
templateName = "",
templateAccount = "",
templateNumber = "",
templateVersion = 0,
busAccount = "",
elsAccount = "",
} = obj;
priceRow.value = {
templateName,
templateAccount,
templateNumber,
templateVersion,
busAccount,
elsAccount,
id: row.id,
};
priceData.value = obj.data
? (obj.data as GlobalPageLayoutTypes.RecordString)
: {};
const filePath: string = getRemoteJsFilePath({
currentRow: priceRow.value,
userInfo: userInfo.value as GlobalPageLayoutTypes.UserInfo,
businessType: "costForm",
role: "sale",
token: token.value,
});
const config = (await loadJS(
`${filePath}?t=${+new Date()}`
)) as GlobalPageLayoutTypes.RemoteConfig;
const groups = (config.groups as GlobalPageLayoutTypes.Group[]) || [];
const itemColumns = config.itemColumns || [];
if (groups && groups.length) {
priceGroups.value = cloneDeep(groups);
// 获取 group 分组的 sum 合计列的 field
priceMap.value = groups.reduce((acc, group) => {
if (acc[group.groupCode]) {
acc[group.groupCode] = "";
}
const { field = "" } = itemColumns.find(
(n) => n.groupCode === group.groupCode && n.sum === "1"
) || { field: "" };
acc[group.groupCode] = field;
return acc;
}, {});
}
// 询价单状态 enquiryStatus_dictText
// 数据字典 srmEnquiryStatus (0: 新建, 1: 报价中, 2: 已报价, 3: 未报价, 4: 接受, 5: 拒绝, 6: 不能报价, 7: 议价中, 8: 重报价, 9: 已定价, 10: 已作废, 11: 已悔标, 12: 发布中, 13: 发布失败, 14: 已转单)
// const props = ['1', '2', '8']
// let status = props.includes(pageData.value.enquiryStatus) || row.itemStatus === '1' || row.itemStatus === '8' ? 'edit' : 'detail'
// const res = await getTimeStamp()
// const timestamp = (res?.timestamp as number) || Date.now()
// // 报价截止日期 expiryDate
// const expiryDate_timestamp = row.expiryDate_timestamp as number
// if (timestamp >= expiryDate_timestamp) {
// status = 'detail'
// }
// cachePageData.value = cloneDeep(pageData.value)
// status === 'edit' ? (priceEditShow.value = true) : (priceDetailShow.value = true)
priceDetailShow.value = true;
} catch (err) {
console.log(err);
}
}
};
// 行赋值 赋值成本报价模型 及 计算 含税价、未税单价、含税总额、未税总额
const handleCostSuccess = ({ costFormJson, pageTotalVal }: SuccessPayload) => {
const saleEnquiryItemList = pageData.value[
"saleEnquiryItemList"
] as GlobalPageLayoutTypes.RecordString[];
// 报价项 quoteType;
// 数据字典 srmQuoteType (0: 含税价, 1: 不含税价);
const quoteType = (pageData.value.quoteType as string) || "0";
const { row } = cachePayload.value || { row: { taxRate: 0 } };
const taxRate = row["taxRate"] || 0;
const formula = Decimal.add(1, Decimal.div(taxRate, 100));
if (cachePayload.value) {
const { rowIndex } = cachePayload.value;
if (saleEnquiryItemList[rowIndex]) {
saleEnquiryItemList[rowIndex]["costFormJson"] = costFormJson;
let requireQuantity = saleEnquiryItemList[rowIndex]["requireQuantity"];
// 根据含税价 price, 实时计算未税单价
if (quoteType === "0") {
saleEnquiryItemList[rowIndex]["price"] = pageTotalVal;
let price = pageTotalVal;
if (price && taxRate) {
let netPrice = Decimal.div(price, formula);
// 4位小数
saleEnquiryItemList[rowIndex]["netPrice"] = new Decimal(
netPrice
).toFixed(DIGIT);
if (requireQuantity) {
let taxAmount = Decimal.mul(price, requireQuantity);
let netAmount = Decimal.div(taxAmount, formula);
saleEnquiryItemList[rowIndex]["taxAmount"] = new Decimal(
taxAmount
).toFixed(DIGIT);
saleEnquiryItemList[rowIndex]["netAmount"] = new Decimal(
netAmount
).toFixed(DIGIT);
}
}
} else {
// 根据未税单价价 netPrice, 实时计算含税价
saleEnquiryItemList[rowIndex]["netPrice"] = pageTotalVal;
let netPrice = pageTotalVal;
if (netPrice && taxRate) {
let price = Decimal.mul(netPrice, formula);
// 4位小数
saleEnquiryItemList[rowIndex]["price"] = new Decimal(price).toFixed(
DIGIT
);
if (requireQuantity) {
let netAmount = Decimal.mul(netPrice, requireQuantity);
let taxAmount = Decimal.mul(netAmount, formula);
saleEnquiryItemList[rowIndex]["netAmount"] = new Decimal(
netAmount
).toFixed(DIGIT);
saleEnquiryItemList[rowIndex]["taxAmount"] = new Decimal(
taxAmount
).toFixed(DIGIT);
}
}
}
}
}
};
// 自定义业务模板数据配置
// 兼容方法设置供应方分组groupCode
const handleAfterRemoteConfig = (config) => {
// 过滤采购方表行编辑规则
const groups = config.groups || [];
config.groups = groups.filter(
(n) => n.groupCode !== "purchaseEnquiryItemList"
);
const formFields = config.formFields || [];
// 去除采购方callback
formFields.forEach((n) => {
if (n.fieldName === "enquiryScope") {
if (n.callback) {
delete n.callback;
}
}
});
const itemColumns = config.itemColumns || [];
itemColumns.forEach((n) => {
if (n.groupCode === "purchaseEnquiryItemList") {
n.groupCode = "saleEnquiryItemList";
}
// 成本组成JSON
if (n.field === "costFormJson") {
n.slots = {
default(
payload: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
) {
const row = payload.row;
const price = row.price;
const netPrice = row.netPrice;
const flag =
!!(price && Number(price) > 0) ||
!!(netPrice && Number(netPrice) > 0);
// debugger
const elem = (
<a onClick={() => openCostPrice(payload)}>
{flag
? srmI18n("i18n_title_quotationPrice", "已报价")
: srmI18n("i18n_title_noQuotationPrice", "未报价")}
</a>
);
// 报价方式 quotePriceWay
// 数据字典 srmQuotePriceWay (0: 常规报价, 1: 阶梯报价, 2: 成本报价)
return [row.quotePriceWay === "2" ? elem : null];
},
};
}
// 是否报价
if (n.field === "quotePrice") {
n.fieldType = "";
n.slots = {
default({
row,
column,
}: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>) {
return [<q-switch v-model:checked={row[column.field]}></q-switch>];
},
};
}
// 阶梯报价JSON
if (n.field === "ladderPriceJson") {
n.fieldType = "";
n.width = 250;
n.slots = {
default(
payload: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
) {
const { row, column } = payload;
const val = row[column.field] || "";
const handleLadderPriceClick = () => {
if (!payload.row.taxCode) {
Message.error(
srmI18n(`i18n_title_pleaseEnterTaxCode`, "请先选择税码")
);
return;
}
ladderPriceCachePayload.value = payload;
ladderPriceValue.value = val;
quoteType.value = pageData.value.quoteType || "0";
ladderPriceVisible.value = true;
};
// 报价方式 quotePriceWay
// 数据字典 srmQuotePriceWay (0: 常规报价, 1: 阶梯报价, 2: 成本报价)
const elem =
row.quotePriceWay === "1" ? (
<SaleLadderPriceDisplay
value={val}
onClick={handleLadderPriceClick}
disabled={true}
></SaleLadderPriceDisplay>
) : null;
return [elem];
},
};
}
});
return config; // 返回处理后的配置数据, 可以是 promise 对象
};
// 组件 ref
const layoutRef = ref<ComponentPublicInstance | null>(null);
const { pageData } = useRefInstanceHook(layoutRef);
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
// ...
// 角色: 供应方
role: "sale",
handleAfterRemoteConfig,
});
</script>vue
<template>
<div :class="['ladder-price-display', str && !props.disabled ? 'set' : '']">
<a-tooltip 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 && !props.disabled"
class="clear"
@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;
disabled?: boolean;
};
const props = withDefaults(defineProps<Props>(), {
value: "",
disabled: false,
});
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" scoped>
.ladder-price-display {
position: relative;
height: 36px;
line-height: 36px;
padding-right: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
a:hover {
color: #1677ff;
}
&.set {
padding-right: 42px;
}
.icons {
position: absolute;
// display: inline-block
font-size: 16px;
right: 0px;
top: 3px;
z-index: 10;
}
.clear {
color: red;
margin-left: 8px;
&.unDelete {
color: rgba(0, 0, 0, 0.25);
cursor: not-allowed;
}
}
}
</style>vue
<template>
<div class="checkPrice">
<q-basic-modal
:visible="modelValue"
:width="1000"
:title="srmI18n('i18n_title_ladderSetting', '阶梯设置')"
:maskClosable="false"
:keyboard="false"
:bodyStyle="{ padding: '0 12px' }"
@cancel="handleCancel"
@ok="handleOk"
>
<div class="form">
<a-descriptions :title="descTitle">
<a-descriptions-item
:label="srmI18n('i18n_field_currency', '币别')"
>{{
cachePayload ? cachePayload.row.currency : ""
}}</a-descriptions-item
>
<a-descriptions-item
:label="srmI18n('i18n_title_enterTaxCode', '税码')"
>{{
cachePayload ? cachePayload.row.taxCode : ""
}}</a-descriptions-item
>
<a-descriptions-item :label="srmI18n('i18n_title_taxRate', '税率')">{{
cachePayload ? cachePayload.row.taxRate : ""
}}</a-descriptions-item>
</a-descriptions>
</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>
</q-basic-modal>
</div>
</template>
<script lang="tsx" setup>
import { ref, computed, 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 qqtUtils from "@qqt-product/utils";
import { Decimal } from "decimal.js";
const { cloneDeep } = qqtUtils;
interface Props {
modelValue: boolean;
value: string;
quoteType: string;
cachePayload:
| VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
| undefined;
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
value: "",
quoteType: "0",
disabled: false,
});
const { modelValue, quoteType, cachePayload, disabled } = toRefs(props);
// 小数位数
const DIGIT = 6;
const descTitle = computed<string>(() => {
if (cachePayload.value) {
return `${srmI18n("i18n_title_material", "物料")}: ${
cachePayload.value.row.materialDesc
}`;
}
return srmI18n("i18n_title_material", "物料");
});
// 报价项 quoteType;
// 数据字典 srmQuoteType (0: 含税价, 1: 不含税价);
const isPrice = computed<boolean>(() => {
return quoteType.value === "0";
});
const emit = defineEmits<{
(e: "update:modelValue", modelValue: boolean): void;
(e: "success", arr: GlobalPageLayoutTypes.RecordString[]): void;
}>();
const xGrid = ref<VxeGridInstance>();
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: "price",
title: srmI18n("i18n_title_price", "含税价"),
minWidth: 150,
editRender: {
enabled: !disabled.value && isPrice.value,
autofocus: ".vxe-input--inner",
},
slots: {
default({
row,
column,
}: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>) {
return [<span>{row[column.field]}</span>];
},
edit({
row,
column,
}: VxeColumnSlotTypes.EditSlotParams<VxeTableDataRow>) {
const props = {
type: "number",
transfer: true,
clearable: true,
};
function onVxeInputBlur({ value }: { value: string }) {
console.log("value :>> ", value);
// 税率 taxRate;
let taxRate = cachePayload.value
? cachePayload.value.row.taxRate
: "0";
// 含税单价 price;
let price = value || "";
// 计算未税单价 netPrice
if (taxRate && price) {
if (Decimal) {
let formula = Decimal.add(1, Decimal.div(taxRate, 100));
let netPrice = Decimal.div(price, formula);
// 4位小数
row.netPrice = new Decimal(netPrice).toFixed(DIGIT);
}
}
}
return [
<vxe-input
v-model={row[column.field]}
{...props}
onChange={onVxeInputBlur}
></vxe-input>,
];
},
},
},
{
field: "netPrice",
title: srmI18n("i18n_title_netPrice", "不含税价"),
minWidth: 150,
editRender: {
enabled: !disabled.value && !isPrice.value,
autofocus: ".vxe-input--inner",
},
slots: {
default({
row,
column,
}: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>) {
return [<span>{row[column.field]}</span>];
},
edit({
row,
column,
}: VxeColumnSlotTypes.EditSlotParams<VxeTableDataRow>) {
const props = {
type: "number",
transfer: true,
clearable: true,
};
function onVxeInputBlur({ value }: { value: string }) {
console.log("value :>> ", value);
// 税率 taxRate;
let taxRate = cachePayload.value
? cachePayload.value.row.taxRate
: "0";
// 未税单价 netPrice
let netPrice = value || "";
// 计算未税单价 netPrice
if (taxRate && netPrice) {
if (Decimal) {
let formula = Decimal.add(1, Decimal.div(taxRate, 100));
let price = Decimal.mul(netPrice, formula);
// 4位小数
row.price = new Decimal(price).toFixed(DIGIT);
}
}
}
return [
<vxe-input
v-model={row[column.field]}
{...props}
onChange={onVxeInputBlur}
></vxe-input>,
];
},
},
},
];
const resetData = () => {
tableData.value = [];
};
watch(modelValue, (bool) => {
if (bool) {
init();
} else {
resetData();
}
});
const handleCancel = () => {
emit("update:modelValue", false);
};
const handleOk = () => {
const arr = cloneDeep(tableData.value);
emit("success", 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>vue
<template>
<div class="cost-price">
<a-modal
:open="modelValue"
width="100%"
wrap-class-name="full-modal"
:title="srmI18n(`i18n_title_costTemplate`, '成本模板')"
@cancel="handleCancel"
@ok="setCostOk"
>
<a-spin :spinning="loading">
<div class="page-container">
<q-edit-page-layout
v-if="show"
class="cost-price-layout"
ref="layoutRef"
v-bind="options"
>
<template
v-for="el in namedSlots"
:key="el.append"
#[el.append]="{ groupCode }"
>
<div class="desc" v-show="getTotal(groupCode)">
<span class="item">
<span class="tit">{{
srmI18n(`i18n_field_quoteType`, "报价项")
}}</span>
<span class="ant-typography">
<strong>{{
isPriceQuoteType
? srmI18n("i18n_title_price", "含税价")
: srmI18n("i18n_title_netPrice", "不含税价")
}}</strong>
</span>
</span>
<span class="item">
<span class="tit">{{
srmI18n(`i18n_field_taxRate`, "税率")
}}</span>
<span class="ant-typography">
<strong>{{ curTaxRate }}</strong>
</span>
</span>
</div>
<div class="total" v-show="getTotal(groupCode)">
<span class="item">
<span class="tit">{{
`${srmI18n("i18n_title_price", "含税价")}${srmI18n(
`i18n_field_kMk_176ba4f`,
"总汇总"
)}`
}}</span>
<span class="ant-typography">
<strong>{{ getTotalPrice(groupCode) }}</strong>
</span>
</span>
<span class="item">
<span class="tit">{{
`${srmI18n("i18n_title_netPrice", "不含税价")}${srmI18n(
`i18n_field_kMk_176ba4f`,
"总汇总"
)}`
}}</span>
<span class="ant-typography">
<strong>{{ getTotalNetPrice(groupCode) }}</strong>
</span>
</span>
</div>
</template>
</q-edit-page-layout>
</div>
</a-spin>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, toRefs, watch, nextTick } from "vue";
import type { ComponentPublicInstance } from "vue";
// 国际化
import { srmI18n } from "@/utils/srmI18n";
// ui组件库 types
import type { GlobalPageLayoutTypes } from "@qqt-product/ui";
import type { VxeColumnSlotTypes, VxeTableDataRow } from "vxe-table";
import { BUTTON_ADD_ONE_ROW, BUTTON_DELETE_ROW } from "@qqt-product/ui";
import { useRefInstanceHook } from "@qqt-product/ui";
// 当前行 currentRow, 当前用户信息 userInfo
import { useGlobalStoreWithDefaultValue } from "@/use/useGlobalStore";
import REGEXP from "@/utils/regexp";
import { Decimal } from "decimal.js";
import qqtApi from "@qqt-product/api";
import { message as Message } from "ant-design-vue";
import qqtUtils from "@qqt-product/utils";
const { cloneDeep } = qqtUtils;
const DIGIT = 4;
const Request: qqtApi.Request = qqtApi.useHttp();
const loading = ref<boolean>(false);
// 组件 ref
const layoutRef = ref<ComponentPublicInstance | null>(null);
const { pageData } = useRefInstanceHook(layoutRef);
interface Props {
modelValue: boolean;
currentRow: GlobalPageLayoutTypes.CurrentRow;
priceData: GlobalPageLayoutTypes.RecordString;
groups: GlobalPageLayoutTypes.Group[];
priceMap: GlobalPageLayoutTypes.RecordString;
cachePageData: GlobalPageLayoutTypes.RecordString;
cachePayload:
| VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
| undefined;
}
export interface SuccessPayload {
costFormJson: string;
pageTotalVal: string;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
currentRow: () => ({
templateNumber: "",
templateVersion: 0,
templateAccount: "",
busAccount: "",
elsAccount: "",
}),
priceData: () => ({}),
groups: () => [],
priceMap: () => ({}),
});
const { modelValue, priceMap } = toRefs(props);
const namedSlots = computed(() => {
return props.groups.map((group) => ({
append: `vertical_${group.groupCode}_append`,
}));
});
const emit = defineEmits<{
(e: "update:modelValue", modelValue: boolean): void;
(e: "success", payload: SuccessPayload): void;
}>();
// 全局数据缓存
const { userInfo, token, manuallyExit } = useGlobalStoreWithDefaultValue();
const show = ref<boolean>(false);
// 是否为“含税价”报价项
const isPriceQuoteType = computed<boolean>(() => {
// 报价项 quoteType;
// 数据字典 srmQuoteType (0: 含税价, 1: 不含税价);
const quoteType = (props.cachePageData.quoteType as string) || "0";
return quoteType === "0";
});
const curTaxRate = computed(() => {
if (props.cachePayload) {
return props.cachePayload.row.taxRate || 0;
}
return 0;
});
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
showPageHeader: false,
showLayoutAnchor: false,
/**
* 模块业务类型 string
* 取值根据各模块配置
* 当定义为"custom"时,使用本地配置
*/
businessType: "costForm",
// 角色: 供应方
role: "sale",
// 布局模式: 平铺布局 vertical (默认), Tab布局 tab, 主从布局 master
pattern: "vertical",
// 仅使用本地布局模式
isUseLocalPattern: true,
// 当前已选行数据
currentRow: {
templateNumber: "",
templateVersion: 0,
templateAccount: "",
busAccount: "",
elsAccount: "",
},
refreshMethods(row?: GlobalPageLayoutTypes.CurrentRow) {
if (options.currentRow) {
options.currentRow =
row ||
Object.assign({}, options.currentRow, {
_t: +new Date(),
});
}
},
// 当前登录租户信息
userInfo: userInfo.value as GlobalPageLayoutTypes.UserInfo,
// 有导入excel 操作的必填
token: token.value,
// 本地页面数据配置
localConfig: {
groups: [],
},
isUseLocalData: true,
localData: {},
});
watch(modelValue, (bool) => {
if (bool) {
if (options.localConfig) {
options.localConfig.groups = props.groups.map((n) => {
return {
...n,
buttons: [BUTTON_ADD_ONE_ROW, BUTTON_DELETE_ROW],
};
});
}
const copyData = cloneDeep(props.priceData);
options.currentRow = { ...props.currentRow, ...copyData };
nextTick(() => {
show.value = true;
});
}
});
const getTotal = (groupCode: string) => {
if (!groupCode || !Decimal) {
return 0;
}
const tableData = pageData.value[
groupCode
] as GlobalPageLayoutTypes.RecordString[];
if (!tableData || !tableData.length) {
return 0;
}
// 合计列配置 property
// 没有配置, 默认取 total
const key = priceMap.value[groupCode] || "total";
const flag = tableData.some((n) => !!n[key]);
if (flag) {
let totalAmount = tableData.reduce((acc: Decimal, row) => {
let count = REGEXP.interger.test(row[key]) ? row[key] : 0;
return Decimal.add(acc, count);
}, new Decimal(0));
const num = new Decimal(totalAmount).toFixed(DIGIT);
return Number(num) > 0 ? num : 0;
}
return 0;
};
const getTotalPrice = (groupCode: string) => {
if (Decimal && getTotal(groupCode)) {
const taxRate = curTaxRate.value;
const formula = Decimal.add(1, Decimal.div(taxRate, 100));
if (isPriceQuoteType.value) {
return getTotal(groupCode);
} else {
let netPrice = getTotal(groupCode);
let price = Decimal.mul(netPrice, formula);
return new Decimal(price).toFixed(DIGIT);
}
}
return 0;
};
const getTotalNetPrice = (groupCode: string) => {
if (Decimal && getTotal(groupCode)) {
const taxRate = curTaxRate.value;
const formula = Decimal.add(1, Decimal.div(taxRate, 100));
if (isPriceQuoteType.value) {
let price = getTotal(groupCode);
let netPrice = Decimal.div(price, formula);
return new Decimal(netPrice).toFixed(DIGIT);
} else {
return getTotal(groupCode);
}
}
return 0;
};
const cancelFunc = () => {
show.value = false;
emit("update:modelValue", false);
};
// 生成成本报价JSON 及 页面价格汇总
const generateCostFormJsonData = () => {
let copyCachePageData = cloneDeep(props.cachePageData);
// 赋值表行成本报价JSON
let copyPageData = cloneDeep(pageData.value);
const costFormJson: GlobalPageLayoutTypes.RecordString = {
...props.currentRow,
data: { ...copyPageData },
groups: [],
};
// 报价项 quoteType;
// 数据字典 srmQuoteType (0: 含税价, 1: 不含税价);
const quoteType = (copyCachePageData.quoteType as string) || "0";
const { row } = props.cachePayload || { row: { taxRate: 0 } };
const taxRate = row["taxRate"] || 0;
const formula = Decimal.add(1, Decimal.div(taxRate, 100));
// 获取页面所有价格汇总
const pageTotal = props.groups.reduce((acc: Decimal, group) => {
// 返回 groups 分组数据供给后端比价接口使用
const obj: GlobalPageLayoutTypes.RecordString = {
groupName: group.groupName || "",
groupNameI18nKey: group.groupNameI18nKey || "",
groupCode: group.groupCode || "",
groupType: group.groupType || "",
totalValue: "",
price: 0,
netPrice: 0,
};
// 分组合计
let groupTotal = getTotal(group.groupCode);
obj.totalValue = groupTotal;
if (quoteType === "0") {
obj.price = groupTotal;
let netPrice = Decimal.div(groupTotal, formula);
obj.netPrice = new Decimal(netPrice).toFixed(DIGIT);
} else {
obj.netPrice = groupTotal;
let price = Decimal.mul(groupTotal, formula);
obj.price = new Decimal(price).toFixed(DIGIT);
}
costFormJson.groups.push(obj);
return Decimal.add(acc, groupTotal);
}, new Decimal(0));
const pageTotalVal = new Decimal(pageTotal).toFixed(DIGIT);
emit("success", {
costFormJson: JSON.stringify(costFormJson),
pageTotalVal,
});
};
const handleCancel = () => {
cancelFunc();
};
const setCostOk = () => {
generateCostFormJsonData();
cancelFunc();
};
</script>
<style lang="less">
.cost-price {
height: 100%;
}
.full-modal {
.ant-modal {
max-width: 100%;
top: 0;
padding-bottom: 0;
margin: 0;
}
.ant-modal-content {
display: flex;
flex-direction: column;
height: calc(100vh);
max-height: calc(100vh);
overflow-x: hidden;
.page-container {
min-height: calc(100vh - 55px - 53px);
}
}
.ant-modal-body {
flex: 1;
padding: 0;
}
}
.total,
.desc {
margin-top: 6px;
.tit {
&::after {
margin-right: 6px;
color: #171832;
content: ":";
}
}
.item {
& + .item {
margin-left: 10px;
}
}
}
</style>vue
<template>
<div class="cost-price">
<a-modal
:open="modelValue"
width="100%"
wrap-class-name="full-modal"
:title="srmI18n(`i18n_title_costTemplate`, '成本模板')"
:footer="null"
@cancel="handleCancel"
>
<a-spin :spinning="loading">
<div class="page-container">
<q-detail-page-layout
v-if="show"
class="cost-price-layout"
ref="layoutRef"
v-bind="options"
>
<template
v-for="el in namedSlots"
:key="el.append"
#[el.append]="{ groupCode }"
>
<div class="desc" v-show="getTotal(groupCode)">
<span class="item">
<span class="tit">{{
srmI18n(`i18n_field_quoteType`, "报价项")
}}</span>
<span class="ant-typography">
<strong>{{
isPriceQuoteType
? srmI18n("i18n_title_price", "含税价")
: srmI18n("i18n_title_netPrice", "不含税价")
}}</strong>
</span>
</span>
<span class="item">
<span class="tit">{{
srmI18n(`i18n_field_taxRate`, "税率")
}}</span>
<span class="ant-typography">
<strong>{{ curTaxRate }}</strong>
</span>
</span>
</div>
<div class="total" v-show="getTotal(groupCode)">
<span class="item">
<span class="tit">{{
`${srmI18n("i18n_title_price", "含税价")}${srmI18n(
`i18n_field_kMk_176ba4f`,
"总汇总"
)}`
}}</span>
<span class="ant-typography">
<strong>{{ getTotalPrice(groupCode) }}</strong>
</span>
</span>
<span class="item">
<span class="tit">{{
`${srmI18n("i18n_title_netPrice", "不含税价")}${srmI18n(
`i18n_field_kMk_176ba4f`,
"总汇总"
)}`
}}</span>
<span class="ant-typography">
<strong>{{ getTotalNetPrice(groupCode) }}</strong>
</span>
</span>
</div>
</template>
</q-detail-page-layout>
</div>
</a-spin>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, toRefs, watch, nextTick } from "vue";
import type { ComponentPublicInstance } from "vue";
// 国际化
import { srmI18n } from "@/utils/srmI18n";
// ui组件库 types
import type { GlobalPageLayoutTypes } from "@qqt-product/ui";
import type { VxeColumnSlotTypes, VxeTableDataRow } from "vxe-table";
import { useRefInstanceHook } from "@qqt-product/ui";
// 当前行 currentRow, 当前用户信息 userInfo
import { useGlobalStoreWithDefaultValue } from "@/use/useGlobalStore";
import REGEXP from "@/utils/regexp";
import { Decimal } from "decimal.js";
import qqtUtils from "@qqt-product/utils";
const { cloneDeep } = qqtUtils;
const DIGIT = 4;
const loading = ref<boolean>(false);
// 组件 ref
const layoutRef = ref<ComponentPublicInstance | null>(null);
const { pageData } = useRefInstanceHook(layoutRef);
interface Props {
modelValue: boolean;
currentRow: GlobalPageLayoutTypes.CurrentRow;
priceData: GlobalPageLayoutTypes.RecordString;
groups: GlobalPageLayoutTypes.Group[];
priceMap: GlobalPageLayoutTypes.RecordString;
cachePageData: GlobalPageLayoutTypes.RecordString;
cachePayload:
| VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
| undefined;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
currentRow: () => ({
templateNumber: "",
templateVersion: 0,
templateAccount: "",
busAccount: "",
elsAccount: "",
}),
priceData: () => ({}),
groups: () => [],
priceMap: () => ({}),
});
const { modelValue, priceMap } = toRefs(props);
const namedSlots = computed(() => {
return props.groups.map((group) => ({
append: `vertical_${group.groupCode}_append`,
}));
});
const emit = defineEmits<{
(e: "update:modelValue", modelValue: boolean): void;
}>();
// 全局数据缓存
const { userInfo, token, manuallyExit } = useGlobalStoreWithDefaultValue();
const show = ref<boolean>(false);
// 是否为“含税价”报价项
const isPriceQuoteType = computed<boolean>(() => {
// 报价项 quoteType;
// 数据字典 srmQuoteType (0: 含税价, 1: 不含税价);
const quoteType = (props.cachePageData.quoteType as string) || "0";
return quoteType === "0";
});
const curTaxRate = computed(() => {
if (props.cachePayload) {
return props.cachePayload.row.taxRate || 0;
}
return 0;
});
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
showPageHeader: false,
showLayoutAnchor: false,
/**
* 模块业务类型 string
* 取值根据各模块配置
* 当定义为"custom"时,使用本地配置
*/
businessType: "costForm",
// 角色: 供应方
role: "sale",
// 布局模式: 平铺布局 vertical (默认), Tab布局 tab, 主从布局 master
pattern: "vertical",
// 仅使用本地布局模式
isUseLocalPattern: true,
// 当前已选行数据
currentRow: {
templateNumber: "",
templateVersion: 0,
templateAccount: "",
busAccount: "",
elsAccount: "",
},
refreshMethods(row?: GlobalPageLayoutTypes.CurrentRow) {
if (options.currentRow) {
options.currentRow =
row ||
Object.assign({}, options.currentRow, {
_t: +new Date(),
});
}
},
// 当前登录租户信息
userInfo: userInfo.value as GlobalPageLayoutTypes.UserInfo,
// 有导入excel 操作的必填
token: token.value,
// 本地页面数据配置
localConfig: {
groups: [],
},
isUseLocalData: true,
localData: {},
});
watch(modelValue, (bool) => {
if (bool) {
if (options.localConfig) {
options.localConfig.groups = cloneDeep(props.groups);
}
const copyData = cloneDeep(props.priceData);
options.currentRow = { ...props.currentRow, ...copyData };
nextTick(() => {
show.value = true;
});
}
});
const getTotal = (groupCode: string) => {
if (!groupCode || !Decimal) {
return 0;
}
const tableData = pageData.value[
groupCode
] as GlobalPageLayoutTypes.RecordString[];
if (!tableData || !tableData.length) {
return 0;
}
// 合计列配置 property
// 没有配置, 默认取 total
const key = priceMap.value[groupCode] || "total";
const flag = tableData.some((n) => !!n[key]);
if (flag) {
let totalAmount = tableData.reduce((acc: Decimal, row) => {
let count = REGEXP.interger.test(row[key]) ? row[key] : 0;
return Decimal.add(acc, count);
}, new Decimal(0));
const num = new Decimal(totalAmount).toFixed(DIGIT);
return Number(num) > 0 ? num : 0;
}
return 0;
};
const getTotalPrice = (groupCode: string) => {
if (Decimal && getTotal(groupCode)) {
const taxRate = curTaxRate.value;
const formula = Decimal.add(1, Decimal.div(taxRate, 100));
if (isPriceQuoteType.value) {
return getTotal(groupCode);
} else {
let netPrice = getTotal(groupCode);
let price = Decimal.mul(netPrice, formula);
return new Decimal(price).toFixed(DIGIT);
}
}
return 0;
};
const getTotalNetPrice = (groupCode: string) => {
if (Decimal && getTotal(groupCode)) {
const taxRate = curTaxRate.value;
const formula = Decimal.add(1, Decimal.div(taxRate, 100));
if (isPriceQuoteType.value) {
let price = getTotal(groupCode);
let netPrice = Decimal.div(price, formula);
return new Decimal(netPrice).toFixed(DIGIT);
} else {
return getTotal(groupCode);
}
}
return 0;
};
const handleCancel = () => {
show.value = false;
emit("update:modelValue", false);
};
</script>
<style lang="less">
.cost-price {
height: 100%;
}
.full-modal {
.ant-modal {
max-width: 100%;
top: 0;
padding-bottom: 0;
margin: 0;
}
.ant-modal-content {
display: flex;
flex-direction: column;
height: calc(100vh);
max-height: calc(100vh);
overflow-x: hidden;
.page-container {
min-height: calc(100vh - 55px - 53px);
}
}
.ant-modal-body {
flex: 1;
padding: 0;
}
}
.total,
.desc {
margin-top: 6px;
.tit {
&::after {
margin-right: 6px;
color: #171832;
content: ":";
}
}
.item {
& + .item {
margin-left: 10px;
}
}
}
</style>示例

返利规则管理模块 父子级联动、可拖拽穿梭选择阶梯报价
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"
:unControl="unControl"
:value="ladderPriceValue"
:cachePayload="ladderPriceCachePayload"
@success="handleLadderPriceSuccess"
></ladder-price-modal>
<!-- 返利商品子级阶梯 -->
<RebateModal
v-model="rebateVisible"
:payload="rebatePayload"
:config="rebateConfig"
@success="handleRebateSuccess"
></RebateModal>
</div>
</template>
<script lang="tsx" setup>
import { ref, reactive } from "vue";
import type { ComponentPublicInstance } from "vue";
import { srmI18n } from "@/utils/srmI18n";
// ui组件库 types
import type { GlobalPageLayoutTypes } from "@qqt-product/ui";
import type { VxeColumnSlotTypes, VxeTableDataRow } from "vxe-table";
import { message as Message, Modal } from "ant-design-vue";
import LadderPriceDisplay from "./components/ladderPriceDisplay.vue";
import LadderPriceModal from "./components/ladderPriceModal.vue";
import RebateProduct from "./components/rebateProduct.vue";
import RebateModal from "./components/rebateModal.vue";
interface Payload {
rebateExtends: string;
ids: string;
names: string;
}
// 编辑组件 ref
const layoutRef = ref<ComponentPublicInstance | null>(null);
const rebateVisible = ref<boolean>(false);
// 缓存行信息
const rebatePayload =
ref<VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>>();
const rebateConfig = ref<GlobalPageLayoutTypes.ColumnItem>({
groupCode: "rebateRuleItems",
title: "返利商品",
fieldLabelI18nKey: "i18n_field_vvXN_42a083d0",
fieldType: "selectModal",
field: "rebateProductText",
defaultValue: "",
});
const ladderPriceVisible = ref<boolean>(false);
const unControl = ref<boolean>(false);
const ladderPriceValue = ref<string>("");
// 缓存行信息
const ladderPriceCachePayload =
ref<VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>>();
const handleLadderPriceSuccess = (
arr: GlobalPageLayoutTypes.RecordString[]
) => {
if (ladderPriceCachePayload.value) {
const { row, column } = ladderPriceCachePayload.value;
row[column.field] = JSON.stringify(arr);
}
};
const handleRebateSuccess = ({ rebateExtends, ids, names }: Payload) => {
if (rebatePayload.value) {
const { row } = rebatePayload.value;
row.rebateProductText = names;
row.rebateProduct = ids;
row.rebateProductExtends = rebateExtends;
}
};
const handleAfterRemoteConfig = (config) => {
const itemColumns = config.itemColumns || [];
itemColumns.forEach((n) => {
// 阶梯报价JSON
// 返利阶梯 rebateLadder
// 追加返利阶梯 rebateLadder
if (
(n.groupCode === "rebateRuleItems" ||
n.groupCode === "rebateRuleSupplements") &&
n.field === "rebateLadder"
) {
n.fieldType = "";
n.width = 250;
n.helpText = srmI18n(
"i18n_alert_NRAPcvvXNIGRJtyDumFxiTcr_fe9d41a5",
"如果当前行返利商品已设置子级阶梯价格则不允许修改"
);
n.slots = {
default(
payload: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
) {
const { row, column } = payload;
const val = row[column.field] || "";
const unDelete = !!row.rebateProductExtends;
const handleLadderPriceClick = () => {
ladderPriceCachePayload.value = payload;
ladderPriceValue.value = val;
unControl.value = unDelete;
ladderPriceVisible.value = true;
};
const handleLadderPriceClear = () => {
row[column.field] = "";
};
return [
<LadderPriceDisplay
value={val}
onClick={handleLadderPriceClick}
unDelete={unDelete}
onClear={handleLadderPriceClear}
></LadderPriceDisplay>,
];
},
};
}
// 基本返利规则 表行 返利商品
// 追加返利规则 表行 追加返利商品
if (
(n.groupCode === "rebateRuleItems" && n.field === "rebateProductText") ||
(n.groupCode === "rebateRuleSupplements" &&
n.field === "rebateProductText")
) {
n.fieldType = "";
n.width = 250;
n.slots = {
default(
payload: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
) {
const { row, column } = payload;
const val = row[column.field] || "";
const handleRebateClick = () => {
if (!row.rebateLadder) {
Message.error(
srmI18n(
"i18n_alert_VWGRAPcvvyD_1cfc1b7f",
"请先设置当前行返利阶梯"
)
);
return;
}
rebatePayload.value = payload;
rebateConfig.value = n;
rebateVisible.value = true;
};
const handleRebateClear = () => {
Modal.confirm({
title: srmI18n(`i18n_title_tip`, "提示"),
content: srmI18n(
"i18n_alert_KQVVvvXNtJtyDum_1ed2bcc3",
"是否清空返利商品及子级阶梯价格"
),
onOk() {
row.rebateProduct = "";
row.rebateProductText = "";
row.rebateProductExtends = null;
},
});
};
return [
<RebateProduct
value={val}
onClick={handleRebateClick}
onClear={handleRebateClear}
></RebateProduct>,
];
},
};
}
});
return config;
};
// 配置
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
// ...
handleAfterRemoteConfig,
});
</script>vue
<template>
<div class="detail-page">
<q-detail-page-layout
ref="layoutRef"
v-bind="options"
></q-detail-page-layout>
<!-- 阶梯报价 -->
<ladder-price-modal
v-model="ladderPriceVisible"
:value="ladderPriceValue"
:cachePayload="ladderPriceCachePayload"
disabled
></ladder-price-modal>
<!-- 返利商品子级阶梯 -->
<RebateModal
v-model="rebateVisible"
:payload="rebatePayload"
:config="rebateConfig"
disabled
></RebateModal>
</div>
</template>
<script setup lang="tsx">
import { reactive, ref } from "vue";
import type { ComponentPublicInstance } from "vue";
import type { GlobalPageLayoutTypes } from "@qqt-product/ui";
import type { VxeColumnSlotTypes, VxeTableDataRow } from "vxe-table";
// 按钮常量引用
import { srmI18n } from "@/utils/srmI18n";
import { message as Message } from "ant-design-vue";
import LadderPriceDisplay from "./components/ladderPriceDisplay.vue";
import LadderPriceModal from "./components/ladderPriceModal.vue";
import RebateProduct from "./components/rebateProduct.vue";
import RebateModal from "./components/rebateModal.vue";
const layoutRef = ref<ComponentPublicInstance | null>(null);
const rebateVisible = ref<boolean>(false);
// 缓存行信息
const rebatePayload =
ref<VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>>();
const rebateConfig = ref<GlobalPageLayoutTypes.ColumnItem>({
groupCode: "rebateRuleItems",
title: "返利商品",
fieldLabelI18nKey: "i18n_field_vvXN_42a083d0",
fieldType: "selectModal",
field: "rebateProductText",
defaultValue: "",
});
const ladderPriceVisible = ref<boolean>(false);
const ladderPriceValue = ref<string>("");
// 缓存行信息
const ladderPriceCachePayload =
ref<VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>>();
const handleAfterRemoteConfig = (config) => {
const itemColumns = config.itemColumns || [];
itemColumns.forEach((n) => {
// 阶梯报价JSON
// 返利阶梯 rebateLadder
// 追加返利阶梯 rebateLadder
if (
(n.groupCode === "rebateRuleItems" ||
n.groupCode === "rebateRuleSupplements") &&
n.field === "rebateLadder"
) {
n.fieldType = "";
n.width = 250;
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;
};
return [
<LadderPriceDisplay
value={val}
disabled={true}
onClick={handleLadderPriceClick}
></LadderPriceDisplay>,
];
},
};
}
// 基本返利规则 表行 返利商品
// 追加返利规则 表行 追加返利商品
if (
(n.groupCode === "rebateRuleItems" && n.field === "rebateProductText") ||
(n.groupCode === "rebateRuleSupplements" &&
n.field === "rebateProductText")
) {
n.fieldType = "";
n.width = 250;
n.slots = {
default(
payload: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>
) {
const { row, column } = payload;
const val = row[column.field] || "";
const handleRebateClick = () => {
if (!row.rebateLadder) {
Message.error(
srmI18n(
"i18n_alert_VWGRAPcvvyD_1cfc1b7f",
"请先设置当前行返利阶梯"
)
);
return;
}
rebatePayload.value = payload;
rebateConfig.value = n;
rebateVisible.value = true;
};
return [
<RebateProduct
value={val}
onClick={handleRebateClick}
disabled={true}
></RebateProduct>,
];
},
};
}
});
return config;
};
const options = reactive<Partial<GlobalPageLayoutTypes.EditPageLayoutProps>>({
// ...
handleAfterRemoteConfig,
});
</script>vue
<template>
<div :class="['ladder-price-display', str && !props.disabled ? 'set' : '']">
<a-tooltip 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="60"
></vxe-table-column>
<vxe-table-column
field="ladder"
:title="srmI18n('i18n_title_ladderLeve', '阶梯级')"
width="150"
></vxe-table-column>
<vxe-table-column
field="ladderQuantity"
:title="srmI18n('i18n_title_ladderCount', '阶梯数量')"
width="150"
></vxe-table-column>
<vxe-table-column
field="type_dictText"
:title="srmI18n('i18n_field_tdCK_4155670c', '计算方式')"
width="150"
></vxe-table-column>
<vxe-table-column
field="fix"
:title="srmI18n('i18n_dict_CIR_151e85c', '固定值')"
width="100"
></vxe-table-column>
<vxe-table-column
field="rate"
:title="srmI18n('i18n_alert_lvR_19eb8e5', '比例值') + '%'"
width="100"
></vxe-table-column>
<vxe-table-column
field="union"
:title="srmI18n('i18n_alert_BtLv_3244ca0d', '每单位返')"
width="100"
></vxe-table-column>
</vxe-table>
</div>
</template>
{{ str }}
</a-tooltip>
<a class="icons">
<StockOutlined @click="handleClick" />
<CloseCircleOutlined
v-show="str && !props.disabled"
:class="['clear', unDelete ? 'unDelete' : '']"
@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;
disabled?: boolean;
unDelete?: boolean;
};
const props = withDefaults(defineProps<Props>(), {
value: "",
disabled: false,
unDelete: false,
});
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.fix || n.rate || n.union || ""}`)
.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 = () => !props.unDelete && emit("clear");
</script>
<style lang="less" scoped>
.ladder-price-display {
position: relative;
height: 36px;
line-height: 36px;
padding-right: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
a:hover {
color: #1677ff;
}
&.set {
padding-right: 42px;
}
.icons {
position: absolute;
// display: inline-block
font-size: 16px;
right: 0px;
top: 3px;
z-index: 10;
}
.clear {
color: red;
margin-left: 8px;
&.unDelete {
color: rgba(0, 0, 0, 0.25);
cursor: not-allowed;
}
}
}
</style>vue
<template>
<div class="checkPrice">
<q-basic-modal
:visible="modelValue"
:width="1000"
:title="srmI18n('i18n_title_ladderSetting', '阶梯设置')"
:maskClosable="false"
:keyboard="false"
:bodyStyle="{ padding: '0 12px' }"
destroyOnClose
@cancel="handleCancel"
@ok="handleOk"
>
<div class="form" v-if="!disabled && !props.unControl">
<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="
srmI18n(
'i18n_alert_VWNfU0jyDWR_ffbcea1c',
'请输入大于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>
</q-basic-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;
disabled?: boolean;
unControl?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
value: "",
disabled: false,
unControl: false, // 状态为不允许操作
});
const { modelValue, disabled } = toRefs(props);
const emit = defineEmits<{
(e: "update:modelValue", modelValue: boolean): void;
(e: "success", arr: GlobalPageLayoutTypes.RecordString[]): 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: {}, // 按键配置项
editConfig: {
trigger: "click", // 双击触发编辑
mode: "cell", // 单元格编辑模式
beforeEditMethod({ row, rowIndex, column, columnIndex }) {
if (disabled.value) {
return false;
}
const props = ["fix", "rate", "union"];
// 计算方式
if (column.field === "type") {
const val = row.fix || row.rate || row.union || "";
if (val) {
Message.warning(
srmI18n(
"i18n_alert_VGcCIRlvRBtLvWRSniTcr_e53af709",
"清除行固定值/比例值%/每单位返数值后才允许修改"
)
);
return false;
}
}
if (props.includes(column.field)) {
if (row.type !== column.field) {
Message.warning(
srmI18n(
"i18n_alert_RiTWNtdCKdiAcWF_39fa99f6",
"只允许输入计算方式所选类型数据"
)
);
return false;
}
}
return true;
},
},
};
const options: GlobalPageLayoutTypes.RecordString = [
{ value: "fix", label: srmI18n("i18n_dict_CIR_151e85c", "固定值") },
{ value: "rate", label: srmI18n("i18n_alert_lvR_19eb8e5", "比例值") + "%" },
{ value: "union", label: srmI18n("i18n_alert_BtLv_3244ca0d", "每单位返") },
];
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: "type",
title: srmI18n("i18n_field_tdCK_4155670c", "计算方式"),
minWidth: 150,
editRender: {
enabled: true,
},
slots: {
default({
row,
column,
}: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>) {
const val = row[`${column.field}_dictText`] || row[column.field];
return [<span>{val}</span>];
},
edit({
row,
column,
}: VxeColumnSlotTypes.EditSlotParams<VxeTableDataRow>) {
const props = {
options,
filterable: true,
clearable: true,
};
const onVxeSelectChange = ({ value }: { value: string }) => {
let obj = options.find((n) => n.value === value);
row[`${column.field}_dictText`] = obj.label || "";
};
return [
<vxe-select
v-model={row[column.field]}
{...props}
onChange={onVxeSelectChange}
></vxe-select>,
];
},
},
},
{
field: "fix",
title: srmI18n("i18n_dict_CIR_151e85c", "固定值"),
minWidth: 100,
editRender: {
enabled: true,
autofocus: ".vxe-input--inner",
},
slots: {
default({
row,
column,
}: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>) {
return [<span>{row[column.field]}</span>];
},
edit({
row,
column,
}: VxeColumnSlotTypes.EditSlotParams<VxeTableDataRow>) {
const props = {
type: "number",
transfer: true,
clearable: true,
};
return [<vxe-input v-model={row[column.field]} {...props}></vxe-input>];
},
},
},
{
field: "rate",
title: srmI18n("i18n_alert_lvR_19eb8e5", "比例值") + "%",
minWidth: 100,
editRender: {
enabled: true,
autofocus: ".vxe-input--inner",
},
slots: {
default({
row,
column,
}: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>) {
return [<span>{row[column.field]}</span>];
},
edit({
row,
column,
}: VxeColumnSlotTypes.EditSlotParams<VxeTableDataRow>) {
const props = {
type: "number",
transfer: true,
clearable: true,
};
return [<vxe-input v-model={row[column.field]} {...props}></vxe-input>];
},
},
},
{
field: "union",
title: srmI18n("i18n_alert_BtLv_3244ca0d", "每单位返"),
minWidth: 100,
editRender: {
enabled: true,
autofocus: ".vxe-input--inner",
},
slots: {
default({
row,
column,
}: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>) {
return [<span>{row[column.field]}</span>];
},
edit({
row,
column,
}: VxeColumnSlotTypes.EditSlotParams<VxeTableDataRow>) {
const props = {
type: "number",
transfer: true,
clearable: true,
};
return [<vxe-input v-model={row[column.field]} {...props}></vxe-input>];
},
},
},
];
if (!props.unControl) {
columns.push({
field: "grid_operation",
title: srmI18n(`i18n_title_operation`, "操作"),
minWidth: 150,
slots: {
default({
$rowIndex,
}: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>) {
const handleDelete = () => {
tableData.value.splice($rowIndex, 1);
setNltRow();
};
return [
<a-button
type="text"
danger
disabled={
disabled.value || $rowIndex !== tableData.value.length - 1
}
onClick={handleDelete}
>
{srmI18n(`i18n_title_delete`, "删除")}
</a-button>,
];
},
},
});
}
// 设置 阶梯级 格式
const setNltRow = () => {
if (!tableData.value.length) return;
let last = tableData.value[tableData.value.length - 1];
last.ladder = `${last.ladderQuantity} ${GT}`;
last.min = last.ladderQuantity;
last.max = "";
if (tableData.value.length > 1) {
let prev = tableData.value[tableData.value.length - 2];
prev.ladder = `${prev.ladderQuantity} ${ELT} ${last.ladderQuantity}`;
prev.min = prev.ladderQuantity;
prev.max = last.ladderQuantity;
}
};
const setData = () => {
const val = form.value.ladderQuantity;
if (!val) {
Message.error(srmI18n("i18n_alert_VWNyDWR_cd05cb41", "请输入阶梯数量"));
return;
}
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}`,
rate: "",
fix: "",
union: "",
min: "",
max: "",
type: "",
type_dictText: "",
};
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 = () => {
emit("success", cloneDeep(tableData.value));
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>vue
<template>
<div :class="['rebateProduct', !props.disabled ? 'set' : '']">
<span @click="handleClick">{{ value }}</span>
<a class="icons">
<StockOutlined @click="handleClick" />
<CloseCircleOutlined
v-show="!props.disabled"
:class="['clear', unDelete ? 'unDelete' : '']"
@click="handleClear"
/>
</a>
</div>
</template>
<script lang="ts" setup>
import { StockOutlined, CloseCircleOutlined } from "@ant-design/icons-vue";
type Props = {
value: string;
disabled?: boolean;
unDelete?: boolean;
};
const props = withDefaults(defineProps<Props>(), {
value: "",
disabled: false,
unDelete: false,
});
const emit = defineEmits<{
(e: "click"): void;
(e: "clear"): void;
}>();
const handleClick = () => emit("click");
const handleClear = () => !props.unDelete && emit("clear");
</script>
<style lang="less" scoped>
.rebateProduct {
position: relative;
height: 36px;
line-height: 36px;
padding-right: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
a:hover {
color: #1677ff;
}
&.set {
padding-right: 42px;
}
.icons {
position: absolute;
// display: inline-block
font-size: 16px;
right: 0px;
top: 3px;
z-index: 10;
}
.clear {
color: red;
margin-left: 8px;
&.unDelete {
color: rgba(0, 0, 0, 0.25);
cursor: not-allowed;
}
}
}
</style>vue
<template>
<div class="rebateModal">
<q-basic-modal
:visible="modelValue"
:title="computedTitle"
:helpMessage="srmI18n('i18n_alert_ERJtyDum_cb99b3f6', '配置子级阶梯价格')"
:width="1200"
:bodyStyle="{ padding: '0 12px' }"
destroyOnClose
centered
@cancel="handleCancel"
@ok="handleOk"
>
<div class="form">
<a-form
layout="inline"
style="display: flex; flex-flow: row wrap; margin: 12px 4px"
>
<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
@change="loadData"
/>
</a-form-item>
<a-form-item style="margin-right: 12px">
<a-checkbox
v-model:checked="form.inverseSelection"
:disabled="disabled"
>{{ srmI18n("i18n_alert_vi_ab5dc", "反选") }}</a-checkbox
>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="loadData">{{
srmI18n("i18n_title_Query", "查询")
}}</a-button>
</a-form-item>
</a-form>
</div>
<div class="container">
<div class="grid_left">
<vxe-grid
ref="xGrid_left"
v-bind="gridConfig"
:pager-config="pagerConfig"
:loading="loading"
:columns="columns_left"
:data="tableData"
@page-change="handlePageChange"
v-on="gridEvents"
>
<template #empty>
<q-empty></q-empty>
</template>
</vxe-grid>
</div>
<div class="tools">
<span
class="right"
:class="['right', disabled ? 'disabled' : '']"
@click="toRight"
>
<i class="vxe-icon-arrow-right"></i>
</span>
</div>
<div
class="grid_right"
:class="{
'drag-over': isRightOver,
}"
>
<vxe-grid
ref="xGrid_right"
v-bind="gridConfig"
:loading="loading_right"
:columns="columns_right"
:data="tableData_right"
>
<template #empty>
<q-empty></q-empty>
</template>
</vxe-grid>
</div>
</div>
</q-basic-modal>
<!-- 阶梯报价 -->
<ladder-price-modal
v-model="ladderPriceVisible"
unControl
:value="ladderPriceValue"
:cachePayload="ladderPriceCachePayload"
@success="handleLadderPriceSuccess"
></ladder-price-modal>
</div>
</template>
<script lang="tsx" setup>
import { ref, toRefs, computed, watch, nextTick, onUnmounted } from "vue";
import { srmI18n } from "@/utils/srmI18n";
// ui组件库 types
import type { GlobalPageLayoutTypes } from "@qqt-product/ui";
import type {
VxeColumnSlotTypes,
VxeTableDataRow,
VxeGridInstance,
VxeGridProps,
VxeGridPropTypes,
VxePagerEvents,
VxeGridListeners,
} from "vxe-table";
import LadderPriceDisplay from "./ladderPriceDisplay.vue";
import LadderPriceModal from "./ladderPriceModal.vue";
import { editVxeTableConfig } from "@qqt-product/ui";
import Sortable from "sortablejs";
import qqtApi from "@qqt-product/api";
import qqtUtils from "@qqt-product/utils";
const { cloneDeep } = qqtUtils;
import { message as Message } from "ant-design-vue";
const Request: qqtApi.Request = qqtApi.useHttp();
const deleteDelay = 2000; // 超出区域删除延迟时间
const DOM_TBODY = ".body--wrapper>.vxe-table--body tbody";
interface Props {
modelValue: boolean;
payload: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow> | undefined;
config: GlobalPageLayoutTypes.ColumnItem;
disabled?: boolean;
}
interface Payload {
rebateExtends: string;
ids: string;
names: string;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
disabled: false,
});
const { modelValue, payload, config, disabled } = toRefs(props);
const emit = defineEmits<{
(e: "update:modelValue", modelValue: boolean): void;
(e: "success", payload: Payload): void;
}>();
const computedTitle = computed<string>(() => {
return `${config.value.title || ""}${srmI18n(
"i18n_title_popupChoose",
"弹窗选择"
)}`;
});
const xGrid_left = ref<VxeGridInstance>();
const xGrid_right = ref<VxeGridInstance>();
let sortable_left: Sortable | null = null;
let sortable_right: Sortable | null = null;
let initTime_left: any;
let initTime_right: any;
const isLeftOver = ref(false); // 监听左侧覆盖状态
const isRightOver = ref(false); // 监听右侧覆盖状态
const rawState = {
inverseSelection: false, // 反选
keyWord: "",
};
const ladderPriceVisible = ref<boolean>(false);
const ladderPriceValue = ref<string>("");
// 缓存行信息
const ladderPriceCachePayload =
ref<VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>>();
const form = ref<GlobalPageLayoutTypes.RecordString>(rawState);
const ids = ref<string>("");
const rebateLadder = ref<string>("");
const rebateProductExtends = ref<GlobalPageLayoutTypes.RecordString[]>([]);
const loading = ref<boolean>(false);
const loading_right = ref<boolean>(false);
const tableData = ref<GlobalPageLayoutTypes.RecordString[]>([]);
const tableData_right = ref<GlobalPageLayoutTypes.RecordString[]>([]);
const pagerConfig = ref({
total: 0,
currentPage: 1,
pageSize: 20,
});
const gridConfig: VxeGridProps = {
...editVxeTableConfig,
rowConfig: {
keyField: "id",
isHover: true,
isCurrent: true,
useKey: true,
},
checkboxConfig: {
highlight: true,
reserve: true,
// trigger: 'cell',
},
height: "auto",
keyboardConfig: {},
menuConfig: {},
mouseConfig: {}, // 鼠标配置项
areaConfig: {}, // 按键配置项
seqConfig: {
startIndex:
((pagerConfig.value.currentPage as number) - 1) *
(pagerConfig.value.pageSize as number),
},
};
const columns: VxeGridPropTypes.Columns = [
{ type: "seq", width: 50, fixed: "left" },
{
title: "",
field: "",
width: 40,
slots: {
default() {
return [
<span class="drag-btn" style="cursor: grab;">
<i class="vxe-icon--menu"></i>
</span>,
];
},
header() {
return [
<vxe-tooltip
content={srmI18n(
"i18n_title_pressHoldDragUpDown",
"按住后可以上下拖动排序!"
)}
enterable
>
<i class="vxe-icon--question"></i>
</vxe-tooltip>,
];
},
},
},
{ field: "materialNumber", title: "物料编码", width: 150 },
{ field: "materialName", title: "物料名称", width: 150 },
{ field: "materialSpec", title: "物料规格", width: 150 },
{ field: "cateLevelCode", title: "物料分类级别编码", width: 150 },
{ field: "cateName", title: "物料分类名称", width: 150 },
{ field: "materialDesc", title: "物料描述", width: 150 },
{ field: "MaterialGroup", title: "物料组", width: 150 },
{ field: "brand", title: "物料品牌", width: 150 },
];
const columns_left: VxeGridPropTypes.Columns = [
{ type: "checkbox", width: 50, fixed: "left" },
...columns,
];
let operationColumn: VxeGridPropTypes.Columns = [
{
field: "grid_operation",
fixed: "right",
align: "center",
title: "操作",
width: 80,
slots: {
default({
$rowIndex,
}: VxeColumnSlotTypes.DefaultSlotParams<VxeTableDataRow>) {
const handleDelete = () => {
tableData_right.value.splice($rowIndex, 1);
};
return [
<span style="color: #ff4d4f; font-size: 14px; cursor: pointer;">
<i class="vxe-icon-delete-fill" onClick={handleDelete}></i>
</span>,
];
},
},
},
];
let columns_right: VxeGridPropTypes.Columns = [
...columns,
{
field: "ladderPriceJson",
fixed: "right",
title: "阶梯价格",
width: 180,
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;
};
return [
<LadderPriceDisplay
value={val}
unDelete={true}
onClick={handleLadderPriceClick}
></LadderPriceDisplay>,
];
},
},
},
];
if (!disabled.value) {
columns_right = [...columns_right, ...operationColumn];
}
const resetData = () => {
pagerConfig.value = {
total: 0,
currentPage: 1,
pageSize: 20,
};
tableData.value = [];
tableData_right.value = [];
form.value = { ...rawState };
xGrid_left.value = undefined;
xGrid_right.value = undefined;
sortable_left = null;
sortable_right = null;
initTime_left = null;
initTime_right = null;
};
const destroySortable = () => {
clearTimeout(initTime_left);
clearTimeout(initTime_right);
sortable_left?.destroy();
sortable_right?.destroy();
};
watch(modelValue, (bool) => {
if (bool) {
getCacheData();
loadData();
loadOtherData();
nextTick(() => {
// 加载完成之后在绑定拖动事件
initTime_left = setTimeout(() => {
rowDrop_left();
}, 500);
initTime_right = setTimeout(() => {
rowDrop_right();
}, 500);
});
} else {
resetData();
destroySortable();
}
});
const handleCancel = () => {
emit("update:modelValue", false);
};
const handleOk = () => {
if (!tableData_right.value.length) {
Message.error(srmI18n(`i18n_title_selectDataMsg`, "请选择数据"));
return;
}
const jsonArr = tableData_right.value.map((item) => ({
id: item.id,
inverseSelection: form.value.inverseSelection,
childRebateLadder: JSON.parse(item.ladderPriceJson),
}));
const ids = tableData_right.value.map((item) => item.id).join(",");
const names = tableData_right.value
.map((item) => item.materialName)
.join(",");
const payload: Payload = {
rebateExtends: JSON.stringify(jsonArr),
ids,
names,
};
emit("success", payload);
handleCancel();
};
const getCacheData = () => {
if (!payload.value) {
return;
}
const { row } = payload.value;
ids.value = (row.rebateProduct as string) || "";
if (row.rebateLadder) {
try {
let jsonArr = JSON.parse(row.rebateLadder);
let props = ["fix", "rate", "union"];
// 重置子阶梯中的固定值、比例值、每单位返
jsonArr.forEach((item) => {
props.forEach((prop) => {
item[prop] = "";
});
});
rebateLadder.value = JSON.stringify(jsonArr);
} catch (err) {
console.log("parse rebateProductExtends error :>> ", err);
}
}
if (row.rebateProductExtends) {
try {
rebateProductExtends.value = JSON.parse(row.rebateProductExtends);
if (Array.isArray(rebateProductExtends.value)) {
form.value.inverseSelection =
rebateProductExtends.value[0].inverseSelection || false;
}
} catch (err) {
console.log("parse rebateProductExtends error :>> ", err);
}
}
};
const handleLadderPriceSuccess = (
arr: GlobalPageLayoutTypes.RecordString[]
) => {
if (ladderPriceCachePayload.value) {
const { row } = ladderPriceCachePayload.value;
row.ladderPriceJson = JSON.stringify(arr);
}
};
const loadData = () => {
const url = "/material/purchaseMaterialHead/queryPageListByCalculation";
const params = {
pageSize: pagerConfig.value.pageSize,
pageNo: pagerConfig.value.currentPage,
column: "id",
order: "asc",
frozenFunctionValue: "1",
keyWord: form.value.keyWord || "",
};
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 loadOtherData = () => {
if (!ids.value) {
return;
}
const url = "/material/purchaseMaterialHead/queryPageListByCalculation";
const params = {
frozenFunctionValue: "1",
ids: ids.value,
column: "id",
order: "asc",
};
loading_right.value = true;
Request.get({ url, params })
.then((res) => {
if (!res.success) {
Message.error(res.message);
return;
}
const records = res.result.records || [];
tableData_right.value = records.map((item) => {
let obj = rebateProductExtends.value.find((n) => n.id === item.id);
let json = obj
? JSON.stringify(obj.childRebateLadder)
: rebateLadder.value;
return {
...item,
ladderPriceJson: json,
};
});
})
.finally(() => {
loading_right.value = false;
});
};
const handlePageChange: VxePagerEvents.PageChange = ({
currentPage,
pageSize,
}) => {
pagerConfig.value.currentPage = currentPage;
pagerConfig.value.pageSize = pageSize;
loadData();
};
const getJSON = (row) => {
let obj = rebateProductExtends.value.find((n) => n.id === row.id);
return obj ? JSON.stringify(obj.childRebateLadder) : rebateLadder.value;
};
const copyRow = (row) => {
let copyRow = cloneDeep(row);
let json = getJSON(row);
tableData_right.value.push({ ...copyRow, ladderPriceJson: json });
};
const toRight = () => {
if (disabled.value) {
return;
}
if (xGrid_left.value) {
let records = xGrid_left.value.getCheckboxRecords() || [];
if (!records.length) {
Message.error(srmI18n(`i18n_title_pleaseSelectRowData`, "请选择行数据"));
return;
}
let ids = tableData_right.value.map((n) => n.id);
let filterRecords = records.filter((n) => !ids.includes(n.id));
if (filterRecords.length !== records.length) {
Message.warning(srmI18n(`i18n_alert_IRIVBtF_5c05f588`, "已过滤重复单据"));
}
filterRecords.forEach((item) => copyRow(item));
xGrid_left.value.clearCheckboxRow();
}
};
const gridEvents: VxeGridListeners<VxeTableDataRow> = {
// 单元格被双击时会触发该事件
cellDblclick({ row }) {
if (disabled.value) {
return;
}
const flag = tableData_right.value.some((n) => n.id === row.id);
if (flag) {
Message.warning(srmI18n(`i18n_alert_rtFIMK_7d4f8a24`, "该单据已存在"));
} else {
copyRow(row);
}
},
};
const rowDrop_left = () => {
const $table = xGrid_left.value;
if ($table) {
sortable_left = new Sortable($table.$el.querySelector(DOM_TBODY), {
group: {
name: "shared_left",
pull: "clone", // true 允许拖出, clone: 拖出时复制数据(原数据保留)
put: false, // 禁止接收
},
sort: false,
animation: 150,
handle: ".drag-btn",
ghostClass: "ghost", // 拖拽幽灵元素样式
chosenClass: "chosen", // 选中项样式
onStart: () => {
isRightOver.value = false; // 重置状态
},
onMove: (evt) => {
if (xGrid_right.value) {
isRightOver.value =
evt.to === xGrid_right.value.$el.querySelector(DOM_TBODY);
}
},
onEnd: () => {
isRightOver.value = false; // 重置状态
},
});
}
};
const rowDrop_right = () => {
const $table = xGrid_right.value;
if ($table) {
sortable_right = Sortable.create($table.$el.querySelector(DOM_TBODY), {
group: {
name: "shared_right",
pull: false,
put: true,
},
animation: 150,
ghostClass: "ghost",
chosenClass: "chosen",
onAdd: (evt) => {
evt.item.remove(); // 移除Sortable自动添加的DOM元素
if (disabled.value) {
return;
}
const newIndex = evt.newIndex as number;
const oldIndex = evt.oldIndex as number;
const row = tableData.value[oldIndex];
if (tableData_right.value.some((n) => n.id === row.id)) {
Message.warning(
srmI18n(`i18n_alert_rtFIMK_7d4f8a24`, "该单据已存在")
);
return;
}
let copyRow = cloneDeep(row);
let json = getJSON(copyRow);
let cloneData = cloneDeep(tableData_right.value);
cloneData.splice(newIndex, 0, { ...copyRow, ladderPriceJson: json });
tableData_right.value = [];
nextTick(() => {
tableData_right.value = cloneData;
});
},
onEnd: (evt) => {
const newIndex = evt.newIndex as number;
const oldIndex = evt.oldIndex as number;
let cloneData = cloneDeep(tableData_right.value);
const currRow = cloneData.splice(oldIndex, 1)[0];
cloneData.splice(newIndex, 0, currRow);
tableData_right.value = [];
nextTick(() => {
tableData_right.value = cloneData;
});
},
});
}
};
onUnmounted(() => {
destroySortable();
});
</script>
<style lang="less" scoped>
.container {
display: flex;
width: 100%;
gap: 16px;
height: calc(100vh - 300px);
.grid_left {
flex: 0 0 50%;
max-width: 50%;
background-color: #f8f9fa;
border: 1.5px dashed transparent;
}
.grid_right {
/* Make it scrollable */
overflow: auto;
/* Take the remaining width */
flex: 1;
background-color: #fff;
border: 1.5px dashed transparent;
}
/* 拖拽覆盖样式(仅右侧) */
.drag-over {
border-color: #409eff;
box-shadow: 15px 15px 30px #409eff, -15px -15px 30px #ffffff;
}
/* 拖拽选中样式 */
:deep(.chosen) {
background-color: #409eff;
color: white;
}
/* 拖拽幽灵元素样式 */
:deep(.ghost) {
opacity: 0.6;
transform: scale(1.02);
}
.tools {
display: flex;
align-items: center;
.right {
color: #5570f1;
font-size: 32px;
cursor: pointer;
&.disabled {
color: rgba(0, 0, 0, 0.25);
cursor: not-allowed;
}
}
}
}
</style>示例
