feat(用户管理): 添加微信头像字段并优化标签功能
- 在Ab98UserDTO接口中添加wxAvatar字段 - 将tagName查询参数改为tagIds以支持多标签筛选 - 重构图标渲染逻辑为通用函数getIconByName - 优化用户详情页标签管理功能,添加标签后自动刷新可用标签列表 - 在用户列表页添加多标签筛选功能 - 调整用户卡片头像显示逻辑,优先显示faceImg,其次显示wxAvatar
This commit is contained in:
parent
b45ca4022a
commit
02bc7e6303
|
|
@ -39,6 +39,8 @@ export interface Ab98UserDTO {
|
|||
wxNickName?: string;
|
||||
/** 微信用户openid */
|
||||
wxUserOpenid?: string;
|
||||
/** 微信用户头像 */
|
||||
wxAvatar?: string;
|
||||
}
|
||||
export interface Ab98UserDetailDTO {
|
||||
/** 主键ID */
|
||||
|
|
@ -87,7 +89,8 @@ export interface Ab98UserQuery extends BasePageQuery {
|
|||
tel?: string;
|
||||
/** 身份证号码 */
|
||||
idnum?: string;
|
||||
tagName?: string;
|
||||
/** 标签ID,多个标签ID用逗号分隔 */
|
||||
tagIds?: string;
|
||||
/** 绑定汇邦云 false未绑定 true已绑定 null 全部 */
|
||||
hasAb98UserId?: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
getList();
|
||||
});
|
||||
|
|
@ -265,19 +282,19 @@ onMounted(() => {
|
|||
</el-select>
|
||||
</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-form-item>
|
||||
<el-form-item>
|
||||
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
|
||||
<el-button :icon="getIconByName('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 type="success" :icon="getIconByName('Plus')" @click="() => openDrawer()">
|
||||
新增标签
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
|
|
@ -311,10 +328,10 @@ onMounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
<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 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>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -164,7 +164,15 @@ async function fetchAvailableTags() {
|
|||
pageNum: 1,
|
||||
pageSize: 100
|
||||
});
|
||||
availableTags.value = data.rows;
|
||||
// 过滤掉用户已经拥有的标签(如果用户标签已加载)
|
||||
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;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch available tags:', error);
|
||||
}
|
||||
|
|
@ -190,6 +198,8 @@ async function handleAddTag() {
|
|||
showAddTagDialog.value = false;
|
||||
addTagForm.value.tagId = null;
|
||||
await fetchUserTags();
|
||||
// 重新获取可用标签列表,过滤掉用户新添加的标签
|
||||
await fetchAvailableTags();
|
||||
ElMessage.success('标签添加成功');
|
||||
} catch (error) {
|
||||
ElMessage.error('标签添加失败');
|
||||
|
|
@ -256,16 +266,24 @@ async function handleShowAddTagDialog() {
|
|||
<template v-if="userType === 'ab98'">
|
||||
<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 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>
|
||||
</template>
|
||||
<template v-else>
|
||||
<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.tel }}</el-descriptions-item>
|
||||
<el-descriptions-item label="微信余额">{{ formatFenToYuan((userInfo as WxUserDTO).wxBalance)
|
||||
}}</el-descriptions-item>
|
||||
</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-card>
|
||||
<el-card class="info-card">
|
||||
|
|
@ -291,20 +309,9 @@ async function handleShowAddTagDialog() {
|
|||
</el-button>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="剩余借呗">{{ formatFenToYuan((userInfo as Ab98UserDetailDTO).balance)
|
||||
}}</el-descriptions-item>
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="已使用借呗">{{ formatFenToYuan((userInfo as Ab98UserDetailDTO).useBalance)
|
||||
}}</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-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
|
@ -312,10 +319,8 @@ async function handleShowAddTagDialog() {
|
|||
<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="创建时间" :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>
|
||||
}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</template>
|
||||
|
|
@ -403,7 +408,7 @@ async function handleShowAddTagDialog() {
|
|||
</el-table-column>
|
||||
<el-table-column label="创建时间" prop="createTime" width="180">
|
||||
<template #default="{ row }">{{ row.createTime ? new Date(row.createTime).toLocaleString() : '-'
|
||||
}}</template>
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="balanceLogPagination.currentPage"
|
||||
|
|
@ -549,5 +554,13 @@ async function handleShowAddTagDialog() {
|
|||
.el-pagination {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 70px;
|
||||
min-width: 70px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -12,7 +12,7 @@ import Refresh from "@iconify-icons/ep/refresh";
|
|||
import Plus from "@iconify-icons/ep/plus";
|
||||
import { useRouter } from "vue-router";
|
||||
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";
|
||||
|
||||
defineOptions({
|
||||
|
|
@ -22,13 +22,13 @@ defineOptions({
|
|||
const router = useRouter();
|
||||
const formRef = ref();
|
||||
const wxStore = useWxStore();
|
||||
const tagOptions = ref<string[]>([]);
|
||||
const searchFormParams = reactive<Ab98UserQuery & { search?: string }>({
|
||||
const tagOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const searchFormParams = reactive<Ab98UserQuery & { search?: string; tagIds?: string }>({
|
||||
corpid: wxStore.corpid || "",
|
||||
name: undefined,
|
||||
tel: undefined,
|
||||
idnum: undefined,
|
||||
tagName: undefined,
|
||||
tagIds: undefined,
|
||||
hasAb98UserId: undefined,
|
||||
search: undefined
|
||||
});
|
||||
|
|
@ -62,8 +62,16 @@ async function onSearch() {
|
|||
async function getList() {
|
||||
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;
|
||||
const { data } = await getAb98UserListApiWithWx(searchFormParams).finally(
|
||||
const { data } = await getAb98UserListApiWithWx(params).finally(
|
||||
() => {
|
||||
pageLoading.value = false;
|
||||
}
|
||||
|
|
@ -98,6 +106,7 @@ const resetForm = formEl => {
|
|||
if (!formEl) return;
|
||||
formEl.resetFields();
|
||||
searchFormParams.hasAb98UserId = undefined;
|
||||
searchFormParams.tagIds = undefined;
|
||||
onSearch();
|
||||
};
|
||||
|
||||
|
|
@ -172,10 +181,26 @@ const addMemberRules = reactive({
|
|||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
getAb98UserTagNamesApi().then(res => {
|
||||
tagOptions.value = res.data;
|
||||
getMembershipTagListApi({ corpid: wxStore.corpid || "" }).then(res => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
|
@ -187,11 +212,12 @@ onMounted(() => {
|
|||
<el-input v-model="searchFormParams.search" placeholder="请输入姓名/手机号/身份证" clearable class="!w-[300px]"
|
||||
@keydown.enter.prevent="onSearch" @change="handleSearchInput" />
|
||||
</el-form-item>
|
||||
<!-- <el-form-item 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-form-item prop="tagIds">
|
||||
<el-select v-model="searchFormParams.tagIds" placeholder="会员标签" multiple clearable collapse-tags
|
||||
class="!w-[200px]">
|
||||
<el-option v-for="item in tagOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item> -->
|
||||
</el-form-item>
|
||||
<el-form-item prop="hasAb98UserId">
|
||||
<el-select v-model="searchFormParams.hasAb98UserId" placeholder="绑定汇邦云状态" clearable class="!w-[160px]">
|
||||
<el-option label="全部" :value="null" />
|
||||
|
|
@ -200,20 +226,20 @@ onMounted(() => {
|
|||
</el-select>
|
||||
</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-form-item>
|
||||
<el-form-item>
|
||||
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
|
||||
<el-button :icon="getIconByName('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="openAddMemberDialog">
|
||||
添加会员
|
||||
<el-button type="success" :icon="getIconByName('Plus')" @click="openAddMemberDialog">
|
||||
关联会员
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
|
@ -224,8 +250,8 @@ onMounted(() => {
|
|||
:xs="24" :sm="12" :md="8" :lg="6">
|
||||
<el-card class="user-card" :body-style="{ padding: '8px 20px' }" @click="handleViewDetail(item)">
|
||||
<div class="card-content">
|
||||
<el-avatar :size="80" :src="item.faceImg" fit="cover" shape="square" class="avatar">
|
||||
<template v-if="!item.faceImg">
|
||||
<el-avatar :size="80" :src="item.faceImg || item.wxAvatar" fit="cover" shape="square" class="avatar">
|
||||
<template v-if="!item.faceImg && !item.wxAvatar">
|
||||
<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="40" r="12" fill="#9e9e9e" />
|
||||
|
|
@ -261,8 +287,8 @@ onMounted(() => {
|
|||
</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-item label="动态码" prop="dynamicCode">
|
||||
<el-input v-model="addMemberForm.dynamicCode" placeholder="请输入动态码" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue