Quasar 企业级用户详情组件实现文档
Quasar 企业级用户详情组件实现文档
组件概述
本组件基于 Quasar 组合式 API 与 TypeScript 实现,提供企业级用户详情展示功能,包含完整的状态管理、权限控制、错误处理和审计日志能力。组件采用响应式设计,适配不同屏幕尺寸,并符合现代企业应用的 UI/UX 标准。
完整代码实现
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useQuasar } from 'quasar'
import { useAuthStore } from 'stores/auth'
import {
QAvatar,
QBadge,
QBtn,
QCard,
QCardSection,
QIcon,
QItem,
QItemLabel,
QItemSection,
QList,
QPage,
QSeparator,
QSkeleton,
QSpace,
QTimeline,
QTimelineEntry
} from 'quasar'
import type { AxiosResponse } from 'axios'
import { type UserProfile, UserRole } from 'src/types/auth/profiles'
import { apiClient } from 'src/services/axios'
import { date } from 'quasar'
import { logSecurityEvent } from 'src/services/audit'
const $q = useQuasar()
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
// 状态管理
const loading = ref(true)
const error = ref<string | null>(null)
const userProfile = ref<UserProfile | null>(null)
const activityLogs = ref<Array<{action: string; timestamp: string}>>([])
// 获取用户ID
const userId = computed(() => route.params.userId as string)
// 权限检查
const isAdmin = computed(() => {
return (
authStore.userRole === UserRole.SYSTEM_ADMIN ||
authStore.userRole === UserRole.SECURITY_ADMIN
)
})
const isCurrentUser = computed(() => {
return authStore.userId === userId.value
})
// 获取用户详情
const fetchUserProfile = async () => {
try {
loading.value = true
error.value = null
const response: AxiosResponse<UserProfile> = await apiClient.get(
`/users/profile/${userId.value}/`
)
userProfile.value = response.data
// 模拟活动日志数据(实际项目中应从API获取)
activityLogs.value = [
{ action: '更新了个人信息', timestamp: '2025-08-01T14:30:00Z' },
{ action: '修改了密码', timestamp: '2025-07-25T09:15:00Z' },
{ action: '登录系统', timestamp: '2025-07-20T08:45:00Z' },
]
// 记录审计日志
logSecurityEvent('USER_PROFILE_VIEWED', {
targetUserId: userId.value,
viewerId: authStore.userId
})
} catch (err) {
handleProfileError(err)
} finally {
loading.value = false
}
}
// 错误处理
const handleProfileError = (err: unknown) => {
let errorMessage = '获取用户信息失败'
if (typeof err === 'object' && err !== null) {
if ('response' in err) {
const axiosError = err as { response?: { status: number; data?: unknown } }
switch (axiosError.response?.status) {
case 403:
errorMessage = '您没有权限查看此用户信息'
break
case 404:
errorMessage = '用户不存在或已被删除'
break
case 500:
errorMessage = '服务器错误,请稍后重试'
break
default:
if (axiosError.response?.data) {
const errorData = axiosError.response.data as { detail?: string }
errorMessage = errorData.detail || errorMessage
}
}
} else if ('message' in err) {
errorMessage = (err as { message: string }).message
}
}
error.value = errorMessage
$q.notify({
type: 'negative',
message: errorMessage,
position: 'top',
timeout: 5000
})
}
// 状态标签颜色
const getStatusColor = (status: string) => {
const statusMap: Record<string, string> = {
'正常': 'positive',
'已锁定': 'negative',
'已停用': 'grey',
'密码过期': 'warning',
'已删除': 'dark'
}
return statusMap[status] || 'info'
}
// 编辑用户
const editUser = () => {
router.push(`/profile/${userId.value}/edit`)
}
// 返回列表
const backToList = () => {
router.push('/users')
}
// 初始化加载
onMounted(() => {
if (!userId.value) {
error.value = '无效的用户ID'
return
}
void fetchUserProfile()
})
</script>
<template>
<QPage class="q-pa-lg">
<!-- 加载状态 -->
<template v-if="loading">
<div class="q-pa-xl text-center">
<QSkeleton type="QAvatar" size="100px" class="q-mb-md" />
<QSkeleton type="text" width="200px" class="q-mb-sm" />
<QSkeleton type="text" width="150px" />
<div class="row q-mt-xl q-gutter-md justify-center">
<div v-for="n in 3" :key="n" class="col-3">
<QSkeleton type="rect" height="100px" />
</div>
</div>
</div>
</template>
<!-- 错误状态 -->
<template v-else-if="error">
<div class="text-center q-pa-xl">
<QIcon name="error_outline" size="xl" color="negative" class="q-mb-md" />
<div class="text-h6 q-mb-md text-negative">{{ error }}</div>
<QBtn label="返回用户列表" color="primary" @click="backToList" />
</div>
</template>
<!-- 用户详情内容 -->
<template v-else-if="userProfile">
<div class="row justify-between items-center q-mb-lg">
<div class="text-h4">用户详情</div>
<div>
<QBtn
label="返回列表"
color="grey-7"
flat
class="q-mr-sm"
@click="backToList"
/>
<QBtn
v-if="isAdmin || isCurrentUser"
label="编辑信息"
color="primary"
icon="edit"
@click="editUser"
/>
</div>
</div>
<QCard class="q-mb-lg">
<QCardSection>
<div class="row items-center q-col-gutter-lg">
<!-- 头像区域 -->
<div class="col-auto">
<QAvatar size="120px">
<img
:src="userProfile.avatar || 'default-avatar.png'"
:alt="userProfile.nickname"
>
</QAvatar>
</div>
<!-- 基本信息 -->
<div class="col">
<div class="text-h5 q-mb-sm">
{{ userProfile.nickname }}
<QBadge
:label="userProfile.status_display"
:color="getStatusColor(userProfile.status_display)"
class="q-ml-sm"
/>
</div>
<div class="text-subtitle1 text-grey-7 q-mb-md">
{{ userProfile.username }} · {{ userProfile.role_display }}
</div>
<div class="row q-col-gutter-lg">
<div class="col-auto">
<div class="text-caption text-grey-6">用户ID</div>
<div>{{ userProfile.id }}</div>
</div>
<div class="col-auto">
<div class="text-caption text-grey-6">部门</div>
<div>{{ userProfile.department_info?.name || '未分配' }}</div>
</div>
<div class="col-auto">
<div class="text-caption text-grey-6">手机</div>
<div>{{ userProfile.phone || '未绑定' }}</div>
</div>
<div class="col-auto">
<div class="text-caption text-grey-6">邮箱</div>
<div>{{ userProfile.email || '未绑定' }}</div>
</div>
</div>
</div>
</div>
</QCardSection>
<QSeparator />
<!-- 详细信息 -->
<QCardSection>
<div class="row q-col-gutter-lg">
<div class="col-12 col-md-6">
<div class="text-h6 q-mb-md">账户信息</div>
<QList bordered separator>
<QItem>
<QItemSection>
<QItemLabel caption>创建时间</QItemLabel>
<QItemLabel>
{{ date.formatDate(userProfile.created_at, 'YYYY-MM-DD HH:mm:ss') }}
</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption>最后登录</QItemLabel>
<QItemLabel>
{{ userProfile.last_login ?
date.formatDate(userProfile.last_login, 'YYYY-MM-DD HH:mm:ss') :
'从未登录' }}
</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption>登录次数</QItemLabel>
<QItemLabel>
{{ userProfile.login_count || 0 }} 次
</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption>密码更新时间</QItemLabel>
<QItemLabel>
{{ userProfile.password_updated_at ?
date.formatDate(userProfile.password_updated_at, 'YYYY-MM-DD') :
'未知' }}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</div>
<div class="col-12 col-md-6">
<div class="text-h6 q-mb-md">安全信息</div>
<QList bordered separator>
<QItem>
<QItemSection>
<QItemLabel caption>双因素认证</QItemLabel>
<QItemLabel>
<QIcon
:name="userProfile.mfa_enabled ? 'check_circle' : 'cancel'"
:color="userProfile.mfa_enabled ? 'positive' : 'negative'"
class="q-mr-xs"
/>
{{ userProfile.mfa_enabled ? '已启用' : '未启用' }}
</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption>最后活动时间</QItemLabel>
<QItemLabel>
{{ userProfile.last_active ?
date.formatDate(userProfile.last_active, 'YYYY-MM-DD HH:mm') :
'无记录' }}
</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption>账户锁定</QItemLabel>
<QItemLabel>
<QIcon
:name="userProfile.is_locked ? 'lock' : 'lock_open'"
:color="userProfile.is_locked ? 'negative' : 'positive'"
class="q-mr-xs"
/>
{{ userProfile.is_locked ? '已锁定' : '未锁定' }}
</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption>IP限制</QItemLabel>
<QItemLabel>
{{ userProfile.ip_restriction ? '已启用' : '未启用' }}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</div>
</div>
</QCardSection>
</QCard>
<!-- 活动时间线 -->
<div class="text-h5 q-mt-xl q-mb-md">最近活动</div>
<QCard>
<QCardSection>
<QTimeline color="secondary" layout="dense" v-if="activityLogs.length > 0">
<QTimelineEntry
v-for="(log, index) in activityLogs"
:key="index"
:title="log.action"
:subtitle="date.formatDate(log.timestamp, 'YYYY-MM-DD HH:mm')"
icon="history"
color="primary"
/>
</QTimeline>
<div v-else class="text-center text-grey-7 q-py-xl">
<QIcon name="history" size="xl" class="q-mb-sm" />
<div>暂无活动记录</div>
</div>
</QCardSection>
</QCard>
</template>
</QPage>
</template>
<style lang="scss" scoped>
.q-card {
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
&__section {
padding: 24px;
}
}
.q-timeline {
padding: 0 16px;
&::before {
left: 8px !important;
}
&-entry {
padding-bottom: 24px;
&__title {
font-weight: 500;
font-size: 1rem;
}
&__subtitle {
font-size: 0.85rem;
color: #6c757d;
}
}
}
</style>
核心特性说明
1. 技术架构
- 组合式API:采用<script setup lang="ts">语法,实现更清晰的代码组织
- TypeScript支持:强类型定义(UserProfile接口等),避免any类型
- 响应式状态:使用ref和computed管理组件状态
- 生命周期管理:通过onMounted处理初始化逻辑
- 完整状态管理:
2. 企业级功能
o 加载状态(骨架屏Skeleton)
o 错误状态(详细错误提示)
o 空数据处理
- 权限控制:
o 基于角色的访问控制(管理员/当前用户)
o 条件渲染编辑按钮
- 安全审计:
o 集成审计日志(logSecurityEvent)
o 记录用户资料查看行为
- 错误处理:
o 分类错误处理(403/404/500等状态码)
o 用户友好的错误提示
o Quasar Notify通知集成
3. UI/UX设计
- 响应式布局:适配移动端和桌面端
- 视觉层次:通过卡片、列表和空间分隔创建清晰结构
- 状态可视化:
o 状态徽章颜色编码(正常/已锁定/已停用等)
o 图标指示(MFA状态、账户锁定状态)
- 加载体验:骨架屏减少感知等待时间
- 交互反馈:按钮状态变化、通知提示
- 用户基本信息:头像、昵称、状态、角色等
- 账户信息:创建时间、登录记录、密码更新时间
- 安全信息:双因素认证、账户锁定状态、IP限制
- 活动时间线:最近操作记录可视化
4. 数据展示
集成说明
1. 路由配置:推荐路由路径/profile/:userId
2. 依赖项:
o Quasar UI组件库
o Vue Router(路由导航)
o Pinia/Vuex(状态管理,如useAuthStore)
o Axios(API请求,apiClient)
3. 类型定义:需确保UserProfile接口与后端API响应匹配
4. 审计日志:logSecurityEvent需后端支持审计记录存储
使用场景
- 企业后台用户管理模块
- 员工信息查看页面
- 账户安全中心
- 管理员用户详情页
该组件可直接集成到现有Quasar应用中,提供即插即用的企业级