Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { ZqDialog } from '#/components/zq-dialog';
|
||||
import { $t } from '#/locales';
|
||||
import OrgChartPanel from '#/views/_core/org-chart/modules/OrgChartPanel.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'UserProfileDialog',
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
userId: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
}>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ZqDialog
|
||||
v-model="visible"
|
||||
:title="$t('user-avatar.profile.organization')"
|
||||
width="80%"
|
||||
:show-footer="false"
|
||||
content-height="70vh"
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="org-chart-wrapper">
|
||||
<OrgChartPanel :user-id="userId" :show-mode-toggle="true" />
|
||||
</div>
|
||||
</ZqDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.org-chart-wrapper {
|
||||
height: 65vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
export const UserAvatar = defineAsyncComponent(() =>
|
||||
import('./index.vue').then((module) => module.default),
|
||||
);
|
||||
@@ -0,0 +1,793 @@
|
||||
<script lang="ts" setup>
|
||||
import type { User } from '#/api/core/user';
|
||||
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { Network } from '@vben/icons';
|
||||
|
||||
import { ElAvatar, ElPopover, ElTooltip } from 'element-plus';
|
||||
|
||||
import { getUserDetailApi } from '#/api/core/user';
|
||||
import DeptTag from '#/components/dept-tag/index.vue';
|
||||
import PostTag from '#/components/post-tag/index.vue';
|
||||
import RoleTag from '#/components/role-tag/index.vue';
|
||||
import { getFileUrl } from '#/composables/useFileUrl';
|
||||
import { $t } from '#/locales';
|
||||
import { generateAvatarGradient, generateAvatarText } from '#/utils/avatar';
|
||||
|
||||
import UserProfileDialog from './UserProfileDialog.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'UserAvatar',
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 56,
|
||||
fontSize: 24,
|
||||
shadow: true,
|
||||
showPopover: true,
|
||||
autoLoad: false,
|
||||
showInfo: false,
|
||||
hideUsername: false,
|
||||
infoPosition: 'bottom',
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 用户对象(包含 id, name, avatar 等信息)
|
||||
*/
|
||||
user?: User;
|
||||
/**
|
||||
* 用户ID(用于获取详细信息和用户对象)
|
||||
*/
|
||||
userId?: string;
|
||||
/**
|
||||
* 用户名字或用户名(可选,如果不提供则从 user 或 userId 中获取)
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* 头像文件路径(不需要调用 getFileStreamUrl,组件内部处理)
|
||||
* @deprecated 建议使用 user 或 userId 代替
|
||||
*/
|
||||
avatar?: string;
|
||||
/**
|
||||
* 头像尺寸(像素)
|
||||
* @default 56
|
||||
*/
|
||||
size?: number;
|
||||
/**
|
||||
* 文字大小(像素)
|
||||
* @default 24
|
||||
*/
|
||||
fontSize?: number;
|
||||
/**
|
||||
* 是否显示阴影
|
||||
* @default true
|
||||
*/
|
||||
shadow?: boolean;
|
||||
/**
|
||||
* 是否启用悬停显示详细信息
|
||||
* @default true
|
||||
*/
|
||||
showPopover?: boolean;
|
||||
/**
|
||||
* 是否自动加载用户信息(用于只传 userId 时自动获取用户名和头像)
|
||||
* @default false
|
||||
*/
|
||||
autoLoad?: boolean;
|
||||
/**
|
||||
* 是否在头像下方显示用户名和username
|
||||
* @default false
|
||||
*/
|
||||
showInfo?: boolean;
|
||||
/**
|
||||
* 是否隐藏 username(只显示 name)
|
||||
* @default false
|
||||
*/
|
||||
hideUsername?: boolean;
|
||||
/**
|
||||
* showInfo 的布局方向:bottom 在下方,right 在右侧
|
||||
* @default 'bottom'
|
||||
*/
|
||||
infoPosition?: 'bottom' | 'right';
|
||||
}
|
||||
|
||||
// Popover 相关状态(需要在其他computed/watch之前定义)
|
||||
const popoverVisible = ref(false);
|
||||
const userDetail = ref<User>();
|
||||
const loading = ref(false);
|
||||
const hasLoaded = ref(false);
|
||||
|
||||
// 用户详情 Dialog
|
||||
const profileDialogVisible = ref(false);
|
||||
|
||||
function openProfileDialog() {
|
||||
popoverVisible.value = false;
|
||||
profileDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 获取有效的 ID
|
||||
const effectiveUserId = computed(() => {
|
||||
return props.userId || props.user?.id;
|
||||
});
|
||||
|
||||
// 获取有效的用户名
|
||||
const effectiveUserName = computed(() => {
|
||||
return props.name || props.user?.name || userDetail.value?.name;
|
||||
});
|
||||
|
||||
// 获取有效的头像 URL(处理 getFileUrl)
|
||||
const effectiveAvatarUrl = ref<string | undefined>(undefined);
|
||||
|
||||
// 异步加载头像URL
|
||||
async function loadAvatarUrl() {
|
||||
const avatarPath =
|
||||
props.avatar || props.user?.avatar || userDetail.value?.avatar;
|
||||
effectiveAvatarUrl.value = avatarPath
|
||||
? await getFileUrl(avatarPath)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
// 监听头像路径变化
|
||||
watch(
|
||||
() => props.avatar || props.user?.avatar || userDetail.value?.avatar,
|
||||
() => {
|
||||
loadAvatarUrl();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const userInitials = computed(() => {
|
||||
return generateAvatarText(effectiveUserName.value);
|
||||
});
|
||||
|
||||
const avatarGradient = computed(() => {
|
||||
return generateAvatarGradient(effectiveUserName.value);
|
||||
});
|
||||
|
||||
// 加载用户详细信息
|
||||
async function loadUserDetail() {
|
||||
if (hasLoaded.value) return; // 已加载过,不再重复加载
|
||||
|
||||
if (!effectiveUserId.value) {
|
||||
// 没有 ID 但有 user 对象,直接使用
|
||||
if (props.user) {
|
||||
userDetail.value = props.user;
|
||||
hasLoaded.value = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
userDetail.value = await getUserDetailApi(effectiveUserId.value);
|
||||
hasLoaded.value = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to load user detail:', error);
|
||||
// API 失败时回退到 props.user
|
||||
if (props.user) {
|
||||
userDetail.value = props.user;
|
||||
hasLoaded.value = true;
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Popover 显示时加载数据
|
||||
function handlePopoverShow() {
|
||||
if (!hasLoaded.value) {
|
||||
loadUserDetail();
|
||||
}
|
||||
}
|
||||
|
||||
// 获取性别显示文本
|
||||
function getGenderText(gender?: number) {
|
||||
if (gender === 1) return '男';
|
||||
if (gender === 0) return '女';
|
||||
return '未知';
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
function getStatusType(isActive?: number) {
|
||||
return isActive === 1 ? 'success' : 'danger';
|
||||
}
|
||||
|
||||
// 获取状态显示文本
|
||||
function getStatusText(isActive?: number) {
|
||||
return isActive === 1 ? '启用' : '禁用';
|
||||
}
|
||||
|
||||
// 自动加载用户信息
|
||||
onMounted(() => {
|
||||
// autoLoad 或 showInfo 时都需要加载用户详情
|
||||
if ((props.autoLoad || props.showInfo) && props.userId && !props.user) {
|
||||
loadUserDetail();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 userId 变化,自动重新加载
|
||||
watch(
|
||||
() => props.userId,
|
||||
(newId, oldId) => {
|
||||
if ((props.autoLoad || props.showInfo) && newId && newId !== oldId) {
|
||||
hasLoaded.value = false;
|
||||
userDetail.value = undefined;
|
||||
loadUserDetail();
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="user-avatar-root">
|
||||
<ElPopover
|
||||
v-if="showPopover && effectiveUserId"
|
||||
v-model:visible="popoverVisible"
|
||||
placement="right"
|
||||
:width="280"
|
||||
trigger="hover"
|
||||
:show-after="300"
|
||||
@show="handlePopoverShow"
|
||||
>
|
||||
<template #reference>
|
||||
<div
|
||||
class="avatar-wrapper"
|
||||
:class="{
|
||||
'with-info': showInfo,
|
||||
'info-bottom': showInfo && infoPosition === 'bottom',
|
||||
'info-right': showInfo && infoPosition === 'right',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="avatar-generator cursor-pointer"
|
||||
:class="{ 'is-loading': loading }"
|
||||
>
|
||||
<div
|
||||
v-if="!effectiveAvatarUrl"
|
||||
class="avatar-gradient"
|
||||
:style="{
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
background: avatarGradient,
|
||||
boxShadow: shadow ? '0 2px 8px rgba(0, 0, 0, 0.15)' : 'none',
|
||||
fontSize: `${fontSize}px`,
|
||||
}"
|
||||
>
|
||||
<span class="avatar-text">{{ userInitials }}</span>
|
||||
</div>
|
||||
<ElAvatar
|
||||
v-else
|
||||
:src="effectiveAvatarUrl"
|
||||
:alt="effectiveUserName"
|
||||
:size="size"
|
||||
:style="{
|
||||
boxShadow: shadow ? '0 2px 8px rgba(0, 0, 0, 0.15)' : 'none',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="showInfo" class="avatar-info">
|
||||
<div v-if="loading" class="info-skeleton">
|
||||
<div class="skeleton-line name"></div>
|
||||
<div class="skeleton-line username"></div>
|
||||
</div>
|
||||
<template v-else-if="userDetail">
|
||||
<div class="info-name">{{ userDetail.name }}</div>
|
||||
<div v-if="!hideUsername" class="info-username">
|
||||
@{{ userDetail.username }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Popover 内容 -->
|
||||
<div class="user-detail-popover">
|
||||
<!-- 加载骨架屏 -->
|
||||
<div v-if="loading || !userDetail" class="popover-skeleton">
|
||||
<div class="skeleton-header">
|
||||
<div class="skeleton-avatar"></div>
|
||||
<div class="skeleton-info">
|
||||
<div class="skeleton-bar skeleton-name"></div>
|
||||
<div class="skeleton-bar skeleton-username"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skeleton-details">
|
||||
<div v-for="i in 6" :key="i" class="skeleton-row">
|
||||
<div class="skeleton-bar skeleton-label"></div>
|
||||
<div
|
||||
class="skeleton-bar skeleton-value"
|
||||
:style="{ width: `${60 + (i % 3) * 20}px` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户详细信息 -->
|
||||
<div v-else class="user-detail-content">
|
||||
<!-- 头像和基本信息 -->
|
||||
<div class="user-header">
|
||||
<div class="user-avatar-large">
|
||||
<div
|
||||
v-if="!userDetail.avatar"
|
||||
class="avatar-gradient"
|
||||
:style="{
|
||||
width: '64px',
|
||||
height: '64px',
|
||||
background: generateAvatarGradient(userDetail.name),
|
||||
fontSize: '24px',
|
||||
}"
|
||||
>
|
||||
<span class="avatar-text">{{
|
||||
generateAvatarText(userDetail.name)
|
||||
}}</span>
|
||||
</div>
|
||||
<ElAvatar
|
||||
v-else
|
||||
:src="effectiveAvatarUrl"
|
||||
:alt="userDetail.name"
|
||||
:size="64"
|
||||
/>
|
||||
</div>
|
||||
<div class="user-basic-info">
|
||||
<div class="user-name">{{ userDetail.name }}</div>
|
||||
<div class="user-username">@{{ userDetail.username }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="user-actions">
|
||||
<ElTooltip
|
||||
:content="$t('user-avatar.profile.organization')"
|
||||
placement="top"
|
||||
>
|
||||
<button class="action-btn" @click="openProfileDialog">
|
||||
<Network :size="15" />
|
||||
</button>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详细信息 -->
|
||||
<div class="user-details">
|
||||
<div v-if="userDetail.email" class="detail-item">
|
||||
<span class="detail-label">邮箱:</span>
|
||||
<span class="detail-value">{{ userDetail.email }}</span>
|
||||
</div>
|
||||
<div v-if="userDetail.mobile" class="detail-item">
|
||||
<span class="detail-label">手机:</span>
|
||||
<span class="detail-value">{{ userDetail.mobile }}</span>
|
||||
</div>
|
||||
<div v-if="userDetail.city" class="detail-item">
|
||||
<span class="detail-label">城市:</span>
|
||||
<span class="detail-value">{{ userDetail.city }}</span>
|
||||
</div>
|
||||
<div v-if="userDetail.dept_id" class="detail-item">
|
||||
<span class="detail-label">部门:</span>
|
||||
<span class="detail-value">
|
||||
<DeptTag :dept-id="userDetail.dept_id" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="userDetail.post_id" class="detail-item">
|
||||
<span class="detail-label">岗位:</span>
|
||||
<span class="detail-value">
|
||||
<PostTag :post-id="userDetail.post_id" />
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="userDetail.role_ids && userDetail.role_ids.length > 0"
|
||||
class="detail-item"
|
||||
>
|
||||
<span class="detail-label">角色:</span>
|
||||
<span class="detail-value">
|
||||
<RoleTag :role-ids="userDetail.role_ids" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="userDetail.manager_id" class="detail-item">
|
||||
<span class="detail-label">经理:</span>
|
||||
<span class="detail-value">
|
||||
<UserAvatar
|
||||
:user-id="userDetail.manager_id"
|
||||
:size="20"
|
||||
:font-size="10"
|
||||
:shadow="false"
|
||||
show-info
|
||||
hide-username
|
||||
info-position="right"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<!-- <div class="detail-item">
|
||||
<span class="detail-label">性别:</span>
|
||||
<span class="detail-value">{{ getGenderText(userDetail.gender) }}</span>
|
||||
</div> -->
|
||||
<div v-if="userDetail.user_type_display" class="detail-item">
|
||||
<span class="detail-label">类型:</span>
|
||||
<span class="detail-value">{{
|
||||
userDetail.user_type_display
|
||||
}}</span>
|
||||
</div>
|
||||
<!-- <div class="detail-item">
|
||||
<span class="detail-label">状态:</span>
|
||||
<ElTag :type="getStatusType(userDetail.is_active)" size="small">
|
||||
{{ getStatusText(userDetail.is_active) }}
|
||||
</ElTag>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElPopover>
|
||||
|
||||
<!-- 用户详情 Dialog -->
|
||||
<UserProfileDialog
|
||||
v-if="effectiveUserId"
|
||||
v-model="profileDialogVisible"
|
||||
:user-id="effectiveUserId"
|
||||
/>
|
||||
|
||||
<!-- 无 Popover 时 -->
|
||||
<div
|
||||
v-if="!(showPopover && effectiveUserId)"
|
||||
class="avatar-wrapper"
|
||||
:class="{
|
||||
'with-info': showInfo,
|
||||
'info-bottom': showInfo && infoPosition === 'bottom',
|
||||
'info-right': showInfo && infoPosition === 'right',
|
||||
}"
|
||||
>
|
||||
<div class="avatar-generator" :class="{ 'is-loading': loading }">
|
||||
<div
|
||||
v-if="!effectiveAvatarUrl"
|
||||
class="avatar-gradient"
|
||||
:style="{
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
background: avatarGradient,
|
||||
boxShadow: shadow ? '0 2px 8px rgba(0, 0, 0, 0.15)' : 'none',
|
||||
fontSize: `${fontSize}px`,
|
||||
}"
|
||||
>
|
||||
<span class="avatar-text">{{ userInitials }}</span>
|
||||
</div>
|
||||
<ElAvatar
|
||||
v-else
|
||||
:src="effectiveAvatarUrl"
|
||||
:alt="effectiveUserName"
|
||||
:size="size"
|
||||
:style="{
|
||||
boxShadow: shadow ? '0 2px 8px rgba(0, 0, 0, 0.15)' : 'none',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="showInfo" class="avatar-info">
|
||||
<div v-if="loading" class="info-skeleton">
|
||||
<div class="skeleton-line name"></div>
|
||||
<div class="skeleton-line username"></div>
|
||||
</div>
|
||||
<template v-else-if="userDetail">
|
||||
<div class="info-name">{{ userDetail.name }}</div>
|
||||
<div v-if="!hideUsername" class="info-username">
|
||||
@{{ userDetail.username }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-avatar-root {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
&.info-bottom {
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&.info-right {
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-generator {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.is-loading {
|
||||
.avatar-gradient,
|
||||
:deep(.el-avatar) {
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-gradient {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
|
||||
.avatar-text {
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 1px 2px rgb(0 0 0 / 20%);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-avatar) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-info {
|
||||
min-width: 60px;
|
||||
max-width: 80px;
|
||||
text-align: center;
|
||||
|
||||
.info-right & {
|
||||
max-width: 120px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.info-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-username {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-skeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
|
||||
.info-right & {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.skeleton-line {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--el-fill-color) 25%,
|
||||
var(--el-fill-color-light) 50%,
|
||||
var(--el-fill-color) 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
border-radius: 4px;
|
||||
animation: shimmer 1.5s infinite;
|
||||
|
||||
&.name {
|
||||
width: 48px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
&.username {
|
||||
width: 56px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.user-detail-popover {
|
||||
padding: 12px;
|
||||
|
||||
.popover-skeleton {
|
||||
.skeleton-header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.skeleton-avatar {
|
||||
flex-shrink: 0;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--el-fill-color-light) 25%,
|
||||
var(--el-fill-color) 50%,
|
||||
var(--el-fill-color-light) 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
.skeleton-info {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.skeleton-bar {
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--el-fill-color-light) 25%,
|
||||
var(--el-fill-color) 50%,
|
||||
var(--el-fill-color-light) 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
.skeleton-name {
|
||||
width: 72px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.skeleton-username {
|
||||
width: 56px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.skeleton-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.skeleton-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.skeleton-label {
|
||||
flex-shrink: 0;
|
||||
width: 42px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.skeleton-value {
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-detail-content {
|
||||
.user-header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding-bottom: 12px;
|
||||
// border-bottom: 1px solid hsl(var(--border));
|
||||
|
||||
.user-avatar-large {
|
||||
flex-shrink: 0;
|
||||
|
||||
.avatar-gradient {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
|
||||
|
||||
.avatar-text {
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
text-shadow: 0 1px 2px rgb(0 0 0 / 20%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-basic-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.user-name {
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.user-username {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 8px 0;
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
// background: var(--el-fill-color-light);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-details {
|
||||
padding-top: 12px;
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
font-size: 13px;
|
||||
|
||||
.detail-label {
|
||||
flex-shrink: 0;
|
||||
width: 60px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--el-text-color-primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user