feat(用户管理): 新增用户标签功能

在用户管理和用户详情页面中新增标签功能,支持添加、删除和筛选用户标签。新增了标签相关的API接口和前端交互逻辑,提升用户管理的灵活性和可扩展性。
This commit is contained in:
dzq 2025-05-22 15:43:38 +08:00
parent d2390361b1
commit b1f4216df6
5 changed files with 199 additions and 6 deletions

59
src/api/ab98/tag.ts Normal file
View File

@ -0,0 +1,59 @@
import { http } from "@/utils/http";
export interface Ab98UserTagDTO {
/** 标签ID */
tagId?: number;
/** 关联用户ID */
ab98UserId?: number;
/** 标签名称 */
tagName?: string;
/** 创建时间 */
createTime?: string;
}
export interface Ab98UserTagQuery extends BasePageQuery {
/** 关联用户ID */
ab98UserId?: number;
/** 标签名称 */
tagName?: string;
/** 开始时间 */
startTime?: string;
/** 结束时间 */
endTime?: string;
}
export interface AddAb98UserTagCommand {
/** 关联用户ID */
ab98UserId: number;
/** 标签名称 */
tagName: string;
}
export interface UpdateAb98UserTagCommand extends AddAb98UserTagCommand {
/** 标签ID */
tagId: number;
}
export const getAb98UserTagListApi = (params: Ab98UserTagQuery) => {
return http.request<ResponseData<PageDTO<Ab98UserTagDTO>>>("get", "/ab98/userTags", { params });
};
export const addAb98UserTagApi = (data: AddAb98UserTagCommand) => {
return http.request<ResponseData<void>>("post", "/ab98/userTags", { data });
};
export const updateAb98UserTagApi = (id: number, data: UpdateAb98UserTagCommand) => {
return http.request<ResponseData<void>>("put", `/ab98/userTags/${id}`, { data });
};
export const deleteAb98UserTagApi = (ids: number[]) => {
return http.request<ResponseData<void>>("delete", `/ab98/userTags/${ids.join(',')}`);
};
export const deleteAb98UserTagConfirmApi = (tagId: number) => {
return http.request<ResponseData<void>>("delete", `/ab98/userTags/${tagId}`);
};
export const getAb98UserTagNamesApi = () => {
return http.request<ResponseData<string[]>>("get", "/ab98/userTags/names");
};

View File

@ -61,6 +61,7 @@ export interface Ab98UserQuery extends BasePageQuery {
tel?: string;
/** 身份证号码 */
idnum?: string;
tagName?: string;
}
export interface AddAb98UserCommand {

View File

@ -3,6 +3,9 @@ import { ref, onMounted, watch } from "vue";
import { useRoute } from "vue-router";
import { type Ab98UserDetailDTO, getAb98UserDetailApi } from "@/api/ab98/user";
import { getOrderListApi, type OrderDTO } from "@/api/shop/order";
import { Ab98UserTagDTO, addAb98UserTagApi, deleteAb98UserTagConfirmApi, getAb98UserTagListApi } from "@/api/ab98/tag";
import { ElMessage, ElMessageBox } from "element-plus";
import { PureTableBar } from "@/components/RePureTableBar";
defineOptions({
name: "Ab98UserDetail"
@ -11,6 +14,12 @@ defineOptions({
const route = useRoute();
const userInfo = ref<Ab98UserDetailDTO>({});
const loading = ref(false);
const tags = ref<Ab98UserTagDTO[]>([]);
const tagsLoading = ref(false);
const showAddTagDialog = ref(false);
const addTagForm = ref({
tagName: ''
});
//
const basicInfo = ref({
@ -57,11 +66,64 @@ async function fetchUserDetail() {
const userId = route.query.id;
const { data } = await getAb98UserDetailApi(Number(userId));
userInfo.value = data;
await fetchUserTags();
} finally {
loading.value = false;
}
}
async function fetchUserTags() {
try {
tagsLoading.value = true;
const { data } = await getAb98UserTagListApi({
ab98UserId: userInfo.value.ab98UserId,
pageSize: 6,
pageNum: 1
});
tags.value = data.rows;
} finally {
tagsLoading.value = false;
}
}
async function handleAddTag() {
try {
tagsLoading.value = true;
await addAb98UserTagApi({
ab98UserId: userInfo.value.ab98UserId,
tagName: addTagForm.value.tagName
});
showAddTagDialog.value = false;
addTagForm.value.tagName = '';
await fetchUserTags();
ElMessage.success('标签添加成功');
} catch (error) {
ElMessage.error('标签添加失败');
} finally {
tagsLoading.value = false;
}
}
async function handleDeleteTag(tagId: number) {
try {
await ElMessageBox.confirm('确定要删除这个标签吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
tagsLoading.value = true;
await deleteAb98UserTagConfirmApi(tagId);
await fetchUserTags();
ElMessage.success('标签删除成功');
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('标签删除失败');
}
} finally {
tagsLoading.value = false;
}
}
onMounted(() => {
fetchUserDetail();
});
@ -107,6 +169,17 @@ onMounted(() => {
<el-descriptions-item label="最后登录">{{ basicInfo.lastLogin }}</el-descriptions-item>
<el-descriptions-item label="登录次数">{{ basicInfo.loginCount }}</el-descriptions-item>
<el-descriptions-item label="常用设备">{{ basicInfo.device }}</el-descriptions-item>
<el-descriptions-item label="标签" :span="2">
<div class="tag-container">
<div class="user-tags" v-if="tags.length > 0">
<el-tag v-for="tag in tags" :key="tag.tagId" class="tag-item" closable
@close="handleDeleteTag(tag.tagId)">
{{ tag.tagName }}
</el-tag>
</div>
<el-button type="primary" size="small" @click="showAddTagDialog = true">添加标签</el-button>
</div>
</el-descriptions-item>
</el-descriptions>
@ -170,6 +243,16 @@ onMounted(() => {
</div>
</el-card>
</div>
<el-dialog v-model="showAddTagDialog" title="添加标签" width="30%">
<el-form :model="addTagForm" label-width="80px">
<el-form-item label="标签名称">
<el-input v-model="addTagForm.tagName" placeholder="请输入标签名称" />
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" @click="handleAddTag">确定</el-button>
</template>
</el-dialog>
</div>
</template>
@ -217,6 +300,45 @@ onMounted(() => {
margin-bottom: 0px;
}
.tag-container {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
.user-tags {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.el-button {
width: 80px;
margin-left: auto;
}
}
.el-button {
margin-top: 5px;
background-color: #409eff;
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-size: 14px;
transition: all 0.3s;
&:hover {
background-color: #66b1ff;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
}
&:active {
background-color: #3a8ee6;
}
}
.order-item {
margin-bottom: 20px;
padding: 10px;

View File

@ -10,6 +10,7 @@ import Refresh from "@iconify-icons/ep/refresh";
import View from "@iconify-icons/ep/view";
import { useRouter } from "vue-router";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import { getAb98UserTagNamesApi } from "@/api/ab98/tag";
defineOptions({
name: "Ab98User"
@ -17,10 +18,12 @@ defineOptions({
const router = useRouter();
const formRef = ref();
const tagOptions = ref<string[]>([]);
const searchFormParams = reactive<Ab98UserQuery>({
name: undefined,
tel: undefined,
idnum: undefined
idnum: undefined,
tagName: undefined
});
const pageLoading = ref(false);
@ -78,6 +81,9 @@ const handleViewDetail = (row: Ab98UserDTO) => {
onMounted(() => {
getList();
getAb98UserTagNamesApi().then(res => {
tagOptions.value = res.data;
});
})
</script>
@ -95,6 +101,11 @@ onMounted(() => {
<el-form-item label="身份证:" prop="idnum">
<el-input v-model="searchFormParams.idnum" placeholder="请输入" clearable class="!w-[160px]" />
</el-form-item>
<el-form-item label="标签:" prop="tagName">
<el-select v-model="searchFormParams.tagName" placeholder="请选择" clearable class="!w-[160px]">
<el-option v-for="item in tagOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="useRenderIcon(Search)" :loading="pageLoading" @click="onSearch">
搜索

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { Goods, Shop, Document, Money } from '@element-plus/icons-vue';
import { getStats, TodayLatestOrderGoodsDTO, TopGoodsDTO } from '@/api/shop/stats';
import { onMounted, ref } from 'vue';
import { markRaw, onMounted, ref } from 'vue';
defineOptions({
name: "Welcome"
@ -36,10 +36,10 @@ onMounted(async () => {
try {
const { data } = await getStats();
shopData.value = [
{ name: '商店', icon: Shop, value: data.shopCount },
{ name: '商品总数量', icon: Goods, value: data.goodsCount },
{ name: '商品总订单', icon: Document, value: data.orderCount },
{ name: '商品总金额', icon: Money, value: data.goodsTotalAmount }
{ name: '商店', icon: markRaw(Shop), value: data.shopCount },
{ name: '商品总数量', icon: markRaw(Goods), value: data.goodsCount },
{ name: '商品总订单', icon: markRaw(Document), value: data.orderCount },
{ name: '商品总金额', icon: markRaw(Money), value: data.goodsTotalAmount }
];
unReturnedData.value = [
{ name: '未还商品', value: data.unReturnedGoodsCount },