feat(用户管理): 添加微信头像字段并优化标签功能

- 在Ab98UserDTO接口中添加wxAvatar字段
- 将tagName查询参数改为tagIds以支持多标签筛选
- 重构图标渲染逻辑为通用函数getIconByName
- 优化用户详情页标签管理功能,添加标签后自动刷新可用标签列表
- 在用户列表页添加多标签筛选功能
- 调整用户卡片头像显示逻辑,优先显示faceImg,其次显示wxAvatar
This commit is contained in:
dzq 2025-12-08 16:34:12 +08:00
parent b45ca4022a
commit 02bc7e6303
4 changed files with 106 additions and 47 deletions

View File

@ -39,6 +39,8 @@ export interface Ab98UserDTO {
wxNickName?: string; wxNickName?: string;
/** 微信用户openid */ /** 微信用户openid */
wxUserOpenid?: string; wxUserOpenid?: string;
/** 微信用户头像 */
wxAvatar?: string;
} }
export interface Ab98UserDetailDTO { export interface Ab98UserDetailDTO {
/** 主键ID */ /** 主键ID */
@ -87,7 +89,8 @@ export interface Ab98UserQuery extends BasePageQuery {
tel?: string; tel?: string;
/** 身份证号码 */ /** 身份证号码 */
idnum?: string; idnum?: string;
tagName?: string; /** 标签ID多个标签ID用逗号分隔 */
tagIds?: string;
/** 绑定汇邦云 false未绑定 true已绑定 null 全部 */ /** 绑定汇邦云 false未绑定 true已绑定 null 全部 */
hasAb98UserId?: boolean; hasAb98UserId?: boolean;
} }

View File

@ -244,6 +244,23 @@ const drawerRules = reactive({
] ]
}); });
const getIconByName = (name: string) => {
switch (name) {
case 'Search':
return useRenderIcon(Search);
case 'Refresh':
return useRenderIcon(Refresh);
case 'Plus':
return useRenderIcon(Plus);
case 'Edit':
return useRenderIcon(Edit);
case 'Delete':
return useRenderIcon(Delete);
default:
return '';
}
}
onMounted(() => { onMounted(() => {
getList(); getList();
}); });
@ -265,19 +282,19 @@ onMounted(() => {
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" :icon="useRenderIcon(Search)" :loading="pageLoading" @click="onSearch"> <el-button type="primary" :icon="getIconByName('Search')" :loading="pageLoading" @click="onSearch">
搜索 搜索
</el-button> </el-button>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)"> <el-button :icon="getIconByName('Refresh')" @click="resetForm(formRef)">
重置 重置
</el-button> </el-button>
</el-form-item> </el-form-item>
<el-form-item class="space-item"> <el-form-item class="space-item">
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="success" :icon="useRenderIcon(Plus)" @click="() => openDrawer()"> <el-button type="success" :icon="getIconByName('Plus')" @click="() => openDrawer()">
新增标签 新增标签
</el-button> </el-button>
</el-form-item> </el-form-item>
@ -311,10 +328,10 @@ onMounted(() => {
</div> </div>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<el-button type="primary" :icon="useRenderIcon(Edit)" size="small" @click.stop="openDrawer(item)"> <el-button type="primary" :icon="getIconByName('Edit')" size="small" @click.stop="openDrawer(item)">
编辑 编辑
</el-button> </el-button>
<el-button type="danger" :icon="useRenderIcon(Delete)" size="small" @click.stop="handleDelete(item)"> <el-button type="danger" :icon="getIconByName('Delete')" size="small" @click.stop="handleDelete(item)">
删除 删除
</el-button> </el-button>
</div> </div>

View File

@ -164,7 +164,15 @@ async function fetchAvailableTags() {
pageNum: 1, pageNum: 1,
pageSize: 100 pageSize: 100
}); });
//
if (tags.value.length > 0) {
const userTagIds = tags.value.map(tag => tag.tagId);
console.log('userTagIds:', userTagIds);
console.log('availableTags before filter:', data.rows);
availableTags.value = data.rows.filter(tag => !userTagIds.includes(tag.id));
} else {
availableTags.value = data.rows; availableTags.value = data.rows;
}
} catch (error) { } catch (error) {
console.error('Failed to fetch available tags:', error); console.error('Failed to fetch available tags:', error);
} }
@ -190,6 +198,8 @@ async function handleAddTag() {
showAddTagDialog.value = false; showAddTagDialog.value = false;
addTagForm.value.tagId = null; addTagForm.value.tagId = null;
await fetchUserTags(); await fetchUserTags();
//
await fetchAvailableTags();
ElMessage.success('标签添加成功'); ElMessage.success('标签添加成功');
} catch (error) { } catch (error) {
ElMessage.error('标签添加失败'); ElMessage.error('标签添加失败');
@ -256,16 +266,24 @@ async function handleShowAddTagDialog() {
<template v-if="userType === 'ab98'"> <template v-if="userType === 'ab98'">
<el-descriptions-item label="性别">{{ (userInfo as Ab98UserDetailDTO).sex }}</el-descriptions-item> <el-descriptions-item label="性别">{{ (userInfo as Ab98UserDetailDTO).sex }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ userInfo.tel }}</el-descriptions-item> <el-descriptions-item label="手机号">{{ userInfo.tel }}</el-descriptions-item>
<el-descriptions-item label="身份证">{{ (userInfo as Ab98UserDetailDTO).idnum }}</el-descriptions-item> <el-descriptions-item label="身份证">{{ (userInfo as Ab98UserDetailDTO).idnum }}</el-descriptions-item>
<el-descriptions-item label="住址">{{ (userInfo as Ab98UserDetailDTO).address }}</el-descriptions-item> <el-descriptions-item label="住址">{{ (userInfo as Ab98UserDetailDTO).address }}</el-descriptions-item>
</template> </template>
<template v-else> <template v-else>
<el-descriptions-item label="OpenID">{{ (userInfo as WxUserDTO).openid }}</el-descriptions-item> <el-descriptions-item label="OpenID">{{ (userInfo as WxUserDTO).openid }}</el-descriptions-item>
<el-descriptions-item label="昵称">{{ (userInfo as WxUserDTO).nickName }}</el-descriptions-item> <el-descriptions-item label="昵称">{{ (userInfo as WxUserDTO).nickName }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ userInfo.tel }}</el-descriptions-item> <el-descriptions-item label="手机号">{{ userInfo.tel }}</el-descriptions-item>
<el-descriptions-item label="微信余额">{{ formatFenToYuan((userInfo as WxUserDTO).wxBalance)
}}</el-descriptions-item>
</template> </template>
<el-descriptions-item label="标签">
<div class="tag-container">
<div class="user-tags" v-if="tags.length > 0">
<el-tag v-for="tag in tags" :key="tag.id" class="tag-item" closable @close="handleDeleteTag(tag.id)">
{{ tag.tagName }}
</el-tag>
</div>
<el-button type="primary" size="small" @click="handleShowAddTagDialog">添加标签</el-button>
</div>
</el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-card> </el-card>
<el-card class="info-card"> <el-card class="info-card">
@ -294,17 +312,6 @@ async function handleShowAddTagDialog() {
}}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item label="已使用借呗">{{ formatFenToYuan((userInfo as Ab98UserDetailDTO).useBalance) <el-descriptions-item label="已使用借呗">{{ formatFenToYuan((userInfo as Ab98UserDetailDTO).useBalance)
}}</el-descriptions-item> }}</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.id" class="tag-item" closable
@close="handleDeleteTag(tag.id)">
{{ tag.tagName }}
</el-tag>
</div>
<el-button type="primary" size="small" @click="handleShowAddTagDialog">添加标签</el-button>
</div>
</el-descriptions-item>
</el-descriptions> </el-descriptions>
</template> </template>
<template v-else> <template v-else>
@ -312,8 +319,6 @@ async function handleShowAddTagDialog() {
<el-descriptions-item label="微信用户ID">{{ (userInfo as WxUserDTO).wxUserId }}</el-descriptions-item> <el-descriptions-item label="微信用户ID">{{ (userInfo as WxUserDTO).wxUserId }}</el-descriptions-item>
<el-descriptions-item label="OpenID">{{ openid }}</el-descriptions-item> <el-descriptions-item label="OpenID">{{ openid }}</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{ userInfo.createTime }}</el-descriptions-item> <el-descriptions-item label="创建时间" :span="2">{{ userInfo.createTime }}</el-descriptions-item>
<el-descriptions-item label="微信余额" :span="2">{{ formatFenToYuan((userInfo as WxUserDTO).wxBalance)
}}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ (userInfo as WxUserDTO).remark || '无' <el-descriptions-item label="备注" :span="2">{{ (userInfo as WxUserDTO).remark || '无'
}}</el-descriptions-item> }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
@ -549,5 +554,13 @@ async function handleShowAddTagDialog() {
.el-pagination { .el-pagination {
margin-top: 10px; margin-top: 10px;
} }
.user-details {
:deep(.el-descriptions__label) {
width: 70px;
min-width: 70px;
text-align: left;
}
}
} }
</style> </style>

View File

@ -12,7 +12,7 @@ import Refresh from "@iconify-icons/ep/refresh";
import Plus from "@iconify-icons/ep/plus"; import Plus from "@iconify-icons/ep/plus";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import { getAb98UserTagNamesApi } from "@/api/ab98/tag"; import { getMembershipTagListApi, type MembershipTagQuery } from "@/api/membership/membershipTag";
import { formatFenToYuan } from "@/utils/currency"; import { formatFenToYuan } from "@/utils/currency";
defineOptions({ defineOptions({
@ -22,13 +22,13 @@ defineOptions({
const router = useRouter(); const router = useRouter();
const formRef = ref(); const formRef = ref();
const wxStore = useWxStore(); const wxStore = useWxStore();
const tagOptions = ref<string[]>([]); const tagOptions = ref<{ label: string; value: number }[]>([]);
const searchFormParams = reactive<Ab98UserQuery & { search?: string }>({ const searchFormParams = reactive<Ab98UserQuery & { search?: string; tagIds?: string }>({
corpid: wxStore.corpid || "", corpid: wxStore.corpid || "",
name: undefined, name: undefined,
tel: undefined, tel: undefined,
idnum: undefined, idnum: undefined,
tagName: undefined, tagIds: undefined,
hasAb98UserId: undefined, hasAb98UserId: undefined,
search: undefined search: undefined
}); });
@ -62,8 +62,16 @@ async function onSearch() {
async function getList() { async function getList() {
CommonUtils.fillPaginationParams(searchFormParams, pagination); CommonUtils.fillPaginationParams(searchFormParams, pagination);
// ID
const params = { ...searchFormParams };
if (params.tagIds && Array.isArray(params.tagIds) && params.tagIds.length > 0) {
params.tagIds = params.tagIds.join(',');
} else {
Reflect.deleteProperty(params, 'tagIds');
}
pageLoading.value = true; pageLoading.value = true;
const { data } = await getAb98UserListApiWithWx(searchFormParams).finally( const { data } = await getAb98UserListApiWithWx(params).finally(
() => { () => {
pageLoading.value = false; pageLoading.value = false;
} }
@ -98,6 +106,7 @@ const resetForm = formEl => {
if (!formEl) return; if (!formEl) return;
formEl.resetFields(); formEl.resetFields();
searchFormParams.hasAb98UserId = undefined; searchFormParams.hasAb98UserId = undefined;
searchFormParams.tagIds = undefined;
onSearch(); onSearch();
}; };
@ -172,10 +181,26 @@ const addMemberRules = reactive({
onMounted(() => { onMounted(() => {
getList(); getList();
getAb98UserTagNamesApi().then(res => { getMembershipTagListApi({ corpid: wxStore.corpid || "" }).then(res => {
tagOptions.value = res.data; tagOptions.value = res.data.rows.map(tag => ({
label: tag.name || '',
value: tag.id || 0
}));
}); });
}) });
const getIconByName = (name: string) => {
switch (name) {
case 'Search':
return useRenderIcon(Search);
case 'Refresh':
return useRenderIcon(Refresh);
case 'Plus':
return useRenderIcon(Plus);
default:
return '';
}
}
</script> </script>
<template> <template>
@ -187,11 +212,12 @@ onMounted(() => {
<el-input v-model="searchFormParams.search" placeholder="请输入姓名/手机号/身份证" clearable class="!w-[300px]" <el-input v-model="searchFormParams.search" placeholder="请输入姓名/手机号/身份证" clearable class="!w-[300px]"
@keydown.enter.prevent="onSearch" @change="handleSearchInput" /> @keydown.enter.prevent="onSearch" @change="handleSearchInput" />
</el-form-item> </el-form-item>
<!-- <el-form-item prop="tagName"> <el-form-item prop="tagIds">
<el-select v-model="searchFormParams.tagName" placeholder="请选择用户标签" clearable class="!w-[160px]"> <el-select v-model="searchFormParams.tagIds" placeholder="会员标签" multiple clearable collapse-tags
<el-option v-for="item in tagOptions" :key="item" :label="item" :value="item" /> class="!w-[200px]">
<el-option v-for="item in tagOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</el-form-item> --> </el-form-item>
<el-form-item prop="hasAb98UserId"> <el-form-item prop="hasAb98UserId">
<el-select v-model="searchFormParams.hasAb98UserId" placeholder="绑定汇邦云状态" clearable class="!w-[160px]"> <el-select v-model="searchFormParams.hasAb98UserId" placeholder="绑定汇邦云状态" clearable class="!w-[160px]">
<el-option label="全部" :value="null" /> <el-option label="全部" :value="null" />
@ -200,20 +226,20 @@ onMounted(() => {
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" :icon="useRenderIcon(Search)" :loading="pageLoading" @click="onSearch"> <el-button type="primary" :icon="getIconByName('Search')" :loading="pageLoading" @click="onSearch">
搜索 搜索
</el-button> </el-button>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)"> <el-button :icon="getIconByName('Refresh')" @click="resetForm(formRef)">
重置 重置
</el-button> </el-button>
</el-form-item> </el-form-item>
<el-form-item class="space-item"> <el-form-item class="space-item">
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="success" :icon="useRenderIcon(Plus)" @click="openAddMemberDialog"> <el-button type="success" :icon="getIconByName('Plus')" @click="openAddMemberDialog">
添加会员 关联会员
</el-button> </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
@ -224,8 +250,8 @@ onMounted(() => {
:xs="24" :sm="12" :md="8" :lg="6"> :xs="24" :sm="12" :md="8" :lg="6">
<el-card class="user-card" :body-style="{ padding: '8px 20px' }" @click="handleViewDetail(item)"> <el-card class="user-card" :body-style="{ padding: '8px 20px' }" @click="handleViewDetail(item)">
<div class="card-content"> <div class="card-content">
<el-avatar :size="80" :src="item.faceImg" fit="cover" shape="square" class="avatar"> <el-avatar :size="80" :src="item.faceImg || item.wxAvatar" fit="cover" shape="square" class="avatar">
<template v-if="!item.faceImg"> <template v-if="!item.faceImg && !item.wxAvatar">
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"> <svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="48" fill="#f5f5f5" stroke="#e0e0e0" stroke-width="1" /> <circle cx="50" cy="50" r="48" fill="#f5f5f5" stroke="#e0e0e0" stroke-width="1" />
<circle cx="50" cy="40" r="12" fill="#9e9e9e" /> <circle cx="50" cy="40" r="12" fill="#9e9e9e" />
@ -261,8 +287,8 @@ onMounted(() => {
</div> </div>
</div> </div>
<!-- 添加会员弹窗 --> <!-- 关联会员弹窗 -->
<el-dialog v-model="dialogVisible" title="添加会员" width="500px" :close-on-click-modal="false"> <el-dialog v-model="dialogVisible" title="关联会员" width="500px" :close-on-click-modal="false">
<el-form ref="addMemberFormRef" :model="addMemberForm" :rules="addMemberRules" label-width="100px"> <el-form ref="addMemberFormRef" :model="addMemberForm" :rules="addMemberRules" label-width="100px">
<el-form-item label="动态码" prop="dynamicCode"> <el-form-item label="动态码" prop="dynamicCode">
<el-input v-model="addMemberForm.dynamicCode" placeholder="请输入动态码" /> <el-input v-model="addMemberForm.dynamicCode" placeholder="请输入动态码" />