eagleye

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. 企业级功能

加载状态(骨架屏Skeleton)

o 错误状态(详细错误提示)

o 空数据处理

  • 权限控制

基于角色的访问控制(管理员/当前用户)

o 条件渲染编辑按钮

  • 安全审计

集成审计日志(logSecurityEvent)

o 记录用户资料查看行为

  • 错误处理

分类错误处理(403/404/500等状态码)

o 用户友好的错误提示

o Quasar Notify通知集成

3. UI/UX设计

  • 响应式布局:适配移动端和桌面端
  • 视觉层次:通过卡片、列表和空间分隔创建清晰结构
  • 状态可视化

状态徽章颜色编码(正常/已锁定/已停用等)

图标指示(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应用中,提供即插即用的企业级

 

posted on 2025-08-06 10:06  GoGrid  阅读(8)  评论(0)    收藏  举报

导航