Files
akmon/pages/mall/admin/user-detail.uvue
2026-01-20 08:04:15 +08:00

909 lines
20 KiB
Plaintext

<!-- 管理端 - 用户详情页 -->
<template>
<view class="user-detail-page">
<!-- 用户基本信息 -->
<view class="user-profile">
<view class="profile-header">
<image :src="user.avatar_url || '/static/default-avatar.png'" class="user-avatar" />
<view class="user-basic">
<text class="user-name">{{ user.nickname || user.phone }}</text>
<text class="user-id">ID: {{ user.id }}</text>
<view class="user-status">
<text class="status-badge" :class="{ active: user.status === 1, inactive: user.status === 0 }">
{{ user.status === 1 ? '正常' : '冻结' }}
</text>
<text class="user-type">{{ getUserTypeText() }}</text>
</view>
</view>
<view class="action-menu" @click="showActionMenu">⋮</view>
</view>
<view class="profile-details">
<view class="detail-item">
<text class="detail-label">手机号码</text>
<text class="detail-value">{{ user.phone }}</text>
</view>
<view class="detail-item">
<text class="detail-label">邮箱地址</text>
<text class="detail-value">{{ user.email || '未设置' }}</text>
</view>
<view class="detail-item">
<text class="detail-label">性别</text>
<text class="detail-value">{{ getGenderText() }}</text>
</view>
<view class="detail-item">
<text class="detail-label">注册时间</text>
<text class="detail-value">{{ formatTime(user.created_at) }}</text>
</view>
</view>
</view>
<!-- 用户统计 -->
<view class="user-stats">
<view class="section-title">用户统计</view>
<view class="stats-grid">
<view class="stat-item">
<text class="stat-value">{{ userStats.total_orders }}</text>
<text class="stat-label">总订单数</text>
</view>
<view class="stat-item">
<text class="stat-value">¥{{ userStats.total_amount }}</text>
<text class="stat-label">消费总额</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ userStats.total_reviews }}</text>
<text class="stat-label">评价数量</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ userStats.avg_rating.toFixed(1) }}</text>
<text class="stat-label">平均评分</text>
</view>
</view>
</view>
<!-- 最近订单 -->
<view class="recent-orders">
<view class="section-header">
<text class="section-title">最近订单</text>
<text class="view-all" @click="viewAllOrders">查看全部</text>
</view>
<view v-if="recentOrders.length === 0" class="empty-orders">
<text class="empty-text">暂无订单记录</text>
</view>
<view v-for="order in recentOrders" :key="order.id" class="order-item" @click="viewOrderDetail(order)">
<view class="order-header">
<text class="order-no">{{ order.order_no }}</text>
<text class="order-status" :class="getOrderStatusClass(order.status)">{{ getOrderStatusText(order.status) }}</text>
</view>
<view class="order-info">
<text class="order-amount">¥{{ order.actual_amount }}</text>
<text class="order-time">{{ formatTime(order.created_at) }}</text>
</view>
</view>
</view>
<!-- 用户行为记录 -->
<view class="user-activities">
<view class="section-header">
<text class="section-title">行为记录</text>
<text class="view-all" @click="viewAllActivities">查看全部</text>
</view>
<view v-for="activity in userActivities" :key="activity.id" class="activity-item">
<view class="activity-icon" :class="activity.type">{{ getActivityIcon(activity.type) }}</view>
<view class="activity-content">
<text class="activity-desc">{{ activity.description }}</text>
<text class="activity-time">{{ formatTime(activity.created_at) }}</text>
</view>
</view>
</view>
<!-- 风险评估 -->
<view class="risk-assessment">
<view class="section-title">风险评估</view>
<view class="risk-score">
<view class="score-circle" :class="getRiskLevel()">
<text class="score-value">{{ riskData.score }}</text>
<text class="score-max">/100</text>
</view>
<view class="risk-info">
<text class="risk-level">{{ getRiskLevelText() }}</text>
<text class="risk-desc">{{ getRiskDescription() }}</text>
</view>
</view>
<view class="risk-factors">
<view v-for="factor in riskData.factors" :key="factor.type" class="factor-item">
<text class="factor-label">{{ factor.label }}</text>
<view class="factor-bar">
<view class="factor-fill" :style="{ width: factor.value + '%' }" :class="factor.level"></view>
</view>
<text class="factor-value">{{ factor.value }}%</text>
</view>
</view>
</view>
<!-- 操作记录 -->
<view class="admin-logs">
<view class="section-header">
<text class="section-title">操作记录</text>
<text class="add-log" @click="addAdminLog">添加记录</text>
</view>
<view v-for="log in adminLogs" :key="log.id" class="log-item">
<view class="log-content">
<text class="log-action">{{ log.action }}</text>
<text class="log-reason">{{ log.reason }}</text>
</view>
<view class="log-meta">
<text class="log-admin">{{ log.admin_name }}</text>
<text class="log-time">{{ formatTime(log.created_at) }}</text>
</view>
</view>
</view>
<!-- 操作菜单弹窗 -->
<view v-if="showMenu" class="action-modal" @click="hideActionMenu">
<view class="action-list" @click.stop>
<view class="action-item" @click="toggleUserStatus">
{{ user.status === 1 ? '冻结用户' : '解冻用户' }}
</view>
<view class="action-item" @click="resetPassword">重置密码</view>
<view class="action-item" @click="sendMessage">发送消息</view>
<view class="action-item danger" @click="deleteUser">删除用户</view>
</view>
</view>
</view>
</template>
<script>
import { UserType, OrderType } from '@/types/mall-types.uts'
type UserStatsType = {
total_orders: number
total_amount: number
total_reviews: number
avg_rating: number
}
type UserActivityType = {
id: string
type: string
description: string
created_at: string
}
type RiskFactorType = {
type: string
label: string
value: number
level: string
}
type RiskDataType = {
score: number
level: string
factors: Array<RiskFactorType>
}
type AdminLogType = {
id: string
action: string
reason: string
admin_name: string
created_at: string
}
export default {
data() {
return {
user: {
id: '',
phone: '',
email: '',
nickname: '',
avatar_url: '',
gender: 0,
user_type: 0,
status: 0,
created_at: ''
} as UserType,
userStats: {
total_orders: 0,
total_amount: 0,
total_reviews: 0,
avg_rating: 0
} as UserStatsType,
recentOrders: [] as Array<OrderType>,
userActivities: [] as Array<UserActivityType>,
riskData: {
score: 0,
level: '',
factors: []
} as RiskDataType,
adminLogs: [] as Array<AdminLogType>,
showMenu: false
}
},
onLoad(options: any) {
const userId = options.userId as string
if (userId) {
this.loadUserDetail(userId)
}
},
methods: {
loadUserDetail(userId: string) {
// 模拟加载用户详情数据
this.user = {
id: userId,
phone: '13800138000',
email: 'user@example.com',
nickname: '张三',
avatar_url: '/static/avatar1.jpg',
gender: 1,
user_type: 1,
status: 1,
created_at: '2023-06-15T10:30:00'
}
this.userStats = {
total_orders: 23,
total_amount: 5680.50,
total_reviews: 18,
avg_rating: 4.3
}
this.recentOrders = [
{
id: 'order_001',
order_no: 'ORD202401150001',
user_id: userId,
merchant_id: 'merchant_001',
status: 4,
total_amount: 299.98,
discount_amount: 30.00,
delivery_fee: 8.00,
actual_amount: 277.98,
payment_method: 1,
payment_status: 1,
delivery_address: {},
created_at: '2024-01-15T14:30:00'
}
]
this.userActivities = [
{
id: 'activity_001',
type: 'login',
description: '用户登录系统',
created_at: '2024-01-15T09:30:00'
},
{
id: 'activity_002',
type: 'order',
description: '创建订单 ORD202401150001',
created_at: '2024-01-15T14:30:00'
},
{
id: 'activity_003',
type: 'review',
description: '对商品进行评价',
created_at: '2024-01-14T16:20:00'
}
]
this.riskData = {
score: 25,
level: 'low',
factors: [
{ type: 'refund', label: '退款率', value: 15, level: 'low' },
{ type: 'complaint', label: '投诉率', value: 5, level: 'low' },
{ type: 'chargeback', label: '拒付率', value: 0, level: 'low' },
{ type: 'fraud', label: '欺诈风险', value: 10, level: 'low' }
]
}
this.adminLogs = [
{
id: 'log_001',
action: '账户验证',
reason: '用户实名认证通过',
admin_name: '管理员小李',
created_at: '2024-01-10T11:00:00'
}
]
},
getUserTypeText(): string {
const types = ['普通用户', '普通用户', '商家用户', '配送员', '管理员']
return types[this.user.user_type] || '未知'
},
getGenderText(): string {
const genders = ['未知', '男', '女']
return genders[this.user.gender] || '未知'
},
getOrderStatusText(status: number): string {
const statusTexts = ['异常', '待支付', '待发货', '待收货', '已完成', '已取消']
return statusTexts[status] || '未知'
},
getOrderStatusClass(status: number): string {
const statusClasses = ['error', 'pending', 'processing', 'shipping', 'completed', 'cancelled']
return statusClasses[status] || 'error'
},
getActivityIcon(type: string): string {
const icons: Record<string, string> = {
login: '🔐',
order: '🛍️',
review: '⭐',
refund: '💰',
complaint: '⚠️'
}
return icons[type] || '📝'
},
getRiskLevel(): string {
if (this.riskData.score < 30) return 'low'
if (this.riskData.score < 70) return 'medium'
return 'high'
},
getRiskLevelText(): string {
const level = this.getRiskLevel()
const levelTexts: Record<string, string> = {
low: '低风险',
medium: '中风险',
high: '高风险'
}
return levelTexts[level] || '未知'
},
getRiskDescription(): string {
const level = this.getRiskLevel()
const descriptions: Record<string, string> = {
low: '用户行为正常,风险较低',
medium: '存在一定风险,需要关注',
high: '高风险用户,建议重点监控'
}
return descriptions[level] || ''
},
formatTime(timeStr: string): string {
return timeStr.replace('T', ' ').split('.')[0]
},
showActionMenu() {
this.showMenu = true
},
hideActionMenu() {
this.showMenu = false
},
toggleUserStatus() {
const action = this.user.status === 1 ? '冻结' : '解冻'
uni.showModal({
title: `${action}用户`,
content: `确定要${action}用户 ${this.user.nickname} 吗?`,
success: (res) => {
if (res.confirm) {
this.user.status = this.user.status === 1 ? 0 : 1
this.hideActionMenu()
uni.showToast({
title: `${action}成功`,
icon: 'success'
})
}
}
})
},
resetPassword() {
uni.showModal({
title: '重置密码',
content: '确定要重置用户密码吗?新密码将发送到用户手机。',
success: (res) => {
if (res.confirm) {
this.hideActionMenu()
uni.showToast({
title: '密码重置成功',
icon: 'success'
})
}
}
})
},
sendMessage() {
this.hideActionMenu()
uni.navigateTo({
url: `/pages/mall/admin/send-message?userId=${this.user.id}`
})
},
deleteUser() {
uni.showModal({
title: '删除用户',
content: '删除用户将无法恢复,确定要删除吗?',
success: (res) => {
if (res.confirm) {
this.hideActionMenu()
uni.showToast({
title: '用户已删除',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
}
})
},
viewAllOrders() {
uni.navigateTo({
url: `/pages/mall/admin/user-orders?userId=${this.user.id}`
})
},
viewOrderDetail(order: OrderType) {
uni.navigateTo({
url: `/pages/mall/admin/order-detail?orderId=${order.id}`
})
},
viewAllActivities() {
uni.navigateTo({
url: `/pages/mall/admin/user-activities?userId=${this.user.id}`
})
},
addAdminLog() {
uni.navigateTo({
url: `/pages/mall/admin/add-log?userId=${this.user.id}`
})
}
}
}
</script>
<style>
.user-detail-page {
background-color: #f5f5f5;
min-height: 100vh;
}
.user-profile, .user-stats, .recent-orders, .user-activities, .risk-assessment, .admin-logs {
background-color: #fff;
margin-bottom: 20rpx;
padding: 30rpx;
}
.profile-header {
display: flex;
align-items: center;
margin-bottom: 30rpx;
}
.user-avatar {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
margin-right: 25rpx;
}
.user-basic {
flex: 1;
}
.user-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.user-id {
font-size: 24rpx;
color: #666;
margin-bottom: 10rpx;
}
.user-status {
display: flex;
align-items: center;
gap: 15rpx;
}
.status-badge {
padding: 8rpx 16rpx;
border-radius: 12rpx;
font-size: 22rpx;
color: #fff;
}
.status-badge.active {
background-color: #4caf50;
}
.status-badge.inactive {
background-color: #ff4444;
}
.user-type {
font-size: 22rpx;
color: #666;
background-color: #f0f0f0;
padding: 8rpx 16rpx;
border-radius: 12rpx;
}
.action-menu {
font-size: 32rpx;
color: #666;
padding: 10rpx;
}
.profile-details {
border-top: 1rpx solid #f5f5f5;
padding-top: 25rpx;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15rpx 0;
}
.detail-label {
font-size: 26rpx;
color: #666;
width: 150rpx;
}
.detail-value {
flex: 1;
font-size: 26rpx;
color: #333;
text-align: right;
}
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 25rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25rpx;
}
.view-all, .add-log {
font-size: 24rpx;
color: #007aff;
}
.stats-grid {
display: flex;
gap: 20rpx;
}
.stat-item {
flex: 1;
text-align: center;
padding: 30rpx 0;
background-color: #f8f9fa;
border-radius: 10rpx;
}
.stat-value {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 22rpx;
color: #666;
}
.empty-orders {
text-align: center;
padding: 60rpx 0;
}
.empty-text {
font-size: 24rpx;
color: #999;
}
.order-item, .log-item {
padding: 25rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.order-item:last-child, .log-item:last-child {
border-bottom: none;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10rpx;
}
.order-no {
font-size: 26rpx;
color: #333;
font-weight: bold;
}
.order-status {
font-size: 22rpx;
padding: 6rpx 12rpx;
border-radius: 10rpx;
color: #fff;
}
.order-status.pending {
background-color: #ffa726;
}
.order-status.processing {
background-color: #2196f3;
}
.order-status.shipping {
background-color: #9c27b0;
}
.order-status.completed {
background-color: #4caf50;
}
.order-status.cancelled {
background-color: #999;
}
.order-info {
display: flex;
justify-content: space-between;
align-items: center;
}
.order-amount {
font-size: 24rpx;
color: #ff4444;
font-weight: bold;
}
.order-time {
font-size: 22rpx;
color: #999;
}
.activity-item {
display: flex;
align-items: flex-start;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.activity-item:last-child {
border-bottom: none;
}
.activity-icon {
font-size: 28rpx;
margin-right: 15rpx;
margin-top: 5rpx;
}
.activity-content {
flex: 1;
}
.activity-desc {
font-size: 26rpx;
color: #333;
margin-bottom: 5rpx;
}
.activity-time {
font-size: 22rpx;
color: #999;
}
.risk-score {
display: flex;
align-items: center;
margin-bottom: 30rpx;
}
.score-circle {
width: 140rpx;
height: 140rpx;
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-right: 30rpx;
border: 6rpx solid;
}
.score-circle.low {
border-color: #4caf50;
background-color: #e8f5e8;
}
.score-circle.medium {
border-color: #ffa726;
background-color: #fff8e1;
}
.score-circle.high {
border-color: #ff4444;
background-color: #ffebee;
}
.score-value {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.score-max {
font-size: 20rpx;
color: #666;
margin-top: -5rpx;
}
.risk-info {
flex: 1;
}
.risk-level {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.risk-desc {
font-size: 24rpx;
color: #666;
line-height: 1.4;
}
.risk-factors {
margin-top: 30rpx;
}
.factor-item {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.factor-label {
font-size: 24rpx;
color: #666;
width: 120rpx;
}
.factor-bar {
flex: 1;
height: 12rpx;
background-color: #f0f0f0;
border-radius: 6rpx;
margin: 0 20rpx;
overflow: hidden;
}
.factor-fill {
height: 100%;
border-radius: 6rpx;
}
.factor-fill.low {
background-color: #4caf50;
}
.factor-fill.medium {
background-color: #ffa726;
}
.factor-fill.high {
background-color: #ff4444;
}
.factor-value {
font-size: 22rpx;
color: #333;
width: 60rpx;
text-align: right;
}
.log-content {
margin-bottom: 10rpx;
}
.log-action {
font-size: 26rpx;
color: #333;
font-weight: bold;
margin-bottom: 5rpx;
}
.log-reason {
font-size: 24rpx;
color: #666;
}
.log-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.log-admin {
font-size: 22rpx;
color: #007aff;
}
.log-time {
font-size: 22rpx;
color: #999;
}
.action-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.action-list {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx 0;
margin: 0 60rpx;
max-width: 500rpx;
}
.action-item {
padding: 25rpx 40rpx;
font-size: 28rpx;
color: #333;
text-align: center;
border-bottom: 1rpx solid #f5f5f5;
}
.action-item:last-child {
border-bottom: none;
}
.action-item.danger {
color: #ff4444;
}
</style>