feat(会员标签): 实现会员标签管理功能
添加会员标签相关API接口和页面 重构用户详情页标签管理逻辑 新增标签管理页面,支持增删改查操作
This commit is contained in:
parent
673c992c09
commit
71e52b3505
|
|
@ -0,0 +1,126 @@
|
||||||
|
import { http } from "@/utils/http";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员标签关联数据传输对象
|
||||||
|
*/
|
||||||
|
export interface MembershipTagMemberDTO {
|
||||||
|
/** 主键,自动递增 */
|
||||||
|
id?: number;
|
||||||
|
/** 企业微信ID */
|
||||||
|
corpid?: string;
|
||||||
|
/** 标签ID */
|
||||||
|
tagId?: number;
|
||||||
|
/** 汇邦云用户ID */
|
||||||
|
ab98UserId?: number;
|
||||||
|
/** 微信用户ID */
|
||||||
|
wxUserId?: number;
|
||||||
|
/** 创建者ID */
|
||||||
|
creatorId?: number;
|
||||||
|
/** 创建时间 */
|
||||||
|
createTime?: string;
|
||||||
|
/** 更新者ID */
|
||||||
|
updaterId?: number;
|
||||||
|
/** 更新时间 */
|
||||||
|
updateTime?: string;
|
||||||
|
/** 标签名称(可选,通过关联查询或缓存获取) */
|
||||||
|
tagName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员标签关联查询参数
|
||||||
|
*/
|
||||||
|
export interface SearchMembershipTagMemberQuery extends BasePageQuery {
|
||||||
|
/** 企业微信ID */
|
||||||
|
corpid?: string;
|
||||||
|
/** 标签ID */
|
||||||
|
tagId?: number;
|
||||||
|
/** 汇邦云用户ID */
|
||||||
|
ab98UserId?: number;
|
||||||
|
/** 微信用户ID */
|
||||||
|
wxUserId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加会员标签关联命令
|
||||||
|
*/
|
||||||
|
export interface AddMembershipTagMemberCommand {
|
||||||
|
/** 企业微信ID */
|
||||||
|
corpid?: string;
|
||||||
|
/** 标签ID */
|
||||||
|
tagId: number;
|
||||||
|
/** 汇邦云用户ID */
|
||||||
|
ab98UserId?: number;
|
||||||
|
/** 微信用户ID */
|
||||||
|
wxUserId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新会员标签关联命令
|
||||||
|
*/
|
||||||
|
export interface UpdateMembershipTagMemberCommand extends AddMembershipTagMemberCommand {
|
||||||
|
/** 主键,自动递增 */
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会员标签关联列表
|
||||||
|
*/
|
||||||
|
export const getMembershipTagMemberListApi = (params: SearchMembershipTagMemberQuery) => {
|
||||||
|
return http.request<ResponseData<PageDTO<MembershipTagMemberDTO>>>("get", "/membership/tagMembers", { params });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取标签关联详情
|
||||||
|
*/
|
||||||
|
export const getMembershipTagMemberDetailApi = (id: number) => {
|
||||||
|
return http.request<ResponseData<MembershipTagMemberDTO>>("get", `/membership/tagMembers/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加会员标签关联
|
||||||
|
*/
|
||||||
|
export const addMembershipTagMemberApi = (data: AddMembershipTagMemberCommand) => {
|
||||||
|
return http.request<ResponseData<void>>("post", "/membership/tagMembers", { data });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新会员标签关联
|
||||||
|
*/
|
||||||
|
export const updateMembershipTagMemberApi = (id: number, data: UpdateMembershipTagMemberCommand) => {
|
||||||
|
return http.request<ResponseData<void>>("put", `/membership/tagMembers/${id}`, { data });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除会员标签关联
|
||||||
|
*/
|
||||||
|
export const deleteMembershipTagMemberApi = (ids: number[]) => {
|
||||||
|
return http.request<ResponseData<void>>("delete", `/membership/tagMembers/${ids.join(',')}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据标签ID查询关联用户
|
||||||
|
*/
|
||||||
|
export const getMembershipTagMemberByTagIdApi = (tagId: number) => {
|
||||||
|
return http.request<ResponseData<MembershipTagMemberDTO[]>>("get", `/membership/tagMembers/byTag/${tagId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据汇邦云用户ID查询标签
|
||||||
|
*/
|
||||||
|
export const getMembershipTagMemberByAb98UserIdApi = (ab98UserId: number) => {
|
||||||
|
return http.request<ResponseData<MembershipTagMemberDTO[]>>("get", `/membership/tagMembers/byAb98User/${ab98UserId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据微信用户ID查询标签
|
||||||
|
*/
|
||||||
|
export const getMembershipTagMemberByWxUserIdApi = (wxUserId: number) => {
|
||||||
|
return http.request<ResponseData<MembershipTagMemberDTO[]>>("get", `/membership/tagMembers/byWxUser/${wxUserId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户的所有标签
|
||||||
|
*/
|
||||||
|
export const getUserTagsApi = (params: { corpid: string; ab98UserId?: number; wxUserId?: number }) => {
|
||||||
|
return http.request<ResponseData<MembershipTagMemberDTO[]>>("get", "/membership/tagMembers/userTags", { params });
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { http } from "@/utils/http";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员标签数据传输对象
|
||||||
|
*/
|
||||||
|
export interface MembershipTagDTO {
|
||||||
|
/** 主键,自动递增 */
|
||||||
|
id?: number;
|
||||||
|
/** 企业微信ID */
|
||||||
|
corpid?: string;
|
||||||
|
/** 标签名称 */
|
||||||
|
name?: string;
|
||||||
|
/** 标签颜色(十六进制代码) */
|
||||||
|
color?: string;
|
||||||
|
/** 标签说明 */
|
||||||
|
remark?: string;
|
||||||
|
/** 状态:0-禁用 1-启用 */
|
||||||
|
status?: number;
|
||||||
|
/** 创建者ID */
|
||||||
|
creatorId?: number;
|
||||||
|
/** 创建时间 */
|
||||||
|
createTime?: string;
|
||||||
|
/** 更新者ID */
|
||||||
|
updaterId?: number;
|
||||||
|
/** 更新时间 */
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员标签查询参数
|
||||||
|
*/
|
||||||
|
export interface MembershipTagQuery extends BasePageQuery {
|
||||||
|
/** 企业微信ID */
|
||||||
|
corpid?: string;
|
||||||
|
/** 标签名称 */
|
||||||
|
name?: string;
|
||||||
|
/** 状态:0-禁用 1-启用 */
|
||||||
|
status?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加会员标签命令
|
||||||
|
*/
|
||||||
|
export interface AddMembershipTagCommand {
|
||||||
|
/** 企业微信ID */
|
||||||
|
corpid?: string;
|
||||||
|
/** 标签名称 */
|
||||||
|
name: string;
|
||||||
|
/** 标签颜色(十六进制代码) */
|
||||||
|
color?: string;
|
||||||
|
/** 标签说明 */
|
||||||
|
remark?: string;
|
||||||
|
/** 状态:0-禁用 1-启用 */
|
||||||
|
status?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新会员标签命令
|
||||||
|
*/
|
||||||
|
export interface UpdateMembershipTagCommand extends AddMembershipTagCommand {
|
||||||
|
/** 主键,自动递增 */
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会员标签列表
|
||||||
|
*/
|
||||||
|
export const getMembershipTagListApi = (params: MembershipTagQuery) => {
|
||||||
|
return http.request<ResponseData<PageDTO<MembershipTagDTO>>>("get", "/membership/tags", { params });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取标签详情
|
||||||
|
*/
|
||||||
|
export const getMembershipTagDetailApi = (id: number) => {
|
||||||
|
return http.request<ResponseData<MembershipTagDTO>>("get", `/membership/tags/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加会员标签
|
||||||
|
*/
|
||||||
|
export const addMembershipTagApi = (data: AddMembershipTagCommand) => {
|
||||||
|
return http.request<ResponseData<void>>("post", "/membership/tags", { data });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新会员标签
|
||||||
|
*/
|
||||||
|
export const updateMembershipTagApi = (id: number, data: UpdateMembershipTagCommand) => {
|
||||||
|
return http.request<ResponseData<void>>("put", `/membership/tags/${id}`, { data });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除会员标签
|
||||||
|
*/
|
||||||
|
export const deleteMembershipTagApi = (ids: number[]) => {
|
||||||
|
return http.request<ResponseData<void>>("delete", `/membership/tags/${ids.join(',')}`);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,485 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, reactive, ref, watch } from "vue";
|
||||||
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
|
import {
|
||||||
|
getMembershipTagListApi,
|
||||||
|
type MembershipTagDTO,
|
||||||
|
type MembershipTagQuery,
|
||||||
|
addMembershipTagApi,
|
||||||
|
type AddMembershipTagCommand,
|
||||||
|
updateMembershipTagApi,
|
||||||
|
type UpdateMembershipTagCommand,
|
||||||
|
deleteMembershipTagApi
|
||||||
|
} from "@/api/membership/membershipTag";
|
||||||
|
import { type PaginationProps } from "@pureadmin/table";
|
||||||
|
import { CommonUtils } from "@/utils/common";
|
||||||
|
import { useWxStore } from "@/store/modules/wx";
|
||||||
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
|
|
||||||
|
import Search from "@iconify-icons/ep/search";
|
||||||
|
import Refresh from "@iconify-icons/ep/refresh";
|
||||||
|
import Plus from "@iconify-icons/ep/plus";
|
||||||
|
import Edit from "@iconify-icons/ep/edit-pen";
|
||||||
|
import Delete from "@iconify-icons/ep/delete";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "MembershipTag"
|
||||||
|
});
|
||||||
|
|
||||||
|
const wxStore = useWxStore();
|
||||||
|
const formRef = ref();
|
||||||
|
const drawerVisible = ref(false);
|
||||||
|
const drawerLoading = ref(false);
|
||||||
|
const drawerFormRef = ref();
|
||||||
|
const drawerForm = reactive<AddMembershipTagCommand>({
|
||||||
|
corpid: wxStore.corpid || "",
|
||||||
|
name: "",
|
||||||
|
color: "#409EFF",
|
||||||
|
remark: "",
|
||||||
|
status: 1
|
||||||
|
});
|
||||||
|
const isEdit = ref(false);
|
||||||
|
const currentTagId = ref<number | null>(null);
|
||||||
|
|
||||||
|
const pageLoading = ref(false);
|
||||||
|
const dataList = ref<MembershipTagDTO[]>([]);
|
||||||
|
const pagination = reactive<PaginationProps>({
|
||||||
|
total: 0,
|
||||||
|
pageSize: 12,
|
||||||
|
currentPage: 1,
|
||||||
|
background: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchFormParams = reactive<MembershipTagQuery & { search?: string }>({
|
||||||
|
corpid: wxStore.corpid || "",
|
||||||
|
name: undefined,
|
||||||
|
status: undefined,
|
||||||
|
search: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
// 搜索输入处理
|
||||||
|
const handleSearchInput = () => {
|
||||||
|
searchFormParams.name = searchFormParams.search || undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
async function onSearch() {
|
||||||
|
handleSearchInput();
|
||||||
|
pagination.currentPage = 1;
|
||||||
|
getList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取列表
|
||||||
|
async function getList() {
|
||||||
|
CommonUtils.fillPaginationParams(searchFormParams, pagination);
|
||||||
|
|
||||||
|
pageLoading.value = true;
|
||||||
|
try {
|
||||||
|
const { data } = await getMembershipTagListApi(searchFormParams);
|
||||||
|
dataList.value = data.rows;
|
||||||
|
pagination.total = data.total;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取标签列表失败:", error);
|
||||||
|
ElMessage.error("获取标签列表失败");
|
||||||
|
} finally {
|
||||||
|
pageLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = formEl => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.resetFields();
|
||||||
|
// 清空非表单字段
|
||||||
|
searchFormParams.name = undefined;
|
||||||
|
onSearch();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开抽屉(新增或编辑)
|
||||||
|
const openDrawer = (tag?: MembershipTagDTO) => {
|
||||||
|
drawerVisible.value = true;
|
||||||
|
if (tag) {
|
||||||
|
// 编辑模式
|
||||||
|
isEdit.value = true;
|
||||||
|
currentTagId.value = tag.id!;
|
||||||
|
drawerForm.name = tag.name || "";
|
||||||
|
drawerForm.color = tag.color || "#409EFF";
|
||||||
|
drawerForm.remark = tag.remark || "";
|
||||||
|
drawerForm.status = tag.status ?? 1;
|
||||||
|
} else {
|
||||||
|
// 新增模式
|
||||||
|
isEdit.value = false;
|
||||||
|
currentTagId.value = null;
|
||||||
|
drawerForm.name = "";
|
||||||
|
drawerForm.color = "#409EFF";
|
||||||
|
drawerForm.remark = "";
|
||||||
|
drawerForm.status = 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭抽屉
|
||||||
|
const closeDrawer = () => {
|
||||||
|
drawerVisible.value = false;
|
||||||
|
drawerFormRef.value?.resetFields();
|
||||||
|
isEdit.value = false;
|
||||||
|
currentTagId.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 颜色转换函数
|
||||||
|
const convertColorToHex = (color: string): string => {
|
||||||
|
if (!color) return "#409EFF";
|
||||||
|
|
||||||
|
// 如果已经是16进制格式,直接返回
|
||||||
|
if (color.startsWith('#')) {
|
||||||
|
return color.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理RGB格式
|
||||||
|
if (color.startsWith('rgb')) {
|
||||||
|
const rgbMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*\d+\.?\d*)?\)/);
|
||||||
|
if (rgbMatch) {
|
||||||
|
const r = parseInt(rgbMatch[1]);
|
||||||
|
const g = parseInt(rgbMatch[2]);
|
||||||
|
const b = parseInt(rgbMatch[3]);
|
||||||
|
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理HSL格式
|
||||||
|
if (color.startsWith('hsl')) {
|
||||||
|
// 这里可以添加HSL到RGB的转换逻辑,但为了简化,先返回默认值
|
||||||
|
return "#409EFF";
|
||||||
|
}
|
||||||
|
|
||||||
|
return color.toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理颜色选择
|
||||||
|
const handleColorChange = (color: string) => {
|
||||||
|
drawerForm.color = convertColorToHex(color);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交标签(新增或编辑)
|
||||||
|
const submitDrawer = async () => {
|
||||||
|
if (!drawerFormRef.value) return;
|
||||||
|
|
||||||
|
await drawerFormRef.value.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
try {
|
||||||
|
drawerLoading.value = true;
|
||||||
|
// 确保颜色是16进制格式
|
||||||
|
const processedForm = {
|
||||||
|
...drawerForm,
|
||||||
|
color: convertColorToHex(drawerForm.color)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEdit.value && currentTagId.value) {
|
||||||
|
const updateData: UpdateMembershipTagCommand = {
|
||||||
|
id: currentTagId.value,
|
||||||
|
...processedForm
|
||||||
|
};
|
||||||
|
await updateMembershipTagApi(currentTagId.value, updateData);
|
||||||
|
ElMessage.success("标签更新成功");
|
||||||
|
} else {
|
||||||
|
await addMembershipTagApi(processedForm);
|
||||||
|
ElMessage.success("标签添加成功");
|
||||||
|
}
|
||||||
|
closeDrawer();
|
||||||
|
getList();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(isEdit.value ? "更新标签失败:" : "添加标签失败:", error);
|
||||||
|
ElMessage.error(isEdit.value ? "更新标签失败,请重试" : "添加标签失败,请重试");
|
||||||
|
} finally {
|
||||||
|
drawerLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除标签
|
||||||
|
const handleDelete = async (row: MembershipTagDTO) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定删除标签 "${row.name}" 吗?`, "提示", {
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning"
|
||||||
|
});
|
||||||
|
await deleteMembershipTagApi([row.id!]);
|
||||||
|
ElMessage.success("删除成功");
|
||||||
|
getList();
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== "cancel") {
|
||||||
|
console.error("删除标签失败:", error);
|
||||||
|
ElMessage.error("删除失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 预定义颜色
|
||||||
|
const predefineColors = [
|
||||||
|
"#409EFF",
|
||||||
|
"#67C23A",
|
||||||
|
"#E6A23C",
|
||||||
|
"#F56C6C",
|
||||||
|
"#909399",
|
||||||
|
"#FF9A6C",
|
||||||
|
"#A0CFFF",
|
||||||
|
"#B3E19D",
|
||||||
|
"#F3D19E",
|
||||||
|
"#FAB6B6",
|
||||||
|
"#C0C4CC",
|
||||||
|
"#FFD7A6"
|
||||||
|
];
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const drawerRules = reactive({
|
||||||
|
name: [
|
||||||
|
{ required: true, message: "请输入标签名称", trigger: "blur" }
|
||||||
|
],
|
||||||
|
color: [
|
||||||
|
{ required: true, message: "请选择标签颜色", trigger: "change" }
|
||||||
|
],
|
||||||
|
status: [
|
||||||
|
{ required: true, message: "请选择状态", trigger: "change" }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="main">
|
||||||
|
<div class="float-right w-full">
|
||||||
|
<el-form ref="formRef" :inline="true" :model="searchFormParams"
|
||||||
|
class="search-form bg-bg_color flex w-[99/100] pl-[22px] pt-[12px]">
|
||||||
|
<el-form-item prop="search">
|
||||||
|
<el-input v-model="searchFormParams.search" placeholder="请输入标签名称" clearable class="!w-[300px]"
|
||||||
|
@keydown.enter.prevent="onSearch" @change="handleSearchInput" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="status">
|
||||||
|
<el-select v-model="searchFormParams.status" placeholder="请选择状态" clearable class="!w-[160px]">
|
||||||
|
<el-option label="启用" :value="1" />
|
||||||
|
<el-option label="禁用" :value="0" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :icon="useRenderIcon(Search)" :loading="pageLoading" @click="onSearch">
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item class="space-item">
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="success" :icon="useRenderIcon(Plus)" @click="() => openDrawer()">
|
||||||
|
新增标签
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div class="grid-container">
|
||||||
|
<el-row :gutter="12">
|
||||||
|
<el-col v-for="(item, index) in dataList" :key="item.id" :xs="24" :sm="12" :md="8" :lg="6">
|
||||||
|
<el-card class="tag-card" :body-style="{ padding: '16px' }">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="tag-color" :style="{ backgroundColor: item.color || '#409EFF' }"></div>
|
||||||
|
<div class="tag-name">{{ item.name }}</div>
|
||||||
|
<div class="tag-status">
|
||||||
|
<el-tag :type="item.status === 1 ? 'success' : 'danger'" size="small">
|
||||||
|
{{ item.status === 1 ? '启用' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="tag-remark" v-if="item.remark">{{ item.remark }}</div>
|
||||||
|
<div class="tag-remark empty" v-else>暂无说明</div>
|
||||||
|
<div class="tag-meta">
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="label">创建时间:</span>
|
||||||
|
<span class="value">{{ item.createTime || '未知' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="label">更新时间:</span>
|
||||||
|
<span class="value">{{ item.updateTime || '未知' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<el-button type="primary" :icon="useRenderIcon(Edit)" size="small" @click.stop="openDrawer(item)">
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" :icon="useRenderIcon(Delete)" size="small" @click.stop="handleDelete(item)">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<div class="pagination-wrapper">
|
||||||
|
<el-pagination background layout="prev, pager, next" :page-size="pagination.pageSize"
|
||||||
|
:total="pagination.total" v-model:current-page="pagination.currentPage" @current-change="getList"
|
||||||
|
@size-change="getList" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增/编辑标签抽屉 -->
|
||||||
|
<el-drawer v-model="drawerVisible" :title="isEdit ? '编辑标签' : '新增标签'" size="400px" :close-on-click-modal="false">
|
||||||
|
<el-form ref="drawerFormRef" :model="drawerForm" :rules="drawerRules" label-width="80px">
|
||||||
|
<el-form-item label="标签名称" prop="name">
|
||||||
|
<el-input v-model="drawerForm.name" placeholder="请输入标签名称" maxlength="20" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标签颜色" prop="color">
|
||||||
|
<el-color-picker v-model="drawerForm.color" show-alpha :predefine="predefineColors"
|
||||||
|
@change="handleColorChange" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-radio-group v-model="drawerForm.status">
|
||||||
|
<el-radio :label="1">启用</el-radio>
|
||||||
|
<el-radio :label="0">禁用</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标签说明" prop="remark">
|
||||||
|
<el-input v-model="drawerForm.remark" type="textarea" placeholder="请输入标签说明" maxlength="100" show-word-limit
|
||||||
|
:rows="3" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div style="flex: auto">
|
||||||
|
<el-button @click="closeDrawer">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="drawerLoading" @click="submitDrawer">{{ isEdit ? '确认更新' : '确认添加'
|
||||||
|
}}</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.el-dropdown-menu__item i) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-card {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
min-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
background: linear-gradient(135deg, #f0f9ff 0%, #e6f7ff 100%);
|
||||||
|
transform: translateY(-4px);
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.tag-color {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-status {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.tag-remark {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
min-height: 20px;
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
color: #909399;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-meta {
|
||||||
|
.meta-item {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-container {
|
||||||
|
margin: 12px 0;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.el-row {
|
||||||
|
margin-bottom: -20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrapper {
|
||||||
|
position: relative;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
padding: 9px 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
:deep(.el-pagination) {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-item {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -4,7 +4,8 @@ import { useRoute } from "vue-router";
|
||||||
import { type Ab98UserDetailDTO, getAb98UserDetailApi } from "@/api/ab98/user";
|
import { type Ab98UserDetailDTO, getAb98UserDetailApi } from "@/api/ab98/user";
|
||||||
import { type WxUserDTO, getWxUserDetailApi } from "@/api/wx/wxUser";
|
import { type WxUserDTO, getWxUserDetailApi } from "@/api/wx/wxUser";
|
||||||
import { getOrderListApi, type OrderDTO } from "@/api/shop/order";
|
import { getOrderListApi, type OrderDTO } from "@/api/shop/order";
|
||||||
import { Ab98UserTagDTO, addAb98UserTagApi, deleteAb98UserTagConfirmApi, getAb98UserTagListApi } from "@/api/ab98/tag";
|
import { MembershipTagMemberDTO, getUserTagsApi, addMembershipTagMemberApi, deleteMembershipTagMemberApi } from "@/api/membership/MembershipTagMember";
|
||||||
|
import { getMembershipTagListApi, addMembershipTagApi, type MembershipTagDTO } from "@/api/membership/membershipTag";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { PureTableBar } from "@/components/RePureTableBar";
|
import { PureTableBar } from "@/components/RePureTableBar";
|
||||||
import { useWxStore } from "@/store/modules/wx";
|
import { useWxStore } from "@/store/modules/wx";
|
||||||
|
|
@ -20,11 +21,12 @@ const wxStore = useWxStore();
|
||||||
const userInfo = ref<Ab98UserDetailDTO | WxUserDTO>({});
|
const userInfo = ref<Ab98UserDetailDTO | WxUserDTO>({});
|
||||||
const userType = ref<'ab98' | 'wx'>('ab98'); // 用户类型:ab98用户或微信用户
|
const userType = ref<'ab98' | 'wx'>('ab98'); // 用户类型:ab98用户或微信用户
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const tags = ref<Ab98UserTagDTO[]>([]);
|
const tags = ref<MembershipTagMemberDTO[]>([]);
|
||||||
const tagsLoading = ref(false);
|
const tagsLoading = ref(false);
|
||||||
const showAddTagDialog = ref(false);
|
const showAddTagDialog = ref(false);
|
||||||
|
const availableTags = ref<MembershipTagDTO[]>([]);
|
||||||
const addTagForm = ref({
|
const addTagForm = ref({
|
||||||
tagName: ''
|
tagId: null as number | null
|
||||||
});
|
});
|
||||||
const balanceVisible = ref(false);
|
const balanceVisible = ref(false);
|
||||||
|
|
||||||
|
|
@ -102,28 +104,48 @@ async function fetchUserTags() {
|
||||||
if (userType.value !== 'ab98') return;
|
if (userType.value !== 'ab98') return;
|
||||||
|
|
||||||
tagsLoading.value = true;
|
tagsLoading.value = true;
|
||||||
const { data } = await getAb98UserTagListApi({
|
const { data } = await getUserTagsApi({
|
||||||
ab98UserId: (userInfo.value as Ab98UserDetailDTO).ab98UserId,
|
corpid: wxStore.corpid,
|
||||||
pageSize: 6,
|
ab98UserId: (userInfo.value as Ab98UserDetailDTO).ab98UserId
|
||||||
pageNum: 1
|
|
||||||
});
|
});
|
||||||
tags.value = data.rows;
|
tags.value = data;
|
||||||
} finally {
|
} finally {
|
||||||
tagsLoading.value = false;
|
tagsLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchAvailableTags() {
|
||||||
|
try {
|
||||||
|
const { data } = await getMembershipTagListApi({
|
||||||
|
corpid: wxStore.corpid,
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 100
|
||||||
|
});
|
||||||
|
availableTags.value = data.rows;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch available tags:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleAddTag() {
|
async function handleAddTag() {
|
||||||
try {
|
try {
|
||||||
if (userType.value !== 'ab98') return;
|
if (userType.value !== 'ab98') return;
|
||||||
|
if (!addTagForm.value.tagId) {
|
||||||
|
ElMessage.warning('请选择一个标签');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
tagsLoading.value = true;
|
tagsLoading.value = true;
|
||||||
await addAb98UserTagApi({
|
|
||||||
ab98UserId: (userInfo.value as Ab98UserDetailDTO).ab98UserId,
|
// 将标签关联到用户
|
||||||
tagName: addTagForm.value.tagName
|
await addMembershipTagMemberApi({
|
||||||
|
corpid: wxStore.corpid,
|
||||||
|
tagId: addTagForm.value.tagId,
|
||||||
|
ab98UserId: (userInfo.value as Ab98UserDetailDTO).ab98UserId
|
||||||
});
|
});
|
||||||
|
|
||||||
showAddTagDialog.value = false;
|
showAddTagDialog.value = false;
|
||||||
addTagForm.value.tagName = '';
|
addTagForm.value.tagId = null;
|
||||||
await fetchUserTags();
|
await fetchUserTags();
|
||||||
ElMessage.success('标签添加成功');
|
ElMessage.success('标签添加成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -133,7 +155,7 @@ async function handleAddTag() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteTag(tagId: number) {
|
async function handleDeleteTag(id: number) {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm('确定要删除这个标签吗?', '提示', {
|
await ElMessageBox.confirm('确定要删除这个标签吗?', '提示', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
|
|
@ -141,7 +163,7 @@ async function handleDeleteTag(tagId: number) {
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
});
|
});
|
||||||
tagsLoading.value = true;
|
tagsLoading.value = true;
|
||||||
await deleteAb98UserTagConfirmApi(tagId);
|
await deleteMembershipTagMemberApi([id]);
|
||||||
await fetchUserTags();
|
await fetchUserTags();
|
||||||
ElMessage.success('标签删除成功');
|
ElMessage.success('标签删除成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -155,11 +177,17 @@ async function handleDeleteTag(tagId: number) {
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchUserDetail();
|
fetchUserDetail();
|
||||||
|
fetchAvailableTags();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleModifyBalance() {
|
async function handleModifyBalance() {
|
||||||
balanceVisible.value = true;
|
balanceVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleShowAddTagDialog() {
|
||||||
|
await fetchAvailableTags();
|
||||||
|
showAddTagDialog.value = true;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -219,12 +247,12 @@ async function handleModifyBalance() {
|
||||||
<el-descriptions-item label="标签" :span="2">
|
<el-descriptions-item label="标签" :span="2">
|
||||||
<div class="tag-container">
|
<div class="tag-container">
|
||||||
<div class="user-tags" v-if="tags.length > 0">
|
<div class="user-tags" v-if="tags.length > 0">
|
||||||
<el-tag v-for="tag in tags" :key="tag.tagId" class="tag-item" closable
|
<el-tag v-for="tag in tags" :key="tag.id" class="tag-item" closable
|
||||||
@close="handleDeleteTag(tag.tagId)">
|
@close="handleDeleteTag(tag.id)">
|
||||||
{{ tag.tagName }}
|
{{ tag.tagName }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" size="small" @click="showAddTagDialog = true">添加标签</el-button>
|
<el-button type="primary" size="small" @click="handleShowAddTagDialog">添加标签</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|
@ -305,8 +333,10 @@ async function handleModifyBalance() {
|
||||||
</div>
|
</div>
|
||||||
<el-dialog v-model="showAddTagDialog" title="添加标签" width="30%">
|
<el-dialog v-model="showAddTagDialog" title="添加标签" width="30%">
|
||||||
<el-form :model="addTagForm" label-width="80px">
|
<el-form :model="addTagForm" label-width="80px">
|
||||||
<el-form-item label="标签名称">
|
<el-form-item label="选择标签">
|
||||||
<el-input v-model="addTagForm.tagName" placeholder="请输入标签名称" />
|
<el-select v-model="addTagForm.tagId" placeholder="请选择标签" filterable>
|
||||||
|
<el-option v-for="tag in availableTags" :key="tag.id" :label="tag.name" :value="tag.id" />
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue