| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683 |
- <template>
- <div class="table-box button-table friend-relation-container">
- <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam" :data-callback="dataCallback">
- <!-- 表格 header 按钮 -->
- <template #tableHeader="scope">
- <div class="header-button">
- <el-button type="primary" @click="openAddDialog"> 添加活动 </el-button>
- </div>
- </template>
- <!-- 状态列 -->
- <template #status="scope">
- <el-tag :type="getStatusType(scope.row.status)">
- {{ getStatusText(scope.row.status) }}
- </el-tag>
- </template>
- <!-- 表格操作 -->
- <template #operation="scope">
- <el-button link type="primary" @click="editRow(scope.row)"> 编辑 </el-button>
- <el-button link type="primary" @click="deleteRow(scope.row)"> 删除 </el-button>
- </template>
- </ProTable>
- <!-- 添加/编辑活动对话框 -->
- <el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px" @close="closeDialog">
- <el-form ref="formRef" :model="formData" :rules="formRules" label-width="140px">
- <el-form-item label="活动名称" prop="acName">
- <el-input v-model="formData.acName" placeholder="请输入" clearable />
- </el-form-item>
- <el-form-item label="赠送类型">
- <el-radio-group v-model="formData.distributeType" @change="onDistributeTypeChange">
- <el-radio :label="1"> 满减券 </el-radio>
- <el-radio :label="2"> 折扣券 </el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="消费门槛金额(¥)">
- <div style="display: flex; gap: 10px; align-items: center; width: 100%">
- <el-form-item prop="moneyLow" style="flex: 1; margin-bottom: 0">
- <el-input
- v-model="formData.moneyLow"
- type="number"
- :min="0"
- step="0.01"
- placeholder="0.00"
- clearable
- @input="handleMoneyInput('moneyLow')"
- >
- <template #prefix> ¥ </template>
- </el-input>
- </el-form-item>
- <span>~</span>
- <el-form-item prop="moneyHigh" style="flex: 1; margin-bottom: 0">
- <el-input
- v-model="formData.moneyHigh"
- type="number"
- :min="0"
- step="0.01"
- placeholder="0.00"
- clearable
- @input="handleMoneyInput('moneyHigh')"
- >
- <template #prefix> ¥ </template>
- </el-input>
- </el-form-item>
- </div>
- </el-form-item>
- <el-form-item label="来源商家及优惠券">
- <div class="merchant-coupon-list">
- <div v-for="(item, index) in formData.coupons" :key="index" class="merchant-coupon-item">
- <el-select
- v-model="item.merchant"
- placeholder="选择商家"
- style="width: 200px; margin-right: 10px"
- filterable
- @change="handleMerchantChange(index)"
- >
- <el-option
- v-for="(m, idx) in effectiveMerchantOptions"
- :key="m.label ? `${String(m.value)}_${m.label}_${idx}` : String(m.value)"
- :label="m.label"
- :value="m.value"
- />
- </el-select>
- <el-select
- v-model="item.coupon"
- :placeholder="formData.distributeType === 2 ? '选择折扣券' : '选择满减券'"
- style="width: 200px; margin-right: 10px"
- filterable
- @change="handleCouponChange(index)"
- >
- <el-option
- v-for="c in getCouponOptions(formData.distributeType)"
- :key="c.value"
- :label="c.label"
- :value="c.value"
- />
- </el-select>
- <span v-if="item.coupon" class="remaining-text">剩余{{ item.remaining }}张</span>
- <el-button type="danger" link @click="removeMerchantCoupon(index)" v-if="formData.coupons.length > 1">
- 删除
- </el-button>
- </div>
- <el-button type="primary" link @click="addMerchantCoupon" style="margin-top: 10px">
- <el-icon><Plus /></el-icon>
- 添加商家优惠券
- </el-button>
- </div>
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="dialog-footer">
- <el-button @click="closeDialog"> 取消 </el-button>
- <el-button type="primary" @click="handleSubmit"> 确定 </el-button>
- </div>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup lang="tsx" name="friendRelation">
- import { computed, onMounted, reactive, ref } from "vue";
- import type { FormInstance, FormRules } from "element-plus";
- import { ElMessage, ElMessageBox } from "element-plus";
- import { Plus } from "@element-plus/icons-vue";
- import ProTable from "@/components/ProTable/index.vue";
- import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
- import { localGet } from "@/utils";
- import {
- getRuleList,
- saveFriendCouponRule,
- getMutualAttention,
- delFriendCouponRule,
- getRuleById,
- getVoucherList,
- getIssueCouponList
- } from "@/api/modules/newLoginApi";
- // ProTable 实例
- const proTable = ref<ProTableInstance>();
- // 对话框相关
- const dialogVisible = ref(false);
- const dialogTitle = computed(() => (isEdit.value ? "编辑活动" : "添加活动"));
- const isEdit = ref(false);
- const currentEditId = ref("");
- // 表单引用
- const formRef = ref<FormInstance>();
- // 商家列表(互相关注接口,仅显示 phoneId 带 store 的商家)
- const merchantList = ref<Array<{ value: string | number; label: string; raw?: any }>>([]);
- const merchantListRaw = ref<any[]>([]);
- // 优惠券/折扣券选项缓存 key: '1-couponList' | '2-couponList'
- const couponOptionsMap = ref<Record<string, Array<{ value: string; label: string }>>>({});
- const couponDataMap = ref<Record<string, any[]>>({});
- const normalizeCouponValue = (v: any) => (v != null && v !== "" ? String(v) : "");
- // 编辑时合并详情中的 storeName,使商家下拉能正确显示;按 label 去重,避免同一商家出现多次
- const effectiveMerchantOptions = computed(() => {
- const base = merchantList.value || [];
- const fromDetail = (formData.coupons || [])
- .filter((c: any) => c.merchant != null && c.merchant !== "" && c.storeName)
- .map((c: any) => ({ value: c.merchant, label: c.storeName }));
- const seenValue = new Set(base.map((o: any) => o.value));
- const extra = fromDetail.filter((o: any) => {
- if (seenValue.has(o.value)) return false;
- seenValue.add(o.value);
- return true;
- });
- const merged = [...base, ...extra];
- // 按 label(店名)去重:同一店名只保留一项,优先保留在 form 中已选中的 value,避免下拉重复显示
- const usedValues = new Set((formData.coupons || []).map((c: any) => c.merchant).filter((v: any) => v != null && v !== ""));
- const byLabel = new Map<string, { value: string | number; label: string }>();
- for (const o of merged) {
- const label = o.label || "";
- const existing = byLabel.get(label);
- if (!existing) {
- byLabel.set(label, o);
- } else if (usedValues.has(o.value) && !usedValues.has(existing.value)) {
- byLabel.set(label, o);
- }
- }
- return Array.from(byLabel.values());
- });
- const getCouponOptions = (distributeType: number) => {
- const key = distributeType === 2 ? "2-couponList" : "1-couponList";
- return couponOptionsMap.value[key] || [];
- };
- // 表单数据
- const formData = reactive({
- acName: "",
- distributeType: 1 as 1 | 2, // 1-满减券 2-折扣券
- moneyLow: "" as string | number,
- moneyHigh: "" as string | number,
- coupons: [] as Array<{
- merchant: string | number | null;
- coupon: string | null;
- remaining: number;
- storeName?: string;
- }>
- });
- // 金额验证器
- const validateMoneyLow = (rule: any, value: any, callback: any) => {
- if (value === "" || value === null || value === undefined) {
- callback(new Error("请输入最低消费金额"));
- return;
- }
- const numValue = Number(value);
- if (isNaN(numValue)) {
- callback(new Error("请输入有效的金额"));
- return;
- }
- if (numValue < 0) {
- callback(new Error("金额不能为负数"));
- return;
- }
- // 验证小数位数不超过2位
- const decimalPart = String(value).split(".")[1];
- if (decimalPart && decimalPart.length > 2) {
- callback(new Error("最多支持2位小数"));
- return;
- }
- // 如果最高金额已填写,验证最低金额不能大于最高金额
- if (formData.moneyHigh !== "" && formData.moneyHigh !== null && formData.moneyHigh !== undefined) {
- const highValue = Number(formData.moneyHigh);
- if (!isNaN(highValue) && numValue > highValue) {
- callback(new Error("最低金额不能大于最高金额"));
- return;
- }
- }
- callback();
- };
- const validateMoneyHigh = (rule: any, value: any, callback: any) => {
- if (value === "" || value === null || value === undefined) {
- callback(new Error("请输入最高消费金额"));
- return;
- }
- const numValue = Number(value);
- if (isNaN(numValue)) {
- callback(new Error("请输入有效的金额"));
- return;
- }
- if (numValue < 0) {
- callback(new Error("金额不能为负数"));
- return;
- }
- // 验证小数位数不超过2位
- const decimalPart = String(value).split(".")[1];
- if (decimalPart && decimalPart.length > 2) {
- callback(new Error("最多支持2位小数"));
- return;
- }
- // 如果最低金额已填写,验证最高金额不能小于最低金额
- if (formData.moneyLow !== "" && formData.moneyLow !== null && formData.moneyLow !== undefined) {
- const lowValue = Number(formData.moneyLow);
- if (!isNaN(lowValue) && numValue < lowValue) {
- callback(new Error("最高金额不能小于最低金额"));
- return;
- }
- }
- callback();
- };
- // 表单验证规则
- const formRules = reactive<FormRules>({
- acName: [{ required: true, message: "请输入活动名称", trigger: "blur" }],
- moneyLow: [{ required: true, validator: validateMoneyLow, trigger: "blur" }],
- moneyHigh: [{ required: true, validator: validateMoneyHigh, trigger: "blur" }]
- });
- // 表格列配置
- const columns = reactive<ColumnProps<any>[]>([
- {
- prop: "acName",
- label: "活动名称",
- search: { el: "input", props: { placeholder: "请输入活动名称" } }
- },
- {
- prop: "endDate",
- label: "有效期至"
- },
- {
- prop: "relationType",
- label: "消费门槛",
- render: (scope: any) => {
- return scope.row.moneyLow + "元" + "~" + scope.row.moneyHigh + "元";
- }
- },
- {
- prop: "status",
- label: "状态",
- enum: [
- { label: "启用", value: 0 },
- { label: "禁用", value: 1 }
- ],
- search: { el: "select", props: { placeholder: "请选择状态" } },
- fieldNames: { label: "label", value: "value" },
- render: (scope: any) => {
- return scope.row.status == 0 ? "启用" : "禁用";
- }
- },
- { prop: "operation", label: "操作", fixed: "right", width: 250 }
- ]);
- // 初始化请求参数
- const initParam = reactive({
- storeId: localGet("createdId") || ""
- });
- // 数据回调处理:兼容接口返回数组或 { records/list, total } 格式,确保有数据时分页显示正确总数(iOS 上 total 需为明确数字)
- const dataCallback = (data: any) => {
- const list = Array.isArray(data) ? data : (data?.records ?? data?.list ?? []);
- const totalNum =
- typeof data?.total === "number" && Number.isFinite(data.total) ? data.total : Array.isArray(list) ? list.length : 0;
- const total = Math.max(0, Math.floor(Number(totalNum)) || 0);
- return {
- list,
- total
- };
- };
- // 获取表格列表
- const getTableList = (params: any) => {
- return getRuleList(params);
- };
- // 获取状态文本
- const getStatusText = (status: number) => {
- const statusMap: Record<number, string> = {
- 0: "待同意",
- 1: "已同意",
- 2: "已拒绝"
- };
- return statusMap[status] || "--";
- };
- // 获取状态类型
- const getStatusType = (status: number): "success" | "warning" | "info" | "danger" => {
- const typeMap: Record<number, "success" | "warning" | "info" | "danger"> = {
- 0: "warning",
- 1: "success",
- 2: "info"
- };
- return typeMap[status] || "info";
- };
- // 获取商家列表(互相关注接口,仅显示 phoneId 带 store 的商家)- 与 group_merchant eachOtherInterest 参数一致
- const loadAddActivityMerchants = async () => {
- const phone = localGet("iphone") || localGet("geeker-user")?.userInfo?.phone || "";
- try {
- const res: any = await getMutualAttention({
- page: 1,
- size: 1000,
- fansId: `store_${phone}`
- });
- const data = res?.data || res;
- const records = data?.records || data?.list || (Array.isArray(data) ? data : []);
- const list = Array.isArray(records) ? records : [];
- const merchantListFiltered = list.filter((item: any) => {
- const pid = String(item.phoneId ?? item.id ?? "");
- return pid.includes("store");
- });
- merchantListRaw.value = merchantListFiltered;
- // 按 value(id/storeId/phoneId)去重,避免同一商家在接口中多次返回导致下拉重复
- const seen = new Set<string | number>();
- merchantList.value = merchantListFiltered
- .map((item: any) => ({
- value: item.id ?? item.storeId ?? item.phoneId,
- label: item.storeName ?? item.name ?? "",
- raw: item
- }))
- .filter((o: any) => {
- if (seen.has(o.value)) return false;
- seen.add(o.value);
- return true;
- });
- } catch (error) {
- console.error("获取商家列表失败", error);
- merchantList.value = [];
- merchantListRaw.value = [];
- }
- };
- // 打开添加对话框
- const openAddDialog = async () => {
- isEdit.value = false;
- currentEditId.value = "";
- await loadAddActivityMerchants();
- couponOptionsMap.value = {};
- couponDataMap.value = {};
- formData.distributeType = 1;
- formData.coupons = [{ merchant: null, coupon: null, remaining: 0 }];
- formData.acName = "";
- formData.moneyLow = "";
- formData.moneyHigh = "";
- dialogVisible.value = true;
- };
- // 处理金额输入
- const handleMoneyInput = (field: "moneyLow" | "moneyHigh") => {
- // 当一个金额字段改变时,触发另一个字段的验证
- if (formRef.value) {
- const otherField = field === "moneyLow" ? "moneyHigh" : "moneyLow";
- // 如果另一个字段有值,触发其验证
- if (formData[otherField] !== "" && formData[otherField] !== null && formData[otherField] !== undefined) {
- formRef.value.validateField(otherField, () => {});
- }
- }
- };
- // 关闭对话框
- const closeDialog = () => {
- dialogVisible.value = false;
- formRef.value?.resetFields();
- Object.assign(formData, {
- acName: "",
- distributeType: 1,
- moneyLow: "",
- moneyHigh: "",
- coupons: []
- });
- currentEditId.value = "";
- };
- // 添加商家优惠券
- const addMerchantCoupon = () => {
- formData.coupons.push({
- merchant: null,
- coupon: null,
- remaining: 0
- });
- };
- // 移除商家优惠券
- const removeMerchantCoupon = (index: number) => {
- formData.coupons.splice(index, 1);
- };
- // 赠送类型切换:清空选项缓存并重置行内选择(与 group_merchant setDistributeType 一致)
- const onDistributeTypeChange = () => {
- couponOptionsMap.value = {};
- couponDataMap.value = {};
- formData.coupons = formData.coupons.map(() => ({
- merchant: null,
- coupon: null,
- remaining: 0
- }));
- if (formData.coupons.length !== 1) formData.coupons = [{ merchant: null, coupon: null, remaining: 0 }];
- loadAddActivityMerchants();
- };
- // 加载当前类型下的优惠券/折扣券选项
- const loadCouponOptionsByType = async (distributeType: number) => {
- const key = distributeType === 2 ? "2-couponList" : "1-couponList";
- if (couponOptionsMap.value[key]?.length) return;
- const storeId = localGet("createdId") || "";
- try {
- const res: any = await getIssueCouponList({
- storeId,
- tab: 1,
- size: 100,
- page: 1,
- couponName: "",
- couponsFromType: 1,
- couponType: formData.distributeType
- });
- const records = res?.data?.records ?? res?.data ?? [];
- const list = Array.isArray(records) ? records : [];
- couponDataMap.value[key] = list;
- couponOptionsMap.value[key] = list.map((item: any) => ({
- value: String(item.id ?? item.couponId ?? ""),
- label: item.couponName ?? item.name ?? ""
- }));
- } catch (e) {
- couponDataMap.value[key] = [];
- couponOptionsMap.value[key] = [];
- }
- };
- // 商家选择改变
- const handleMerchantChange = async (index: number) => {
- const item = formData.coupons[index];
- item.coupon = null;
- item.remaining = 0;
- if (item.merchant) {
- const raw = merchantListRaw.value.find(
- (m: any) => (m.id ?? m.friendStoreUserId) === item.merchant || (m.storeId ?? m.phoneId) === item.merchant
- );
- if (raw) item.remaining = raw.singleQty ?? raw.couponNum ?? 0;
- await loadCouponOptionsByType(formData.distributeType);
- }
- };
- // 优惠券/折扣券选择改变
- const handleCouponChange = (index: number) => {
- const item = formData.coupons[index];
- const key = formData.distributeType === 2 ? "2-couponList" : "1-couponList";
- const list = couponDataMap.value[key] || [];
- const idKey = formData.distributeType === 2 ? "voucherId" : "couponId";
- if (item.coupon && list.length) {
- const found = list.find((c: any) => String(c[idKey] ?? c.couponId ?? c.id ?? "") === String(item.coupon));
- if (found) item.remaining = found.singleQty ?? found.couponNum ?? 0;
- }
- };
- // 编辑行数据(与 group_merchant editActivity 逻辑及 getRuleById 回显一致)
- const editRow = async (row: any) => {
- isEdit.value = true;
- currentEditId.value = row.id;
- const id = row.id;
- if (!id) return;
- await loadAddActivityMerchants();
- const type = row.type === 2 ? 2 : 1;
- formData.distributeType = type;
- couponOptionsMap.value = {};
- couponDataMap.value = {};
- try {
- const res: any = await getRuleById({ id });
- if (res.code != 200 || !res.data) {
- ElMessage.error(res.msg || "获取活动详情失败");
- return;
- }
- const d = res.data;
- formData.acName = d.acName;
- formData.moneyLow = d.moneyLow;
- formData.moneyHigh = d.moneyHigh;
- formData.distributeType = d.couponType;
- formData.coupons = (d.lifeDiscountCouponFriendRuleDetailVos || []).map((vo: any) => ({
- merchant: vo.friendStoreUserId ?? null,
- coupon: normalizeCouponValue(vo.couponId ?? vo.voucherId),
- remaining: vo.singleQty ?? vo.couponNum ?? 0,
- storeName: vo.storeName ?? ""
- }));
- if (formData.coupons.length === 0) {
- formData.coupons = [{ merchant: null, coupon: null, remaining: 0 }];
- }
- await loadCouponOptionsByType(formData.distributeType);
- // 编辑回显:从已加载的券列表中按券 id 同步剩余张数,避免详情接口未返回 singleQty/couponNum 时一直显示 0
- const key = formData.distributeType === 2 ? "2-couponList" : "1-couponList";
- const list = couponDataMap.value[key] || [];
- const idKey = formData.distributeType === 2 ? "voucherId" : "couponId";
- formData.coupons.forEach((row: any) => {
- if (!row.coupon) return;
- const found = list.find((c: any) => String(c[idKey] ?? c.couponId ?? c.id ?? "") === String(row.coupon));
- if (found) row.remaining = found.singleQty ?? found.couponNum ?? 0;
- });
- dialogVisible.value = true;
- } catch (error: any) {
- ElMessage.error(error?.msg || "获取活动详情失败");
- }
- };
- // 删除行数据
- const deleteRow = (row: any) => {
- ElMessageBox.confirm("确定要删除这个活动吗?", "提示", {
- confirmButtonText: "确定",
- cancelButtonText: "取消",
- type: "warning"
- })
- .then(async () => {
- try {
- const res: any = await delFriendCouponRule({ id: row.id });
- if (res.code == 200) {
- ElMessage.success("删除成功");
- proTable.value?.getTableList();
- } else {
- ElMessage.error(res.msg || "删除失败");
- }
- } catch (error: any) {
- ElMessage.error(error?.msg || "删除失败");
- }
- })
- .catch(() => {
- // 用户取消删除
- });
- };
- // 提交表单(与 group_merchant saveActivity 请求参数一致)
- const handleSubmit = async () => {
- if (!formRef.value) return;
- await formRef.value.validate(async (valid: boolean) => {
- if (!valid) return;
- if (!formData.coupons.length) {
- ElMessage.warning("请至少添加一个商家优惠券");
- return;
- }
- const hasEmpty = formData.coupons.some((item: any) => !item.merchant || !item.coupon);
- if (hasEmpty) {
- ElMessage.warning("请完善所有商家优惠券信息");
- return;
- }
- const isVoucher = formData.distributeType === 2;
- const details = formData.coupons.map((item: any) =>
- isVoucher
- ? { voucherId: item.coupon, friendStoreUserId: item.merchant }
- : { couponId: item.coupon, friendStoreUserId: item.merchant }
- );
- const requestData: any = {
- storeId: localGet("createdId") || "",
- acName: formData.acName.trim(),
- couponType: formData.distributeType,
- moneyHigh: Number(formData.moneyHigh),
- moneyLow: Number(formData.moneyLow),
- details
- };
- if (isEdit.value && currentEditId.value) {
- requestData.id = currentEditId.value;
- }
- try {
- const res: any = await saveFriendCouponRule(requestData);
- if (res.code == 200) {
- ElMessage.success(isEdit.value ? "编辑成功" : "添加成功");
- closeDialog();
- proTable.value?.getTableList();
- } else {
- ElMessage.error(res.msg || "操作失败");
- }
- } catch (error: any) {
- ElMessage.error(error?.msg || (isEdit.value ? "编辑失败" : "添加失败"));
- }
- });
- };
- // 页面加载时触发查询
- onMounted(() => {
- proTable.value?.getTableList();
- });
- </script>
- <style lang="scss" scoped>
- .friend-relation-container {
- .header-button {
- margin-bottom: 16px;
- }
- .merchant-coupon-list {
- width: 100%;
- .merchant-coupon-item {
- display: flex;
- align-items: center;
- margin-bottom: 10px;
- }
- .remaining-text {
- margin-right: 10px;
- font-size: 13px;
- color: var(--el-text-color-regular);
- }
- }
- .dialog-footer {
- display: flex;
- gap: 10px;
- justify-content: flex-end;
- }
- }
- </style>
|