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