feat(用户管理): 新增用户标签功能
在用户管理和用户详情页面中新增标签功能,支持添加、删除和筛选用户标签。新增了标签相关的API接口和前端交互逻辑,提升用户管理的灵活性和可扩展性。
This commit is contained in:
parent
d2390361b1
commit
b1f4216df6
|
@ -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");
|
||||
};
|
|
@ -61,6 +61,7 @@ export interface Ab98UserQuery extends BasePageQuery {
|
|||
tel?: string;
|
||||
/** 身份证号码 */
|
||||
idnum?: string;
|
||||
tagName?: string;
|
||||
}
|
||||
|
||||
export interface AddAb98UserCommand {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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">
|
||||
搜索
|
||||
|
|
|
@ -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 },
|
||||
|
|
Loading…
Reference in New Issue