mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-04 23:18:44 +08:00
重构用户卡片页面
This commit is contained in:
Generated
+14601
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -26,6 +26,7 @@
|
|||||||
"@fullcalendar/timegrid": "^6.1.15",
|
"@fullcalendar/timegrid": "^6.1.15",
|
||||||
"@fullcalendar/vue3": "^6.1.15",
|
"@fullcalendar/vue3": "^6.1.15",
|
||||||
"@iconify/utils": "^2.2.1",
|
"@iconify/utils": "^2.2.1",
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@vue-flow/background": "^1.3.2",
|
"@vue-flow/background": "^1.3.2",
|
||||||
"@vue-flow/controls": "^1.1.2",
|
"@vue-flow/controls": "^1.1.2",
|
||||||
"@vue-flow/core": "^1.42.1",
|
"@vue-flow/core": "^1.42.1",
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-http-proxy": "^2.1.1",
|
"express-http-proxy": "^2.1.1",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"mousetrap": "^1.6.5",
|
"mousetrap": "^1.6.5",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
@@ -108,4 +110,4 @@
|
|||||||
"workbox-window": "^7.3.0"
|
"workbox-window": "^7.3.0"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.18"
|
"packageManager": "yarn@1.22.18"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -795,6 +795,8 @@ export interface User {
|
|||||||
permissions: { [key: string]: any }
|
permissions: { [key: string]: any }
|
||||||
// 用户个性化设置 json
|
// 用户个性化设置 json
|
||||||
settings: { [key: string]: string | null }
|
settings: { [key: string]: string | null }
|
||||||
|
// 昵称
|
||||||
|
nickname?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 存储空间
|
// 存储空间
|
||||||
|
|||||||
@@ -7,11 +7,16 @@ import { useToast } from 'vue-toast-notification'
|
|||||||
import { useConfirm } from 'vuetify-use-dialog'
|
import { useConfirm } from 'vuetify-use-dialog'
|
||||||
import UserAddEditDialog from '@/components/dialog/UserAddEditDialog.vue'
|
import UserAddEditDialog from '@/components/dialog/UserAddEditDialog.vue'
|
||||||
|
|
||||||
|
// 扩展User类型以包含昵称字段
|
||||||
|
interface ExtendedUser extends User {
|
||||||
|
nickname?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// 定义输入变量
|
// 定义输入变量
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
// 用户信息
|
// 用户信息
|
||||||
user: {
|
user: {
|
||||||
type: Object as PropType<User>,
|
type: Object as PropType<ExtendedUser>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
// 所有用户
|
// 所有用户
|
||||||
@@ -45,11 +50,28 @@ const movieSubscriptions = ref(0)
|
|||||||
// 用户电视剧订阅数量
|
// 用户电视剧订阅数量
|
||||||
const tvShowSubscriptions = ref(0)
|
const tvShowSubscriptions = ref(0)
|
||||||
|
|
||||||
// 新增:用户状态背景颜色计算
|
// 是否显示更多操作菜单
|
||||||
const statusClass = computed(() => ({
|
const showMenu = ref(false)
|
||||||
'bg-error-lighten-4': !props.user.is_active, // 假设用户状态使用 is_active 字段
|
|
||||||
'border-error': !props.user.is_active, // 非活跃用户添加红色边框
|
// 鼠标悬停状态
|
||||||
}))
|
const isHovered = ref(false)
|
||||||
|
|
||||||
|
// 是否为移动设备
|
||||||
|
const isMobile = ref(window.innerWidth < 600)
|
||||||
|
|
||||||
|
// 显示名称 - 如果有昵称则优先显示昵称
|
||||||
|
const displayName = computed(() => {
|
||||||
|
const settingsNickname = props.user.settings?.nickname as string | undefined;
|
||||||
|
const nickname = props.user.nickname || settingsNickname;
|
||||||
|
return nickname || props.user.name;
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算用户卡片状态类
|
||||||
|
const cardStatusClass = computed(() => {
|
||||||
|
if (!props.user.is_active) return 'user-card-inactive'
|
||||||
|
if (props.user.is_superuser) return 'user-card-admin'
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
// 按用户查询订阅数量
|
// 按用户查询订阅数量
|
||||||
async function fetchSubscriptions() {
|
async function fetchSubscriptions() {
|
||||||
@@ -93,81 +115,180 @@ function editUser() {
|
|||||||
userEditDialog.value = true
|
userEditDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户重新完成时
|
// 用户更新完成时
|
||||||
function onUserUpdate() {
|
function onUserUpdate() {
|
||||||
userEditDialog.value = false
|
userEditDialog.value = false
|
||||||
emit('save')
|
emit('save')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新窗口大小监听
|
||||||
|
function handleResize() {
|
||||||
|
isMobile.value = window.innerWidth < 600
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchSubscriptions()
|
fetchSubscriptions()
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VHover v-slot="hover">
|
<VCard
|
||||||
<VCard
|
class="user-card"
|
||||||
v-bind="hover.props"
|
:class="[
|
||||||
class="w-full h-full"
|
{'user-card-hover': isHovered},
|
||||||
:class="{ 'transition transform-cpu duration-300 -translate-y-1': hover.isHovering, ...statusClass }"
|
cardStatusClass,
|
||||||
@click.stop="editUser"
|
{'mobile-card': isMobile}
|
||||||
>
|
]"
|
||||||
<!-- 用户头像 -->
|
@mouseenter="isHovered = true"
|
||||||
<VImg height="12rem" :src="user.avatar ?? avatar1" cover>
|
@mouseleave="isHovered = false"
|
||||||
<div v-if="!user.is_active" class="img-overlay" />
|
>
|
||||||
</VImg>
|
<!-- 管理员卡片装饰 -->
|
||||||
<div class="flex flex-col">
|
<div v-if="user.is_superuser" class="admin-decoration">
|
||||||
<!-- 用户基本信息 -->
|
<div class="decoration-line"></div>
|
||||||
<VCardTitle class="pt-2">{{ user.name }}</VCardTitle>
|
<div class="decoration-circle"><VIcon icon="mdi-shield-star" size="x-small" color="warning" /></div>
|
||||||
<VCardSubtitle v-if="user.email" class="text-wrap">
|
<div class="decoration-line"></div>
|
||||||
<VIcon size="16">mdi-email</VIcon>
|
</div>
|
||||||
{{ user.email }}
|
|
||||||
</VCardSubtitle>
|
<!-- 用户头像和基本信息 -->
|
||||||
<!-- 订阅信息 -->
|
<div class="user-card-header" :class="{'admin-header': user.is_superuser}">
|
||||||
<VCardActions>
|
<div class="user-avatar-container">
|
||||||
<div class="mt-3 flex gap-3" dense>
|
<VAvatar :size="isMobile ? 50 : 74" rounded="lg" class="user-avatar"
|
||||||
<VChip v-if="user.is_otp" size="small" color="error">
|
:class="{'admin-avatar': user.is_superuser, 'inactive-avatar': !user.is_active}">
|
||||||
<VIcon>mdi-lock</VIcon>
|
<VImg :src="user.avatar || avatar1" :alt="user.name" />
|
||||||
</VChip>
|
<div v-if="!user.is_active" class="avatar-overlay">
|
||||||
<VChip size="small" color="info">
|
<VIcon icon="mdi-account-lock" color="white" size="small" />
|
||||||
<VIcon left class="me-2">mdi-movie</VIcon>
|
|
||||||
{{ movieSubscriptions }}
|
|
||||||
</VChip>
|
|
||||||
<VChip size="small" color="warning">
|
|
||||||
<VIcon left class="me-2">mdi-television</VIcon>
|
|
||||||
{{ tvShowSubscriptions }}
|
|
||||||
</VChip>
|
|
||||||
</div>
|
</div>
|
||||||
</VCardActions>
|
</VAvatar>
|
||||||
|
<div v-if="user.is_superuser" class="admin-crown">
|
||||||
|
<VIcon icon="mdi-crown" color="warning" size="small" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 管理员标签 -->
|
|
||||||
<VChip
|
<div class="user-info">
|
||||||
variant="elevated"
|
<div class="user-name-section">
|
||||||
size="small"
|
<div class="name-and-badges">
|
||||||
class="absolute right-2 top-2"
|
<h3 class="user-name" :class="{'admin-name': user.is_superuser, 'inactive-name': !user.is_active}">
|
||||||
:color="user.is_superuser ? 'primary' : 'secondary'"
|
{{ displayName }}
|
||||||
>
|
<VIcon v-if="user.nickname || user.settings?.nickname" icon="mdi-format-quote-close" size="x-small" color="info" class="nickname-icon" />
|
||||||
{{ user.is_superuser ? '管理员' : '普通用户' }}
|
</h3>
|
||||||
</VChip>
|
<div class="user-badges">
|
||||||
<!-- 删除按钮 -->
|
<VChip
|
||||||
<div class="absolute bottom-2 w-full flex items-center justify-center">
|
v-if="user.is_superuser"
|
||||||
|
size="x-small"
|
||||||
|
color="error"
|
||||||
|
class="user-badge admin-badge"
|
||||||
|
>管理员</VChip>
|
||||||
|
<VChip
|
||||||
|
v-else
|
||||||
|
size="x-small"
|
||||||
|
color="default"
|
||||||
|
class="user-badge"
|
||||||
|
>普通用户</VChip>
|
||||||
|
<VChip
|
||||||
|
size="x-small"
|
||||||
|
:color="user.is_active ? 'success' : 'grey'"
|
||||||
|
variant="tonal"
|
||||||
|
class="user-badge"
|
||||||
|
>
|
||||||
|
{{ user.is_active ? '激活' : '已停用' }}
|
||||||
|
</VChip>
|
||||||
|
<VChip
|
||||||
|
v-if="user.is_otp"
|
||||||
|
size="x-small"
|
||||||
|
color="info"
|
||||||
|
variant="tonal"
|
||||||
|
class="user-badge"
|
||||||
|
>
|
||||||
|
2FA
|
||||||
|
</VChip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 移动端订阅数据信息 -->
|
||||||
|
<div v-if="isMobile" class="mobile-stats">
|
||||||
|
<div class="mobile-stat-item">
|
||||||
|
<VIcon size="x-small" icon="mdi-movie-outline" color="primary" />
|
||||||
|
<span>{{ movieSubscriptions }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-stat-item">
|
||||||
|
<VIcon size="x-small" icon="mdi-television-classic" color="primary" />
|
||||||
|
<span>{{ tvShowSubscriptions }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 头部操作按钮 -->
|
||||||
|
<div class="user-actions" :class="{ 'mobile-actions': isMobile }">
|
||||||
<VBtn
|
<VBtn
|
||||||
v-show="hover.isHovering && currentUserIsSuperuser"
|
icon
|
||||||
@click.stop="removeUser"
|
|
||||||
icon="mdi-delete"
|
|
||||||
color="error"
|
|
||||||
size="small"
|
size="small"
|
||||||
class="shadow-xl"
|
:color="user.is_superuser ? 'warning' : 'primary'"
|
||||||
/>
|
variant="text"
|
||||||
|
@click="editUser"
|
||||||
|
class="action-btn"
|
||||||
|
>
|
||||||
|
<VIcon icon="mdi-pencil" />
|
||||||
|
<VTooltip v-if="!isMobile" activator="parent" location="bottom">编辑用户</VTooltip>
|
||||||
|
</VBtn>
|
||||||
|
|
||||||
|
<VBtn
|
||||||
|
v-if="props.user.id != currentLoginUserId && currentUserIsSuperuser"
|
||||||
|
icon
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
@click="removeUser"
|
||||||
|
class="action-btn"
|
||||||
|
>
|
||||||
|
<VIcon icon="mdi-delete" />
|
||||||
|
<VTooltip v-if="!isMobile" activator="parent" location="bottom">删除用户</VTooltip>
|
||||||
|
</VBtn>
|
||||||
</div>
|
</div>
|
||||||
</VCard>
|
</div>
|
||||||
</VHover>
|
|
||||||
|
<!-- 独立的邮箱显示 -->
|
||||||
|
<div class="email-container" :class="{'admin-email': user.is_superuser, 'inactive-email': !user.is_active}">
|
||||||
|
<VIcon icon="mdi-email-outline" size="small" color="primary" class="email-icon" />
|
||||||
|
<span class="email-text">{{ user.email || '未设置邮箱' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PC端显示订阅统计信息 -->
|
||||||
|
<div v-if="!isMobile" class="user-card-body">
|
||||||
|
<div class="user-stats-container">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-icon-container" :class="{'admin-stat': user.is_superuser}">
|
||||||
|
<VIcon :color="user.is_superuser ? 'warning' : 'primary'" icon="mdi-movie-outline" size="20" />
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-value">{{ movieSubscriptions }}</div>
|
||||||
|
<div class="stat-label">电影订阅</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-icon-container" :class="{'admin-stat': user.is_superuser}">
|
||||||
|
<VIcon :color="user.is_superuser ? 'warning' : 'primary'" icon="mdi-television-classic" size="20" />
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-value">{{ tvShowSubscriptions }}</div>
|
||||||
|
<div class="stat-label">剧集订阅</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
|
||||||
<!-- 用户编辑弹窗 -->
|
<!-- 用户编辑弹窗 -->
|
||||||
<UserAddEditDialog
|
<UserAddEditDialog
|
||||||
v-if="userEditDialog"
|
v-if="userEditDialog"
|
||||||
v-model="userEditDialog"
|
v-model="userEditDialog"
|
||||||
:username="user?.name"
|
:username="props.user?.name"
|
||||||
:usernames="users.map(item => item.name)"
|
:usernames="props.users.map(item => item.name)"
|
||||||
oper="edit"
|
oper="edit"
|
||||||
@save="onUserUpdate"
|
@save="onUserUpdate"
|
||||||
@close="userEditDialog = false"
|
@close="userEditDialog = false"
|
||||||
@@ -175,13 +296,507 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.img-overlay {
|
.user-card {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 15px rgba(var(--v-theme-on-surface), 0.08);
|
||||||
|
background: rgb(var(--v-theme-surface));
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card-hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 20px rgba(var(--v-theme-on-surface), 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card-admin {
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background-image: linear-gradient(rgb(var(--v-theme-surface)), rgb(var(--v-theme-surface))),
|
||||||
|
linear-gradient(120deg, rgba(var(--v-theme-warning), 0.5), rgba(var(--v-theme-error), 0.5));
|
||||||
|
background-origin: border-box;
|
||||||
|
background-clip: content-box, border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card-inactive {
|
||||||
|
background-color: rgba(var(--v-theme-surface), 0.95);
|
||||||
|
opacity: 0.85;
|
||||||
|
border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card-inactive::before {
|
||||||
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
right: 0;
|
||||||
height: 100%;
|
bottom: 0;
|
||||||
background: rgba(255, 255, 255, 0.5);
|
backdrop-filter: grayscale(30%);
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-decoration {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 4px 12px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.decoration-line {
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, rgba(var(--v-theme-warning), 0.1), rgba(var(--v-theme-warning), 0.7));
|
||||||
|
}
|
||||||
|
|
||||||
|
.decoration-line:last-child {
|
||||||
|
background: linear-gradient(90deg, rgba(var(--v-theme-warning), 0.7), rgba(var(--v-theme-warning), 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.decoration-circle {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid rgba(var(--v-theme-warning), 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 8px;
|
||||||
|
background: rgb(var(--v-theme-surface));
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card-header {
|
||||||
|
padding: 20px 16px 12px;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header {
|
||||||
|
background: linear-gradient(to bottom, rgba(var(--v-theme-warning), 0.05), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-container {
|
||||||
|
position: relative;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
box-shadow: 0 4px 8px rgba(var(--v-theme-on-surface), 0.1);
|
||||||
|
border: 4px solid rgb(var(--v-theme-surface));
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-avatar {
|
||||||
|
border: 4px solid rgba(var(--v-theme-warning), 0.1);
|
||||||
|
box-shadow: 0 5px 15px rgba(var(--v-theme-warning), 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-avatar:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
left: -5px;
|
||||||
|
right: -5px;
|
||||||
|
bottom: -5px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(var(--v-theme-warning), 0.3);
|
||||||
|
pointer-events: none;
|
||||||
|
animation: pulse 2.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inactive-avatar {
|
||||||
|
filter: grayscale(50%);
|
||||||
|
opacity: 0.9;
|
||||||
|
border-color: rgba(var(--v-theme-on-surface), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(var(--v-theme-on-surface), 0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
backdrop-filter: blur(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.otp-badge {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
animation: glow 2s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.otp-badge .v-icon {
|
||||||
|
color: #4CAF50 !important;
|
||||||
|
font-size: 18px;
|
||||||
|
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.4));
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow {
|
||||||
|
from {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-otp {
|
||||||
|
bottom: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-otp .v-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-crown {
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
left: -6px;
|
||||||
|
background: transparent;
|
||||||
|
z-index: 5;
|
||||||
|
transform: rotate(-25deg);
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.4));
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-crown .v-icon {
|
||||||
|
color: #ffc107 !important;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0% {
|
||||||
|
transform: rotate(-25deg) translateY(0px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(-25deg) translateY(-3px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(-25deg) translateY(0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-icon {
|
||||||
|
margin-left: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
animation: pulse-nickname 2s ease infinite;
|
||||||
|
opacity: 0.9;
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-nickname {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
cursor: move;
|
||||||
|
margin-right: 6px;
|
||||||
|
opacity: 0.3;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card:hover .drag-handle {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name-section {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-and-badges {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-name {
|
||||||
|
color: rgb(var(--v-theme-warning));
|
||||||
|
text-shadow: 0 1px 2px rgba(var(--v-theme-warning), 0.1);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inactive-name {
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-badges::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-badge {
|
||||||
|
border: 1px solid rgba(var(--v-theme-error), 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-account, .user-email {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.7);
|
||||||
|
margin-top: 4px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-label {
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.5);
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-value {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 8px;
|
||||||
|
z-index: 5;
|
||||||
|
position: relative;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-stat-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-stat-item .v-icon {
|
||||||
|
font-size: 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-stat-item span {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card-body {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-stats-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
margin-top: 8px;
|
||||||
|
background-color: rgba(var(--v-theme-on-surface), 0.02);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon-container {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: rgba(var(--v-theme-primary), 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 2px 6px rgba(var(--v-theme-on-surface), 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-stat {
|
||||||
|
background-color: rgba(var(--v-theme-warning), 0.1);
|
||||||
|
box-shadow: 0 2px 6px rgba(var(--v-theme-warning), 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-error {
|
||||||
|
color: rgb(var(--v-theme-error));
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-container {
|
||||||
|
padding: 8px 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
border-top: 1px solid rgba(var(--v-theme-on-surface), 0.05);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-email {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inactive-email {
|
||||||
|
background-color: transparent;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-container .email-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-container .email-text {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.8);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-card .email-container {
|
||||||
|
padding: 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-card .email-container .email-text {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-card .user-avatar-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-card .otp-badge {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -56,8 +56,13 @@ const statusItems = [
|
|||||||
{ title: '已停用', value: 0 },
|
{ title: '已停用', value: 0 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 扩展User类型以包含note字段
|
||||||
|
interface ExtendedUser extends User {
|
||||||
|
nickname?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// 用户编辑表单数据
|
// 用户编辑表单数据
|
||||||
const userForm = ref<User>({
|
const userForm = ref<ExtendedUser>({
|
||||||
id: 0,
|
id: 0,
|
||||||
name: props.username ?? '',
|
name: props.username ?? '',
|
||||||
password: '',
|
password: '',
|
||||||
@@ -74,6 +79,7 @@ const userForm = ref<User>({
|
|||||||
vocechat_userid: null,
|
vocechat_userid: null,
|
||||||
synologychat_userid: null,
|
synologychat_userid: null,
|
||||||
},
|
},
|
||||||
|
nickname: '', // 昵称字段
|
||||||
})
|
})
|
||||||
|
|
||||||
// 更新头像
|
// 更新头像
|
||||||
@@ -190,6 +196,15 @@ async function updateUser() {
|
|||||||
}
|
}
|
||||||
userForm.value.password = newPassword.value
|
userForm.value.password = newPassword.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将nickname保存到settings中,后端可以直接处理JSON对象
|
||||||
|
if (userForm.value.nickname) {
|
||||||
|
if (!userForm.value.settings) {
|
||||||
|
userForm.value.settings = {};
|
||||||
|
}
|
||||||
|
userForm.value.settings.nickname = userForm.value.nickname;
|
||||||
|
}
|
||||||
|
|
||||||
const oldUserName = userForm.value.name
|
const oldUserName = userForm.value.name
|
||||||
userForm.value.name = currentUserName.value
|
userForm.value.name = currentUserName.value
|
||||||
const oldAvatar = userForm.value.avatar
|
const oldAvatar = userForm.value.avatar
|
||||||
@@ -197,7 +212,11 @@ async function updateUser() {
|
|||||||
isUpdating.value = true
|
isUpdating.value = true
|
||||||
startNProgress()
|
startNProgress()
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.put('user/', userForm.value)
|
// 确保昵称保存,使用一个临时变量存储完整数据
|
||||||
|
const userData = { ...userForm.value };
|
||||||
|
|
||||||
|
const result: { [key: string]: any } = await api.put('user/', userData)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
if (oldUserName !== currentUserName.value) {
|
if (oldUserName !== currentUserName.value) {
|
||||||
$toast.success(`【${oldUserName}】更名【${currentUserName.value}】, 更新成功!`)
|
$toast.success(`【${oldUserName}】更名【${currentUserName.value}】, 更新成功!`)
|
||||||
@@ -229,7 +248,7 @@ async function updateUser() {
|
|||||||
userForm.value.password = ''
|
userForm.value.password = ''
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
$toast.error(`【${userForm.value?.name}】更新失败!`)
|
$toast.error(`【${userForm.value?.name}】更新失败!`)
|
||||||
console.error(error)
|
console.error('更新失败:', error)
|
||||||
}
|
}
|
||||||
doneNProgress()
|
doneNProgress()
|
||||||
isUpdating.value = false
|
isUpdating.value = false
|
||||||
@@ -355,6 +374,15 @@ onMounted(() => {
|
|||||||
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
|
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="userForm.nickname"
|
||||||
|
density="comfortable"
|
||||||
|
clearable
|
||||||
|
label="昵称"
|
||||||
|
placeholder="显示昵称,优先于用户名显示"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
<VCol cols="12" md="6" v-if="canControl">
|
<VCol cols="12" md="6" v-if="canControl">
|
||||||
<VSelect
|
<VSelect
|
||||||
v-model="userStatus"
|
v-model="userStatus"
|
||||||
|
|||||||
+155
-34
@@ -9,6 +9,7 @@ import UserAddEditDialog from '@/components/dialog/UserAddEditDialog.vue'
|
|||||||
// APP
|
// APP
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
const appMode = inject('pwaMode') && display.mdAndDown.value
|
const appMode = inject('pwaMode') && display.mdAndDown.value
|
||||||
|
const isMobile = computed(() => display.mobile.value)
|
||||||
|
|
||||||
// 是否刷新过
|
// 是否刷新过
|
||||||
const isRefreshed = ref(false)
|
const isRefreshed = ref(false)
|
||||||
@@ -19,6 +20,9 @@ const loading = ref(false)
|
|||||||
// 新增用户窗口
|
// 新增用户窗口
|
||||||
const addUserDialog = ref(false)
|
const addUserDialog = ref(false)
|
||||||
|
|
||||||
|
// 要编辑的用户
|
||||||
|
const userToEdit = ref<User | null>(null)
|
||||||
|
|
||||||
// 所有用户信息
|
// 所有用户信息
|
||||||
const allUsers = ref<User[]>([])
|
const allUsers = ref<User[]>([])
|
||||||
|
|
||||||
@@ -41,6 +45,11 @@ const onUserAdd = () => {
|
|||||||
loadAllUsers()
|
loadAllUsers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开添加用户对话框
|
||||||
|
const openAddUserDialog = () => {
|
||||||
|
addUserDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// 加载当前用户数据
|
// 加载当前用户数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadAllUsers()
|
loadAllUsers()
|
||||||
@@ -54,39 +63,151 @@ onActivated(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
<div class="user-list-container">
|
||||||
|
<!-- 页面标题 -->
|
||||||
<div v-if="allUsers.length > 0" class="grid gap-3 grid-user-card">
|
<div class="page-header">
|
||||||
<UserCard v-for="user in allUsers" :user="user" :users="allUsers" @remove="loadAllUsers" @save="loadAllUsers" />
|
<div class="page-title">
|
||||||
|
<VIcon icon="mdi-account-group" size="large" color="primary" class="title-icon" />
|
||||||
|
<h1 class="title-text">用户管理</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载中提示 -->
|
||||||
|
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||||
|
|
||||||
|
<!-- 用户卡片网格 -->
|
||||||
|
<div v-if="allUsers.length > 0 && isRefreshed" class="users-grid">
|
||||||
|
<!-- 普通用户卡片 -->
|
||||||
|
<UserCard
|
||||||
|
v-for="user in allUsers"
|
||||||
|
:key="user.id"
|
||||||
|
:user="user"
|
||||||
|
:users="allUsers"
|
||||||
|
@remove="loadAllUsers"
|
||||||
|
@save="loadAllUsers"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 添加用户卡片 -->
|
||||||
|
<div class="add-user-card" @click="openAddUserDialog">
|
||||||
|
<div class="add-user-content">
|
||||||
|
<VIcon icon="mdi-account-plus" size="large" color="primary" />
|
||||||
|
<span class="add-user-text">添加用户</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 无数据提示 -->
|
||||||
|
<div v-if="allUsers.length === 0 && isRefreshed">
|
||||||
|
<NoDataFound
|
||||||
|
error-code="404"
|
||||||
|
error-title="没有用户"
|
||||||
|
error-description="点击添加用户卡片添加用户"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户添加弹窗 -->
|
||||||
|
<UserAddEditDialog
|
||||||
|
v-if="addUserDialog"
|
||||||
|
v-model="addUserDialog"
|
||||||
|
oper="add"
|
||||||
|
max-width="50rem"
|
||||||
|
persistent
|
||||||
|
z-index="1010"
|
||||||
|
@save="onUserAdd"
|
||||||
|
@close="addUserDialog = false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NoDataFound
|
|
||||||
v-if="allUsers.length === 0 && isRefreshed"
|
|
||||||
error-code="404"
|
|
||||||
error-title="没有用户"
|
|
||||||
error-description="点击右下角按钮添加用户"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<VFab
|
|
||||||
v-if="isRefreshed"
|
|
||||||
icon="mdi-plus"
|
|
||||||
location="bottom"
|
|
||||||
size="x-large"
|
|
||||||
fixed
|
|
||||||
app
|
|
||||||
appear
|
|
||||||
@click="addUserDialog = true"
|
|
||||||
:class="{ 'mb-12': appMode }"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 弹窗 -->
|
|
||||||
<UserAddEditDialog
|
|
||||||
v-if="addUserDialog"
|
|
||||||
v-model="addUserDialog"
|
|
||||||
oper="add"
|
|
||||||
max-width="50rem"
|
|
||||||
persistent
|
|
||||||
@save="onUserAdd"
|
|
||||||
@close="addUserDialog = false"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-list-container {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-icon {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.87);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.users-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-user-card {
|
||||||
|
border-radius: 16px;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 160px;
|
||||||
|
background-color: rgba(var(--v-theme-surface), 1);
|
||||||
|
border: 1.5px dashed rgba(var(--v-theme-primary), 0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 15px rgba(var(--v-theme-on-surface), 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-user-card:hover {
|
||||||
|
background-color: rgba(var(--v-theme-primary), 0.05);
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 20px rgba(var(--v-theme-on-surface), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-user-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-user-text {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
color: rgba(var(--v-theme-primary), 0.8);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.user-list-container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.users-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 601px) and (max-width: 960px) {
|
||||||
|
.users-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 961px) {
|
||||||
|
.users-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ const accountInfo = ref<User>({
|
|||||||
is_otp: false,
|
is_otp: false,
|
||||||
permissions: {},
|
permissions: {},
|
||||||
settings: {},
|
settings: {},
|
||||||
|
nickname: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// 二维码信息
|
// 二维码信息
|
||||||
@@ -102,17 +103,16 @@ function restoreCurrentAvatar() {
|
|||||||
$toast.success('已还原当前使用头像!')
|
$toast.success('已还原当前使用头像!')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用API,加载当前用户数据
|
// 加载当前用户信息
|
||||||
async function loadAccountInfo() {
|
async function fetchUserInfo() {
|
||||||
try {
|
try {
|
||||||
const user: User = await api.get('user/current')
|
const result: User = await api.get(`user/${userStore.userName}`)
|
||||||
console.log(user)
|
if (result) {
|
||||||
accountInfo.value = user
|
accountInfo.value = result
|
||||||
if (!accountInfo.value.avatar) {
|
accountInfo.value.avatar = accountInfo.value.avatar ? accountInfo.value.avatar : avatar1
|
||||||
accountInfo.value.avatar = avatar1
|
currentUserName.value = accountInfo.value.name
|
||||||
|
currentAvatar.value = accountInfo.value.avatar
|
||||||
}
|
}
|
||||||
currentAvatar.value = accountInfo.value.avatar
|
|
||||||
currentUserName.value = accountInfo.value.name
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
@@ -135,13 +135,26 @@ async function saveAccountInfo() {
|
|||||||
}
|
}
|
||||||
accountInfo.value.password = newPassword.value
|
accountInfo.value.password = newPassword.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将nickname保存到settings中,后端可以直接处理JSON对象
|
||||||
|
if (accountInfo.value.nickname) {
|
||||||
|
if (!accountInfo.value.settings) {
|
||||||
|
accountInfo.value.settings = {};
|
||||||
|
}
|
||||||
|
accountInfo.value.settings.nickname = accountInfo.value.nickname;
|
||||||
|
}
|
||||||
|
|
||||||
const oldUserName = accountInfo.value.name
|
const oldUserName = accountInfo.value.name
|
||||||
const oldAvatar = accountInfo.value.avatar
|
const oldAvatar = accountInfo.value.avatar
|
||||||
accountInfo.value.avatar = currentAvatar.value
|
accountInfo.value.avatar = currentAvatar.value
|
||||||
accountInfo.value.name = currentUserName.value
|
accountInfo.value.name = currentUserName.value
|
||||||
isSaving.value = true
|
isSaving.value = true
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.put('user/', accountInfo.value)
|
// 创建一个临时对象来保存用户数据,确保所有字段都会发送
|
||||||
|
const userData = { ...accountInfo.value };
|
||||||
|
|
||||||
|
const result: { [key: string]: any } = await api.put('user/', userData)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
if (oldUserName !== currentUserName.value) {
|
if (oldUserName !== currentUserName.value) {
|
||||||
$toast.success(`【${oldUserName}】更名【${currentUserName.value}】,用户信息保存成功!`)
|
$toast.success(`【${oldUserName}】更名【${currentUserName.value}】,用户信息保存成功!`)
|
||||||
@@ -167,7 +180,7 @@ async function saveAccountInfo() {
|
|||||||
accountInfo.value.avatar = oldAvatar
|
accountInfo.value.avatar = oldAvatar
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log('保存失败:', error)
|
||||||
}
|
}
|
||||||
isSaving.value = false
|
isSaving.value = false
|
||||||
}
|
}
|
||||||
@@ -230,7 +243,7 @@ async function judgeOtpPassword() {
|
|||||||
|
|
||||||
// 加载当前用户数据
|
// 加载当前用户数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadAccountInfo()
|
fetchUserInfo()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听 localStorage 中的用户头像变化
|
// 监听 localStorage 中的用户头像变化
|
||||||
@@ -298,8 +311,13 @@ watch(
|
|||||||
<!-- 👉 Form -->
|
<!-- 👉 Form -->
|
||||||
<VForm class="mt-6">
|
<VForm class="mt-6">
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol md="6" cols="12">
|
<VCol cols="12" md="6">
|
||||||
<VTextField v-model="currentUserName" density="comfortable" readonly label="用户名" />
|
<VTextField
|
||||||
|
v-model="currentUserName"
|
||||||
|
density="comfortable"
|
||||||
|
readonly
|
||||||
|
label="用户名"
|
||||||
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="12" md="6">
|
<VCol cols="12" md="6">
|
||||||
<VTextField v-model="accountInfo.email" density="comfortable" clearable label="邮箱" type="email" />
|
<VTextField v-model="accountInfo.email" density="comfortable" clearable label="邮箱" type="email" />
|
||||||
@@ -311,7 +329,7 @@ watch(
|
|||||||
:type="isNewPasswordVisible ? 'text' : 'password'"
|
:type="isNewPasswordVisible ? 'text' : 'password'"
|
||||||
:append-inner-icon="isNewPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
:append-inner-icon="isNewPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||||
clearable
|
clearable
|
||||||
label="新密码"
|
label="密码"
|
||||||
autocomplete=""
|
autocomplete=""
|
||||||
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
|
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
|
||||||
/>
|
/>
|
||||||
@@ -324,10 +342,19 @@ watch(
|
|||||||
:type="isConfirmPasswordVisible ? 'text' : 'password'"
|
:type="isConfirmPasswordVisible ? 'text' : 'password'"
|
||||||
:append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
:append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||||
clearable
|
clearable
|
||||||
label="确认新密码"
|
label="确认密码"
|
||||||
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
|
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="accountInfo.nickname"
|
||||||
|
density="comfortable"
|
||||||
|
clearable
|
||||||
|
label="昵称"
|
||||||
|
placeholder="显示昵称,优先于用户名显示"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
|
|
||||||
<VDivider class="my-10">
|
<VDivider class="my-10">
|
||||||
|
|||||||
Reference in New Issue
Block a user