新增商品关联商品静态部分
This commit is contained in:
@@ -34,6 +34,7 @@ function returnPrice(skuList) {
|
||||
const emits = defineEmits(['success'])
|
||||
function selectItem(item) {
|
||||
emits('success', {
|
||||
id: item.id,
|
||||
coverImg: item.coverImg,
|
||||
name: item.name,
|
||||
price: returnPrice(item.skuList)
|
||||
|
||||
@@ -222,7 +222,6 @@
|
||||
<el-form-item label="库存数量">
|
||||
<el-input-number v-model="ruleForm.stockNumber" :min="0" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="打包费" prop="delivery">
|
||||
<div style="display: block;">
|
||||
<el-input-number v-model="ruleForm.packFee" controls-position="right"
|
||||
@@ -230,6 +229,41 @@
|
||||
<div style="color: #999;">单份商品打包费。注:店铺开启外卖模式下该数据才生效</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="关联推荐商品">
|
||||
<div class="column">
|
||||
<div class="row">
|
||||
<div class="center">
|
||||
<el-button type="primary" @click="selecProductDialogRef?.show()"
|
||||
:disabled="goodsList.length >= goodsListMax">添加商品</el-button>
|
||||
<div class="tips">设置商品后,用户可以在商品详情页中看到推荐商品可拖动调整顺序,最多设置{{ goodsListMax }}个商品</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div id="goods_table_drag">
|
||||
<el-table :data="goodsList" border stripe style="width: 500px;" row-key="id">
|
||||
<!-- 拖拽列 -->
|
||||
<el-table-column label="排序" width="60">
|
||||
<template v-slot="scope">
|
||||
<div class="drag-handle">☰</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品图片" prop="coverImg" width="90">
|
||||
<template v-slot="scope">
|
||||
<el-image :src="scope.row.coverImg" style="width: 50px;height: 50px;border-radius: 4px;"
|
||||
fit="cover"></el-image>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品名称" prop="name"></el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template v-slot="scope">
|
||||
<el-button link type="danger" @click="goodsList.splice(scope.$index, 1)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitForm(ruleFormRef)">确定</el-button>
|
||||
<el-button @click="resetForm(ruleFormRef)">取消</el-button>
|
||||
@@ -256,11 +290,13 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 选择商品弹窗 -->
|
||||
<selecProductDialog ref="selecProductDialogRef" @success="selecProductSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from "vue";
|
||||
import { reactive, ref, onMounted, nextTick } from "vue";
|
||||
import type { FormInstance, FormRules } from "element-plus";
|
||||
// 规格属性
|
||||
import SpecificationAttribute from "./SpecificationAttribute.vue";
|
||||
@@ -272,7 +308,65 @@ import UserAPI4 from "@/api/product/specificationsconfig";
|
||||
import shopList from "@/components/mycomponents/shopList.vue";
|
||||
import AddImg from "@/components/mycomponents/addImg.vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import Sortable from "sortablejs";
|
||||
import { useTagsViewStore } from "@/store";
|
||||
import selecProductDialog from "@/views/marketing_center/group_booking/components/selecProductDialog.vue";
|
||||
|
||||
const selecProductDialogRef = ref(null)
|
||||
const goodsListMax = ref(9)
|
||||
const goodsList = ref<any[]>([]);
|
||||
|
||||
onMounted(() => {
|
||||
// Sortable 需要在 el-table 渲染 tbody 后初始化,尝试多次以确保 DOM 可用
|
||||
const initSortable = async () => {
|
||||
await nextTick();
|
||||
let attempts = 0;
|
||||
const maxAttempts = 8;
|
||||
const tryInit = () => {
|
||||
const el = document.querySelector("#goods_table_drag .el-table__body-wrapper tbody");
|
||||
if (!el) {
|
||||
attempts++;
|
||||
if (attempts < maxAttempts) {
|
||||
setTimeout(tryInit, 80);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 已找到表体,初始化 Sortable
|
||||
new Sortable(el as HTMLElement, {
|
||||
animation: 150,
|
||||
handle: ".drag-handle",
|
||||
ghostClass: "sortable-ghost",
|
||||
chosenClass: "sortable-chosen",
|
||||
onEnd: (e) => {
|
||||
if (e.oldIndex == null || e.newIndex == null) return;
|
||||
if (e.oldIndex === e.newIndex) return;
|
||||
const from = e.oldIndex;
|
||||
const to = e.newIndex;
|
||||
const item = goodsList.value.splice(from, 1)[0];
|
||||
goodsList.value.splice(to, 0, item);
|
||||
// 触发响应式更新
|
||||
goodsList.value = goodsList.value.slice();
|
||||
console.log("排序后的数据", goodsList.value);
|
||||
},
|
||||
});
|
||||
};
|
||||
tryInit();
|
||||
};
|
||||
|
||||
initSortable();
|
||||
});
|
||||
|
||||
// 已选择的商品
|
||||
function selecProductSuccess(res: any) {
|
||||
let obj = goodsList.value.find((item) => item.id == res.id);
|
||||
if (obj && obj.id) {
|
||||
ElMessage.error("该商品已选择");
|
||||
return;
|
||||
}
|
||||
goodsList.value.push({ ...res });
|
||||
}
|
||||
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
const router = useRouter();
|
||||
let list = ref<any[]>([
|
||||
@@ -915,6 +1009,27 @@ const resetForm = (formEl: FormInstance | undefined) => {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: move;
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
/* SortableJS classes */
|
||||
.sortable-ghost {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.sortable-chosen {
|
||||
background: #f5f7ff !important;
|
||||
}
|
||||
|
||||
.el-table__body-wrapper tbody tr {
|
||||
transition: transform 150ms ease, background-color 150ms ease;
|
||||
}
|
||||
|
||||
.showStyle:hover>.buttonstyle {
|
||||
display: block;
|
||||
}
|
||||
@@ -942,4 +1057,26 @@ const resetForm = (formEl: FormInstance | undefined) => {
|
||||
top: -10px;
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
|
||||
.column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.row {
|
||||
flex: 1;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.tips {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -13,7 +13,7 @@
|
||||
</p>
|
||||
<p class="u-font-14 color-666 u-m-t-10">
|
||||
<span class="money">余额:{{ carts.vipUser.amount }}</span>
|
||||
<span class="score u-m-l-10">积分:{{ carts.vipUser.accountPoints }}</span>
|
||||
<span class="score u-m-l-10">积分:{{ carts.vipUser.pointBalance }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,7 +46,8 @@
|
||||
</el-radio-group>
|
||||
<el-input-number class="u-m-l-10" v-if="score.sel != -1" v-model="usePointsNumber" step-strictly
|
||||
:step="pointsRes.equivalentPoints" placeholder="请输入积分抵扣数量" :min="pointsRes.minDeductionPoints"
|
||||
:max="pointsRes.maxUsablePoints" @change="pointsToMoney"></el-input-number>
|
||||
:max="pointsRes.maxUsablePoints" :disabled="!pointsRes.usable"
|
||||
@change="pointsToMoney"></el-input-number>
|
||||
</div>
|
||||
<p class="u-font-14 color-666 u-m-t-10" v-if="pointsRes.unusableReason && !pointsRes.usable">
|
||||
<span class="color-red">*</span>
|
||||
@@ -398,8 +399,16 @@ watch(
|
||||
);
|
||||
|
||||
//002-获取订单可用积分及抵扣金额(支付页面使用)
|
||||
const pointsRes = ref({ usable: true, maxUsablePoints: 0, minDeductionPoints: 0 });
|
||||
const usePointsNumber = ref(0);
|
||||
const pointsRes = ref({
|
||||
usable: false,
|
||||
equivalentPoints: 0, // 积分换算比例 eg: 20 积分 = 1 元
|
||||
maxDeductionRatio: 0, // 下单最高抵扣比例(小数)
|
||||
minPaymentAmount: 0, // 下单实付抵扣门槛(元)
|
||||
maxUsablePoints: 0,
|
||||
minDeductionPoints: 0,
|
||||
unusableReason: "",
|
||||
});
|
||||
const usePointsNumber = ref(0); // 输入的积分数量(用户填写)
|
||||
//积分可抵扣最大金额
|
||||
const scoreMaxMoney = computed(() => {
|
||||
return (
|
||||
@@ -423,24 +432,127 @@ async function pointsInit() {
|
||||
}
|
||||
const { pointsConfig, pointsUser } = await PointsApi.calcOrderUsablePoints({
|
||||
shopUserId: carts.vipUser.id,
|
||||
// orderAmount: scoreMaxMoney.value,
|
||||
});
|
||||
|
||||
const res = pointsConfig
|
||||
carts.vipUser.accountPoints = pointsUser.id ? pointsUser.pointBalance : 0;
|
||||
// 不修改 accountPoints(为余额),将积分保存到 pointBalance 字段
|
||||
carts.vipUser.pointBalance = pointsUser && pointsUser.id ? pointsUser.pointBalance : 0;
|
||||
|
||||
// 保险取值
|
||||
const eq = pointsConfig?.equivalentPoints || 0;
|
||||
const maxRatio = pointsConfig?.maxDeductionRatio || 0;
|
||||
const minPay = pointsConfig?.minPaymentAmount || 0;
|
||||
|
||||
// 计算当前订单可抵扣金额上限(元)
|
||||
let finalPay = Number(carts.orderCostSummary.finalPayAmount) || 0;
|
||||
|
||||
const res = {
|
||||
usable: true,
|
||||
equivalentPoints: eq,
|
||||
maxDeductionRatio: maxRatio,
|
||||
minPaymentAmount: minPay,
|
||||
maxUsablePoints: 0,
|
||||
minDeductionPoints: pointsConfig?.minDeductionPoints || eq,
|
||||
unusableReason: "",
|
||||
};
|
||||
|
||||
// 如果订单实付低于最小使用门槛,则不可用
|
||||
if (finalPay <= 0 || (minPay > 0 && finalPay < minPay)) {
|
||||
res.usable = false;
|
||||
res.unusableReason = `订单实付金额低于 ${minPay} 元,无法使用积分抵扣`;
|
||||
} else if (eq <= 0) {
|
||||
res.usable = false;
|
||||
res.unusableReason = `积分换算比例配置错误,无法使用积分抵扣`;
|
||||
} else {
|
||||
// 计算基于比例限制的最大抵扣金额(元)
|
||||
let maxByRatio = finalPay * maxRatio;
|
||||
// 保证抵扣后剩余金额 >= minPaymentAmount
|
||||
if (minPay > 0) {
|
||||
const allowed = finalPay - minPay;
|
||||
if (allowed <= 0) {
|
||||
res.usable = false;
|
||||
res.unusableReason = `抵扣后实付金额必须大于等于 ${minPay} 元,当前不可使用积分`;
|
||||
} else {
|
||||
maxByRatio = Math.min(maxByRatio, allowed);
|
||||
}
|
||||
}
|
||||
|
||||
if (res.usable) {
|
||||
// 可用积分上限(向下取整为 eq 的倍数)
|
||||
const maxByMoney = Math.floor(maxByRatio * eq);
|
||||
const userPoints = carts.vipUser.pointBalance || 0;
|
||||
res.maxUsablePoints = Math.min(userPoints, maxByMoney);
|
||||
// 最小抵扣积分为配置值或等于换算比
|
||||
res.minDeductionPoints = pointsConfig?.minDeductionPoints || eq;
|
||||
if (res.maxUsablePoints < res.minDeductionPoints) {
|
||||
res.usable = false;
|
||||
res.unusableReason = `可用积分不足,至少需要 ${res.minDeductionPoints} 积分才可抵扣`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pointsRes.value = res;
|
||||
carts.pointDeductionRule.pointsPerYuan = res.equivalentPoints;
|
||||
|
||||
if (score.sel == -1) {
|
||||
// 未选择使用积分
|
||||
return res;
|
||||
}
|
||||
|
||||
// 如果可用则默认填充可用最大值,否则清零
|
||||
usePointsNumber.value = res.usable ? res.maxUsablePoints : 0;
|
||||
if (!res.usable) score.sel = -1;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// 将输入的积分数转换为抵扣金额并写回 carts.orderCostSummary
|
||||
function pointsToMoney(val) {
|
||||
const cfg = pointsRes.value;
|
||||
if (!cfg.usable || cfg.equivalentPoints <= 0) {
|
||||
usePointsNumber.value = 0;
|
||||
carts.orderCostSummary.pointUsed = 0;
|
||||
carts.orderCostSummary.pointDeductionAmount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
usePointsNumber.value = res.usable ? res.maxUsablePoints : 0;
|
||||
if (res.usable) {
|
||||
} else {
|
||||
score.sel = -1;
|
||||
// 确保为整数积分
|
||||
let pts = parseInt(usePointsNumber.value || 0, 10);
|
||||
if (isNaN(pts) || pts <= 0) {
|
||||
pts = 0;
|
||||
}
|
||||
return res;
|
||||
|
||||
// 限制最大值
|
||||
if (cfg.maxUsablePoints && pts > cfg.maxUsablePoints) {
|
||||
pts = cfg.maxUsablePoints;
|
||||
}
|
||||
|
||||
// 计算抵扣金额(元),向下保留两位
|
||||
const money = new BigNumber(pts).div(cfg.equivalentPoints).decimalPlaces(2, BigNumber.ROUND_DOWN).toNumber();
|
||||
|
||||
// 再次校验不超过允许的最大抵扣金额(基于比例或门槛)
|
||||
let finalPay = Number(carts.orderCostSummary.finalPayAmount) || 0;
|
||||
let maxByRatio = finalPay * cfg.maxDeductionRatio;
|
||||
if (cfg.minPaymentAmount > 0) {
|
||||
const allowed = finalPay - cfg.minPaymentAmount;
|
||||
if (allowed <= 0) {
|
||||
usePointsNumber.value = 0;
|
||||
carts.orderCostSummary.pointUsed = 0;
|
||||
carts.orderCostSummary.pointDeductionAmount = 0;
|
||||
return;
|
||||
}
|
||||
maxByRatio = Math.min(maxByRatio, allowed);
|
||||
}
|
||||
const maxAllowedMoney = new BigNumber(maxByRatio).decimalPlaces(2, BigNumber.ROUND_DOWN).toNumber();
|
||||
if (money > maxAllowedMoney) {
|
||||
// 调整积分到允许的最大金额对应的积分
|
||||
const allowedPts = Math.floor(maxAllowedMoney * cfg.equivalentPoints);
|
||||
pts = Math.min(allowedPts, cfg.maxUsablePoints);
|
||||
}
|
||||
|
||||
usePointsNumber.value = pts;
|
||||
const finalMoney = new BigNumber(pts).div(cfg.equivalentPoints).decimalPlaces(2, BigNumber.ROUND_DOWN).toNumber();
|
||||
carts.orderCostSummary.pointUsed = pts;
|
||||
carts.orderCostSummary.pointDeductionAmount = finalMoney;
|
||||
}
|
||||
|
||||
const emits = defineEmits(["chooseUser", "paysuccess"]);
|
||||
|
||||
Reference in New Issue
Block a user