新增商品关联商品静态部分

This commit is contained in:
gyq
2025-12-24 16:13:43 +08:00
parent 1d7ba844f6
commit bf35a1b35b
3 changed files with 265 additions and 15 deletions

View File

@@ -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)

View File

@@ -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>

View File

@@ -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"]);