shop-front-end/src/views/user/ab98/detail.vue

566 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { ref, onMounted, watch, computed } from "vue";
import { useRoute } from "vue-router";
import { type Ab98UserDetailDTO, getAb98UserDetailApi } from "@/api/ab98/user";
import { type WxUserDTO, getWxUserDetailApi } from "@/api/wx/wxUser";
import { getOrderListApi, type OrderDTO } from "@/api/shop/order";
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 { PureTableBar } from "@/components/RePureTableBar";
import { useWxStore } from "@/store/modules/wx";
import { formatFenToYuan } from "@/utils/currency";
import BalanceEditModal from "./BalanceEditModal.vue";
import { getUserBalanceLogListApi, type UserBalanceLog, type UserBalanceLogListParams } from "@/api/ab98/balanceLog";
defineOptions({
name: "Ab98UserDetail"
});
const route = useRoute();
const wxStore = useWxStore();
const userInfo = ref<Ab98UserDetailDTO | WxUserDTO>({});
const userType = ref<'ab98' | 'wx'>('ab98'); // 用户类型ab98用户或微信用户
const loading = ref(false);
const tags = ref<MembershipTagMemberDTO[]>([]);
const tagsLoading = ref(false);
const showAddTagDialog = ref(false);
const availableTags = ref<MembershipTagDTO[]>([]);
const addTagForm = ref({
tagId: null as number | null
});
const balanceVisible = ref(false);
// 基础信息
const basicInfo = ref({
registerTime: "2023-01-15",
lastLogin: "2023-06-20",
loginCount: 42,
device: "iPhone 13"
});
// 订单记录
const orderRecords = ref<OrderDTO[]>([]);
const pagination = ref({
pageSize: 5,
currentPage: 1,
total: 0
});
const orderLoading = ref(false);
const activeTab = ref('basic');
// 借呗记录
const balanceLogRecords = ref<UserBalanceLog[]>([]);
const balanceLogPagination = ref({
pageSize: 10,
currentPage: 1,
total: 0
});
const balanceLogLoading = ref(false);
const openid = computed(() => {
if (userType.value === 'ab98') {
return (userInfo.value as Ab98UserDetailDTO).wxUserList?.[0]?.openid || '';
} else {
return (userInfo.value as WxUserDTO).openid || '';
}
});
async function fetchOrders() {
try {
orderLoading.value = true;
const params: any = {
pageSize: pagination.value.pageSize,
pageNum: pagination.value.currentPage
};
if (userType.value === 'ab98') {
params.ab98UserId = (userInfo.value as Ab98UserDetailDTO).ab98UserId;
params.openid = userInfo.value.openid;
} else {
params.openid = (userInfo.value as WxUserDTO).openid;
}
const { data } = await getOrderListApi(params);
orderRecords.value = data.rows;
pagination.value.total = data.total;
} finally {
orderLoading.value = false;
}
}
async function fetchBalanceLogs() {
try {
if (userType.value !== 'ab98') return;
const userBalanceId = (userInfo.value as Ab98UserDetailDTO).userBalanceEntity?.userBalanceId;
if (!userBalanceId) {
balanceLogRecords.value = [];
return;
}
balanceLogLoading.value = true;
const params: UserBalanceLogListParams = {
pageSize: balanceLogPagination.value.pageSize,
pageNum: balanceLogPagination.value.currentPage,
userBalanceId
};
const { data } = await getUserBalanceLogListApi(params);
balanceLogRecords.value = data.rows;
balanceLogPagination.value.total = data.total;
} finally {
balanceLogLoading.value = false;
}
}
watch(activeTab, (newVal) => {
if (newVal === 'orders') {
fetchOrders();
} else if (newVal === 'balanceLog') {
fetchBalanceLogs();
}
});
async function fetchUserDetail() {
try {
loading.value = true;
const userId = route.query.id;
const wxId = route.query.wxId;
if (userId) {
userType.value = 'ab98';
const { data } = await getAb98UserDetailApi(Number(userId), wxStore.corpid);
userInfo.value = data;
await fetchUserTags();
} else if (wxId) {
userType.value = 'wx';
const { data } = await getWxUserDetailApi(Number(wxId));
userInfo.value = data;
}
} finally {
loading.value = false;
}
}
async function fetchUserTags() {
try {
if (userType.value !== 'ab98') return;
tagsLoading.value = true;
const { data } = await getUserTagsApi({
corpid: wxStore.corpid,
ab98UserId: (userInfo.value as Ab98UserDetailDTO).ab98UserId
});
tags.value = data;
} finally {
tagsLoading.value = false;
}
}
async function fetchAvailableTags() {
try {
const { data } = await getMembershipTagListApi({
corpid: wxStore.corpid,
pageNum: 1,
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;
}
} catch (error) {
console.error('Failed to fetch available tags:', error);
}
}
async function handleAddTag() {
try {
if (userType.value !== 'ab98') return;
if (!addTagForm.value.tagId) {
ElMessage.warning('请选择一个标签');
return;
}
tagsLoading.value = true;
// 将标签关联到用户
await addMembershipTagMemberApi({
corpid: wxStore.corpid,
tagId: addTagForm.value.tagId,
ab98UserId: (userInfo.value as Ab98UserDetailDTO).ab98UserId
});
showAddTagDialog.value = false;
addTagForm.value.tagId = null;
await fetchUserTags();
// 重新获取可用标签列表,过滤掉用户新添加的标签
await fetchAvailableTags();
ElMessage.success('标签添加成功');
} catch (error) {
ElMessage.error('标签添加失败');
} finally {
tagsLoading.value = false;
}
}
async function handleDeleteTag(id: number) {
try {
await ElMessageBox.confirm('确定要删除这个标签吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
tagsLoading.value = true;
await deleteMembershipTagMemberApi([id]);
await fetchUserTags();
ElMessage.success('标签删除成功');
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('标签删除失败');
}
} finally {
tagsLoading.value = false;
}
}
onMounted(() => {
fetchUserDetail();
fetchAvailableTags();
});
async function handleModifyBalance() {
balanceVisible.value = true;
}
async function handleShowAddTagDialog() {
await fetchAvailableTags();
showAddTagDialog.value = true;
}
</script>
<template>
<div class="detail-container">
<div class="flex-container">
<el-card class="user-info-card">
<div class="user-header">
<el-avatar :size="100" :src="userType === 'ab98' ? (userInfo as Ab98UserDetailDTO).faceImg : undefined"
fit="cover" shape="square">
<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" />
<rect x="40" y="52" width="20" height="30" rx="2" fill="#9e9e9e" />
</svg>
</el-avatar>
<div class="user-name">{{ userType === 'ab98' ? (userInfo as Ab98UserDetailDTO).name : (userInfo as
WxUserDTO).nickName }}</div>
</div>
<el-divider />
<el-descriptions class="user-details" :column="1" border>
<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).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>
</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">
<div class="tab-header">
<el-tabs type="card" v-model="activeTab">
<el-tab-pane label="基础信息" name="basic"></el-tab-pane>
<el-tab-pane label="订单记录" name="orders"></el-tab-pane>
<el-tab-pane v-if="userType === 'ab98'" label="借呗记录" name="balanceLog"></el-tab-pane>
</el-tabs>
</div>
<template v-if="activeTab === 'basic'">
<template v-if="userType === 'ab98'">
<el-descriptions class="info-details" :column="2" border>
<el-descriptions-item label="会员ID">{{ (userInfo as Ab98UserDetailDTO).ab98UserId }}</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
Ab98UserDetailDTO).balanceLimit) }}
<el-button type="primary" size="small" @click="handleModifyBalance">
修改
</el-button>
</el-descriptions-item>
<el-descriptions-item label="剩余借呗">{{ formatFenToYuan((userInfo as Ab98UserDetailDTO).balance)
}}</el-descriptions-item>
<el-descriptions-item label="已使用借呗">{{ formatFenToYuan((userInfo as Ab98UserDetailDTO).useBalance)
}}</el-descriptions-item>
</el-descriptions>
</template>
<template v-else>
<el-descriptions class="info-details" :column="2" border>
<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">{{ (userInfo as WxUserDTO).remark || ''
}}</el-descriptions-item>
</el-descriptions>
</template>
</template>
<div class="info-details" v-if="activeTab === 'orders'">
<el-table ref="tableRef" v-loading="orderLoading" :data="orderRecords" row-key="orderId" border>
<el-table-column label="订单ID" prop="orderId" width="120" />
<el-table-column label="商品名称" prop="goodsNames" width="180">
<template #default="{ row }">
<span v-if="row.goodsNames">
{{ row.goodsNames.split(',').join(', ') }}
</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="商品封面" prop="coverImgs" width="100">
<template #default="{ row }">
<div v-if="row.coverImgs" class="flex gap-2">
<el-image v-for="(img, index) in row.coverImgs.split(',')" :key="index" :src="img"
:preview-src-list="row.coverImgs.split(',')" :z-index="9999" :preview-teleported="true"
:hide-on-click-modal="true" fit="cover" class="rounded" width="60" height="60" />
</div>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="姓名" prop="name" width="120" />
<el-table-column label="手机号" prop="mobile" width="120" />
<el-table-column label="订单金额" prop="totalAmount" width="120">
<template #default="{ row }">{{ row.totalAmount }}元</template>
</el-table-column>
<el-table-column label="订单状态" prop="status" width="120">
<template #default="{ row }">
<el-tag :type="row.status === 2 ? 'success' : row.status === 5 ? 'danger' : 'info'">
{{ { 1: '待付款', 2: '已付款', 3: '已发货', 4: '已完成', 5: '已取消' }[row.status] }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="支付状态" prop="payStatus" width="120">
<template #default="{ row }">
<el-tag :type="row.payStatus === 2 ? 'success' : 'info'">
{{ { 1: '未支付', 2: '已支付', 3: '退款中', 4: '已退款' }[row.payStatus] }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="支付方式" prop="paymentMethod" width="120">
<template #default="{ row }">
{{ { wechat: '微信支付', balance: '借呗支付' }[row.paymentMethod] || row.paymentMethod }}
</template>
</el-table-column>
<el-table-column label="支付时间" prop="payTime" width="180">
<template #default="{ row }">
{{ row.payTime ? new Date(row.payTime).toLocaleString() : '-' }}
</template>
</el-table-column>
</el-table>
<el-pagination v-model:current-page="pagination.currentPage" v-model:page-size="pagination.pageSize"
:page-sizes="[5, 10, 20, 50]" layout="total, sizes, prev, pager, next, jumper" :total="pagination.total"
@size-change="fetchOrders" @current-change="fetchOrders" />
</div>
<div class="info-details" v-if="activeTab === 'balanceLog' && userType === 'ab98'">
<el-table v-loading="balanceLogLoading" :data="balanceLogRecords" row-key="logId" border>
<el-table-column label="日志ID" prop="logId" width="100" />
<el-table-column label="变更类型" prop="changeType" width="120">
<template #default="{ row }">
{{ { 1: '消费', 2: '审批归还', 3: '系统调整' }[row.changeType] || row.changeType }}
</template>
</el-table-column>
<el-table-column label="变更金额" prop="changeAmount" width="120">
<template #default="{ row }">{{ formatFenToYuan(row.changeAmount) }}</template>
</el-table-column>
<el-table-column label="变更前已用余额" prop="useBalanceBefore" width="150">
<template #default="{ row }">{{ formatFenToYuan(row.useBalanceBefore) }}</template>
</el-table-column>
<el-table-column label="变更后已用余额" prop="useBalanceAfter" width="150">
<template #default="{ row }">{{ formatFenToYuan(row.useBalanceAfter) }}</template>
</el-table-column>
<el-table-column label="关联订单ID" prop="orderId" width="120">
<template #default="{ row }">{{ row.orderId || '-' }}</template>
</el-table-column>
<el-table-column label="关联审批ID" prop="approvalId" width="120">
<template #default="{ row }">{{ row.approvalId || '-' }}</template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime" width="180">
<template #default="{ row }">{{ row.createTime ? new Date(row.createTime).toLocaleString() : '-'
}}</template>
</el-table-column>
</el-table>
<el-pagination v-model:current-page="balanceLogPagination.currentPage"
v-model:page-size="balanceLogPagination.pageSize" :page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper" :total="balanceLogPagination.total"
@size-change="fetchBalanceLogs" @current-change="fetchBalanceLogs" />
</div>
</el-card>
</div>
<el-dialog v-model="showAddTagDialog" title="添加标签" width="30%">
<el-form :model="addTagForm" label-width="80px">
<el-form-item label="选择标签">
<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>
<template #footer>
<el-button type="primary" @click="handleAddTag">确定</el-button>
</template>
</el-dialog>
<BalanceEditModal v-if="userType === 'ab98'" v-model:visible="balanceVisible" :row="userInfo"
@refresh="fetchUserDetail" />
</div>
</template>
<style scoped lang="scss">
.detail-container {
display: flex;
flex-direction: column;
height: 100%;
.flex-container {
display: flex;
flex: 1;
gap: 20px;
min-height: 0;
.user-info-card {
width: 20%;
}
.info-card {
width: 80%;
}
}
.user-info-card,
.shop-info-card {
height: 100%;
min-height: 85vh;
}
.user-header {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
.user-name {
margin-top: 15px;
font-size: 20px;
font-weight: bold;
}
}
.tab-header {
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;
background-color: #f9f9f9;
border-radius: 4px;
.order-id {
font-weight: bold;
margin-bottom: 8px;
}
.order-detail {
display: flex;
justify-content: space-between;
font-size: 13px;
color: #666;
}
}
.detail-item {
margin: 15px 0;
font-size: 14px;
.label {
color: #606266;
margin-right: 10px;
}
.value {
color: #303133;
}
}
.el-pagination {
margin-top: 10px;
}
.user-details {
:deep(.el-descriptions__label) {
width: 70px;
min-width: 70px;
text-align: left;
}
}
}
</style>