lxr 2 месяцев назад
Родитель
Сommit
8bc9c8529e

+ 2 - 2
src/views/dynamicManagement/friendCoupon.vue

@@ -11,9 +11,9 @@
       <!-- 表格 header 按钮 -->
       <template #tableHeader="scope">
         <div class="table-header-content">
-          <div class="header-button">
+          <!-- <div class="header-button">
             <el-button type="primary" @click="openGiftDialog"> 赠送好友优惠券 </el-button>
-          </div>
+          </div> -->
           <el-tabs v-model="activeName" class="header-tabs" @tab-click="handleTabClick">
             <el-tab-pane label="好友赠我" name="friendMessage" />
             <el-tab-pane label="我赠好友" name="myGift" />

+ 357 - 2
src/views/dynamicManagement/userDynamic.vue

@@ -37,6 +37,7 @@
             </el-button>
             <template #dropdown>
               <el-dropdown-menu>
+                <el-dropdown-item v-if="isFriend && isStoreUser" command="gift"> 赠券 </el-dropdown-item>
                 <el-dropdown-item command="report"> 举报 </el-dropdown-item>
                 <el-dropdown-item command="block"> 拉黑 </el-dropdown-item>
               </el-dropdown-menu>
@@ -501,6 +502,69 @@
         </div>
       </template>
     </el-dialog>
+
+    <!-- 赠送好友优惠券弹窗(按设计:赠送类型 + 多行优惠券 + 添加商家优惠券) -->
+    <el-dialog
+      v-model="giftCouponDialogVisible"
+      title="赠送好友优惠券"
+      width="600px"
+      destroy-on-close
+      @close="closeGiftCouponDialog"
+    >
+      <div class="gift-coupon-dialog-body">
+        <div class="gift-type-section">
+          <div class="section-label">请选择赠送类型</div>
+          <el-radio-group v-model="giftCouponFormData.giftType" @change="loadGiftCouponList(giftCouponFormData.giftType)">
+            <el-radio label="coupon"> 优惠券 </el-radio>
+            <el-radio label="voucher"> 代金券 </el-radio>
+          </el-radio-group>
+        </div>
+        <div class="coupon-rows-section">
+          <div class="section-label">选择优惠券</div>
+          <div v-for="(row, index) in giftCouponFormData.rows" :key="row.key" class="coupon-row">
+            <el-select
+              v-model="row.couponId"
+              placeholder="选择优惠券"
+              style="flex: 1; min-width: 0"
+              clearable
+              @focus="loadGiftCouponList(giftCouponFormData.giftType)"
+              @change="onGiftRowCouponChange(row)"
+            >
+              <el-option
+                v-for="coupon in giftCouponList"
+                :key="coupon.id"
+                :label="coupon.name"
+                :value="coupon.id"
+                :disabled="isGiftCouponSelectedInOtherRow(coupon.id, index)"
+              />
+            </el-select>
+            <div class="quantity-cell">
+              <el-input
+                type="number"
+                :model-value="row.quantity"
+                placeholder="请输入赠券数量"
+                class="quantity-input"
+                :min="1"
+                :max="getGiftRowMaxQuantity(row)"
+                @input="(val: string | number) => setGiftRowQuantity(row, val)"
+              />
+              <span v-if="row.couponId" class="quantity-limit-hint"> 最多可赠 {{ getGiftRowMaxQuantity(row) }} 张 </span>
+            </div>
+            <el-button type="danger" link :icon="Delete" circle title="删除" @click="removeGiftCouponRow(index)" />
+          </div>
+          <el-button type="primary" link class="add-coupon-btn" @click="addGiftCouponRow">
+            <el-icon><Plus /></el-icon>
+            添加商家优惠券
+          </el-button>
+        </div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="closeGiftCouponDialog"> 取消 </el-button>
+          <el-button type="primary" :disabled="!canSubmitGiftCoupon" @click="handleGiftCouponSubmit"> 确定 </el-button>
+        </div>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
@@ -516,6 +580,7 @@ import {
   Share,
   MoreFilled,
   Plus,
+  Delete,
   Warning,
   CircleClose,
   ChatDotRound,
@@ -528,10 +593,21 @@ import {
   likeDynamicNew,
   unlikeDynamicNew
 } from "@/api/modules/dynamicManagement";
-import { getUserDynamicsList, cancelFollewed, toggleFollowUser } from "@/api/modules/newLoginApi";
+import {
+  getUserDynamicsList,
+  cancelFollewed,
+  toggleFollowUser,
+  getMutualAttention,
+  addTransferCount,
+  getCouponList,
+  setFriendCoupon,
+  saveComment,
+  commentList
+} from "@/api/modules/newLoginApi";
 import { uploadImg } from "@/api/modules/upload";
 import { useUserStore } from "@/stores/modules/user";
-import { saveComment, commentList, getMutualAttention, addTransferCount } from "@/api/modules/newLoginApi";
+import { localGet } from "@/utils";
+import { s } from "node_modules/vite/dist/node/types.d-aGj9QkWt";
 
 const route = useRoute();
 const router = useRouter();
@@ -575,6 +651,7 @@ interface ShareFriend {
 const activeTab = ref("dynamic");
 const contentList = ref<ContentItem[]>([]);
 const isFollowed = ref(false); // 是否已关注
+const isFriend = ref(false); // 是否互为好友(互相关注)
 
 // 详情 Drawer 相关
 const detailDrawerVisible = ref(false);
@@ -644,6 +721,9 @@ const userInfo = reactive({
   likeCount: 0 // 获赞数
 });
 
+// 当前主页用户是否为商家(phoneId 以 store_ 开头)
+const isStoreUser = computed(() => !!targetPhoneId.value && targetPhoneId.value.startsWith("store_"));
+
 // 判断是否是当前用户自己的主页
 const isMyPage = computed(() => {
   const currentUserStoreId = userStore.userInfo?.storeId;
@@ -1084,6 +1164,9 @@ const handleFollowInDetail = async () => {
 // 更多操作菜单
 const handleCommand = (command: string) => {
   switch (command) {
+    case "gift":
+      handleGiftCoupon();
+      break;
     case "report":
       handleReportUser();
       break;
@@ -1093,6 +1176,214 @@ const handleCommand = (command: string) => {
   }
 };
 
+// 赠券弹窗相关(按设计:赠送类型 + 多行优惠券)
+const giftCouponDialogVisible = ref(false);
+const giftFriendList = ref<{ id: number | string; name: string; phoneId?: string }[]>([]);
+const giftFriendListLoading = ref(false);
+const giftCouponList = ref<{ id: number | string; name: string; singleQty?: number }[]>([]);
+const giftCouponListLoaded = ref(false);
+let giftCouponRowKey = 0;
+const giftCouponFormData = reactive<{
+  friendId: string | number;
+  giftType: "coupon" | "voucher";
+  rows: { key: number; couponId: string | number; quantity: number }[];
+}>({
+  friendId: "",
+  giftType: "coupon",
+  rows: [{ key: ++giftCouponRowKey, couponId: "", quantity: 1 }]
+});
+
+const loadGiftFriendList = async () => {
+  giftFriendListLoading.value = true;
+  try {
+    const phone = userStore.userInfo?.phone || "";
+    const fansId = phone.startsWith("store_") ? phone : `store_${phone}`;
+    const res: any = await getMutualAttention({ page: 1, size: 999, fansId, name: "" });
+    if (res?.code === 200) {
+      const records = res.data?.records || res.data?.list || res.data || [];
+      giftFriendList.value = records.map((item: any) => ({
+        id: item.id ?? item.userId,
+        name: item.username ?? item.userName ?? item.nickname ?? "用户",
+        phoneId: item.phoneId ?? item.fansId
+      }));
+      const target = giftFriendList.value.find(
+        (f: any) => String(f.phoneId) === targetPhoneId.value || String(f.id) === targetUserId.value
+      );
+      if (target) giftCouponFormData.friendId = target.id;
+    } else {
+      giftFriendList.value = [];
+    }
+  } catch {
+    giftFriendList.value = [];
+  } finally {
+    giftFriendListLoading.value = false;
+  }
+};
+
+const loadGiftCouponList = async (giftType: "coupon" | "voucher" = giftCouponFormData.giftType) => {
+  try {
+    const storeId = localGet("createdId") || userStore.userInfo?.storeId || userStore.userInfo?.createdId;
+    let params: any = {};
+    if (giftType === "coupon") {
+      params = { storeId, status: 0 };
+    } else if (giftType === "voucher") {
+      params = { storeId, status: 0, type: 4 };
+    }
+
+    const api = getCouponList;
+    const res: any = await api(params);
+    if (res?.code === 200) {
+      const list = res.data?.records ?? res.data?.list ?? res.data ?? [];
+      const rawList = Array.isArray(list) ? list : [];
+      if (giftType === "coupon") {
+        giftCouponList.value = rawList.map((item: any) => ({
+          id: item.id ?? item.couponId,
+          name: item.name ?? item.couponName ?? "",
+          singleQty: item.singleQty != null ? Number(item.singleQty) : 100
+        }));
+      } else if (giftType === "voucher") {
+        giftCouponList.value = rawList.map((item: any) => ({
+          id: item.voucherId,
+          name: item.name ?? "",
+          singleQty: item.singleQty != null ? Number(item.singleQty) : 100
+        }));
+      }
+    } else {
+      giftCouponList.value = [];
+      giftCouponListLoaded.value = true;
+    }
+  } catch {
+    giftCouponList.value = [];
+  } finally {
+  }
+};
+
+const addGiftCouponRow = () => {
+  giftCouponFormData.rows.push({
+    key: ++giftCouponRowKey,
+    couponId: "",
+    quantity: 1
+  });
+};
+
+const removeGiftCouponRow = (index: number) => {
+  giftCouponFormData.rows.splice(index, 1);
+};
+
+// 判断该券是否已被其他行选中(当前行可选,其他行已选则禁用)
+const isGiftCouponSelectedInOtherRow = (couponId: string | number, currentRowIndex: number) => {
+  return giftCouponFormData.rows.some(
+    (row, i) => i !== currentRowIndex && row.couponId !== "" && row.couponId != null && String(row.couponId) === String(couponId)
+  );
+};
+
+// 根据所选券的 singleQty 限制该行数量上限
+const getGiftRowMaxQuantity = (row: { couponId: string | number; quantity: number }) => {
+  if (row.couponId === "" || row.couponId == null) return 100;
+  const coupon = giftCouponList.value.find((c: any) => String(c.id) === String(row.couponId));
+  const max = coupon?.singleQty != null ? Number(coupon.singleQty) : 100;
+  return Math.max(1, max);
+};
+
+// 切换所选券时,若当前数量超过新券 singleQty 则自动压到上限
+const onGiftRowCouponChange = (row: { couponId: string | number; quantity: number }) => {
+  const max = getGiftRowMaxQuantity(row);
+  if (row.quantity > max) row.quantity = max;
+};
+
+// 输入时即时限制:不允许超出最大数量,输入即截断,输入框从不显示超出的数
+const setGiftRowQuantity = (row: { couponId: string | number; quantity: number }, val: string | number | undefined) => {
+  const max = getGiftRowMaxQuantity(row);
+  const num = val === "" || val == null || Number.isNaN(Number(val)) ? 1 : Number(val);
+  row.quantity = Math.min(Math.max(1, num), max);
+};
+
+// 严格校验:赠送对象存在 + 至少一行已选券且数量有效 + 每一行要么未选券(可忽略)要么已选券且数量有效,不允许存在「未选券但占着一行」的不完整行
+const canSubmitGiftCoupon = computed(() => {
+  if (!giftCouponFormData.friendId) return false;
+  const validRows = giftCouponFormData.rows.filter(r => r.couponId !== "" && r.couponId != null && r.quantity >= 1);
+  if (validRows.length < 1) return false;
+  // 存在未选券的行则不允许提交(避免一行有券、一行没选券仍能点确定)
+  const hasIncompleteRow = giftCouponFormData.rows.some(r => (r.couponId === "" || r.couponId == null) && r.quantity >= 1);
+  return !hasIncompleteRow;
+});
+
+const closeGiftCouponDialog = () => {
+  giftCouponDialogVisible.value = false;
+  giftCouponFormData.friendId = "";
+  giftCouponFormData.giftType = "coupon";
+  giftCouponFormData.rows = [{ key: ++giftCouponRowKey, couponId: "", quantity: 1 }];
+  giftCouponListLoaded.value = false;
+};
+
+const handleGiftCouponSubmit = async () => {
+  if (!giftCouponFormData.friendId) {
+    ElMessage.warning("数据填写不完整,无法确定赠送对象");
+    return;
+  }
+  const validRows = giftCouponFormData.rows.filter(r => r.couponId !== "" && r.couponId != null && r.quantity >= 1);
+  if (validRows.length === 0) {
+    ElMessage.warning("数据填写不完整,请至少选择一张优惠券并填写数量");
+    return;
+  }
+  const hasIncompleteRow = giftCouponFormData.rows.some(r => (r.couponId === "" || r.couponId == null) && r.quantity >= 1);
+  if (hasIncompleteRow) {
+    ElMessage.warning("数据填写不完整,请为每一行选择优惠券或删除未选券的行");
+    return;
+  }
+  if (!canSubmitGiftCoupon.value) {
+    ElMessage.warning("数据填写不完整,请至少选择一张优惠券并填写数量");
+    return;
+  }
+  try {
+    let params: any = {};
+    if (giftCouponFormData.giftType === "coupon") {
+      params = {
+        couponIds: validRows.map(r => ({
+          couponId: r.couponId,
+          singleQty: r.quantity
+        })),
+        friendStoreUserId: String(giftCouponFormData.friendId)
+      };
+    } else if (giftCouponFormData.giftType === "voucher") {
+      params = {
+        voucherIds: validRows.map(r => ({
+          voucherId: r.couponId,
+          singleQty: r.quantity
+        })),
+        friendStoreUserId: String(giftCouponFormData.friendId)
+      };
+    }
+    const res: any = await setFriendCoupon(params);
+    if (res?.code === 200) {
+      ElMessage.success("赠送成功");
+      closeGiftCouponDialog();
+    } else {
+      ElMessage.error(res?.msg || "赠送失败");
+    }
+  } catch (error: any) {
+    console.error("赠送失败:", error);
+    ElMessage.error(error?.message || "赠送失败");
+  }
+};
+
+// 赠券:打开赠送好友优惠券弹窗(不跳转)
+const handleGiftCoupon = () => {
+  giftCouponDialogVisible.value = true;
+  loadGiftFriendList();
+  loadGiftCouponList();
+};
+
+// 切换优惠券/代金券时:清空下拉列表、只保留一行选择,下次聚焦下拉时按类型重新加载
+watch(
+  () => giftCouponFormData.giftType,
+  () => {
+    giftCouponList.value = [];
+    giftCouponListLoaded.value = false;
+    giftCouponFormData.rows = [{ key: ++giftCouponRowKey, couponId: "", quantity: 1 }];
+  }
+);
+
 // 举报用户
 const handleReportUser = () => {
   reportDialogVisible.value = true;
@@ -1358,9 +1649,33 @@ const loadContentList = async () => {
   }
 };
 
+// 加载是否与当前主页用户互为好友(用于展示赠券入口)
+const loadIsFriend = async () => {
+  if (!targetPhoneId.value) return;
+  try {
+    const phone = userStore.userInfo?.phone || "";
+    const fansId = phone.startsWith("store_") ? phone : `store_${phone}`;
+    const res: any = await getMutualAttention({
+      page: 1,
+      size: 1000,
+      fansId,
+      name: ""
+    });
+    if (res?.code === 200) {
+      const dataList = res.data?.records || res.data?.list || res.data || [];
+      isFriend.value = dataList.some(
+        (item: any) => (item.phoneId || item.fansId || item.storeUserId || "") === targetPhoneId.value
+      );
+    }
+  } catch {
+    isFriend.value = false;
+  }
+};
+
 // 初始化
 onMounted(() => {
   loadContentList();
+  loadIsFriend();
 });
 </script>
 
@@ -2168,6 +2483,46 @@ onMounted(() => {
     }
   }
 }
+.gift-coupon-dialog-body {
+  .gift-type-section,
+  .coupon-rows-section {
+    margin-bottom: 20px;
+  }
+  .section-label {
+    margin-bottom: 12px;
+    font-size: 14px;
+    font-weight: 500;
+    color: var(--el-text-color-primary);
+  }
+  .coupon-row {
+    display: flex;
+    gap: 12px;
+    align-items: center;
+    align-items: start;
+    margin-bottom: 12px;
+    .quantity-cell {
+      display: flex;
+      flex-shrink: 0;
+      flex-direction: column;
+      gap: 4px;
+    }
+    .quantity-input {
+      width: 140px;
+    }
+    .quantity-limit-hint {
+      font-size: 12px;
+      line-height: 1.2;
+      color: var(--el-color-warning);
+    }
+  }
+  .add-coupon-btn {
+    display: inline-flex;
+    gap: 4px;
+    align-items: center;
+    padding-left: 0;
+    color: var(--el-color-primary);
+  }
+}
 </style>
 
 <style scoped>